diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index 8d36e9aee..2543a4172 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -26,10 +26,11 @@ class CommandDescriptor: plugin_display_name: str | None = None module_path: str = "" description: str = "" - command_type: str = "command" + command_type: str = "command" # "command" | "group" | "sub_command" raw_command_name: str | None = None current_fragment: str | None = None parent_signature: str = "" + parent_group_handler: str = "" # 父指令组的 handler_full_name original_command: str | None = None effective_command: str | None = None aliases: list[str] = field(default_factory=list) @@ -39,6 +40,7 @@ class CommandDescriptor: is_sub_command: bool = False config: CommandConfig | None = None has_conflict: bool = False + sub_commands: list[CommandDescriptor] = field(default_factory=list) async def sync_command_configs() -> None: @@ -127,7 +129,7 @@ async def rename_command( async def list_commands() -> list[dict[str, Any]]: - descriptors = _collect_raw_descriptors() + descriptors = _collect_all_descriptors() config_records = await db_helper.get_command_configs() config_map = {cfg.handler_full_name: cfg for cfg in config_records} @@ -135,7 +137,7 @@ async def list_commands() -> list[dict[str, Any]]: if cfg := config_map.get(desc.handler_full_name): _bind_descriptor_with_config(desc, cfg) - # 检测冲突:按 effective_command 分组 + # 检测冲突:按 effective_command 分组(只检测已启用的指令) conflict_groups: dict[str, list[CommandDescriptor]] = defaultdict(list) for desc in descriptors: if desc.effective_command and desc.enabled: @@ -147,10 +149,36 @@ async def list_commands() -> list[dict[str, Any]]: for desc in group: conflict_handler_names.add(desc.handler_full_name) - result = [] + # 构建层级结构:将子指令挂载到父指令组 + group_map: dict[str, CommandDescriptor] = {} + sub_commands: list[CommandDescriptor] = [] + root_commands: list[CommandDescriptor] = [] + for desc in descriptors: desc.has_conflict = desc.handler_full_name in conflict_handler_names + if desc.is_group: + group_map[desc.handler_full_name] = desc + elif desc.is_sub_command: + sub_commands.append(desc) + else: + root_commands.append(desc) + + # 将子指令挂载到对应的指令组 + for sub in sub_commands: + sub.has_conflict = sub.handler_full_name in conflict_handler_names + if sub.parent_group_handler and sub.parent_group_handler in group_map: + group_map[sub.parent_group_handler].sub_commands.append(sub) + else: + # 如果找不到父指令组,作为独立指令处理 + root_commands.append(sub) + + # 合并结果:指令组(含子指令)+ 普通指令 + result = [] + for desc in group_map.values(): result.append(_descriptor_to_dict(desc)) + for desc in root_commands: + result.append(_descriptor_to_dict(desc)) + return result @@ -193,6 +221,7 @@ async def list_command_conflicts() -> list[dict[str, Any]]: def _collect_raw_descriptors() -> list[CommandDescriptor]: + """收集所有根级指令(不含子指令)。""" descriptors: list[CommandDescriptor] = [] for handler in star_handlers_registry: desc = _build_descriptor(handler) @@ -202,6 +231,17 @@ def _collect_raw_descriptors() -> list[CommandDescriptor]: return descriptors +def _collect_all_descriptors() -> list[CommandDescriptor]: + """收集所有指令,包括子指令。""" + descriptors: list[CommandDescriptor] = [] + for handler in star_handlers_registry: + desc = _build_descriptor(handler) + if not desc: + continue + descriptors.append(desc) + return descriptors + + def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: filter_ref = _locate_primary_filter(handler) if filter_ref is None: @@ -213,12 +253,20 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: ) or handler.handler_module_path plugin_display = plugin_meta.display_name if plugin_meta else None + is_sub_command = bool(handler.extras_configs.get("sub_command")) + parent_group_handler = "" + if isinstance(filter_ref, CommandFilter): raw_fragment = getattr( filter_ref, "_original_command_name", filter_ref.command_name ) current_fragment = filter_ref.command_name parent_signature = (filter_ref.parent_command_names or [""])[0].strip() + # 如果是子指令,尝试找到父指令组的 handler_full_name + if is_sub_command and parent_signature: + parent_group_handler = _find_parent_group_handler( + handler.handler_module_path, parent_signature + ) else: raw_fragment = getattr( filter_ref, "_original_group_name", filter_ref.group_name @@ -229,6 +277,14 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: original_command = _compose_command(parent_signature, raw_fragment) effective_command = _compose_command(parent_signature, current_fragment) + # 确定 command_type + if isinstance(filter_ref, CommandGroupFilter): + command_type = "group" + elif is_sub_command: + command_type = "sub_command" + else: + command_type = "command" + descriptor = CommandDescriptor( handler=handler, filter_ref=filter_ref, @@ -238,19 +294,18 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: plugin_display_name=plugin_display, module_path=handler.handler_module_path, description=handler.desc or "", - command_type="group" - if isinstance(filter_ref, CommandGroupFilter) - else "command", + command_type=command_type, raw_command_name=raw_fragment, current_fragment=current_fragment, parent_signature=parent_signature, + parent_group_handler=parent_group_handler, original_command=original_command, effective_command=effective_command, aliases=sorted(getattr(filter_ref, "alias", set())), permission=_determine_permission(handler), enabled=handler.enabled, is_group=isinstance(filter_ref, CommandGroupFilter), - is_sub_command=bool(handler.extras_configs.get("sub_command")), + is_sub_command=is_sub_command, ) return descriptor @@ -291,6 +346,22 @@ def _resolve_group_parent_signature(group_filter: CommandGroupFilter) -> str: return " ".join(reversed(signatures)).strip() +def _find_parent_group_handler(module_path: str, parent_signature: str) -> str: + """根据模块路径和父级签名,找到对应的指令组 handler_full_name。""" + parent_sig_normalized = parent_signature.strip() + for handler in star_handlers_registry: + if handler.handler_module_path != module_path: + continue + filter_ref = _locate_primary_filter(handler) + if not isinstance(filter_ref, CommandGroupFilter): + continue + # 检查该指令组的完整指令名是否匹配 parent_signature + group_names = filter_ref.get_complete_command_names() + if parent_sig_normalized in group_names: + return handler.handler_full_name + return "" + + def _compose_command(parent_signature: str, fragment: str | None) -> str: fragment = (fragment or "").strip() parent_signature = parent_signature.strip() @@ -353,7 +424,7 @@ def _is_command_in_use( def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]: - return { + result = { "handler_full_name": desc.handler_full_name, "handler_name": desc.handler_name, "plugin": desc.plugin_name, @@ -362,6 +433,7 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]: "description": desc.description, "type": desc.command_type, "parent_signature": desc.parent_signature, + "parent_group_handler": desc.parent_group_handler, "original_command": desc.original_command, "current_fragment": desc.current_fragment, "effective_command": desc.effective_command, @@ -371,3 +443,9 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]: "is_group": desc.is_group, "has_conflict": desc.has_conflict, } + # 如果是指令组,包含子指令列表 + if desc.is_group and desc.sub_commands: + result["sub_commands"] = [_descriptor_to_dict(sub) for sub in desc.sub_commands] + else: + result["sub_commands"] = [] + return result diff --git a/dashboard/src/i18n/locales/en-US/features/command.json b/dashboard/src/i18n/locales/en-US/features/command.json index 32f92813e..35e1e3b3d 100644 --- a/dashboard/src/i18n/locales/en-US/features/command.json +++ b/dashboard/src/i18n/locales/en-US/features/command.json @@ -13,6 +13,7 @@ "table": { "headers": { "command": "Command", + "type": "Type", "plugin": "Plugin", "description": "Description", "permission": "Permission", @@ -20,6 +21,12 @@ "actions": "Actions" } }, + "type": { + "command": "Command", + "group": "Group", + "subCommand": "Sub-command" + }, + "subCommandCount": "sub-commands", "status": { "enabled": "Enabled", "disabled": "Disabled", @@ -44,10 +51,13 @@ }, "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" @@ -73,6 +83,7 @@ "disabled": "Disabled", "conflict": "Conflict", "byPlugin": "Filter by plugin", + "byType": "Filter by type", "byPermission": "Filter by permission", "byStatus": "Filter by status" } diff --git a/dashboard/src/i18n/locales/zh-CN/features/command.json b/dashboard/src/i18n/locales/zh-CN/features/command.json index 58abf6fb6..325e8a2b8 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/command.json +++ b/dashboard/src/i18n/locales/zh-CN/features/command.json @@ -13,6 +13,7 @@ "table": { "headers": { "command": "指令", + "type": "类型", "plugin": "所属插件", "description": "描述", "permission": "权限", @@ -20,6 +21,12 @@ "actions": "操作" } }, + "type": { + "command": "指令", + "group": "指令组", + "subCommand": "子指令" + }, + "subCommandCount": "个子指令", "status": { "enabled": "已启用", "disabled": "已禁用", @@ -44,10 +51,13 @@ }, "details": { "title": "指令详情", + "type": "指令类型", "handler": "处理函数", "module": "模块路径", "originalCommand": "原始指令", "effectiveCommand": "生效指令", + "parentGroup": "所属指令组", + "subCommands": "子指令列表", "aliases": "别名", "permission": "权限要求", "conflictStatus": "冲突状态" @@ -73,6 +83,7 @@ "disabled": "已禁用", "conflict": "有冲突", "byPlugin": "按插件筛选", + "byType": "按类型筛选", "byPermission": "按权限筛选", "byStatus": "按状态筛选" } diff --git a/dashboard/src/views/CommandPage.vue b/dashboard/src/views/CommandPage.vue index c9a4b84bc..139250548 100644 --- a/dashboard/src/views/CommandPage.vue +++ b/dashboard/src/views/CommandPage.vue @@ -10,8 +10,9 @@ interface CommandItem { plugin_display_name: string | null; module_path: string; description: string; - type: string; + type: string; // "command" | "group" | "sub_command" parent_signature: string; + parent_group_handler: string; original_command: string; current_fragment: string; effective_command: string; @@ -20,6 +21,7 @@ interface CommandItem { enabled: boolean; is_group: boolean; has_conflict: boolean; + sub_commands: CommandItem[]; } interface CommandSummary { @@ -49,6 +51,10 @@ const searchQuery = ref(''); const pluginFilter = ref('all'); const permissionFilter = ref('all'); const statusFilter = ref('all'); +const typeFilter = ref('all'); + +// Track expanded groups +const expandedGroups = ref>(new Set()); // Rename dialog const renameDialog = reactive({ @@ -66,12 +72,13 @@ const detailsDialog = reactive({ // Table headers const commandHeaders = computed(() => [ - { title: tm('table.headers.command'), key: 'effective_command', width: '180px' }, + { title: tm('table.headers.command'), key: 'effective_command', width: '150px' }, + { title: tm('table.headers.type'), key: 'type', sortable: false, width: '110px' }, { title: tm('table.headers.plugin'), key: 'plugin', width: '140px' }, - { title: tm('table.headers.description'), key: 'description', sortable: false, maxWidth: '260px' }, + { title: tm('table.headers.description'), key: 'description', sortable: false, maxWidth: '240px' }, { title: tm('table.headers.permission'), key: 'permission', sortable: false, width: '100px' }, - { title: tm('table.headers.status'), key: 'enabled', sortable: false, width: '120px' }, - { title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '160px' } + { title: tm('table.headers.status'), key: 'enabled', sortable: false, width: '100px' }, + { title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '140px' } ]); // Computed: unique plugins for filter @@ -80,66 +87,126 @@ const availablePlugins = computed(() => { return Array.from(plugins).sort(); }); -// Computed: filtered commands -const filteredCommands = computed(() => { - let result = commands.value; - - if (searchQuery.value) { - const query = searchQuery.value.toLowerCase(); - result = result.filter(cmd => +// Helper: check if a command matches filters +const matchesFilters = (cmd: CommandItem, query: string): boolean => { + // Search filter + if (query) { + const matchesSearch = cmd.effective_command?.toLowerCase().includes(query) || cmd.description?.toLowerCase().includes(query) || - cmd.plugin?.toLowerCase().includes(query) - ); + cmd.plugin?.toLowerCase().includes(query); + if (!matchesSearch) return false; } - if (pluginFilter.value !== 'all') { - result = result.filter(cmd => cmd.plugin === pluginFilter.value); + // Plugin filter + if (pluginFilter.value !== 'all' && cmd.plugin !== pluginFilter.value) { + return false; } + // Permission filter if (permissionFilter.value !== 'all') { if (permissionFilter.value === 'everyone') { - // "所有人"筛选:包括 everyone 和 member 权限(当前 member 权限实际作用与 everyone 相同) - result = result.filter(cmd => cmd.permission === 'everyone' || cmd.permission === 'member'); - } else { - result = result.filter(cmd => cmd.permission === permissionFilter.value); + if (cmd.permission !== 'everyone' && cmd.permission !== 'member') return false; + } else if (cmd.permission !== permissionFilter.value) { + return false; } } + // Status filter if (statusFilter.value !== 'all') { - if (statusFilter.value === 'enabled') { - result = result.filter(cmd => cmd.enabled); - } else if (statusFilter.value === 'disabled') { - result = result.filter(cmd => !cmd.enabled); - } else if (statusFilter.value === 'conflict') { - result = result.filter(cmd => cmd.has_conflict); - } + 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; } - // Sort: conflict commands first, grouped by effective_command + // Type filter + 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; +}; + +// Computed: filtered commands with hierarchy support +const filteredCommands = computed(() => { + const query = searchQuery.value.toLowerCase(); + const result: CommandItem[] = []; const conflictCmds: CommandItem[] = []; const normalCmds: CommandItem[] = []; - - const conflictGroupMap: Map = new Map(); - for (const cmd of result) { - if (cmd.has_conflict) { - const key = cmd.effective_command || ''; - if (!conflictGroupMap.has(key)) { - conflictGroupMap.set(key, []); + + for (const cmd of commands.value) { + // For groups, check if group or any sub-command matches + if (cmd.is_group) { + const groupMatches = matchesFilters(cmd, query); + const matchingSubCmds = (cmd.sub_commands || []).filter(sub => matchesFilters(sub, query)); + + // If group matches or has matching sub-commands, include it + if (groupMatches || matchingSubCmds.length > 0) { + if (cmd.has_conflict) { + conflictCmds.push(cmd); + } else { + normalCmds.push(cmd); + } + + // If group is expanded, add matching sub-commands + 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') { + // Regular commands (not sub-commands, they're handled via groups) + if (matchesFilters(cmd, query)) { + if (cmd.has_conflict) { + conflictCmds.push(cmd); + } else { + normalCmds.push(cmd); + } } - conflictGroupMap.get(key)!.push(cmd); - } else { - normalCmds.push(cmd); } } - - for (const [_, group] of conflictGroupMap) { - conflictCmds.push(...group); - } + + // Sort conflicts by effective_command to group them together + conflictCmds.sort((a, b) => (a.effective_command || '').localeCompare(b.effective_command || '')); return [...conflictCmds, ...normalCmds]; }); +// Toggle group expansion +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); + } +}; + +// Check if group is expanded +const isGroupExpanded = (cmd: CommandItem): boolean => { + return expandedGroups.value.has(cmd.handler_full_name); +}; + +// Get type display info +const getTypeInfo = (type: string) => { + switch (type) { + case 'group': + return { text: tm('type.group'), color: 'info', icon: 'mdi-folder-outline' }; + case 'sub_command': + return { text: tm('type.subCommand'), color: 'secondary', icon: 'mdi-subdirectory-arrow-right' }; + default: + return { text: tm('type.command'), color: 'primary', icon: 'mdi-console-line' }; + } +}; + // Toast helper const toast = (message: string, color: string = 'success') => { snackbar.message = message; @@ -250,12 +317,19 @@ const getStatusInfo = (cmd: CommandItem) => { return { text: tm('status.disabled'), color: 'error', variant: 'outlined' as const }; }; -// Get row props for conflict highlighting +// Get row props for conflict highlighting and sub-command styling const getRowProps = ({ item }: { item: CommandItem }) => { + const classes: string[] = []; if (item.has_conflict) { - return { class: 'conflict-row' }; + classes.push('conflict-row'); } - return {}; + 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(' ') } : {}; }; onMounted(async () => { @@ -270,7 +344,7 @@ onMounted(async () => { - + { hide-details /> - + + + + +
+ + + {{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }} + + +
- {{ item.effective_command }} + {{ item.effective_command }} + + {{ item.sub_commands.length }} {{ tm('subCommandCount') }} +
+ + @@ -505,6 +627,19 @@ onMounted(async () => { {{ tm('dialogs.details.title') }} + + {{ tm('dialogs.details.type') }} + + + {{ getTypeInfo(detailsDialog.command.type).icon }} + {{ getTypeInfo(detailsDialog.command.type).text }} + + + {{ tm('dialogs.details.handler') }} {{ detailsDialog.command.handler_name }} @@ -521,6 +656,10 @@ onMounted(async () => { {{ tm('dialogs.details.effectiveCommand') }} {{ detailsDialog.command.effective_command }} + + {{ tm('dialogs.details.parentGroup') }} + {{ detailsDialog.command.parent_signature }} + {{ tm('dialogs.details.aliases') }} @@ -529,6 +668,21 @@ onMounted(async () => { + + {{ tm('dialogs.details.subCommands') }} + +
+ + {{ sub.current_fragment }} + +
+
+
{{ tm('dialogs.details.permission') }} @@ -567,6 +721,11 @@ code { border-radius: 4px; font-size: 0.9em; } + +code.sub-command-code { + background-color: rgba(var(--v-theme-secondary), 0.1); + color: rgb(var(--v-theme-secondary)); +}