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)