mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-14 08:53:31 +08:00
feat: add agent MCP settings
This commit is contained in:
449
src/components/dialog/AgentMcpSettingsDialog.vue
Normal file
449
src/components/dialog/AgentMcpSettingsDialog.vue
Normal file
@@ -0,0 +1,449 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
type AgentMcpTransport = 'stdio' | 'sse' | 'http' | 'streamable_http'
|
||||
|
||||
interface AgentMcpServer {
|
||||
id: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
transport: AgentMcpTransport
|
||||
description?: string | null
|
||||
command?: string | null
|
||||
args: string[]
|
||||
env: Record<string, string>
|
||||
url?: string | null
|
||||
headers: Record<string, string>
|
||||
timeout: number
|
||||
tool_prefix?: string | null
|
||||
require_admin: boolean
|
||||
}
|
||||
|
||||
interface EditableAgentMcpServer extends AgentMcpServer {
|
||||
argsText: string
|
||||
envText: string
|
||||
headersText: string
|
||||
}
|
||||
|
||||
interface AgentMcpToolInfo {
|
||||
name: string
|
||||
agent_tool_name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface AgentMcpTestState {
|
||||
loading: boolean
|
||||
success?: boolean
|
||||
message?: string
|
||||
tools?: AgentMcpToolInfo[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
servers: AgentMcpServer[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
(event: 'saved', value: AgentMcpServer[]): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const display = useDisplay()
|
||||
|
||||
const saving = ref(false)
|
||||
const localServers = ref<EditableAgentMcpServer[]>([])
|
||||
const testStates = ref<Record<string, AgentMcpTestState>>({})
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const transportItems = computed(() => [
|
||||
{ title: t('setting.system.aiAgentMcpTransportStdio'), value: 'stdio' },
|
||||
{ title: t('setting.system.aiAgentMcpTransportSse'), value: 'sse' },
|
||||
{ title: t('setting.system.aiAgentMcpTransportHttp'), value: 'http' },
|
||||
])
|
||||
|
||||
function createServerId() {
|
||||
return `mcp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
function dictToLines(value?: Record<string, string>) {
|
||||
return Object.entries(value || {})
|
||||
.map(([key, item]) => `${key}=${item}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function linesToDict(value: string) {
|
||||
return String(value || '')
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.reduce<Record<string, string>>((result, line) => {
|
||||
const separatorIndex = line.indexOf('=')
|
||||
const key = separatorIndex >= 0 ? line.slice(0, separatorIndex).trim() : line
|
||||
if (!key) return result
|
||||
result[key] = separatorIndex >= 0 ? line.slice(separatorIndex + 1).trim() : ''
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
function toEditableServer(server: AgentMcpServer): EditableAgentMcpServer {
|
||||
return {
|
||||
id: server.id || createServerId(),
|
||||
name: server.name || t('setting.system.aiAgentMcpUnnamedServer'),
|
||||
enabled: server.enabled !== false,
|
||||
transport: server.transport || 'stdio',
|
||||
description: server.description || '',
|
||||
command: server.command || '',
|
||||
args: [...(server.args || [])],
|
||||
env: { ...(server.env || {}) },
|
||||
url: server.url || '',
|
||||
headers: { ...(server.headers || {}) },
|
||||
timeout: Number(server.timeout || 30),
|
||||
tool_prefix: server.tool_prefix || '',
|
||||
require_admin: server.require_admin !== false,
|
||||
argsText: (server.args || []).join('\n'),
|
||||
envText: dictToLines(server.env),
|
||||
headersText: dictToLines(server.headers),
|
||||
}
|
||||
}
|
||||
|
||||
function toServerPayload(server: EditableAgentMcpServer): AgentMcpServer {
|
||||
return {
|
||||
id: server.id || createServerId(),
|
||||
name: String(server.name || '').trim() || t('setting.system.aiAgentMcpUnnamedServer'),
|
||||
enabled: Boolean(server.enabled),
|
||||
transport: server.transport || 'stdio',
|
||||
description: String(server.description || '').trim() || null,
|
||||
command: String(server.command || '').trim() || null,
|
||||
args: String(server.argsText || '')
|
||||
.split('\n')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean),
|
||||
env: linesToDict(server.envText),
|
||||
url: String(server.url || '').trim() || null,
|
||||
headers: linesToDict(server.headersText),
|
||||
timeout: Math.max(1, Number(server.timeout || 30)),
|
||||
tool_prefix: String(server.tool_prefix || '').trim() || null,
|
||||
require_admin: Boolean(server.require_admin),
|
||||
}
|
||||
}
|
||||
|
||||
function resetLocalServers() {
|
||||
localServers.value = (props.servers || []).map(toEditableServer)
|
||||
testStates.value = {}
|
||||
}
|
||||
|
||||
function addServer() {
|
||||
localServers.value.push(
|
||||
toEditableServer({
|
||||
id: createServerId(),
|
||||
name: t('setting.system.aiAgentMcpNewServer'),
|
||||
enabled: true,
|
||||
transport: 'stdio',
|
||||
description: '',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
url: '',
|
||||
headers: {},
|
||||
timeout: 30,
|
||||
tool_prefix: '',
|
||||
require_admin: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function removeServer(server: EditableAgentMcpServer) {
|
||||
localServers.value = localServers.value.filter(item => item.id !== server.id)
|
||||
}
|
||||
|
||||
async function testServer(server: EditableAgentMcpServer) {
|
||||
const payload = toServerPayload(server)
|
||||
testStates.value[payload.id] = { loading: true }
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('message/agent/mcp/servers/test', { server: payload })
|
||||
testStates.value[payload.id] = {
|
||||
loading: false,
|
||||
success: Boolean(result.success),
|
||||
message: result.message || result.data?.message || '',
|
||||
tools: result.data?.tools || [],
|
||||
}
|
||||
} catch (error) {
|
||||
testStates.value[payload.id] = {
|
||||
loading: false,
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
tools: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveServers() {
|
||||
saving.value = true
|
||||
try {
|
||||
const servers = localServers.value.map(toServerPayload)
|
||||
const result: { [key: string]: any } = await api.post('message/agent/mcp/servers', { servers })
|
||||
if (result.success) {
|
||||
toast.success(t('setting.system.aiAgentMcpSaveSuccess'))
|
||||
emit('saved', servers)
|
||||
dialogVisible.value = false
|
||||
return
|
||||
}
|
||||
toast.error(result.message || t('setting.system.aiAgentMcpSaveFailed'))
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value) resetLocalServers()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.servers,
|
||||
() => {
|
||||
if (props.modelValue) resetLocalServers()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog v-model="dialogVisible" scrollable max-width="72rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-server-network" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('setting.system.aiAgentMcpDialogTitle') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.system.aiAgentMcpDialogDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="dialogVisible = false" />
|
||||
|
||||
<VCardText>
|
||||
<div class="d-flex justify-end mb-4">
|
||||
<VBtn color="success" variant="tonal" prepend-icon="mdi-plus" @click="addServer">
|
||||
{{ t('setting.system.aiAgentMcpAddServer') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VAlert v-if="localServers.length === 0" type="info" variant="tonal">
|
||||
{{ t('setting.system.aiAgentMcpEmpty') }}
|
||||
</VAlert>
|
||||
|
||||
<VExpansionPanels v-else variant="accordion" class="agent-mcp-panels">
|
||||
<VExpansionPanel v-for="server in localServers" :key="server.id">
|
||||
<VExpansionPanelTitle>
|
||||
<div class="agent-mcp-panel-title">
|
||||
<VIcon :icon="server.enabled ? 'mdi-lan-connect' : 'mdi-lan-disconnect'" />
|
||||
<span>{{ server.name || t('setting.system.aiAgentMcpUnnamedServer') }}</span>
|
||||
<VChip size="small" variant="tonal">{{ server.transport }}</VChip>
|
||||
</div>
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="server.name"
|
||||
:label="t('setting.system.aiAgentMcpName')"
|
||||
prepend-inner-icon="mdi-label-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="server.transport"
|
||||
:label="t('setting.system.aiAgentMcpTransport')"
|
||||
:items="transportItems"
|
||||
prepend-inner-icon="mdi-transit-connection-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="server.enabled"
|
||||
:label="t('setting.system.aiAgentMcpEnabled')"
|
||||
:hint="t('setting.system.aiAgentMcpEnabledHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="server.require_admin"
|
||||
:label="t('setting.system.aiAgentMcpRequireAdmin')"
|
||||
:hint="t('setting.system.aiAgentMcpRequireAdminHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model.number="server.timeout"
|
||||
:label="t('setting.system.aiAgentMcpTimeout')"
|
||||
type="number"
|
||||
min="1"
|
||||
prepend-inner-icon="mdi-timer-sand"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="server.tool_prefix"
|
||||
:label="t('setting.system.aiAgentMcpToolPrefix')"
|
||||
:hint="t('setting.system.aiAgentMcpToolPrefixHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-form-textbox"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="server.description"
|
||||
:label="t('setting.system.aiAgentMcpDescription')"
|
||||
rows="1"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-text-box-outline"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<template v-if="server.transport === 'stdio'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="server.command"
|
||||
:label="t('setting.system.aiAgentMcpCommand')"
|
||||
placeholder="npx"
|
||||
prepend-inner-icon="mdi-console"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextarea
|
||||
v-model="server.argsText"
|
||||
:label="t('setting.system.aiAgentMcpArgs')"
|
||||
:hint="t('setting.system.aiAgentMcpArgsHint')"
|
||||
persistent-hint
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-format-list-bulleted"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="server.envText"
|
||||
:label="t('setting.system.aiAgentMcpEnv')"
|
||||
:hint="t('setting.system.aiAgentMcpKeyValueHint')"
|
||||
persistent-hint
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-code-braces"
|
||||
/>
|
||||
</VCol>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="server.url"
|
||||
:label="t('setting.system.aiAgentMcpUrl')"
|
||||
placeholder="http://127.0.0.1:3001/api/v1/mcp"
|
||||
prepend-inner-icon="mdi-link-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="server.headersText"
|
||||
:label="t('setting.system.aiAgentMcpHeaders')"
|
||||
:hint="t('setting.system.aiAgentMcpKeyValueHint')"
|
||||
persistent-hint
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-format-align-left"
|
||||
/>
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
|
||||
<VAlert
|
||||
v-if="testStates[server.id]?.message"
|
||||
:type="testStates[server.id]?.success ? 'success' : 'error'"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="mt-3"
|
||||
>
|
||||
<div>{{ testStates[server.id]?.message }}</div>
|
||||
<div v-if="testStates[server.id]?.tools?.length" class="agent-mcp-tool-list mt-2">
|
||||
<VChip
|
||||
v-for="tool in testStates[server.id]?.tools"
|
||||
:key="tool.agent_tool_name"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tool.agent_tool_name }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VAlert>
|
||||
|
||||
<div class="agent-mcp-actions mt-4">
|
||||
<VBtn
|
||||
color="info"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-connection"
|
||||
:loading="testStates[server.id]?.loading"
|
||||
@click="testServer(server)"
|
||||
>
|
||||
{{ t('setting.system.aiAgentMcpTest') }}
|
||||
</VBtn>
|
||||
<VBtn color="error" variant="text" prepend-icon="mdi-delete-outline" @click="removeServer(server)">
|
||||
{{ t('common.delete') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VExpansionPanelText>
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions class="app-dialog-actions">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
:loading="saving"
|
||||
@click="saveServers"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agent-mcp-panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-mcp-actions,
|
||||
.agent-mcp-tool-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1741,6 +1741,38 @@ export default {
|
||||
aiAgentJobInterval24h: '24 Hours',
|
||||
aiAgentJobInterval1w: '1 Week',
|
||||
aiAgentJobInterval1M: '1 Month',
|
||||
aiAgentMcpTitle: 'External MCP Tools',
|
||||
aiAgentMcpSummary: '{total} MCP servers configured, {enabled} enabled.',
|
||||
aiAgentMcpSettings: 'Set MCP',
|
||||
aiAgentMcpDialogTitle: 'Agent MCP Settings',
|
||||
aiAgentMcpDialogDesc: 'Configure external MCP servers and test tools available to the AI assistant.',
|
||||
aiAgentMcpAddServer: 'Add Server',
|
||||
aiAgentMcpEmpty: 'No MCP servers configured yet.',
|
||||
aiAgentMcpNewServer: 'New MCP Server',
|
||||
aiAgentMcpUnnamedServer: 'Unnamed Server',
|
||||
aiAgentMcpName: 'Server Name',
|
||||
aiAgentMcpTransport: 'Transport',
|
||||
aiAgentMcpTransportStdio: 'stdio',
|
||||
aiAgentMcpTransportSse: 'SSE',
|
||||
aiAgentMcpTransportHttp: 'HTTP',
|
||||
aiAgentMcpEnabled: 'Enable Server',
|
||||
aiAgentMcpEnabledHint: 'When enabled, the AI assistant loads tools from this server at runtime',
|
||||
aiAgentMcpRequireAdmin: 'Admin Only',
|
||||
aiAgentMcpRequireAdminHint: 'Recommended for external tools that may have elevated permissions',
|
||||
aiAgentMcpTimeout: 'Timeout (seconds)',
|
||||
aiAgentMcpToolPrefix: 'Tool Prefix',
|
||||
aiAgentMcpToolPrefixHint: 'Leave empty to derive it from the server name and avoid tool name conflicts',
|
||||
aiAgentMcpDescription: 'Description',
|
||||
aiAgentMcpCommand: 'Command',
|
||||
aiAgentMcpArgs: 'Arguments',
|
||||
aiAgentMcpArgsHint: 'One argument per line, such as -y or server-filesystem',
|
||||
aiAgentMcpEnv: 'Environment Variables',
|
||||
aiAgentMcpKeyValueHint: 'One KEY=VALUE pair per line',
|
||||
aiAgentMcpUrl: 'Service URL',
|
||||
aiAgentMcpHeaders: 'Headers',
|
||||
aiAgentMcpTest: 'Test Connection',
|
||||
aiAgentMcpSaveSuccess: 'MCP configuration saved',
|
||||
aiAgentMcpSaveFailed: 'Failed to save MCP configuration',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases',
|
||||
downloaders: 'Downloaders',
|
||||
|
||||
@@ -1723,6 +1723,38 @@ export default {
|
||||
aiAgentJobInterval24h: '24小时',
|
||||
aiAgentJobInterval1w: '1周',
|
||||
aiAgentJobInterval1M: '1个月',
|
||||
aiAgentMcpTitle: '外部 MCP 工具',
|
||||
aiAgentMcpSummary: '已配置 {total} 个 MCP 服务器,启用 {enabled} 个。',
|
||||
aiAgentMcpSettings: '设置 MCP',
|
||||
aiAgentMcpDialogTitle: 'Agent MCP 设置',
|
||||
aiAgentMcpDialogDesc: '配置外部 MCP 服务器,并测试可提供给智能助手的工具。',
|
||||
aiAgentMcpAddServer: '添加服务器',
|
||||
aiAgentMcpEmpty: '尚未配置 MCP 服务器。',
|
||||
aiAgentMcpNewServer: '新的 MCP 服务器',
|
||||
aiAgentMcpUnnamedServer: '未命名服务器',
|
||||
aiAgentMcpName: '服务器名称',
|
||||
aiAgentMcpTransport: '传输协议',
|
||||
aiAgentMcpTransportStdio: 'stdio',
|
||||
aiAgentMcpTransportSse: 'SSE',
|
||||
aiAgentMcpTransportHttp: 'HTTP',
|
||||
aiAgentMcpEnabled: '启用服务器',
|
||||
aiAgentMcpEnabledHint: '启用后,智能助手会在运行时加载该服务器提供的工具',
|
||||
aiAgentMcpRequireAdmin: '仅管理员可调用',
|
||||
aiAgentMcpRequireAdminHint: '建议保持开启,避免普通用户调用高权限外部工具',
|
||||
aiAgentMcpTimeout: '超时时间(秒)',
|
||||
aiAgentMcpToolPrefix: '工具名前缀',
|
||||
aiAgentMcpToolPrefixHint: '留空时会根据服务器名称自动生成,避免不同服务器工具名冲突',
|
||||
aiAgentMcpDescription: '说明',
|
||||
aiAgentMcpCommand: '启动命令',
|
||||
aiAgentMcpArgs: '启动参数',
|
||||
aiAgentMcpArgsHint: '每行一个参数,例如 -y 或 server-filesystem',
|
||||
aiAgentMcpEnv: '环境变量',
|
||||
aiAgentMcpKeyValueHint: '每行一个 KEY=VALUE',
|
||||
aiAgentMcpUrl: '服务地址',
|
||||
aiAgentMcpHeaders: '请求头',
|
||||
aiAgentMcpTest: '测试连接',
|
||||
aiAgentMcpSaveSuccess: 'MCP 配置已保存',
|
||||
aiAgentMcpSaveFailed: 'MCP 配置保存失败',
|
||||
advancedSettings: '高级设置',
|
||||
advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整',
|
||||
downloaders: '下载器',
|
||||
|
||||
@@ -1722,6 +1722,38 @@ export default {
|
||||
aiAgentJobInterval24h: '24小時',
|
||||
aiAgentJobInterval1w: '1週',
|
||||
aiAgentJobInterval1M: '1個月',
|
||||
aiAgentMcpTitle: '外部 MCP 工具',
|
||||
aiAgentMcpSummary: '已配置 {total} 個 MCP 伺服器,啟用 {enabled} 個。',
|
||||
aiAgentMcpSettings: '設置 MCP',
|
||||
aiAgentMcpDialogTitle: 'Agent MCP 設置',
|
||||
aiAgentMcpDialogDesc: '配置外部 MCP 伺服器,並測試可提供給智能助手的工具。',
|
||||
aiAgentMcpAddServer: '添加伺服器',
|
||||
aiAgentMcpEmpty: '尚未配置 MCP 伺服器。',
|
||||
aiAgentMcpNewServer: '新的 MCP 伺服器',
|
||||
aiAgentMcpUnnamedServer: '未命名伺服器',
|
||||
aiAgentMcpName: '伺服器名稱',
|
||||
aiAgentMcpTransport: '傳輸協議',
|
||||
aiAgentMcpTransportStdio: 'stdio',
|
||||
aiAgentMcpTransportSse: 'SSE',
|
||||
aiAgentMcpTransportHttp: 'HTTP',
|
||||
aiAgentMcpEnabled: '啟用伺服器',
|
||||
aiAgentMcpEnabledHint: '啟用後,智能助手會在運行時載入該伺服器提供的工具',
|
||||
aiAgentMcpRequireAdmin: '僅管理員可調用',
|
||||
aiAgentMcpRequireAdminHint: '建議保持開啟,避免普通用戶調用高權限外部工具',
|
||||
aiAgentMcpTimeout: '超時時間(秒)',
|
||||
aiAgentMcpToolPrefix: '工具名稱前綴',
|
||||
aiAgentMcpToolPrefixHint: '留空時會根據伺服器名稱自動生成,避免不同伺服器工具名衝突',
|
||||
aiAgentMcpDescription: '說明',
|
||||
aiAgentMcpCommand: '啟動命令',
|
||||
aiAgentMcpArgs: '啟動參數',
|
||||
aiAgentMcpArgsHint: '每行一個參數,例如 -y 或 server-filesystem',
|
||||
aiAgentMcpEnv: '環境變數',
|
||||
aiAgentMcpKeyValueHint: '每行一個 KEY=VALUE',
|
||||
aiAgentMcpUrl: '服務地址',
|
||||
aiAgentMcpHeaders: '請求頭',
|
||||
aiAgentMcpTest: '測試連接',
|
||||
aiAgentMcpSaveSuccess: 'MCP 配置已保存',
|
||||
aiAgentMcpSaveFailed: 'MCP 配置保存失敗',
|
||||
advancedSettings: '高級設置',
|
||||
advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整',
|
||||
downloaders: '下載器',
|
||||
|
||||
@@ -33,6 +33,7 @@ const props = defineProps({
|
||||
// 下载器/媒体服务器排序按需加载,降低系统设置页入口解析量。
|
||||
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
|
||||
const LlmProviderAuthDialog = defineAsyncComponent(() => import('@/components/dialog/LlmProviderAuthDialog.vue'))
|
||||
const AgentMcpSettingsDialog = defineAsyncComponent(() => import('@/components/dialog/AgentMcpSettingsDialog.vue'))
|
||||
|
||||
// 系统设置项
|
||||
const SystemSettings = ref<any>({
|
||||
@@ -208,6 +209,9 @@ const advancedDialog = ref(false)
|
||||
const savingBasic = ref(false)
|
||||
const testingLlm = ref(false)
|
||||
const rustAccelAvailable = ref(false)
|
||||
const agentMcpDialog = ref(false)
|
||||
const agentMcpServers = ref<AgentMcpServer[]>([])
|
||||
const loadingAgentMcpServers = ref(false)
|
||||
|
||||
// 智能助手配置项较多,默认收起以降低基础设置页的视觉占用。
|
||||
const aiAgentSettingsCollapsed = ref(true)
|
||||
@@ -225,6 +229,24 @@ type LlmSettingsSnapshot = {
|
||||
LLM_TEMPERATURE: number
|
||||
}
|
||||
|
||||
type AgentMcpTransport = 'stdio' | 'sse' | 'http' | 'streamable_http'
|
||||
|
||||
interface AgentMcpServer {
|
||||
id: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
transport: AgentMcpTransport
|
||||
description?: string | null
|
||||
command?: string | null
|
||||
args: string[]
|
||||
env: Record<string, string>
|
||||
url?: string | null
|
||||
headers: Record<string, string>
|
||||
timeout: number
|
||||
tool_prefix?: string | null
|
||||
require_admin: boolean
|
||||
}
|
||||
|
||||
let llmTestRequestId = 0
|
||||
let llmTestAbortController: AbortController | null = null
|
||||
|
||||
@@ -461,6 +483,8 @@ const selectedLlmModelInfo = computed(() => {
|
||||
source: selectedLlmModel.value.source || 'models.dev',
|
||||
})
|
||||
})
|
||||
const agentMcpEnabledCount = computed(() => agentMcpServers.value.filter(server => server.enabled).length)
|
||||
const agentMcpServerPreview = computed(() => agentMcpServers.value.slice(0, 3))
|
||||
|
||||
const canTestLlm = computed(() => {
|
||||
const snapshot = currentLlmSnapshot.value
|
||||
@@ -682,6 +706,23 @@ async function loadSystemSettings() {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
await loadAgentMcpServers()
|
||||
}
|
||||
|
||||
async function loadAgentMcpServers() {
|
||||
loadingAgentMcpServers.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('message/agent/mcp/servers')
|
||||
if (result.success) agentMcpServers.value = result.data?.servers || []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
loadingAgentMcpServers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAgentMcpSaved(servers: AgentMcpServer[]) {
|
||||
agentMcpServers.value = servers
|
||||
}
|
||||
|
||||
// 调用API保存设置
|
||||
@@ -1409,6 +1450,46 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
||||
prepend-inner-icon="mdi-timer-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
|
||||
<VAlert type="info" variant="tonal" class="agent-mcp-summary">
|
||||
<div class="agent-mcp-summary__content">
|
||||
<div>
|
||||
<div class="text-subtitle-2">{{ t('setting.system.aiAgentMcpTitle') }}</div>
|
||||
<div class="text-body-2">
|
||||
{{
|
||||
t('setting.system.aiAgentMcpSummary', {
|
||||
enabled: agentMcpEnabledCount,
|
||||
total: agentMcpServers.length,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div v-if="agentMcpServers.length" class="agent-mcp-summary__chips mt-2">
|
||||
<VChip
|
||||
v-for="server in agentMcpServerPreview"
|
||||
:key="server.id"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
:color="server.enabled ? 'success' : 'default'"
|
||||
>
|
||||
{{ server.name }}
|
||||
</VChip>
|
||||
<VChip v-if="agentMcpServers.length > 3" size="small" variant="tonal">
|
||||
+{{ agentMcpServers.length - 3 }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-server-network"
|
||||
:loading="loadingAgentMcpServers"
|
||||
@click="agentMcpDialog = true"
|
||||
>
|
||||
{{ t('setting.system.aiAgentMcpSettings') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
|
||||
@@ -1768,6 +1849,13 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<AgentMcpSettingsDialog
|
||||
v-if="agentMcpDialog"
|
||||
v-model="agentMcpDialog"
|
||||
:servers="agentMcpServers"
|
||||
@saved="handleAgentMcpSaved"
|
||||
/>
|
||||
|
||||
<!-- 高级系统设置 -->
|
||||
<VDialog
|
||||
v-if="advancedDialog"
|
||||
@@ -2477,4 +2565,24 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
||||
.llm-test-trigger {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.agent-mcp-summary__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.agent-mcp-summary__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.agent-mcp-summary__content {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user