From 644986de4f5685a34256db28c5329c8eb0cf276a Mon Sep 17 00:00:00 2001 From: chufeng Date: Thu, 13 Aug 2026 19:33:53 +0800 Subject: [PATCH 1/2] fix: gray out commands of disabled plugins in behavior manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show inactive-plugin commands as 未启用, dim the whole row with theme on-surface colors, and disable row actions. Fixes #9562 --- astrbot/core/star/command_management.py | 17 +++++++ .../components/CommandTable.vue | 51 ++++++++++++++++--- .../extension/componentPanel/types.ts | 1 + .../i18n/locales/en-US/features/command.json | 4 +- .../i18n/locales/ru-RU/features/command.json | 4 +- .../i18n/locales/zh-CN/features/command.json | 4 +- tests/unit/test_command_plugin_activation.py | 33 ++++++++++++ 7 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 tests/unit/test_command_plugin_activation.py diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index da3add17c6..0037e87971 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -189,6 +189,7 @@ async def list_commands() -> list[dict[str, Any]]: descriptors = _collect_descriptors(include_sub_commands=True) config_records = await db_helper.get_command_configs() _bind_configs_to_descriptors(descriptors, config_records) + _apply_plugin_activation_to_descriptors(descriptors) conflict_groups = _group_conflicts(descriptors) conflict_handler_names: set[str] = { @@ -228,6 +229,7 @@ async def list_command_conflicts() -> list[dict[str, Any]]: descriptors = _collect_descriptors(include_sub_commands=False) config_records = await db_helper.get_command_configs() _bind_configs_to_descriptors(descriptors, config_records) + _apply_plugin_activation_to_descriptors(descriptors) conflict_groups = _group_conflicts(descriptors) details = [ @@ -250,6 +252,20 @@ 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 _apply_plugin_activation_to_descriptors( + descriptors: list[CommandDescriptor], +) -> None: + """Keep per-command config, but treat inactive-plugin commands as not live.""" + for desc in descriptors: + if not _is_plugin_activated(desc): + desc.enabled = False + + def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]: """收集指令,按需包含子指令。""" descriptors: list[CommandDescriptor] = [] @@ -531,6 +547,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..2ed4d98768 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 === false; + // 获取状态信息 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,30 +207,32 @@ 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') }} - + mdi-information - {{ tm('tooltips.viewDetails') }} + {{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.viewDetails') }} @@ -285,5 +296,33 @@ 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, +.v-data-table .plugin-inactive-row .v-chip.cursor-pointer { + cursor: not-allowed !important; + pointer-events: none !important; +} diff --git a/dashboard/src/components/extension/componentPanel/types.ts b/dashboard/src/components/extension/componentPanel/types.ts index cb96f33a7a..c8b2595a3f 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..e004ddd79f --- /dev/null +++ b/tests/unit/test_command_plugin_activation.py @@ -0,0 +1,33 @@ +"""Commands from disabled plugins should not look enabled in the dashboard.""" + +from types import SimpleNamespace + + +def test_inactive_plugin_commands_are_not_effectively_enabled(): + from astrbot.core.star.command_management import ( + _apply_plugin_activation_to_descriptors, + _is_plugin_activated, + star_map, + ) + + 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 = SimpleNamespace(module_path="data.plugins.foo.main", enabled=True) + inactive = SimpleNamespace(module_path="data.plugins.bar.main", enabled=True) + unknown = SimpleNamespace(module_path="data.plugins.missing.main", enabled=True) + + _apply_plugin_activation_to_descriptors([active, inactive, unknown]) + + 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 False + assert unknown.enabled is True + finally: + star_map.clear() + star_map.update(original) From 680388546ac3f04f02624119695049f34f092dcd Mon Sep 17 00:00:00 2001 From: chufeng Date: Fri, 14 Aug 2026 11:15:48 +0800 Subject: [PATCH 2/2] fix: address review comments on disabled-plugin commands Keep the stored command enabled flag unchanged and expose plugin activation separately. Allow viewing details, keep hover tooltips, and cover serialized plugin_activated in unit tests. --- astrbot/core/star/command_management.py | 13 +--- .../components/CommandTable.vue | 11 ++-- .../extension/componentPanel/types.ts | 2 +- tests/unit/test_command_plugin_activation.py | 59 +++++++++++++++---- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index 0037e87971..60e9fc4e50 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -189,7 +189,6 @@ async def list_commands() -> list[dict[str, Any]]: descriptors = _collect_descriptors(include_sub_commands=True) config_records = await db_helper.get_command_configs() _bind_configs_to_descriptors(descriptors, config_records) - _apply_plugin_activation_to_descriptors(descriptors) conflict_groups = _group_conflicts(descriptors) conflict_handler_names: set[str] = { @@ -229,7 +228,6 @@ async def list_command_conflicts() -> list[dict[str, Any]]: descriptors = _collect_descriptors(include_sub_commands=False) config_records = await db_helper.get_command_configs() _bind_configs_to_descriptors(descriptors, config_records) - _apply_plugin_activation_to_descriptors(descriptors) conflict_groups = _group_conflicts(descriptors) details = [ @@ -257,15 +255,6 @@ def _is_plugin_activated(desc: CommandDescriptor) -> bool: return bool(plugin_meta.activated) if plugin_meta else True -def _apply_plugin_activation_to_descriptors( - descriptors: list[CommandDescriptor], -) -> None: - """Keep per-command config, but treat inactive-plugin commands as not live.""" - for desc in descriptors: - if not _is_plugin_activated(desc): - desc.enabled = False - - def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]: """收集指令,按需包含子指令。""" descriptors: list[CommandDescriptor] = [] @@ -481,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} diff --git a/dashboard/src/components/extension/componentPanel/components/CommandTable.vue b/dashboard/src/components/extension/componentPanel/components/CommandTable.vue index 2ed4d98768..2c73611519 100644 --- a/dashboard/src/components/extension/componentPanel/components/CommandTable.vue +++ b/dashboard/src/components/extension/componentPanel/components/CommandTable.vue @@ -65,7 +65,7 @@ const getPermissionLabel = (permission: string): string => { } }; -const isPluginInactive = (cmd: CommandItem) => cmd.plugin_activated === false; +const isPluginInactive = (cmd: CommandItem) => !cmd.plugin_activated; // 获取状态信息 const getStatusInfo = (cmd: CommandItem): StatusInfo => { @@ -230,9 +230,9 @@ const getRowProps = ({ item }: { item: CommandItem }) => { {{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.rename') }} - + mdi-information - {{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.viewDetails') }} + {{ tm('tooltips.viewDetails') }} @@ -319,10 +319,9 @@ code.sub-command-code { opacity: 1; } -.v-data-table .plugin-inactive-row .v-btn-group .v-btn, -.v-data-table .plugin-inactive-row .v-chip.cursor-pointer { +.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; - pointer-events: none !important; } diff --git a/dashboard/src/components/extension/componentPanel/types.ts b/dashboard/src/components/extension/componentPanel/types.ts index c8b2595a3f..7f38194b3b 100644 --- a/dashboard/src/components/extension/componentPanel/types.ts +++ b/dashboard/src/components/extension/componentPanel/types.ts @@ -19,7 +19,7 @@ export interface CommandItem { aliases: string[]; permission: PermissionType; enabled: boolean; - plugin_activated?: boolean; + plugin_activated: boolean; is_group: boolean; has_conflict: boolean; reserved: boolean; diff --git a/tests/unit/test_command_plugin_activation.py b/tests/unit/test_command_plugin_activation.py index e004ddd79f..396e18db65 100644 --- a/tests/unit/test_command_plugin_activation.py +++ b/tests/unit/test_command_plugin_activation.py @@ -2,32 +2,69 @@ from types import SimpleNamespace +from astrbot.core.star.command_management import ( + CommandDescriptor, + _descriptor_to_dict, + _group_conflicts, + _is_plugin_activated, + star_map, +) -def test_inactive_plugin_commands_are_not_effectively_enabled(): - from astrbot.core.star.command_management import ( - _apply_plugin_activation_to_descriptors, - _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 = SimpleNamespace(module_path="data.plugins.foo.main", enabled=True) - inactive = SimpleNamespace(module_path="data.plugins.bar.main", enabled=True) - unknown = SimpleNamespace(module_path="data.plugins.missing.main", enabled=True) - - _apply_plugin_activation_to_descriptors([active, inactive, unknown]) + 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 False + 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)