fix: gray out commands of disabled plugins in behavior manager (#9664)

* fix: gray out commands of disabled plugins in behavior manager

Show inactive-plugin commands as 未启用, dim the whole row with
theme on-surface colors, and disable row actions.

Fixes #9562

* 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.

* fix: preserve tooltips for disabled command actions

---------

Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
楚风礼赞
2026-08-25 16:19:57 +08:00
committed by GitHub
parent 828412d537
commit 68d0a99db4
7 changed files with 163 additions and 29 deletions
+7 -1
View File
@@ -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,
@@ -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(' ') } : {};
};
</script>
@@ -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) }}
@@ -193,31 +202,37 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
<template v-slot:item.actions="{ item }">
<div class="d-flex align-center">
<v-btn-group density="default" variant="text" color="primary">
<v-btn
v-if="!item.enabled"
icon
size="small"
color="success"
@click="emit('toggle-command', item)"
>
<v-icon size="22">mdi-play</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
</v-btn>
<v-btn
v-else
icon
size="small"
color="error"
@click="emit('toggle-command', item)"
>
<v-icon size="22">mdi-pause</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
</v-btn>
<span v-if="!item.enabled" class="command-action-tooltip">
<v-btn
icon
size="small"
color="success"
:disabled="isPluginInactive(item)"
@click="emit('toggle-command', item)"
>
<v-icon size="22">mdi-play</v-icon>
</v-btn>
<v-tooltip activator="parent" location="top">{{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.enable') }}</v-tooltip>
</span>
<span v-else class="command-action-tooltip">
<v-btn
icon
size="small"
color="error"
:disabled="isPluginInactive(item)"
@click="emit('toggle-command', item)"
>
<v-icon size="22">mdi-pause</v-icon>
</v-btn>
<v-tooltip activator="parent" location="top">{{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.disable') }}</v-tooltip>
</span>
<v-btn icon size="small" color="warning" @click="emit('rename', item)">
<v-icon size="22">mdi-pencil</v-icon>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.rename') }}</v-tooltip>
</v-btn>
<span class="command-action-tooltip">
<v-btn icon size="small" color="warning" :disabled="isPluginInactive(item)" @click="emit('rename', item)">
<v-icon size="22">mdi-pencil</v-icon>
</v-btn>
<v-tooltip activator="parent" location="top">{{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.rename') }}</v-tooltip>
</span>
<v-btn icon size="small" @click="emit('view-details', item)">
<v-icon size="22">mdi-information</v-icon>
@@ -285,5 +300,41 @@ code.sub-command-code {
.cursor-pointer {
cursor: pointer;
}
</style>
.command-action-tooltip {
display: inline-flex;
height: 100%;
}
.v-btn-group .command-action-tooltip .v-btn {
border-radius: 0;
height: 100%;
}
.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 .command-action-tooltip,
.v-data-table .plugin-inactive-row .v-chip.cursor-pointer.v-chip--disabled {
cursor: not-allowed !important;
}
</style>
@@ -19,6 +19,7 @@ export interface CommandItem {
aliases: string[];
permission: PermissionType;
enabled: boolean;
plugin_activated: boolean;
is_group: boolean;
has_conflict: boolean;
reserved: boolean;
@@ -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": {
@@ -29,6 +29,7 @@
"status": {
"enabled": "Активна",
"disabled": "Отключена",
"pluginDisabled": "Плагин отключён",
"conflict": "Конфликт"
},
"permission": {
@@ -39,7 +40,8 @@
"enable": "Включить",
"disable": "Выключить",
"rename": "Переименовать",
"viewDetails": "Подробности"
"viewDetails": "Подробности",
"pluginInactive": "Сначала включите плагин"
},
"dialogs": {
"rename": {
@@ -29,6 +29,7 @@
"status": {
"enabled": "已启用",
"disabled": "已禁用",
"pluginDisabled": "未启用",
"conflict": "有冲突"
},
"permission": {
@@ -39,7 +40,8 @@
"enable": "启用指令",
"disable": "禁用指令",
"rename": "重命名指令",
"viewDetails": "查看详情"
"viewDetails": "查看详情",
"pluginInactive": "请先启用所属插件"
},
"dialogs": {
"rename": {
@@ -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)