diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index da3add17c6..60e9fc4e50 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -250,6 +250,11 @@ async def list_command_conflicts() -> list[dict[str, Any]]: # Internal helpers ---------------------------------------------------------- +def _is_plugin_activated(desc: CommandDescriptor) -> bool: + plugin_meta = star_map.get(desc.module_path) + return bool(plugin_meta.activated) if plugin_meta else True + + def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]: """收集指令,按需包含子指令。""" descriptors: list[CommandDescriptor] = [] @@ -465,7 +470,7 @@ def _group_conflicts( ) -> dict[str, list[CommandDescriptor]]: conflicts: dict[str, list[CommandDescriptor]] = defaultdict(list) for desc in descriptors: - if desc.effective_command and desc.enabled: + if desc.effective_command and desc.enabled and _is_plugin_activated(desc): conflicts[desc.effective_command].append(desc) return {k: v for k, v in conflicts.items() if len(v) > 1} @@ -531,6 +536,7 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]: "aliases": desc.aliases, "permission": desc.permission, "enabled": desc.enabled, + "plugin_activated": _is_plugin_activated(desc), "is_group": desc.is_group, "has_conflict": desc.has_conflict, "reserved": desc.reserved, diff --git a/dashboard/src/components/extension/componentPanel/components/CommandTable.vue b/dashboard/src/components/extension/componentPanel/components/CommandTable.vue index be2ae9892d..2c73611519 100644 --- a/dashboard/src/components/extension/componentPanel/components/CommandTable.vue +++ b/dashboard/src/components/extension/componentPanel/components/CommandTable.vue @@ -65,8 +65,13 @@ const getPermissionLabel = (permission: string): string => { } }; +const isPluginInactive = (cmd: CommandItem) => !cmd.plugin_activated; + // 获取状态信息 const getStatusInfo = (cmd: CommandItem): StatusInfo => { + if (isPluginInactive(cmd)) { + return { text: tm('status.pluginDisabled'), color: 'default', variant: 'outlined' }; + } if (cmd.has_conflict) { return { text: tm('status.conflict'), color: 'warning', variant: 'flat' }; } @@ -88,6 +93,9 @@ const getRowProps = ({ item }: { item: CommandItem }) => { if (item.is_group) { classes.push('group-row'); } + if (isPluginInactive(item)) { + classes.push('plugin-inactive-row'); + } return classes.length > 0 ? { class: classes.join(' ') } : {}; }; @@ -154,6 +162,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => { :color="getPermissionColor(item.permission)" size="small" class="font-weight-medium cursor-pointer" + :disabled="isPluginInactive(item)" link > {{ getPermissionLabel(item.permission) }} @@ -198,25 +207,27 @@ const getRowProps = ({ item }: { item: CommandItem }) => { icon size="small" color="success" + :disabled="isPluginInactive(item)" @click="emit('toggle-command', item)" > mdi-play - {{ tm('tooltips.enable') }} + {{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.enable') }} mdi-pause - {{ tm('tooltips.disable') }} + {{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.disable') }} - + mdi-pencil - {{ tm('tooltips.rename') }} + {{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.rename') }} @@ -285,5 +296,32 @@ code.sub-command-code { .cursor-pointer { cursor: pointer; } + +.v-data-table .plugin-inactive-row, +.v-data-table .plugin-inactive-row td, +.v-data-table .plugin-inactive-row .v-data-table__td { + background-color: rgba(var(--v-theme-on-surface), 0.06) !important; + color: rgba(var(--v-theme-on-surface), 0.72) !important; +} + +.v-data-table .plugin-inactive-row:hover, +.v-data-table .plugin-inactive-row:hover td, +.v-data-table .plugin-inactive-row:hover .v-data-table__td { + background-color: rgba(var(--v-theme-on-surface), 0.09) !important; +} + +.v-data-table .plugin-inactive-row .v-chip, +.v-data-table .plugin-inactive-row code, +.v-data-table .plugin-inactive-row .text-body-2, +.v-data-table .plugin-inactive-row .text-subtitle-1 { + color: rgba(var(--v-theme-on-surface), 0.72) !important; + filter: grayscale(1); + opacity: 1; +} + +.v-data-table .plugin-inactive-row .v-btn-group .v-btn:disabled, +.v-data-table .plugin-inactive-row .v-chip.cursor-pointer.v-chip--disabled { + cursor: not-allowed !important; +} diff --git a/dashboard/src/components/extension/componentPanel/types.ts b/dashboard/src/components/extension/componentPanel/types.ts index cb96f33a7a..7f38194b3b 100644 --- a/dashboard/src/components/extension/componentPanel/types.ts +++ b/dashboard/src/components/extension/componentPanel/types.ts @@ -19,6 +19,7 @@ export interface CommandItem { aliases: string[]; permission: PermissionType; enabled: boolean; + plugin_activated: boolean; is_group: boolean; has_conflict: boolean; reserved: boolean; diff --git a/dashboard/src/i18n/locales/en-US/features/command.json b/dashboard/src/i18n/locales/en-US/features/command.json index 95ecc9891c..28d6d3b6f6 100644 --- a/dashboard/src/i18n/locales/en-US/features/command.json +++ b/dashboard/src/i18n/locales/en-US/features/command.json @@ -29,6 +29,7 @@ "status": { "enabled": "Enabled", "disabled": "Disabled", + "pluginDisabled": "Plugin off", "conflict": "Conflict" }, "permission": { @@ -39,7 +40,8 @@ "enable": "Enable command", "disable": "Disable command", "rename": "Rename command", - "viewDetails": "View details" + "viewDetails": "View details", + "pluginInactive": "Enable the plugin first" }, "dialogs": { "rename": { diff --git a/dashboard/src/i18n/locales/ru-RU/features/command.json b/dashboard/src/i18n/locales/ru-RU/features/command.json index 7d887c8ef7..049ad0a147 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/command.json +++ b/dashboard/src/i18n/locales/ru-RU/features/command.json @@ -29,6 +29,7 @@ "status": { "enabled": "Активна", "disabled": "Отключена", + "pluginDisabled": "Плагин отключён", "conflict": "Конфликт" }, "permission": { @@ -39,7 +40,8 @@ "enable": "Включить", "disable": "Выключить", "rename": "Переименовать", - "viewDetails": "Подробности" + "viewDetails": "Подробности", + "pluginInactive": "Сначала включите плагин" }, "dialogs": { "rename": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/command.json b/dashboard/src/i18n/locales/zh-CN/features/command.json index ccaf3434ef..4a3a4578f0 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/command.json +++ b/dashboard/src/i18n/locales/zh-CN/features/command.json @@ -29,6 +29,7 @@ "status": { "enabled": "已启用", "disabled": "已禁用", + "pluginDisabled": "未启用", "conflict": "有冲突" }, "permission": { @@ -39,7 +40,8 @@ "enable": "启用指令", "disable": "禁用指令", "rename": "重命名指令", - "viewDetails": "查看详情" + "viewDetails": "查看详情", + "pluginInactive": "请先启用所属插件" }, "dialogs": { "rename": { diff --git a/tests/unit/test_command_plugin_activation.py b/tests/unit/test_command_plugin_activation.py new file mode 100644 index 0000000000..396e18db65 --- /dev/null +++ b/tests/unit/test_command_plugin_activation.py @@ -0,0 +1,70 @@ +"""Commands from disabled plugins should not look enabled in the dashboard.""" + +from types import SimpleNamespace + +from astrbot.core.star.command_management import ( + CommandDescriptor, + _descriptor_to_dict, + _group_conflicts, + _is_plugin_activated, + star_map, +) + + +def _descriptor(module_path: str, *, enabled: bool = True) -> CommandDescriptor: + return CommandDescriptor( + handler=SimpleNamespace(), # type: ignore[arg-type] + module_path=module_path, + enabled=enabled, + effective_command="demo", + ) + + +def test_plugin_activation_is_serialized_without_mutating_enabled(): + original = dict(star_map) + try: + star_map.clear() + star_map["data.plugins.foo.main"] = SimpleNamespace(activated=True) + star_map["data.plugins.bar.main"] = SimpleNamespace(activated=False) + + active = _descriptor("data.plugins.foo.main", enabled=True) + inactive = _descriptor("data.plugins.bar.main", enabled=True) + unknown = _descriptor("data.plugins.missing.main", enabled=True) + + assert _is_plugin_activated(active) is True + assert _is_plugin_activated(inactive) is False + assert _is_plugin_activated(unknown) is True + assert active.enabled is True + assert inactive.enabled is True + assert unknown.enabled is True + + active_dict = _descriptor_to_dict(active) + inactive_dict = _descriptor_to_dict(inactive) + unknown_dict = _descriptor_to_dict(unknown) + + assert active_dict["enabled"] is True + assert inactive_dict["enabled"] is True + assert unknown_dict["enabled"] is True + assert active_dict["plugin_activated"] is True + assert inactive_dict["plugin_activated"] is False + assert unknown_dict["plugin_activated"] is True + finally: + star_map.clear() + star_map.update(original) + + +def test_inactive_plugin_commands_are_excluded_from_conflicts(): + original = dict(star_map) + try: + star_map.clear() + star_map["data.plugins.foo.main"] = SimpleNamespace(activated=True) + star_map["data.plugins.bar.main"] = SimpleNamespace(activated=False) + + live = _descriptor("data.plugins.foo.main") + off = _descriptor("data.plugins.bar.main") + conflicts = _group_conflicts([live, off]) + + assert conflicts == {} + finally: + star_map.clear() + star_map.update(original)