From 6b73b19e54d7e18c3edbb2206e458749c851d582 Mon Sep 17 00:00:00 2001 From: i0cLiceao Date: Sun, 14 Dec 2025 18:23:29 +0800 Subject: [PATCH 01/20] fix: support using GitHub Raw content as plugin source (#3975) * Update plugin.py * Update plugin.py * Update plugin.py * Update plugin.py --- astrbot/dashboard/routes/plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index c249b07b7..fd808c6c9 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -124,7 +124,11 @@ class PluginRoute(Route): session.get(url) as response, ): if response.status == 200: - remote_data = await response.json() + try: + remote_data = await response.json() + except aiohttp.ContentTypeError: + remote_text = await response.text() + remote_data = json.loads(remote_text) # 检查远程数据是否为空 if not remote_data or ( From 16df64c405b20b9bf60b978316357d39c63fbaf3 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:31:17 +0800 Subject: [PATCH 02/20] fix: lark domain and log_level of Lark API client (#4038) fixes: #4035 --- astrbot/core/platform/sources/lark/lark_adapter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index 883d81217..08df1f359 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -81,7 +81,12 @@ class LarkPlatformAdapter(Platform): ) self.lark_api = ( - lark.Client.builder().app_id(self.appid).app_secret(self.appsecret).build() + lark.Client.builder() + .app_id(self.appid) + .app_secret(self.appsecret) + .log_level(lark.LogLevel.ERROR) + .domain(self.domain) + .build() ) self.webhook_server = None From 65da469debcaa10ae50eca34ed20b8a3e815ec33 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:44:12 +0800 Subject: [PATCH 03/20] feat: add conversation export feature to JSONL for AI training (#4037) * Initial plan * Add conversation export functionality (backend and frontend) Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * Address code review feedback: move imports, simplify logic, improve i18n Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * Simplify frontend download logic: remove redundant Blob wrapper and complex filename parsing Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * fix: update conversation export filename format for consistency --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> Co-authored-by: Soulter <905617992@qq.com> --- astrbot/dashboard/routes/conversation.py | 92 ++++++++++++++++++- .../locales/en-US/features/conversation.json | 11 ++- .../locales/zh-CN/features/conversation.json | 11 ++- dashboard/src/views/ConversationPage.vue | 58 ++++++++++++ 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/astrbot/dashboard/routes/conversation.py b/astrbot/dashboard/routes/conversation.py index d19fdf793..513d3603f 100644 --- a/astrbot/dashboard/routes/conversation.py +++ b/astrbot/dashboard/routes/conversation.py @@ -1,7 +1,9 @@ import json import traceback +from datetime import datetime +from io import BytesIO -from quart import request +from quart import request, send_file from astrbot.core import logger from astrbot.core.core_lifecycle import AstrBotCoreLifecycle @@ -30,6 +32,7 @@ class ConversationRoute(Route): "POST", self.update_history, ), + "/conversation/export": ("POST", self.export_conversations), } self.db_helper = db_helper self.conv_mgr = core_lifecycle.conversation_manager @@ -283,3 +286,90 @@ class ConversationRoute(Route): except Exception as e: logger.error(f"更新对话历史失败: {e!s}\n{traceback.format_exc()}") return Response().error(f"更新对话历史失败: {e!s}").__dict__ + + async def export_conversations(self): + """批量导出对话为 JSONL 格式""" + try: + data = await request.get_json() + conversations_to_export = data.get("conversations", []) + + if not conversations_to_export: + return Response().error("导出列表不能为空").__dict__ + + # 收集所有对话的内容 + jsonl_lines = [] + exported_count = 0 + failed_items = [] + + for conv_info in conversations_to_export: + user_id = conv_info.get("user_id") + cid = conv_info.get("cid") + + if not user_id or not cid: + failed_items.append( + f"user_id:{user_id}, cid:{cid} - 缺少必要参数", + ) + continue + + try: + conversation = await self.conv_mgr.get_conversation( + unified_msg_origin=user_id, + conversation_id=cid, + ) + + if not conversation: + failed_items.append( + f"user_id:{user_id}, cid:{cid} - 对话不存在" + ) + continue + + # 解析对话内容 (history is always a JSON string from _convert_conv_from_v2_to_v1) + content = json.loads(conversation.history) + + # 创建导出记录 + export_record = { + "cid": cid, + "user_id": user_id, + "platform_id": conversation.platform_id, + "title": conversation.title, + "persona_id": conversation.persona_id, + "created_at": conversation.created_at, + "updated_at": conversation.updated_at, + "content": content, + } + + # 将记录转换为 JSON 字符串并添加到 JSONL + jsonl_lines.append(json.dumps(export_record, ensure_ascii=False)) + exported_count += 1 + + except Exception as e: + failed_items.append(f"user_id:{user_id}, cid:{cid} - {e!s}") + logger.error( + f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}" + ) + + if exported_count == 0: + return Response().error("没有成功导出任何对话").__dict__ + + # 创建 JSONL 内容 + jsonl_content = "\n".join(jsonl_lines) + + # 创建一个内存文件对象 + file_obj = BytesIO(jsonl_content.encode("utf-8")) + file_obj.seek(0) + + # 生成文件名 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"astrbot_conversations_export_{timestamp}.jsonl" + + # 返回文件流 + return await send_file( + file_obj, + mimetype="application/jsonl", + as_attachment=True, + attachment_filename=filename, + ) + + except Exception as e: + logger.error(f"批量导出对话失败: {e!s}\n{traceback.format_exc()}") + return Response().error(f"批量导出对话失败: {e!s}").__dict__ diff --git a/dashboard/src/i18n/locales/en-US/features/conversation.json b/dashboard/src/i18n/locales/en-US/features/conversation.json index 2c427274c..3e8cb7128 100644 --- a/dashboard/src/i18n/locales/en-US/features/conversation.json +++ b/dashboard/src/i18n/locales/en-US/features/conversation.json @@ -13,7 +13,8 @@ "refresh": "Refresh" }, "batch": { - "deleteSelected": "Delete Selected ({count})" + "deleteSelected": "Delete Selected ({count})", + "exportSelected": "Export Selected ({count})" }, "pagination": { "itemsPerPage": "Items per page", @@ -76,7 +77,8 @@ "message": "Are you sure you want to delete the selected {count} conversations? This action cannot be undone, please proceed with caution!", "andMore": "and {count} more", "cancel": "Cancel", - "confirm": "Batch Delete" + "confirm": "Batch Delete", + "warning": "Warning: This action cannot be undone!" } }, "messages": { @@ -92,6 +94,9 @@ "noItemSelected": "Please select conversations to delete first", "batchDeleteSuccess": "Successfully deleted {count} conversations", "batchDeleteError": "Batch delete failed", - "batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed" + "batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed", + "exportSuccess": "Export successful", + "exportError": "Export failed", + "noItemSelectedForExport": "Please select conversations to export first" } } \ No newline at end of file diff --git a/dashboard/src/i18n/locales/zh-CN/features/conversation.json b/dashboard/src/i18n/locales/zh-CN/features/conversation.json index cc20bf640..8a5ca6eb5 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/conversation.json +++ b/dashboard/src/i18n/locales/zh-CN/features/conversation.json @@ -13,7 +13,8 @@ "refresh": "刷新" }, "batch": { - "deleteSelected": "删除选中 ({count})" + "deleteSelected": "删除选中 ({count})", + "exportSelected": "导出选中 ({count})" }, "pagination": { "itemsPerPage": "每页", @@ -76,7 +77,8 @@ "message": "确定要删除选中的 {count} 个对话吗?此操作不可恢复,请谨慎操作!", "andMore": "等 {count} 个", "cancel": "取消", - "confirm": "批量删除" + "confirm": "批量删除", + "warning": "警告:此操作不可撤销!" } }, "messages": { @@ -92,6 +94,9 @@ "noItemSelected": "请先选择要删除的对话", "batchDeleteSuccess": "成功删除 {count} 个对话", "batchDeleteError": "批量删除失败", - "batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个" + "batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个", + "exportSuccess": "导出成功", + "exportError": "导出失败", + "noItemSelectedForExport": "请先选择要导出的对话" } } \ No newline at end of file diff --git a/dashboard/src/views/ConversationPage.vue b/dashboard/src/views/ConversationPage.vue index d6ec28100..8a4debd5e 100644 --- a/dashboard/src/views/ConversationPage.vue +++ b/dashboard/src/views/ConversationPage.vue @@ -40,6 +40,17 @@ :loading="loading" size="small" class="mr-2"> {{ tm('history.refresh') }} + + {{ tm('batch.exportSelected', { count: selectedItems.length }) }} + ({ + user_id: item.user_id, + cid: item.cid + })); + + const response = await axios.post('/api/conversation/export', { + conversations: conversations + }, { + responseType: 'blob' // 重要:告诉 axios 响应是一个 blob + }); + + // 创建一个下载链接 + const url = window.URL.createObjectURL(response.data); + const link = document.createElement('a'); + link.href = url; + + // 生成文件名(使用时间戳) + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); + const filename = `conversations_export_${timestamp}.jsonl`; + + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + + // 清理 + link.remove(); + window.URL.revokeObjectURL(url); + + this.showSuccessMessage(this.tm('messages.exportSuccess')); + } catch (error) { + console.error(this.tm('messages.exportError'), error); + this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.exportError')); + } finally { + this.loading = false; + } + }, + // 格式化时间戳 formatTimestamp(timestamp) { if (!timestamp) return this.tm('status.unknown'); From c3f45449e873a3f9b0f05bca349d6657b47b7981 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:47:21 +0800 Subject: [PATCH 04/20] docs: readme wa ta shi wa ko sei no de su ka ra! --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a45fa1fc5..46254b2b4 100644 --- a/README.md +++ b/README.md @@ -243,4 +243,10 @@ pre-commit install +
+ _私は、高性能ですから!_ + + +
Date: Mon, 15 Dec 2025 15:27:42 +0800 Subject: [PATCH 05/20] fix: remove unnecessary persistent attribute from ReadmeDialog and adjust dialog structure in ExtensionPage --- dashboard/src/components/shared/ReadmeDialog.vue | 2 +- dashboard/src/views/ExtensionPage.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/src/components/shared/ReadmeDialog.vue b/dashboard/src/components/shared/ReadmeDialog.vue index dd72444c7..6c3077178 100644 --- a/dashboard/src/components/shared/ReadmeDialog.vue +++ b/dashboard/src/components/shared/ReadmeDialog.vue @@ -115,7 +115,7 @@ const _show = computed({ @@ -183,8 +150,7 @@ - - + @@ -240,115 +206,8 @@ - - - - - {{ tm('functionTools.title') }} - {{ tools.length }} - - - -
-
- mdi-api-off -

{{ tm('functionTools.empty') }}

-
- -
- - - 复选框代表该工具是否被启用。 - - - - - - - - - -
- - {{ tool.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant' }} - - - {{ formatToolName(tool.name) }} - -
-
- - {{ tool.description }} - -
-
- - - - -

- mdi-information - {{ tm('functionTools.description') }} -

-

{{ tool.description }}

- - -
- mdi-code-brackets -

{{ tm('functionTools.noParameters') }}

-
-
-
-
-
-
-
-
-
-
- - - - - {{ tm('dialogs.serverDetail.buttons.close') }} - - -
-
- - + {{ save_message }} @@ -356,15 +215,13 @@ \ No newline at end of file + diff --git a/dashboard/src/components/extension/componentPanel/components/CommandFilters.vue b/dashboard/src/components/extension/componentPanel/components/CommandFilters.vue new file mode 100644 index 000000000..c4b212803 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/components/CommandFilters.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/dashboard/src/components/extension/componentPanel/components/CommandTable.vue b/dashboard/src/components/extension/componentPanel/components/CommandTable.vue new file mode 100644 index 000000000..f8bb6fa82 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/components/CommandTable.vue @@ -0,0 +1,257 @@ + + + + + + + + diff --git a/dashboard/src/components/extension/componentPanel/components/DetailsDialog.vue b/dashboard/src/components/extension/componentPanel/components/DetailsDialog.vue new file mode 100644 index 000000000..6d9188374 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/components/DetailsDialog.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/dashboard/src/components/extension/componentPanel/components/RenameDialog.vue b/dashboard/src/components/extension/componentPanel/components/RenameDialog.vue new file mode 100644 index 000000000..ffdc5a826 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/components/RenameDialog.vue @@ -0,0 +1,53 @@ + + + diff --git a/dashboard/src/components/extension/componentPanel/components/ToolTable.vue b/dashboard/src/components/extension/componentPanel/components/ToolTable.vue new file mode 100644 index 000000000..1b6fecfc1 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/components/ToolTable.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/dashboard/src/components/extension/componentPanel/composables/useCommandActions.ts b/dashboard/src/components/extension/componentPanel/composables/useCommandActions.ts new file mode 100644 index 000000000..a285c473f --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/composables/useCommandActions.ts @@ -0,0 +1,177 @@ +/** + * 指令操作方法 Composable + */ +import { reactive } from 'vue'; +import axios from 'axios'; +import type { CommandItem, RenameDialogState, DetailsDialogState, TypeInfo, StatusInfo } from '../types'; + +export function useCommandActions( + toast: (message: string, color?: string) => void, + fetchCommands: () => Promise +) { + // 重命名对话框状态 + const renameDialog = reactive({ + show: false, + command: null, + newName: '', + loading: false + }); + + // 详情对话框状态 + const detailsDialog = reactive({ + show: false, + command: null + }); + + /** + * 切换指令启用/禁用状态 + */ + const toggleCommand = async ( + cmd: CommandItem, + successMessage: string, + errorMessage: string + ) => { + try { + const res = await axios.post('/api/commands/toggle', { + handler_full_name: cmd.handler_full_name, + enabled: !cmd.enabled + }); + if (res.data.status === 'ok') { + toast(successMessage, 'success'); + await fetchCommands(); + } else { + toast(res.data.message || errorMessage, 'error'); + } + } catch (err: any) { + toast(err?.message || errorMessage, 'error'); + } + }; + + /** + * 打开重命名对话框 + */ + const openRenameDialog = (cmd: CommandItem) => { + renameDialog.command = cmd; + renameDialog.newName = cmd.current_fragment || ''; + renameDialog.show = true; + }; + + /** + * 确认重命名 + */ + const confirmRename = async (successMessage: string, errorMessage: string) => { + if (!renameDialog.command || !renameDialog.newName.trim()) return; + + renameDialog.loading = true; + try { + const res = await axios.post('/api/commands/rename', { + handler_full_name: renameDialog.command.handler_full_name, + new_name: renameDialog.newName.trim() + }); + if (res.data.status === 'ok') { + toast(successMessage, 'success'); + renameDialog.show = false; + await fetchCommands(); + } else { + toast(res.data.message || errorMessage, 'error'); + } + } catch (err: any) { + toast(err?.message || errorMessage, 'error'); + } finally { + renameDialog.loading = false; + } + }; + + /** + * 打开详情对话框 + */ + const openDetailsDialog = (cmd: CommandItem) => { + detailsDialog.command = cmd; + detailsDialog.show = true; + }; + + /** + * 获取类型显示信息 + */ + const getTypeInfo = (type: string, translations: { group: string; subCommand: string; command: string }): TypeInfo => { + switch (type) { + case 'group': + return { text: translations.group, color: 'info', icon: 'mdi-folder-outline' }; + case 'sub_command': + return { text: translations.subCommand, color: 'secondary', icon: 'mdi-subdirectory-arrow-right' }; + default: + return { text: translations.command, color: 'primary', icon: 'mdi-console-line' }; + } + }; + + /** + * 获取权限颜色 + */ + const getPermissionColor = (permission: string): string => { + switch (permission) { + case 'admin': return 'error'; + default: return 'success'; + } + }; + + /** + * 获取权限标签 + */ + const getPermissionLabel = (permission: string, translations: { admin: string; everyone: string }): string => { + switch (permission) { + case 'admin': return translations.admin; + default: return translations.everyone; + } + }; + + /** + * 获取状态显示信息 + */ + const getStatusInfo = ( + cmd: CommandItem, + translations: { conflict: string; enabled: string; disabled: string } + ): StatusInfo => { + if (cmd.has_conflict) { + return { text: translations.conflict, color: 'warning', variant: 'flat' }; + } + if (cmd.enabled) { + return { text: translations.enabled, color: 'success', variant: 'flat' }; + } + return { text: translations.disabled, color: 'error', variant: 'outlined' }; + }; + + /** + * 获取表格行属性(用于冲突高亮和子指令样式) + */ + const getRowProps = ({ item }: { item: CommandItem }) => { + const classes: string[] = []; + if (item.has_conflict) { + classes.push('conflict-row'); + } + if (item.type === 'sub_command') { + classes.push('sub-command-row'); + } + if (item.is_group) { + classes.push('group-row'); + } + return classes.length > 0 ? { class: classes.join(' ') } : {}; + }; + + return { + // 状态 + renameDialog, + detailsDialog, + + // 方法 + toggleCommand, + openRenameDialog, + confirmRename, + openDetailsDialog, + getTypeInfo, + getPermissionColor, + getPermissionLabel, + getStatusInfo, + getRowProps + }; +} + diff --git a/dashboard/src/components/extension/componentPanel/composables/useCommandFilters.ts b/dashboard/src/components/extension/componentPanel/composables/useCommandFilters.ts new file mode 100644 index 000000000..f7d5bbc0e --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/composables/useCommandFilters.ts @@ -0,0 +1,187 @@ +/** + * 指令过滤逻辑 Composable + */ +import { ref, computed, type Ref } from 'vue'; +import type { CommandItem, FilterState } from '../types'; + +export function useCommandFilters(commands: Ref) { + // 过滤状态 + const searchQuery = ref(''); + const pluginFilter = ref('all'); + const permissionFilter = ref('all'); + const statusFilter = ref('all'); + const typeFilter = ref('all'); + const showSystemPlugins = ref(false); + + // 展开的指令组 + const expandedGroups = ref>(new Set()); + + /** + * 检查是否有涉及系统插件的冲突 + */ + const hasSystemPluginConflict = computed(() => { + return commands.value.some(cmd => cmd.has_conflict && cmd.reserved); + }); + + /** + * 实际是否显示系统插件(如果有系统插件冲突则强制显示) + */ + const effectiveShowSystemPlugins = computed(() => { + return showSystemPlugins.value || hasSystemPluginConflict.value; + }); + + /** + * 获取可用的插件列表(用于过滤下拉框) + */ + const availablePlugins = computed(() => { + const plugins = new Set( + commands.value + .filter(cmd => effectiveShowSystemPlugins.value || !cmd.reserved) + .map(cmd => cmd.plugin) + ); + return Array.from(plugins).sort(); + }); + + /** + * 检查指令是否匹配过滤条件 + */ + const matchesFilters = (cmd: CommandItem, query: string): boolean => { + // 系统插件过滤(除非显示系统插件) + if (!effectiveShowSystemPlugins.value && cmd.reserved) { + return false; + } + + // 搜索过滤 + if (query) { + const matchesSearch = + cmd.effective_command?.toLowerCase().includes(query) || + cmd.description?.toLowerCase().includes(query) || + cmd.plugin?.toLowerCase().includes(query); + if (!matchesSearch) return false; + } + + // 插件过滤 + if (pluginFilter.value !== 'all' && cmd.plugin !== pluginFilter.value) { + return false; + } + + // 权限过滤 + if (permissionFilter.value !== 'all') { + if (permissionFilter.value === 'everyone') { + if (cmd.permission !== 'everyone' && cmd.permission !== 'member') return false; + } else if (cmd.permission !== permissionFilter.value) { + return false; + } + } + + // 状态过滤 + if (statusFilter.value !== 'all') { + if (statusFilter.value === 'enabled' && !cmd.enabled) return false; + if (statusFilter.value === 'disabled' && cmd.enabled) return false; + if (statusFilter.value === 'conflict' && !cmd.has_conflict) return false; + } + + // 类型过滤 + if (typeFilter.value !== 'all') { + if (typeFilter.value === 'group' && cmd.type !== 'group') return false; + if (typeFilter.value === 'command' && cmd.type !== 'command') return false; + if (typeFilter.value === 'sub_command' && cmd.type !== 'sub_command') return false; + } + + return true; + }; + + /** + * 过滤后的指令列表(支持层级结构) + */ + const filteredCommands = computed(() => { + const query = searchQuery.value.toLowerCase(); + const conflictCmds: CommandItem[] = []; + const normalCmds: CommandItem[] = []; + + for (const cmd of commands.value) { + // 对于指令组,检查组本身或子指令是否匹配 + if (cmd.is_group) { + const groupMatches = matchesFilters(cmd, query); + const matchingSubCmds = (cmd.sub_commands || []).filter(sub => matchesFilters(sub, query)); + + // 如果组匹配或有匹配的子指令,则包含它 + if (groupMatches || matchingSubCmds.length > 0) { + if (cmd.has_conflict) { + conflictCmds.push(cmd); + } else { + normalCmds.push(cmd); + } + + // 如果组已展开,添加匹配的子指令 + if (expandedGroups.value.has(cmd.handler_full_name)) { + const subsToShow = query ? matchingSubCmds : (cmd.sub_commands || []); + for (const sub of subsToShow) { + if (sub.has_conflict) { + conflictCmds.push(sub); + } else { + normalCmds.push(sub); + } + } + } + } + } else if (cmd.type !== 'sub_command') { + // 普通指令(子指令通过组处理) + if (matchesFilters(cmd, query)) { + if (cmd.has_conflict) { + conflictCmds.push(cmd); + } else { + normalCmds.push(cmd); + } + } + } + } + + // 按 effective_command 排序冲突指令,使其分组在一起 + conflictCmds.sort((a, b) => (a.effective_command || '').localeCompare(b.effective_command || '')); + + return [...conflictCmds, ...normalCmds]; + }); + + /** + * 切换指令组的展开/折叠状态 + */ + const toggleGroupExpand = (cmd: CommandItem) => { + if (!cmd.is_group) return; + if (expandedGroups.value.has(cmd.handler_full_name)) { + expandedGroups.value.delete(cmd.handler_full_name); + } else { + expandedGroups.value.add(cmd.handler_full_name); + } + }; + + /** + * 检查指令组是否已展开 + */ + const isGroupExpanded = (cmd: CommandItem): boolean => { + return expandedGroups.value.has(cmd.handler_full_name); + }; + + return { + // 状态 + searchQuery, + pluginFilter, + permissionFilter, + statusFilter, + typeFilter, + showSystemPlugins, + expandedGroups, + + // 计算属性 + hasSystemPluginConflict, + effectiveShowSystemPlugins, + availablePlugins, + filteredCommands, + + // 方法 + matchesFilters, + toggleGroupExpand, + isGroupExpanded + }; +} + diff --git a/dashboard/src/components/extension/componentPanel/composables/useComponentData.ts b/dashboard/src/components/extension/componentPanel/composables/useComponentData.ts new file mode 100644 index 000000000..291ba53c4 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/composables/useComponentData.ts @@ -0,0 +1,83 @@ +/** + * 指令数据管理 Composable + */ +import { ref, reactive } from 'vue'; +import axios from 'axios'; +import type { CommandItem, CommandSummary, SnackbarState, ToolItem } from '../types'; + +export function useComponentData() { + const loading = ref(false); + const commands = ref([]); + const tools = ref([]); + const toolsLoading = ref(false); + const summary = reactive({ + disabled: 0, + conflicts: 0 + }); + + const snackbar = reactive({ + show: false, + message: '', + color: 'success' + }); + + /** + * 显示 Toast 消息 + */ + const toast = (message: string, color: string = 'success') => { + snackbar.message = message; + snackbar.color = color; + snackbar.show = true; + }; + + /** + * 获取指令列表 + */ + const fetchCommands = async (errorMessage: string) => { + loading.value = true; + try { + const res = await axios.get('/api/commands'); + if (res.data.status === 'ok') { + commands.value = res.data.data.items || []; + const s = res.data.data.summary || {}; + summary.disabled = s.disabled || 0; + summary.conflicts = s.conflicts || 0; + } else { + toast(res.data.message || errorMessage, 'error'); + } + } catch (err: any) { + toast(err?.message || errorMessage, 'error'); + } finally { + loading.value = false; + } + }; + + 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, + fetchTools + }; +} + diff --git a/dashboard/src/components/extension/componentPanel/index.vue b/dashboard/src/components/extension/componentPanel/index.vue new file mode 100644 index 000000000..912af9156 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/index.vue @@ -0,0 +1,307 @@ + + + diff --git a/dashboard/src/components/extension/componentPanel/types.ts b/dashboard/src/components/extension/componentPanel/types.ts new file mode 100644 index 000000000..d2b388ec9 --- /dev/null +++ b/dashboard/src/components/extension/componentPanel/types.ts @@ -0,0 +1,102 @@ +/** + * 指令管理模块 - 类型定义 + */ + +/** 指令项接口 */ +export interface CommandItem { + handler_full_name: string; + handler_name: string; + plugin: string; + plugin_display_name: string | null; + module_path: string; + description: string; + type: CommandType; + parent_signature: string; + parent_group_handler: string; + original_command: string; + current_fragment: string; + effective_command: string; + aliases: string[]; + permission: PermissionType; + enabled: boolean; + is_group: boolean; + has_conflict: boolean; + reserved: boolean; + sub_commands: CommandItem[]; +} + +/** 指令类型 */ +export type CommandType = 'command' | 'group' | 'sub_command'; + +/** 权限类型 */ +export type PermissionType = 'admin' | 'everyone' | 'member'; + +/** 指令摘要统计 */ +export interface CommandSummary { + disabled: number; + conflicts: number; +} + +/** 过滤器状态 */ +export interface FilterState { + searchQuery: string; + pluginFilter: string; + permissionFilter: string; + statusFilter: string; + typeFilter: string; + showSystemPlugins: boolean; +} + +/** 重命名对话框状态 */ +export interface RenameDialogState { + show: boolean; + command: CommandItem | null; + newName: string; + loading: boolean; +} + +/** 详情对话框状态 */ +export interface DetailsDialogState { + show: boolean; + command: CommandItem | null; +} + +/** Toast 消息状态 */ +export interface SnackbarState { + show: boolean; + message: string; + color: string; +} + +/** 类型信息展示 */ +export interface TypeInfo { + text: string; + color: string; + icon: string; +} + +/** 状态信息展示 */ +export interface StatusInfo { + text: string; + color: string; + 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; + }; + origin?: string; + origin_name?: string; +} + diff --git a/dashboard/src/components/shared/SidebarCustomizer.vue b/dashboard/src/components/shared/SidebarCustomizer.vue index 625320d0d..098f32f46 100644 --- a/dashboard/src/components/shared/SidebarCustomizer.vue +++ b/dashboard/src/components/shared/SidebarCustomizer.vue @@ -121,7 +121,8 @@ import sidebarItems from '@/layouts/full/vertical-sidebar/sidebarItem'; import { getSidebarCustomization, setSidebarCustomization, - clearSidebarCustomization + clearSidebarCustomization, + resolveSidebarItems } from '@/utils/sidebarCustomization'; const { t } = useI18n(); @@ -133,35 +134,12 @@ const draggedItem = ref(null); function initializeItems() { const customization = getSidebarCustomization(); - - if (customization) { - // Load from customization - const allItemsMap = new Map(); - - sidebarItems.forEach(item => { - if (item.children) { - item.children.forEach(child => { - allItemsMap.set(child.title, child); - }); - } else { - allItemsMap.set(item.title, item); - } - }); - - mainItems.value = customization.mainItems - .map(title => allItemsMap.get(title)) - .filter(item => item); - - moreItems.value = customization.moreItems - .map(title => allItemsMap.get(title)) - .filter(item => item); - } else { - // Load default structure - mainItems.value = sidebarItems.filter(item => !item.children); - - const moreGroup = sidebarItems.find(item => item.title === 'core.navigation.groups.more'); - moreItems.value = moreGroup ? [...moreGroup.children] : []; - } + const { mainItems: resolvedMain, moreItems: resolvedMore } = resolveSidebarItems( + sidebarItems, + customization + ); + mainItems.value = resolvedMain; + moreItems.value = resolvedMore; } function openDialog() { diff --git a/dashboard/src/i18n/locales/en-US/core/actions.json b/dashboard/src/i18n/locales/en-US/core/actions.json index a1ba76e08..d53a0cac3 100644 --- a/dashboard/src/i18n/locales/en-US/core/actions.json +++ b/dashboard/src/i18n/locales/en-US/core/actions.json @@ -19,5 +19,6 @@ "submit": "Submit", "reset": "Reset", "clear": "Clear", - "save": "Save" + "save": "Save", + "close": "Close" } \ No newline at end of file diff --git a/dashboard/src/i18n/locales/en-US/core/navigation.json b/dashboard/src/i18n/locales/en-US/core/navigation.json index ba18041e2..e1aaf9fd8 100644 --- a/dashboard/src/i18n/locales/en-US/core/navigation.json +++ b/dashboard/src/i18n/locales/en-US/core/navigation.json @@ -2,6 +2,7 @@ "dashboard": "Dashboard", "platforms": "Platforms", "providers": "Providers", + "commands": "Commands", "persona": "Persona", "toolUse": "MCP Tools", "config": "Config", diff --git a/dashboard/src/i18n/locales/en-US/features/command.json b/dashboard/src/i18n/locales/en-US/features/command.json new file mode 100644 index 000000000..ab17d7bb0 --- /dev/null +++ b/dashboard/src/i18n/locales/en-US/features/command.json @@ -0,0 +1,91 @@ +{ + "title": "Command Management", + "summary": { + "total": "Displayed commands", + "disabled": "Disabled", + "conflicts": "Conflicts" + }, + "conflictAlert": { + "title": "Command Conflicts Detected", + "description": "There are {count} conflicting commands. Conflicting commands will trigger multiple plugins simultaneously, which may cause unexpected behavior.", + "hint": "Click the \"Rename\" button to rename conflicting commands and resolve conflicts." + }, + "table": { + "headers": { + "command": "Command", + "type": "Type", + "plugin": "Plugin", + "description": "Description", + "permission": "Permission", + "status": "Status", + "actions": "Actions" + } + }, + "type": { + "command": "Command", + "group": "Group", + "subCommand": "Sub-command" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "conflict": "Conflict" + }, + "permission": { + "everyone": "Everyone", + "admin": "Admin" + }, + "tooltips": { + "enable": "Enable command", + "disable": "Disable command", + "rename": "Rename command", + "viewDetails": "View details" + }, + "dialogs": { + "rename": { + "title": "Rename Command", + "newName": "New command name", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "details": { + "title": "Command Details", + "type": "Command Type", + "handler": "Handler", + "module": "Module Path", + "originalCommand": "Original Command", + "effectiveCommand": "Effective Command", + "parentGroup": "Parent Group", + "subCommands": "Sub-commands", + "aliases": "Aliases", + "permission": "Permission", + "conflictStatus": "Conflict Status" + } + }, + "messages": { + "toggleSuccess": "Command status updated", + "toggleFailed": "Failed to update command status", + "renameSuccess": "Command renamed", + "renameFailed": "Rename failed", + "loadFailed": "Failed to load commands" + }, + "search": { + "placeholder": "Search commands..." + }, + "empty": { + "noCommands": "No Commands", + "noCommandsDesc": "No commands found" + }, + "filters": { + "all": "All", + "enabled": "Enabled", + "disabled": "Disabled", + "conflict": "Conflict", + "byPlugin": "Filter by plugin", + "byType": "Filter by type", + "byPermission": "Filter by permission", + "byStatus": "Filter by status", + "showSystemPlugins": "Show system plugins commands", + "systemPluginConflictHint": "System plugin conflicts detected. Resolve conflicts to hide." + } +} diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json index ab8d7b855..c313452bc 100644 --- a/dashboard/src/i18n/locales/en-US/features/extension.json +++ b/dashboard/src/i18n/locales/en-US/features/extension.json @@ -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 Handlers", "market": "Extension Market" }, "search": { @@ -197,5 +199,12 @@ "errors": { "confirmNotRegistered": "$confirm not properly registered" } + }, + "conflicts": { + "title": "Command Conflicts Detected", + "message": "This will cause some commands to work abnormally. It is recommended to go to the [Command Management] panel to handle it.", + "pairs": "command conflicts", + "goToManage": "Go to Manage", + "later": "Later" } } diff --git a/dashboard/src/i18n/locales/en-US/features/tool-use.json b/dashboard/src/i18n/locales/en-US/features/tool-use.json index 8a6ccd492..2c68b8243 100644 --- a/dashboard/src/i18n/locales/en-US/features/tool-use.json +++ b/dashboard/src/i18n/locales/en-US/features/tool-use.json @@ -42,7 +42,10 @@ "paramName": "Parameter Name", "type": "Type", "description": "Description", - "required": "Required" + "required": "Required", + "origin": "Origin", + "originName": "Origin Name", + "actions": "Actions" } }, "marketplace": { diff --git a/dashboard/src/i18n/locales/zh-CN/core/actions.json b/dashboard/src/i18n/locales/zh-CN/core/actions.json index 69b29598b..a0a8db1bf 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/actions.json +++ b/dashboard/src/i18n/locales/zh-CN/core/actions.json @@ -19,5 +19,6 @@ "submit": "提交", "reset": "重置", "clear": "清空", - "save": "保存" + "save": "保存", + "close": "关闭" } \ No newline at end of file diff --git a/dashboard/src/i18n/locales/zh-CN/core/navigation.json b/dashboard/src/i18n/locales/zh-CN/core/navigation.json index b361b7b08..b7d2d174c 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/navigation.json +++ b/dashboard/src/i18n/locales/zh-CN/core/navigation.json @@ -2,6 +2,7 @@ "dashboard": "数据统计", "platforms": "机器人", "providers": "模型提供商", + "commands": "指令管理", "persona": "人格设定", "toolUse": "MCP", "extension": "插件", diff --git a/dashboard/src/i18n/locales/zh-CN/features/command.json b/dashboard/src/i18n/locales/zh-CN/features/command.json new file mode 100644 index 000000000..514a9837a --- /dev/null +++ b/dashboard/src/i18n/locales/zh-CN/features/command.json @@ -0,0 +1,91 @@ +{ + "title": "指令管理", + "summary": { + "total": "展示的指令数", + "disabled": "已禁用", + "conflicts": "有冲突" + }, + "conflictAlert": { + "title": "检测到指令冲突", + "description": "当前有 {count} 对指令存在冲突,冲突的指令会同时触发多个插件响应,可能导致意外行为。", + "hint": "请点击「重命名」按钮修改冲突指令的名称以解决冲突。" + }, + "table": { + "headers": { + "command": "指令", + "type": "类型", + "plugin": "所属插件", + "description": "描述", + "permission": "权限", + "status": "状态", + "actions": "操作" + } + }, + "type": { + "command": "指令", + "group": "指令组", + "subCommand": "子指令" + }, + "status": { + "enabled": "已启用", + "disabled": "已禁用", + "conflict": "有冲突" + }, + "permission": { + "everyone": "所有人", + "admin": "管理员" + }, + "tooltips": { + "enable": "启用指令", + "disable": "禁用指令", + "rename": "重命名指令", + "viewDetails": "查看详情" + }, + "dialogs": { + "rename": { + "title": "重命名指令", + "newName": "新指令名", + "cancel": "取消", + "confirm": "确认" + }, + "details": { + "title": "指令详情", + "type": "指令类型", + "handler": "处理函数", + "module": "模块路径", + "originalCommand": "原始指令", + "effectiveCommand": "生效指令", + "parentGroup": "所属指令组", + "subCommands": "子指令列表", + "aliases": "别名", + "permission": "权限要求", + "conflictStatus": "冲突状态" + } + }, + "messages": { + "toggleSuccess": "指令状态已更新", + "toggleFailed": "更新指令状态失败", + "renameSuccess": "指令已重命名", + "renameFailed": "重命名失败", + "loadFailed": "加载指令列表失败" + }, + "search": { + "placeholder": "搜索指令..." + }, + "empty": { + "noCommands": "暂无指令", + "noCommandsDesc": "当前筛选条件下没有找到任何指令" + }, + "filters": { + "all": "全部", + "enabled": "已启用", + "disabled": "已禁用", + "conflict": "有冲突", + "byPlugin": "按插件筛选", + "byType": "按类型筛选", + "byPermission": "按权限筛选", + "byStatus": "按状态筛选", + "showSystemPlugins": "显示系统插件指令", + "systemPluginConflictHint": "存在涉及系统插件的冲突,需解决冲突后才能隐藏" + } +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json index e31057fd1..37ca08d56 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/extension.json +++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json @@ -2,7 +2,9 @@ "title": "插件管理", "subtitle": "管理和配置系统插件", "tabs": { - "installed": "已安装", + "installedPlugins": "已安装的插件", + "installedMcpServers": "已安装的 MCP 服务器", + "handlersOperation": "管理行为", "market": "插件市场" }, "search": { @@ -197,5 +199,12 @@ "errors": { "confirmNotRegistered": "$confirm 未正确注册" } + }, + "conflicts": { + "title": "检测到指令冲突", + "message": "这会导致部分指令工作异常,建议前往【指令管理】面板进行处理。", + "pairs": "对指令冲突", + "goToManage": "前往处理", + "later": "稍后处理" } } diff --git a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json index c9b902c02..f6e6c4407 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json +++ b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json @@ -42,7 +42,10 @@ "paramName": "参数名", "type": "类型", "description": "描述", - "required": "必填" + "required": "必填", + "origin": "来源", + "originName": "来源名称", + "actions": "操作" } }, "marketplace": { diff --git a/dashboard/src/i18n/translations.ts b/dashboard/src/i18n/translations.ts index d4444225b..8cff882be 100644 --- a/dashboard/src/i18n/translations.ts +++ b/dashboard/src/i18n/translations.ts @@ -32,6 +32,7 @@ import zhCNKnowledgeBaseDetail from './locales/zh-CN/features/knowledge-base/det import zhCNKnowledgeBaseDocument from './locales/zh-CN/features/knowledge-base/document.json'; import zhCNPersona from './locales/zh-CN/features/persona.json'; import zhCNMigration from './locales/zh-CN/features/migration.json'; +import zhCNCommand from './locales/zh-CN/features/command.json'; import zhCNErrors from './locales/zh-CN/messages/errors.json'; import zhCNSuccess from './locales/zh-CN/messages/success.json'; @@ -68,6 +69,7 @@ import enUSKnowledgeBaseDetail from './locales/en-US/features/knowledge-base/det import enUSKnowledgeBaseDocument from './locales/en-US/features/knowledge-base/document.json'; import enUSPersona from './locales/en-US/features/persona.json'; import enUSMigration from './locales/en-US/features/migration.json'; +import enUSCommand from './locales/en-US/features/command.json'; import enUSErrors from './locales/en-US/messages/errors.json'; import enUSSuccess from './locales/en-US/messages/success.json'; @@ -111,7 +113,8 @@ export const translations = { document: zhCNKnowledgeBaseDocument }, persona: zhCNPersona, - migration: zhCNMigration + migration: zhCNMigration, + command: zhCNCommand }, messages: { errors: zhCNErrors, @@ -155,7 +158,8 @@ export const translations = { document: enUSKnowledgeBaseDocument }, persona: enUSPersona, - migration: enUSMigration + migration: enUSMigration, + command: enUSCommand }, messages: { errors: enUSErrors, diff --git a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts index 42f763402..e5628ce51 100644 --- a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts +++ b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts @@ -33,11 +33,6 @@ 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', diff --git a/dashboard/src/router/MainRoutes.ts b/dashboard/src/router/MainRoutes.ts index 276d37444..0a8617426 100644 --- a/dashboard/src/router/MainRoutes.ts +++ b/dashboard/src/router/MainRoutes.ts @@ -31,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', diff --git a/dashboard/src/utils/sidebarCustomization.js b/dashboard/src/utils/sidebarCustomization.js index 75cc5896f..10004322f 100644 --- a/dashboard/src/utils/sidebarCustomization.js +++ b/dashboard/src/utils/sidebarCustomization.js @@ -41,59 +41,97 @@ export function clearSidebarCustomization() { } /** - * Apply customization to sidebar items - * @param {Array} defaultItems - Default sidebar items array - * @returns {Array} Customized sidebar items array (new array, doesn't mutate input) + * 解析侧边栏默认项与用户定制,返回主区/更多区及可选的合并结果 + * @param {Array} defaultItems - 默认侧边栏结构 + * @param {Object|null} customization - 用户定制(mainItems/moreItems) + * @param {Object} options + * @param {boolean} [options.cloneItems=false] - 是否克隆条目以避免外部引用被修改 + * @param {boolean} [options.assembleMoreGroup=false] - 是否组装带更多分组的整体数组 + * @returns {{ mainItems: Array, moreItems: Array, merged?: Array }} + */ +export function resolveSidebarItems(defaultItems, customization, options = {}) { + const { cloneItems = false, assembleMoreGroup = false } = options; + + const all = new Map(); + const defaultMain = []; + const defaultMore = []; + + // 收集所有条目,按 title 建索引 + defaultItems.forEach(item => { + if (item.children) { + item.children.forEach(child => { + all.set(child.title, cloneItems ? { ...child } : child); + defaultMore.push(child.title); + }); + } else { + all.set(item.title, cloneItems ? { ...item } : item); + defaultMain.push(item.title); + } + }); + + const hasCustomization = Boolean(customization); + const mainKeys = hasCustomization ? customization.mainItems || [] : defaultMain; + const moreKeys = hasCustomization ? customization.moreItems || [] : defaultMore; + const used = hasCustomization ? new Set([...mainKeys, ...moreKeys]) : new Set(defaultMain.concat(defaultMore)); + + const mainItems = mainKeys + .map(title => all.get(title)) + .filter(Boolean); + + if (hasCustomization) { + // 补充新增默认主区项 + defaultMain.forEach(title => { + if (!used.has(title)) { + const item = all.get(title); + if (item) mainItems.push(item); + } + }); + } + + const moreItems = moreKeys + .map(title => all.get(title)) + .filter(Boolean); + + if (hasCustomization) { + // 补充新增默认更多区项 + defaultMore.forEach(title => { + if (!used.has(title)) { + const item = all.get(title); + if (item) moreItems.push(item); + } + }); + } + + let merged; + if (assembleMoreGroup) { + const children = cloneItems ? moreItems.map(item => ({ ...item })) : [...moreItems]; + if (children.length > 0) { + merged = [ + ...mainItems, + { + title: 'core.navigation.groups.more', + icon: 'mdi-dots-horizontal', + children + } + ]; + } else { + merged = [...mainItems]; + } + } + + return { mainItems, moreItems, merged }; +} + +/** + * 应用侧边栏定制,返回包含更多分组的完整结构 + * @param {Array} defaultItems - 默认侧边栏结构 + * @returns {Array} 自定义后的结构(新数组,不修改入参) */ export function applySidebarCustomization(defaultItems) { const customization = getSidebarCustomization(); - if (!customization) { - return defaultItems; - } - - const { mainItems, moreItems } = customization; - - // Create a map of all items by title for quick lookup - // Deep clone items to avoid mutating originals - const allItemsMap = new Map(); - defaultItems.forEach(item => { - if (item.children) { - // If it's the "More" group, add children to map - item.children.forEach(child => { - allItemsMap.set(child.title, { ...child }); - }); - } else { - allItemsMap.set(item.title, { ...item }); - } + const { merged } = resolveSidebarItems(defaultItems, customization, { + cloneItems: true, + assembleMoreGroup: true }); - - const customizedItems = []; - - // Add main items in custom order - mainItems.forEach(title => { - const item = allItemsMap.get(title); - if (item) { - customizedItems.push(item); - } - }); - - // If there are items in moreItems, create the "More Features" group - if (moreItems && moreItems.length > 0) { - const moreGroup = { - title: 'core.navigation.groups.more', - icon: 'mdi-dots-horizontal', - children: [] - }; - - moreItems.forEach(title => { - const item = allItemsMap.get(title); - if (item) { - moreGroup.children.push(item); - } - }); - - customizedItems.push(moreGroup); - } - - return customizedItems; + return merged || defaultItems; } diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index 802664443..5a5037efb 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -5,18 +5,45 @@ 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'; import { useI18n, useModuleI18n } from '@/i18n/composables'; import defaultPluginIcon from '@/assets/images/plugin_icon.png'; -import { ref, computed, onMounted, reactive, inject, watch } from 'vue'; - +import { ref, computed, onMounted, reactive, watch } from 'vue'; +import { useRouter } from 'vue-router'; const commonStore = useCommonStore(); const { t } = useI18n(); const { tm } = useModuleI18n('features/extension'); +const router = useRouter(); + +// 检查指令冲突并提示 +const conflictDialog = reactive({ + show: false, + count: 0 +}); +const checkAndPromptConflicts = async () => { + try { + const res = await axios.get('/api/commands'); + if (res.data.status === 'ok') { + const conflicts = res.data.data.summary?.conflicts || 0; + if (conflicts > 0) { + conflictDialog.count = conflicts; + conflictDialog.show = true; + } + } + } catch (err) { + console.debug('Failed to check command conflicts:', err); + } +}; +const handleConflictConfirm = () => { + activeTab.value = 'commands'; +}; + const fileInput = ref(null); const activeTab = ref('installed'); const extension_data = reactive({ @@ -448,7 +475,9 @@ const pluginOn = async (extension) => { return; } toast(res.data.message, "success"); - getExtensions(); + await getExtensions(); + + await checkAndPromptConflicts(); } catch (err) { toast(err, "error"); } @@ -782,6 +811,8 @@ const newExtension = async () => { name: res.data.data.name, repo: res.data.data.repo || null }); + + await checkAndPromptConflicts(); }).catch((err) => { loading_.value = false; onLoadingDialogResult(2, err, -1); @@ -808,6 +839,8 @@ const newExtension = async () => { name: res.data.data.name, repo: res.data.data.repo || null }); + + await checkAndPromptConflicts(); }).catch((err) => { loading_.value = false; toast(tm('messages.installFailed') + " " + err, "error"); @@ -900,21 +933,29 @@ watch(marketSearch, (newVal) => { mdi-puzzle - {{ tm('tabs.installed') }} + {{ tm('tabs.installedPlugins') }} + + + mdi-server-network + {{ tm('tabs.installedMcpServers') }} mdi-store {{ tm('tabs.market') }} + + mdi-wrench + {{ tm('tabs.handlersOperation') }} +
- -
@@ -1118,6 +1159,24 @@ watch(marketSearch, (newVal) => { + + + + + + + + + + + + + + + + + + @@ -1544,6 +1603,34 @@ watch(marketSearch, (newVal) => { + + + + + mdi-alert-circle + {{ tm('conflicts.title') }} + + +
+ + {{ conflictDialog.count }} + + {{ tm('conflicts.pairs') }} +
+

+ {{ tm('conflicts.message') }} +

+
+ + + {{ tm('conflicts.later') }} + + {{ tm('conflicts.goToManage') }} + + +
+
+ diff --git a/packages/builtin_commands/commands/admin.py b/packages/builtin_commands/commands/admin.py index 2073f45a2..83d4b5974 100644 --- a/packages/builtin_commands/commands/admin.py +++ b/packages/builtin_commands/commands/admin.py @@ -71,6 +71,7 @@ class AdminCommands: event.set_result(MessageEventResult().message("此 SID 不在白名单内。")) async def update_dashboard(self, event: AstrMessageEvent): + """更新管理面板""" await event.send(MessageChain().message("正在尝试更新管理面板...")) await download_dashboard(version=f"v{VERSION}", latest=False) await event.send(MessageChain().message("管理面板更新完成。")) diff --git a/packages/builtin_commands/commands/help.py b/packages/builtin_commands/commands/help.py index 7f5b6c170..092fc59ec 100644 --- a/packages/builtin_commands/commands/help.py +++ b/packages/builtin_commands/commands/help.py @@ -3,6 +3,7 @@ import aiohttp from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.core.config.default import VERSION +from astrbot.core.star import command_management from astrbot.core.utils.io import get_dashboard_version @@ -21,6 +22,46 @@ class HelpCommand: except BaseException: return "" + async def _build_reserved_command_lines(self) -> list[str]: + """ + 使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。 + """ + try: + commands = await command_management.list_commands() + except BaseException: + return [] + + lines: list[str] = [] + hidden_commands = {"set", "unset", "websearch"} + + def walk(items: list[dict], indent: int = 0): + for item in items: + if not item.get("reserved") or not item.get("enabled"): + continue + # 仅展示顶级指令或指令组 + if item.get("type") == "sub_command": + continue + if item.get("parent_signature"): + continue + + effective = ( + item.get("effective_command") + or item.get("original_command") + or item.get("handler_name") + ) + if not effective: + continue + if effective in hidden_commands: + continue + + description = item.get("description") or "" + desc_text = f" - {description}" if description else "" + indent_prefix = " " * indent + lines.append(f"{indent_prefix}/{effective}{desc_text}") + + walk(commands) + return lines + async def help(self, event: AstrMessageEvent): """查看帮助""" notice = "" @@ -30,34 +71,18 @@ class HelpCommand: pass dashboard_version = await get_dashboard_version() + command_lines = await self._build_reserved_command_lines() + commands_section = ( + "\n".join(command_lines) if command_lines else "暂无启用的内置指令" + ) - msg = f"""AstrBot v{VERSION}(WebUI: {dashboard_version}) -内置指令: -[System] -/plugin: 查看插件、插件帮助 -/t2i: 开关文本转图片 -/tts: 开关文本转语音 -/sid: 获取会话 ID -/op: 管理员 -/wl: 白名单 -/dashboard_update: 更新管理面板(op) -/alter_cmd: 设置指令权限(op) - -[大模型] -/llm: 开启/关闭 LLM -/provider: 大模型提供商 -/model: 模型列表 -/ls: 对话列表 -/new: 创建新对话 -/groupnew 群号: 为群聊创建新对话(op) -/switch 序号: 切换对话 -/rename 新名字: 重命名当前对话 -/del: 删除当前会话对话(op) -/reset: 重置 LLM 会话 -/history: 当前对话的对话记录 -/persona: 人格情景(op) -/key: API Key(op) -/websearch: 网页搜索 -{notice}""" + msg_parts = [ + f"AstrBot v{VERSION}(WebUI: {dashboard_version})", + "内置指令:", + commands_section, + ] + if notice: + msg_parts.append(notice) + msg = "\n".join(msg_parts) event.set_result(MessageEventResult().message(msg).use_t2i(False)) diff --git a/packages/builtin_commands/main.py b/packages/builtin_commands/main.py index 291bed456..7809c4359 100644 --- a/packages/builtin_commands/main.py +++ b/packages/builtin_commands/main.py @@ -49,7 +49,7 @@ class Main(star.Star): @filter.command_group("tool") def tool(self): - pass + """函数工具管理""" @tool.command("ls") async def tool_ls(self, event: AstrMessageEvent): @@ -73,7 +73,7 @@ class Main(star.Star): @filter.command_group("plugin") def plugin(self): - pass + """插件管理""" @plugin.command("ls") async def plugin_ls(self, event: AstrMessageEvent): @@ -219,6 +219,7 @@ class Main(star.Star): @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("dashboard_update") async def update_dashboard(self, event: AstrMessageEvent): + """更新管理面板""" await self.admin_c.update_dashboard(event) @filter.command("set") diff --git a/packages/python_interpreter/main.py b/packages/python_interpreter/main.py index 98496157a..afbef7560 100644 --- a/packages/python_interpreter/main.py +++ b/packages/python_interpreter/main.py @@ -249,7 +249,7 @@ class Main(star.Star): @filter.command_group("pi") def pi(self): - pass + """代码执行器配置""" @pi.command("absdir") async def pi_absdir(self, event: AstrMessageEvent, path: str = ""): diff --git a/packages/reminder/main.py b/packages/reminder/main.py index 8f61e02fe..62af7ae56 100644 --- a/packages/reminder/main.py +++ b/packages/reminder/main.py @@ -179,7 +179,7 @@ class Main(star.Star): @filter.command_group("reminder") def reminder(self): - """The command group of the reminder.""" + """待办提醒""" async def get_upcoming_reminders(self, unified_msg_origin: str): """Get upcoming reminders.""" diff --git a/packages/web_searcher/main.py b/packages/web_searcher/main.py index 118ef2483..4745cd0c0 100644 --- a/packages/web_searcher/main.py +++ b/packages/web_searcher/main.py @@ -185,6 +185,7 @@ class Main(star.Star): @filter.command("websearch") async def websearch(self, event: AstrMessageEvent, oper: str | None = None): + """网页搜索指令(已废弃)""" event.set_result( MessageEventResult().message( "此指令已经被废弃,请在 WebUI 中开启或关闭网页搜索功能。", diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index f5439e9d5..969f0da6d 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -160,6 +160,34 @@ async def test_plugins(app: Quart, authenticated_header: dict): assert exists is False, "插件 astrbot_plugin_essential 未成功卸载" +@pytest.mark.asyncio +async def test_commands_api(app: Quart, authenticated_header: dict): + """Tests the command management API endpoints.""" + test_client = app.test_client() + + # GET /api/commands - list commands + response = await test_client.get("/api/commands", headers=authenticated_header) + assert response.status_code == 200 + data = await response.get_json() + assert data["status"] == "ok" + assert "items" in data["data"] + assert "summary" in data["data"] + summary = data["data"]["summary"] + assert "total" in summary + assert "disabled" in summary + assert "conflicts" in summary + + # GET /api/commands/conflicts - list conflicts + response = await test_client.get( + "/api/commands/conflicts", headers=authenticated_header + ) + assert response.status_code == 200 + data = await response.get_json() + assert data["status"] == "ok" + # conflicts is a list + assert isinstance(data["data"], list) + + @pytest.mark.asyncio async def test_check_update(app: Quart, authenticated_header: dict): test_client = app.test_client() From 58e32b7b708e9041ececa3902174e0d692c50e8b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:12:05 +0800 Subject: [PATCH 19/20] fix: inverted logic in segmented reply LLM-only filter (#4071) * Initial plan * Fix: Correct inverted logic in is_seg_reply_required for only_llm_result option Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> --- astrbot/core/pipeline/respond/stage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index bfbcaf33a..60ab168b3 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -119,7 +119,7 @@ class RespondStage(Stage): if (result := event.get_result()) is None: return False - if self.only_llm_result and result.is_llm_result(): + if self.only_llm_result and not result.is_llm_result(): return False if event.get_platform_name() in [ From fd05b0bf09a8875def2be61587e61feb136a9d0f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 17 Dec 2025 13:26:18 +0800 Subject: [PATCH 20/20] docs: update contributing guidelines to include code style and formatting instructions --- CONTRIBUTING.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e261bfa3..47404d563 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,20 @@ - 请使用英文描述您的 PR。 - 标题请使用 `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` 等语义化前缀,并简要描述更改内容。如:`fix: correct login page typo`。 +#### 代码规范 + +##### Core + +我们使用 Ruff 作为代码格式化和静态分析工具。在提交代码之前,请运行以下命令以确保代码符合规范: + +```bash +ruff format . +ruff check . +``` + +如果您使用 VSCode,可以安装 `Ruff` 插件。 + + ## Contributing Guide First off, thanks for taking the time to contribute! ❤️ @@ -62,4 +76,15 @@ We use the `fix/` prefix for bug fixes and the `feat/` prefix for new features. #### PR Description - Please use English to describe your PR. -- Use semantic prefixes like `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` in the title, followed by a brief description of the changes, e.g., `fix: correct login page typo`. \ No newline at end of file +- Use semantic prefixes like `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` in the title, followed by a brief description of the changes, e.g., `fix: correct login page typo`. + +#### Code Style + +##### Core + +We use Ruff as our code formatter and static analysis tool. Before submitting your code, please run the following commands to ensure your code adheres to the style guidelines: + +```bash +ruff format . +ruff check . +```