refactor: move mcp and command page to extension page
This commit is contained in:
@@ -3,6 +3,7 @@ import traceback
|
||||
from quart import request
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.agent.mcp_client import MCPTool
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.star import star_map
|
||||
|
||||
@@ -296,15 +297,30 @@ class ToolsRoute(Route):
|
||||
"""获取所有注册的工具列表"""
|
||||
try:
|
||||
tools = self.tool_mgr.func_list
|
||||
tools_dict = [
|
||||
{
|
||||
tools_dict = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, MCPTool):
|
||||
origin = "mcp"
|
||||
origin_name = tool.mcp_server_name
|
||||
elif tool.handler_module_path and star_map.get(
|
||||
tool.handler_module_path
|
||||
):
|
||||
star = star_map[tool.handler_module_path]
|
||||
origin = "plugin"
|
||||
origin_name = star.name
|
||||
else:
|
||||
origin = "unknown"
|
||||
origin_name = "unknown"
|
||||
|
||||
tool_info = {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters,
|
||||
"active": tool.active,
|
||||
"origin": origin,
|
||||
"origin_name": origin_name,
|
||||
}
|
||||
for tool in tools
|
||||
]
|
||||
tools_dict.append(tool_info)
|
||||
return Response().ok(data=tools_dict).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
+38
-328
@@ -4,42 +4,18 @@
|
||||
<!-- 页面标题 -->
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<h1 class="text-h1 font-weight-bold mb-2">
|
||||
<v-icon color="black" class="me-2">mdi-function-variant</v-icon>{{ tm('title') }}
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mb-4 d-flex align-center">
|
||||
{{ tm('subtitle') }}
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="small" color="primary" class="ms-1 cursor-pointer"
|
||||
@click="openurl('https://astrbot.app/use/function-calling.html')">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('tooltip.info') }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<v-btn color="primary" prepend-icon="mdi-tools" class="me-2" variant="tonal" @click="showToolsDialog = true"
|
||||
rounded="xl" size="x-large">
|
||||
{{ tm('functionTools.buttons.view') }}({{ tools.length }})
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-plus" class="me-2" variant="tonal"
|
||||
@click="showMcpServerDialog = true" rounded="xl" size="x-large">
|
||||
@click="showMcpServerDialog = true" >
|
||||
{{ tm('mcpServers.buttons.add') }}
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-refresh" variant="tonal" @click="showSyncMcpServerDialog = true"
|
||||
rounded="xl" size="x-large">
|
||||
>
|
||||
{{ tm('mcpServers.buttons.sync') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<!-- 本地服务器列表 -->
|
||||
|
||||
<!-- MCP 服务器部分 -->
|
||||
|
||||
<div v-if="mcpServers.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-server-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('mcpServers.empty') }}</p>
|
||||
@@ -57,7 +33,6 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<div>
|
||||
<div v-if="item.tools && item.tools.length > 0">
|
||||
@@ -67,8 +42,7 @@
|
||||
<template v-slot:activator="{ props: listToolsProps }">
|
||||
<span class="text-caption text-medium-emphasis cursor-pointer" v-bind="listToolsProps"
|
||||
style="text-decoration: underline;">
|
||||
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{
|
||||
item.tools.length }})
|
||||
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{ item.tools.length }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
@@ -78,10 +52,7 @@
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{
|
||||
tool
|
||||
}}
|
||||
</li>
|
||||
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{ tool }}</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
@@ -91,8 +62,6 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,8 +74,6 @@
|
||||
<v-progress-circular indeterminate color="primary" size="16"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
@@ -183,8 +150,7 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
|
||||
<!-- 添加/编辑 MCP 服务器对话框 -->
|
||||
<!-- 同步 MCP 服务器对话框 -->
|
||||
<v-dialog v-model="showSyncMcpServerDialog" max-width="500px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3">
|
||||
@@ -240,115 +206,8 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 函数工具对话框 -->
|
||||
<v-dialog v-model="showToolsDialog" max-width="800px">
|
||||
<v-card elevation="0" class="mt-4">
|
||||
<v-card-title class="d-flex align-center py-3 px-4">
|
||||
{{ tm('functionTools.title') }}
|
||||
<v-chip color="info" size="small" class="ml-2">{{ tools.length }}</v-chip>
|
||||
</v-card-title>
|
||||
<v-expand-transition>
|
||||
<v-card-text class="pa-0" v-if="showTools">
|
||||
<div class="pa-4">
|
||||
<div v-if="tools.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('functionTools.empty') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<v-text-field v-model="toolSearch" prepend-inner-icon="mdi-magnify" :label="tm('functionTools.search')"
|
||||
variant="outlined" density="compact" class="mb-4" hide-details clearable></v-text-field>
|
||||
|
||||
<small>复选框代表该工具是否被启用。</small>
|
||||
|
||||
<v-expansion-panels v-model="openedPanel" multiple style="max-height: 500px; overflow-y: auto;">
|
||||
<v-expansion-panel v-for="(tool, index) in filteredTools" :key="index" :value="index"
|
||||
class="mb-2 tool-panel" rounded="lg">
|
||||
<v-expansion-panel-title>
|
||||
<v-row no-gutters align="center">
|
||||
<v-col cols="1">
|
||||
<v-checkbox v-model="tool.active" color="primary" hide-details density="compact" @click.stop
|
||||
@change="toggleToolStatus(tool)"></v-checkbox>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon color="primary" class="me-2" size="small">
|
||||
{{ tool.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }}
|
||||
</v-icon>
|
||||
<span class="text-body-1 text-high-emphasis font-weight-medium text-truncate"
|
||||
:title="tool.name">
|
||||
{{ formatToolName(tool.name) }}
|
||||
</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="8" class="text-grey">
|
||||
{{ tool.description }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-information</v-icon>
|
||||
{{ tm('functionTools.description') }}
|
||||
</p>
|
||||
<p class="text-body-2 ml-6 mb-4">{{ tool.description }}</p>
|
||||
|
||||
<template v-if="tool.parameters && tool.parameters.properties">
|
||||
<p class="text-body-1 font-weight-medium mb-3">
|
||||
<v-icon color="primary" size="small" class="me-1">mdi-code-json</v-icon>
|
||||
{{ tm('functionTools.parameters') }}
|
||||
</p>
|
||||
|
||||
<v-table density="compact" class="params-table mt-1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tm('functionTools.table.paramName') }}</th>
|
||||
<th>{{ tm('functionTools.table.type') }}</th>
|
||||
<th>{{ tm('functionTools.table.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(param, paramName) in tool.parameters.properties" :key="paramName">
|
||||
<td class="font-weight-medium">{{ paramName }}</td>
|
||||
<td>
|
||||
<v-chip size="x-small" color="primary" text class="text-caption">
|
||||
{{ param.type }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ param.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</template>
|
||||
<div v-else class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="large" color="grey-lighten-1">mdi-code-brackets</v-icon>
|
||||
<p>{{ tm('functionTools.noParameters') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showToolsDialog = false">
|
||||
{{ tm('dialogs.serverDetail.buttons.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack" location="top">
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
@@ -356,15 +215,13 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
|
||||
import ItemCard from '@/components/shared/ItemCard.vue';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'ToolUsePage',
|
||||
name: 'McpServersSection',
|
||||
components: {
|
||||
AstrBotConfig,
|
||||
VueMonacoEditor,
|
||||
ItemCard
|
||||
},
|
||||
@@ -377,20 +234,15 @@ export default {
|
||||
return {
|
||||
refreshInterval: null,
|
||||
mcpServers: [],
|
||||
tools: [],
|
||||
showMcpServerDialog: false,
|
||||
|
||||
selectedMcpServerProvider: "modelscope",
|
||||
mcpServerProviderList: ["modelscope"],
|
||||
selectedMcpServerProvider: 'modelscope',
|
||||
mcpServerProviderList: ['modelscope'],
|
||||
mcpProviderToken: '',
|
||||
|
||||
showSyncMcpServerDialog: false,
|
||||
addServerDialogMessage: "",
|
||||
showToolsDialog: false,
|
||||
showTools: true,
|
||||
addServerDialogMessage: '',
|
||||
loading: false,
|
||||
loadingGettingServers: false,
|
||||
mcpServerUpdateLoaders: {}, // record loading state for each server update
|
||||
mcpServerUpdateLoaders: {},
|
||||
isEditMode: false,
|
||||
serverConfigJson: '',
|
||||
jsonError: null,
|
||||
@@ -400,87 +252,50 @@ export default {
|
||||
tools: []
|
||||
},
|
||||
save_message_snack: false,
|
||||
save_message: "",
|
||||
save_message_success: "success",
|
||||
toolSearch: '',
|
||||
openedPanel: [], // 存储打开的面板索引
|
||||
}
|
||||
save_message: '',
|
||||
save_message_success: 'success'
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredTools() {
|
||||
if (!this.toolSearch) return this.tools;
|
||||
|
||||
const searchTerm = this.toolSearch.toLowerCase();
|
||||
return this.tools.filter(tool =>
|
||||
tool.name.toLowerCase().includes(searchTerm) ||
|
||||
tool.description.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
},
|
||||
|
||||
isServerFormValid() {
|
||||
return !!this.currentServer.name && !this.jsonError;
|
||||
},
|
||||
|
||||
// 显示服务器配置的文本摘要
|
||||
getServerConfigSummary() {
|
||||
return (server) => {
|
||||
if (server.command) {
|
||||
return `${server.command} ${(server.args || []).join(' ')}`;
|
||||
}
|
||||
|
||||
// 如果没有command字段,尝试显示其他有意义的配置信息
|
||||
const configKeys = Object.keys(server).filter(key =>
|
||||
!['name', 'active', 'tools'].includes(key)
|
||||
);
|
||||
|
||||
if (configKeys.length > 0) {
|
||||
return this.tm('mcpServers.status.configSummary', { keys: configKeys.join(', ') });
|
||||
}
|
||||
|
||||
return this.tm('mcpServers.status.noConfig');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
// 清除定时器 if it exists
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openurl(url) {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
formatToolName(name) {
|
||||
if (name.includes(':')) {
|
||||
// MCP 工具通常命名为 mcp:server:tool
|
||||
const parts = name.split(':');
|
||||
return parts[parts.length - 1]; // 返回最后一部分
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
getServers() {
|
||||
this.loadingGettingServers = true;
|
||||
axios.get('/api/tools/mcp/servers')
|
||||
.then(response => {
|
||||
this.mcpServers = response.data.data || [];
|
||||
this.mcpServers.forEach(server => {
|
||||
// Ensure each server has a loader state
|
||||
if (!this.mcpServerUpdateLoaders[server.name]) {
|
||||
this.mcpServerUpdateLoaders[server.name] = false;
|
||||
}
|
||||
@@ -492,24 +307,12 @@ export default {
|
||||
this.loadingGettingServers = false;
|
||||
});
|
||||
},
|
||||
|
||||
getTools() {
|
||||
axios.get('/api/tools/list')
|
||||
.then(response => {
|
||||
this.tools = response.data.data || [];
|
||||
})
|
||||
.catch(error => {
|
||||
this.showError(this.tm('messages.getToolsError', { error: error.message }));
|
||||
});
|
||||
},
|
||||
|
||||
validateJson() {
|
||||
try {
|
||||
if (!this.serverConfigJson.trim()) {
|
||||
this.jsonError = this.tm('dialogs.addServer.errors.configEmpty');
|
||||
return false;
|
||||
}
|
||||
|
||||
JSON.parse(this.serverConfigJson);
|
||||
this.jsonError = null;
|
||||
return true;
|
||||
@@ -518,61 +321,51 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
setConfigTemplate(type = 'stdio') {
|
||||
let template = {};
|
||||
if (type === 'streamable_http') {
|
||||
template = {
|
||||
transport: "streamable_http",
|
||||
url: "your mcp server url",
|
||||
transport: 'streamable_http',
|
||||
url: 'your mcp server url',
|
||||
headers: {},
|
||||
timeout: 5,
|
||||
sse_read_timeout: 300,
|
||||
sse_read_timeout: 300
|
||||
};
|
||||
} else if (type === 'sse') {
|
||||
template = {
|
||||
transport: "sse",
|
||||
url: "your mcp server url",
|
||||
transport: 'sse',
|
||||
url: 'your mcp server url',
|
||||
headers: {},
|
||||
timeout: 5,
|
||||
sse_read_timeout: 300,
|
||||
sse_read_timeout: 300
|
||||
};
|
||||
} else {
|
||||
template = {
|
||||
command: "python",
|
||||
args: ["-m", "your_module"],
|
||||
command: 'python',
|
||||
args: ['-m', 'your_module']
|
||||
};
|
||||
}
|
||||
this.serverConfigJson = JSON.stringify(template, null, 2);
|
||||
},
|
||||
|
||||
saveServer() {
|
||||
if (!this.validateJson()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
// 解析JSON配置并与基本信息合并
|
||||
try {
|
||||
const configObj = JSON.parse(this.serverConfigJson);
|
||||
|
||||
// 创建要发送的完整配置对象
|
||||
const serverData = {
|
||||
name: this.currentServer.name,
|
||||
active: this.currentServer.active,
|
||||
...configObj
|
||||
};
|
||||
|
||||
const endpoint = this.isEditMode ? '/api/tools/mcp/update' : '/api/tools/mcp/add';
|
||||
|
||||
axios.post(endpoint, serverData)
|
||||
.then(response => {
|
||||
this.loading = false;
|
||||
this.showMcpServerDialog = false;
|
||||
this.addServerDialogMessage = "";
|
||||
this.addServerDialogMessage = '';
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
this.showSuccess(response.data.message || this.tm('messages.saveSuccess'));
|
||||
this.resetForm();
|
||||
})
|
||||
@@ -585,14 +378,12 @@ export default {
|
||||
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
|
||||
}
|
||||
},
|
||||
|
||||
deleteServer(server) {
|
||||
let serverName = server.name || server;
|
||||
const serverName = server.name || server;
|
||||
if (confirm(this.tm('dialogs.confirmDelete', { name: serverName }))) {
|
||||
axios.post('/api/tools/mcp/delete', { name: serverName })
|
||||
.then(response => {
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -600,37 +391,22 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
editServer(server) {
|
||||
// 创建一个不包含基本字段的配置对象副本
|
||||
const configCopy = { ...server };
|
||||
|
||||
// 移除基本字段,只保留配置相关字段
|
||||
try {
|
||||
delete configCopy.name;
|
||||
delete configCopy.active;
|
||||
delete configCopy.tools;
|
||||
delete configCopy.errlogs;
|
||||
} catch (e) {
|
||||
console.error("Error removing basic fields: ", e);
|
||||
}
|
||||
|
||||
// 设置当前服务器的基本信息
|
||||
delete configCopy.name;
|
||||
delete configCopy.active;
|
||||
delete configCopy.tools;
|
||||
delete configCopy.errlogs;
|
||||
this.currentServer = {
|
||||
name: server.name,
|
||||
active: server.active,
|
||||
tools: server.tools || []
|
||||
};
|
||||
|
||||
// 将剩余配置转换为JSON字符串
|
||||
this.serverConfigJson = JSON.stringify(configCopy, null, 2);
|
||||
|
||||
this.isEditMode = true;
|
||||
this.showMcpServerDialog = true;
|
||||
},
|
||||
|
||||
updateServerStatus(server) {
|
||||
// 切换服务器状态
|
||||
this.mcpServerUpdateLoaders[server.name] = true;
|
||||
server.active = !server.active;
|
||||
axios.post('/api/tools/mcp/update', server)
|
||||
@@ -646,20 +422,16 @@ export default {
|
||||
this.mcpServerUpdateLoaders[server.name] = false;
|
||||
});
|
||||
},
|
||||
|
||||
closeServerDialog() {
|
||||
this.showMcpServerDialog = false;
|
||||
this.addServerDialogMessage = '';
|
||||
this.resetForm();
|
||||
},
|
||||
|
||||
testServerConnection() {
|
||||
if (!this.validateJson()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
let configObj;
|
||||
try {
|
||||
configObj = JSON.parse(this.serverConfigJson);
|
||||
@@ -668,9 +440,8 @@ export default {
|
||||
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/tools/mcp/test', {
|
||||
"mcp_server_config": configObj,
|
||||
mcp_server_config: configObj
|
||||
})
|
||||
.then(response => {
|
||||
this.loading = false;
|
||||
@@ -681,7 +452,6 @@ export default {
|
||||
this.showError(this.tm('messages.testError', { error: error.response?.data?.message || error.message }));
|
||||
});
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.currentServer = {
|
||||
name: '',
|
||||
@@ -692,58 +462,26 @@ export default {
|
||||
this.jsonError = null;
|
||||
this.isEditMode = false;
|
||||
},
|
||||
|
||||
showSuccess(message) {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "success";
|
||||
this.save_message_success = 'success';
|
||||
this.save_message_snack = true;
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "error";
|
||||
this.save_message_success = 'error';
|
||||
this.save_message_snack = true;
|
||||
},
|
||||
|
||||
// MCP 市场相关方法已移除
|
||||
|
||||
// 切换工具状态
|
||||
async toggleToolStatus(tool) {
|
||||
try {
|
||||
const response = await axios.post('/api/tools/toggle-tool', {
|
||||
name: tool.name,
|
||||
activate: tool.active
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.message || this.tm('messages.toggleToolSuccess'));
|
||||
} else {
|
||||
// 如果失败,恢复原状态
|
||||
tool.active = !tool.active;
|
||||
this.showError(response.data.message || this.tm('messages.toggleToolError'));
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果失败,恢复原状态
|
||||
tool.active = !tool.active;
|
||||
this.showError(this.tm('messages.toggleToolError', { error: error.response?.data?.message || error.message }));
|
||||
}
|
||||
},
|
||||
|
||||
// 同步 MCP 服务器
|
||||
async syncMcpServers() {
|
||||
if (!this.selectedMcpServerProvider) {
|
||||
this.showError(this.tm('syncProvider.status.selectProvider'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
name: this.selectedMcpServerProvider
|
||||
};
|
||||
|
||||
// 根据不同平台添加相应的参数
|
||||
if (this.selectedMcpServerProvider === 'modelscope') {
|
||||
if (!this.mcpProviderToken.trim()) {
|
||||
this.showError(this.tm('syncProvider.status.enterToken'));
|
||||
@@ -752,61 +490,33 @@ export default {
|
||||
}
|
||||
requestData.access_token = this.mcpProviderToken.trim();
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/tools/mcp/sync-provider', requestData);
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSuccess(response.data.message || this.tm('syncProvider.messages.syncSuccess'));
|
||||
this.showSyncMcpServerDialog = false;
|
||||
this.mcpProviderToken = '';
|
||||
// 刷新服务器列表
|
||||
this.getServers();
|
||||
this.getTools();
|
||||
} else {
|
||||
this.showError(response.data.message || this.tm('syncProvider.messages.syncError', { error: 'Unknown error' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('同步 MCP 服务器失败:', error);
|
||||
this.showError(this.tm('syncProvider.messages.syncError', {
|
||||
error: error.response?.data?.message || error.message || '网络连接或访问令牌问题'
|
||||
this.showError(this.tm('syncProvider.messages.syncError', {
|
||||
error: error.response?.data?.message || error.message || '网络连接或访问令牌问题'
|
||||
}));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tools-page {
|
||||
padding: 20px;
|
||||
padding: 0px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.tool-chips {
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-panel:hover {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.params-table {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.params-table th {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.monaco-container {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
@@ -814,4 +524,4 @@ export default {
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
+2
@@ -9,6 +9,7 @@ const { tm } = useModuleI18n('features/command');
|
||||
const props = defineProps<{
|
||||
items: CommandItem[];
|
||||
expandedGroups: Set<string>;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
// Emits
|
||||
@@ -98,6 +99,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
item-key="handler_full_name"
|
||||
hover
|
||||
:row-props="getRowProps"
|
||||
:loading="props.loading"
|
||||
>
|
||||
<template v-slot:item.effective_command="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { ToolItem } from '../types';
|
||||
|
||||
const { tm: tmTool } = useModuleI18n('features/tooluse');
|
||||
const { tm: tmCommand } = useModuleI18n('features/command');
|
||||
|
||||
const props = defineProps<{
|
||||
items: ToolItem[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-tool', tool: ToolItem): void;
|
||||
}>();
|
||||
|
||||
const toolHeaders = computed(() => [
|
||||
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '160px' },
|
||||
{ title: tmTool('functionTools.description'), key: 'description' },
|
||||
{ title: tmTool('functionTools.table.origin'), key: 'origin', sortable: false, width: '120px' },
|
||||
{ title: tmTool('functionTools.table.originName'), key: 'origin_name', sortable: false, width: '160px' },
|
||||
{ title: tmCommand('status.enabled'), key: 'active', sortable: false, width: '120px' },
|
||||
{ title: tmTool('functionTools.table.actions'), key: 'actions', sortable: false, width: '120px' }
|
||||
]);
|
||||
|
||||
const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.properties || {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="rounded-lg overflow-hidden elevation-1">
|
||||
<v-data-table
|
||||
:headers="toolHeaders"
|
||||
:items="items"
|
||||
item-key="name"
|
||||
hover
|
||||
show-expand
|
||||
class="tool-table"
|
||||
:loading="props.loading"
|
||||
>
|
||||
<template #item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<v-icon color="primary" class="mr-2" size="18">
|
||||
{{ item.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }}
|
||||
</v-icon>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.description="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.description || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.origin="{ item }">
|
||||
<v-chip size="small" variant="tonal" color="info" class="text-caption font-weight-medium">
|
||||
{{ item.origin || '-' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.origin_name="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ item.origin_name || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.active="{ item }">
|
||||
<v-chip :color="item.active ? 'success' : 'error'" size="small" class="font-weight-medium" :variant="item.active ? 'flat' : 'outlined'">
|
||||
{{ item.active ? tmCommand('status.enabled') : tmCommand('status.disabled') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-switch
|
||||
:model-value="item.active"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
inset
|
||||
@update:model-value="emit('toggle-tool', item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-function-variant</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tmTool('functionTools.empty') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ item }">
|
||||
<td :colspan="toolHeaders.length + 1" class="pa-4">
|
||||
<div class="d-flex align-start ga-4">
|
||||
<v-icon size="20" color="primary">mdi-code-json</v-icon>
|
||||
<div class="flex-1">
|
||||
<div class="text-subtitle-2 font-weight-medium mb-2">{{ tmTool('functionTools.parameters') }}</div>
|
||||
<div v-if="parameterEntries(item).length === 0" class="text-caption text-medium-emphasis">
|
||||
{{ tmTool('functionTools.noParameters') }}
|
||||
</div>
|
||||
<v-table
|
||||
v-else
|
||||
density="compact"
|
||||
class="param-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.paramName') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis" style="width: 140px;">{{ tmTool('functionTools.table.type') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="([paramName, param]) in parameterEntries(item)" :key="paramName">
|
||||
<td class="font-weight-medium text-body-2">{{ paramName }}</td>
|
||||
<td class="text-body-2">
|
||||
<v-chip size="x-small" color="primary" class="text-caption">
|
||||
{{ param?.type || '-' }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td class="text-body-2 text-medium-emphasis">{{ param?.description || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.param-table {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tool-table :deep(.v-data-table__td) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
+24
-3
@@ -3,11 +3,13 @@
|
||||
*/
|
||||
import { ref, reactive } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type { CommandItem, CommandSummary, SnackbarState } from '../types';
|
||||
import type { CommandItem, CommandSummary, SnackbarState, ToolItem } from '../types';
|
||||
|
||||
export function useCommandData() {
|
||||
export function useComponentData() {
|
||||
const loading = ref(false);
|
||||
const commands = ref<CommandItem[]>([]);
|
||||
const tools = ref<ToolItem[]>([]);
|
||||
const toolsLoading = ref(false);
|
||||
const summary = reactive<CommandSummary>({
|
||||
disabled: 0,
|
||||
conflicts: 0
|
||||
@@ -50,13 +52,32 @@ export function useCommandData() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTools = async (errorMessage: string) => {
|
||||
toolsLoading.value = true;
|
||||
try {
|
||||
const res = await axios.get('/api/tools/list');
|
||||
if (res.data.status === 'ok') {
|
||||
tools.value = res.data.data || [];
|
||||
} else {
|
||||
toast(res.data.message || errorMessage, 'error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast(err?.message || errorMessage, 'error');
|
||||
} finally {
|
||||
toolsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
commands,
|
||||
tools,
|
||||
toolsLoading,
|
||||
summary,
|
||||
snackbar,
|
||||
toast,
|
||||
fetchCommands
|
||||
fetchCommands,
|
||||
fetchTools
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 组件管理页面 - 主入口
|
||||
*
|
||||
* 模块化结构:
|
||||
* - types.ts: 类型定义
|
||||
* - composables/useComponentData.ts: 数据获取和状态管理
|
||||
* - composables/useCommandFilters.ts: 过滤逻辑
|
||||
* - composables/useCommandActions.ts: 操作方法
|
||||
* - components/CommandFilters.vue: 过滤器组件
|
||||
* - components/CommandTable.vue: 表格组件
|
||||
* - components/RenameDialog.vue: 重命名对话框
|
||||
* - components/DetailsDialog.vue: 详情对话框
|
||||
*/
|
||||
import { computed, onActivated, onMounted, ref, watch, withDefaults, defineProps } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
// Composables
|
||||
import { useComponentData } from './composables/useComponentData';
|
||||
import { useCommandFilters } from './composables/useCommandFilters';
|
||||
import { useCommandActions } from './composables/useCommandActions';
|
||||
|
||||
// Components
|
||||
import CommandFilters from './components/CommandFilters.vue';
|
||||
import CommandTable from './components/CommandTable.vue';
|
||||
import ToolTable from './components/ToolTable.vue';
|
||||
import RenameDialog from './components/RenameDialog.vue';
|
||||
import DetailsDialog from './components/DetailsDialog.vue';
|
||||
|
||||
// Types
|
||||
import type { CommandItem, ToolItem } from './types';
|
||||
|
||||
defineOptions({ name: 'ComponentPanel' });
|
||||
const props = withDefaults(defineProps<{ active?: boolean }>(), {
|
||||
active: true
|
||||
});
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
const { tm: tmTool } = useModuleI18n('features/tooluse');
|
||||
|
||||
const viewMode = ref<'commands' | 'tools'>('commands');
|
||||
const toolSearch = ref('');
|
||||
|
||||
// 数据管理
|
||||
const {
|
||||
loading,
|
||||
commands,
|
||||
tools,
|
||||
toolsLoading,
|
||||
summary,
|
||||
snackbar,
|
||||
toast,
|
||||
fetchCommands,
|
||||
fetchTools
|
||||
} = useComponentData();
|
||||
|
||||
// 过滤逻辑
|
||||
const {
|
||||
searchQuery,
|
||||
pluginFilter,
|
||||
permissionFilter,
|
||||
statusFilter,
|
||||
typeFilter,
|
||||
showSystemPlugins,
|
||||
expandedGroups,
|
||||
hasSystemPluginConflict,
|
||||
effectiveShowSystemPlugins,
|
||||
availablePlugins,
|
||||
filteredCommands,
|
||||
toggleGroupExpand
|
||||
} = useCommandFilters(commands);
|
||||
|
||||
// 操作方法
|
||||
const {
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
toggleCommand,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog
|
||||
} = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed')));
|
||||
|
||||
const filteredTools = computed(() => {
|
||||
const query = toolSearch.value.trim().toLowerCase();
|
||||
if (!query) return tools.value;
|
||||
return tools.value.filter(tool =>
|
||||
tool.name?.toLowerCase().includes(query) ||
|
||||
tool.description?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// 处理切换指令状态
|
||||
const handleToggleCommand = async (cmd: CommandItem) => {
|
||||
await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed'));
|
||||
};
|
||||
|
||||
const handleToggleTool = async (tool: ToolItem) => {
|
||||
const previous = tool.active;
|
||||
tool.active = !tool.active;
|
||||
try {
|
||||
const res = await axios.post('/api/tools/toggle-tool', {
|
||||
name: tool.name,
|
||||
activate: tool.active
|
||||
});
|
||||
if (res.data.status === 'ok') {
|
||||
toast(res.data.message || tmTool('messages.toggleToolSuccess'));
|
||||
} else {
|
||||
tool.active = previous;
|
||||
toast(res.data.message || tmTool('messages.toggleToolError', { error: '' }), 'error');
|
||||
}
|
||||
} catch (error: any) {
|
||||
tool.active = previous;
|
||||
toast(error?.response?.data?.message || error?.message || tmTool('messages.toggleToolError', { error: '' }), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理确认重命名
|
||||
const handleConfirmRename = async () => {
|
||||
await confirmRename(tm('messages.renameSuccess'), tm('messages.renameFailed'));
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
fetchCommands(tm('messages.loadFailed')),
|
||||
fetchTools(tmTool('messages.getToolsError', { error: '' }))
|
||||
]);
|
||||
});
|
||||
|
||||
watch(() => props.active, async (isActive) => {
|
||||
if (!isActive) return;
|
||||
if (viewMode.value === 'commands') {
|
||||
await fetchCommands(tm('messages.loadFailed'));
|
||||
} else {
|
||||
await fetchTools(tmTool('messages.getToolsError', { error: '' }));
|
||||
}
|
||||
});
|
||||
|
||||
watch(viewMode, async (mode) => {
|
||||
if (mode === 'commands') {
|
||||
await fetchCommands(tm('messages.loadFailed'));
|
||||
} else {
|
||||
await fetchTools(tmTool('messages.getToolsError', { error: '' }));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card variant="flat" style="background-color: transparent">
|
||||
<v-card-text style="padding: 20px 12px; padding-top: 0px;">
|
||||
<div class="d-flex justify-space-between align-center mb-6 flex-wrap ga-3">
|
||||
<v-btn-toggle v-model="viewMode" color="primary" variant="outlined" density="comfortable" mandatory>
|
||||
<v-btn value="commands">
|
||||
<v-icon size="18" class="mr-1">mdi-console-line</v-icon>
|
||||
{{ tm('type.command') }}
|
||||
</v-btn>
|
||||
<v-btn value="tools">
|
||||
<v-icon size="18" class="mr-1">mdi-function-variant</v-icon>
|
||||
{{ tmTool('functionTools.title') }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-progress-linear
|
||||
v-if="viewMode === 'commands' && loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
style="max-width: 220px; flex: 1;"
|
||||
/>
|
||||
<v-progress-linear
|
||||
v-else-if="viewMode === 'tools' && toolsLoading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
style="max-width: 220px; flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'commands'">
|
||||
<CommandFilters
|
||||
:plugin-filter="pluginFilter"
|
||||
@update:plugin-filter="pluginFilter = $event"
|
||||
:type-filter="typeFilter"
|
||||
@update:type-filter="typeFilter = $event"
|
||||
:permission-filter="permissionFilter"
|
||||
@update:permission-filter="permissionFilter = $event"
|
||||
:status-filter="statusFilter"
|
||||
@update:status-filter="statusFilter = $event"
|
||||
:show-system-plugins="showSystemPlugins"
|
||||
@update:show-system-plugins="showSystemPlugins = $event"
|
||||
:search-query="searchQuery"
|
||||
@update:search-query="searchQuery = $event"
|
||||
:available-plugins="availablePlugins"
|
||||
:has-system-plugin-conflict="hasSystemPluginConflict"
|
||||
:effective-show-system-plugins="effectiveShowSystemPlugins"
|
||||
>
|
||||
<template #stats>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-console-line</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredCommands.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="error" class="mr-1">mdi-close-circle-outline</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.disabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-error">{{ summary.disabled }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</CommandFilters>
|
||||
|
||||
<v-alert
|
||||
v-if="summary.conflicts > 0"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
prominent
|
||||
border="start"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="28">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
<v-alert-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ tm('conflictAlert.title') }}
|
||||
</v-alert-title>
|
||||
<div class="text-body-2 mt-1">
|
||||
{{ tm('conflictAlert.description', { count: summary.conflicts }) }}
|
||||
</div>
|
||||
<div class="text-body-2 mt-2">
|
||||
<v-icon size="16" class="mr-1">mdi-lightbulb-outline</v-icon>
|
||||
{{ tm('conflictAlert.hint') }}
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<CommandTable
|
||||
:items="filteredCommands"
|
||||
:expanded-groups="expandedGroups"
|
||||
:loading="loading"
|
||||
@toggle-expand="toggleGroupExpand"
|
||||
@toggle-command="handleToggleCommand"
|
||||
@rename="openRenameDialog"
|
||||
@view-details="openDetailsDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="d-flex flex-wrap align-center ga-3 mb-4">
|
||||
<div style="min-width: 240px; max-width: 380px; flex: 1;">
|
||||
<v-text-field
|
||||
v-model="toolSearch"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tmTool('functionTools.search')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-function-variant</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredTools.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="success" class="mr-1">mdi-check-circle-outline</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('status.enabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-success">{{ filteredTools.filter(t => t.active).length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToolTable
|
||||
:items="filteredTools"
|
||||
:loading="toolsLoading"
|
||||
@toggle-tool="handleToggleTool"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<RenameDialog
|
||||
:show="renameDialog.show"
|
||||
@update:show="renameDialog.show = $event"
|
||||
:new-name="renameDialog.newName"
|
||||
@update:new-name="renameDialog.newName = $event"
|
||||
:command="renameDialog.command"
|
||||
:loading="renameDialog.loading"
|
||||
@confirm="handleConfirmRename"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<DetailsDialog
|
||||
:show="detailsDialog.show"
|
||||
@update:show="detailsDialog.show = $event"
|
||||
:command="detailsDialog.command"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
+18
@@ -82,3 +82,21 @@ export interface StatusInfo {
|
||||
variant: 'flat' | 'outlined' | 'text' | 'elevated' | 'tonal' | 'plain';
|
||||
}
|
||||
|
||||
/** MCP/函数工具参数定义 */
|
||||
export interface ToolParameter {
|
||||
type?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** MCP/函数工具对象 */
|
||||
export interface ToolItem {
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
parameters?: {
|
||||
properties?: Record<string, ToolParameter>;
|
||||
};
|
||||
origin?: string;
|
||||
origin_name?: string;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"title": "Extension Management",
|
||||
"subtitle": "Manage and configure system extensions",
|
||||
"tabs": {
|
||||
"installed": "Installed",
|
||||
"installedPlugins": "Installed Plugins",
|
||||
"installedMcpServers": "Installed MCP Servers",
|
||||
"handlersOperation": "Manage Components",
|
||||
"market": "Extension Market"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
"paramName": "Parameter Name",
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"required": "Required"
|
||||
"required": "Required",
|
||||
"origin": "Origin",
|
||||
"originName": "Origin Name",
|
||||
"actions": "Actions"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"title": "插件管理",
|
||||
"subtitle": "管理和配置系统插件",
|
||||
"tabs": {
|
||||
"installed": "已安装",
|
||||
"installedPlugins": "已安装的插件",
|
||||
"installedMcpServers": "已安装的 MCP 服务器",
|
||||
"handlersOperation": "管理组件",
|
||||
"market": "插件市场"
|
||||
},
|
||||
"search": {
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
"paramName": "参数名",
|
||||
"type": "类型",
|
||||
"description": "描述",
|
||||
"required": "必填"
|
||||
"required": "必填",
|
||||
"origin": "来源",
|
||||
"originName": "来源名称",
|
||||
"actions": "操作"
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
@@ -33,21 +33,11 @@ const sidebarItem: menu[] = [
|
||||
icon: 'mdi-cog',
|
||||
to: '/config',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.toolUse',
|
||||
icon: 'mdi-function-variant',
|
||||
to: '/tool-use'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.extension',
|
||||
icon: 'mdi-puzzle',
|
||||
to: '/extension'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.commands',
|
||||
icon: 'mdi-console-line',
|
||||
to: '/commands'
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.knowledgeBase',
|
||||
icon: 'mdi-book-open-variant',
|
||||
|
||||
@@ -16,11 +16,6 @@ const MainRoutes = {
|
||||
path: '/extension',
|
||||
component: () => import('@/views/ExtensionPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Commands',
|
||||
path: '/commands',
|
||||
component: () => import('@/views/commandPanel/index.vue')
|
||||
},
|
||||
{
|
||||
name: 'ExtensionMarketplace',
|
||||
path: '/extension-marketplace',
|
||||
@@ -36,11 +31,6 @@ const MainRoutes = {
|
||||
path: '/providers',
|
||||
component: () => import('@/views/ProviderPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'ToolUsePage',
|
||||
path: '/tool-use',
|
||||
component: () => import('@/views/ToolUsePage.vue')
|
||||
},
|
||||
{
|
||||
name: 'Configs',
|
||||
path: '/config',
|
||||
|
||||
@@ -5,6 +5,8 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
|
||||
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
|
||||
import ProxySelector from '@/components/shared/ProxySelector.vue';
|
||||
import UninstallConfirmDialog from '@/components/shared/UninstallConfirmDialog.vue';
|
||||
import McpServersSection from '@/components/extension/McpServersSection.vue';
|
||||
import ComponentPanel from '@/components/extension/componentPanel/index.vue';
|
||||
import axios from 'axios';
|
||||
import { pinyin } from 'pinyin-pro';
|
||||
import { useCommonStore } from '@/stores/common';
|
||||
@@ -39,7 +41,7 @@ const checkAndPromptConflicts = async () => {
|
||||
}
|
||||
};
|
||||
const handleConflictConfirm = () => {
|
||||
router.push('/commands');
|
||||
activeTab.value = 'commands';
|
||||
};
|
||||
|
||||
const fileInput = ref(null);
|
||||
@@ -931,21 +933,29 @@ watch(marketSearch, (newVal) => {
|
||||
<v-tabs v-model="activeTab" color="primary">
|
||||
<v-tab value="installed">
|
||||
<v-icon class="mr-2">mdi-puzzle</v-icon>
|
||||
{{ tm('tabs.installed') }}
|
||||
{{ tm('tabs.installedPlugins') }}
|
||||
</v-tab>
|
||||
<v-tab value="mcp">
|
||||
<v-icon class="mr-2">mdi-server-network</v-icon>
|
||||
{{ tm('tabs.installedMcpServers') }}
|
||||
</v-tab>
|
||||
<v-tab value="market">
|
||||
<v-icon class="mr-2">mdi-store</v-icon>
|
||||
{{ tm('tabs.market') }}
|
||||
</v-tab>
|
||||
<v-tab value="components">
|
||||
<v-icon class="mr-2">mdi-wrench</v-icon>
|
||||
{{ tm('tabs.handlersOperation') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 搜索栏 - 在移动端时独占一行 -->
|
||||
<div style="flex-grow: 1; min-width: 250px; max-width: 400px; margin-left: auto; margin-top: 8px;">
|
||||
<v-text-field v-if="activeTab == 'market'" v-model="marketSearch" density="compact"
|
||||
<v-text-field v-if="activeTab === 'market'" v-model="marketSearch" density="compact"
|
||||
:label="tm('search.marketPlaceholder')" prepend-inner-icon="mdi-magnify" variant="solo-filled" flat
|
||||
hide-details single-line>
|
||||
</v-text-field>
|
||||
<v-text-field v-else v-model="pluginSearch" density="compact" :label="tm('search.placeholder')"
|
||||
<v-text-field v-else-if="activeTab === 'installed'" v-model="pluginSearch" density="compact" :label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details single-line>
|
||||
</v-text-field>
|
||||
</div>
|
||||
@@ -1149,6 +1159,24 @@ watch(marketSearch, (newVal) => {
|
||||
</v-fade-transition>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 指令面板标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'components'">
|
||||
<v-card class="rounded-lg" variant="flat" style="background-color: transparent;">
|
||||
<v-card-text class="pa-0">
|
||||
<ComponentPanel :active="activeTab === 'components'" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 已安装的 MCP 服务器标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'mcp'">
|
||||
<v-card class="rounded-lg" variant="flat" style="background-color: transparent;">
|
||||
<v-card-text class="pa-0">
|
||||
<McpServersSection />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- 插件市场标签页内容 -->
|
||||
<v-tab-item v-show="activeTab === 'market'">
|
||||
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 指令管理页面 - 主入口
|
||||
*
|
||||
* 模块化结构:
|
||||
* - types.ts: 类型定义
|
||||
* - composables/useCommandData.ts: 数据获取和状态管理
|
||||
* - composables/useCommandFilters.ts: 过滤逻辑
|
||||
* - composables/useCommandActions.ts: 操作方法
|
||||
* - components/CommandFilters.vue: 过滤器组件
|
||||
* - components/CommandTable.vue: 表格组件
|
||||
* - components/RenameDialog.vue: 重命名对话框
|
||||
* - components/DetailsDialog.vue: 详情对话框
|
||||
*/
|
||||
import { onMounted } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
// Composables
|
||||
import { useCommandData } from './composables/useCommandData';
|
||||
import { useCommandFilters } from './composables/useCommandFilters';
|
||||
import { useCommandActions } from './composables/useCommandActions';
|
||||
|
||||
// Components
|
||||
import CommandFilters from './components/CommandFilters.vue';
|
||||
import CommandTable from './components/CommandTable.vue';
|
||||
import RenameDialog from './components/RenameDialog.vue';
|
||||
import DetailsDialog from './components/DetailsDialog.vue';
|
||||
|
||||
// Types
|
||||
import type { CommandItem } from './types';
|
||||
|
||||
const { tm } = useModuleI18n('features/command');
|
||||
|
||||
// 数据管理
|
||||
const {
|
||||
loading,
|
||||
commands,
|
||||
summary,
|
||||
snackbar,
|
||||
toast,
|
||||
fetchCommands
|
||||
} = useCommandData();
|
||||
|
||||
// 过滤逻辑
|
||||
const {
|
||||
searchQuery,
|
||||
pluginFilter,
|
||||
permissionFilter,
|
||||
statusFilter,
|
||||
typeFilter,
|
||||
showSystemPlugins,
|
||||
expandedGroups,
|
||||
hasSystemPluginConflict,
|
||||
effectiveShowSystemPlugins,
|
||||
availablePlugins,
|
||||
filteredCommands,
|
||||
toggleGroupExpand
|
||||
} = useCommandFilters(commands);
|
||||
|
||||
// 操作方法
|
||||
const {
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
toggleCommand,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog
|
||||
} = useCommandActions(toast, () => fetchCommands(tm('messages.loadFailed')));
|
||||
|
||||
// 处理切换指令状态
|
||||
const handleToggleCommand = async (cmd: CommandItem) => {
|
||||
await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed'));
|
||||
};
|
||||
|
||||
// 处理确认重命名
|
||||
const handleConfirmRename = async () => {
|
||||
await confirmRename(tm('messages.renameSuccess'), tm('messages.renameFailed'));
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await fetchCommands(tm('messages.loadFailed'));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card variant="flat" style="background-color: transparent">
|
||||
<v-card-text style="padding: 20px 12px;">
|
||||
<!-- 过滤器组件 -->
|
||||
<CommandFilters
|
||||
:plugin-filter="pluginFilter"
|
||||
@update:plugin-filter="pluginFilter = $event"
|
||||
:type-filter="typeFilter"
|
||||
@update:type-filter="typeFilter = $event"
|
||||
:permission-filter="permissionFilter"
|
||||
@update:permission-filter="permissionFilter = $event"
|
||||
:status-filter="statusFilter"
|
||||
@update:status-filter="statusFilter = $event"
|
||||
:show-system-plugins="showSystemPlugins"
|
||||
@update:show-system-plugins="showSystemPlugins = $event"
|
||||
:search-query="searchQuery"
|
||||
@update:search-query="searchQuery = $event"
|
||||
:available-plugins="availablePlugins"
|
||||
:has-system-plugin-conflict="hasSystemPluginConflict"
|
||||
:effective-show-system-plugins="effectiveShowSystemPlugins"
|
||||
>
|
||||
<template #stats>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-console-line</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredCommands.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="error" class="mr-1">mdi-close-circle-outline</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.disabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-error">{{ summary.disabled }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</CommandFilters>
|
||||
|
||||
<!-- 冲突警告 -->
|
||||
<v-alert
|
||||
v-if="summary.conflicts > 0"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
prominent
|
||||
border="start"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="28">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
<v-alert-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ tm('conflictAlert.title') }}
|
||||
</v-alert-title>
|
||||
<div class="text-body-2 mt-1">
|
||||
{{ tm('conflictAlert.description', { count: summary.conflicts }) }}
|
||||
</div>
|
||||
<div class="text-body-2 mt-2">
|
||||
<v-icon size="16" class="mr-1">mdi-lightbulb-outline</v-icon>
|
||||
{{ tm('conflictAlert.hint') }}
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<!-- 指令表格 -->
|
||||
<CommandTable
|
||||
:items="filteredCommands"
|
||||
:expanded-groups="expandedGroups"
|
||||
@toggle-expand="toggleGroupExpand"
|
||||
@toggle-command="handleToggleCommand"
|
||||
@rename="openRenameDialog"
|
||||
@view-details="openDetailsDialog"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<RenameDialog
|
||||
:show="renameDialog.show"
|
||||
@update:show="renameDialog.show = $event"
|
||||
:new-name="renameDialog.newName"
|
||||
@update:new-name="renameDialog.newName = $event"
|
||||
:command="renameDialog.command"
|
||||
:loading="renameDialog.loading"
|
||||
@confirm="handleConfirmRename"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<DetailsDialog
|
||||
:show="detailsDialog.show"
|
||||
@update:show="detailsDialog.show = $event"
|
||||
:command="detailsDialog.command"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
Reference in New Issue
Block a user