feat(ComponentPanel): implement permission management for dashboard (#4887)

* feat(backend): add permission update api

* feat(useCommandActions): add updatePermission action and translations

* feat(dashboard): implement permission editing ui

* style: fix import sorting in command.py

* refactor(backend): extract permission update logic to service

* feat(i18n): add success and failure messages for command updates

---------

Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
Helian Nuits
2026-02-08 12:27:32 +08:00
committed by GitHub
parent 30d1d55e3c
commit 4e0b5063c6
7 changed files with 145 additions and 9 deletions
+46
View File
@@ -4,6 +4,7 @@ from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
from astrbot.api import sp
from astrbot.core import db_helper, logger from astrbot.core import db_helper, logger
from astrbot.core.db.po import CommandConfig from astrbot.core.db.po import CommandConfig
from astrbot.core.star.filter.command import CommandFilter from astrbot.core.star.filter.command import CommandFilter
@@ -139,6 +140,51 @@ async def rename_command(
return descriptor return descriptor
async def update_command_permission(
handler_full_name: str,
permission_type: str,
) -> CommandDescriptor:
descriptor = _build_descriptor_by_full_name(handler_full_name)
if not descriptor:
raise ValueError("指定的处理函数不存在或不是指令。")
if permission_type not in ["admin", "member"]:
raise ValueError("权限类型必须为 admin 或 member。")
handler = descriptor.handler
found_plugin = star_map.get(handler.handler_module_path)
if not found_plugin:
raise ValueError("未找到指令所属插件")
# 1. Update Persistent Config (alter_cmd)
alter_cmd_cfg = await sp.global_get("alter_cmd", {})
plugin_ = alter_cmd_cfg.get(found_plugin.name, {})
cfg = plugin_.get(handler.handler_name, {})
cfg["permission"] = permission_type
plugin_[handler.handler_name] = cfg
alter_cmd_cfg[found_plugin.name] = plugin_
await sp.global_put("alter_cmd", alter_cmd_cfg)
# 2. Update Runtime Filter
found_permission_filter = False
target_perm_type = (
PermissionType.ADMIN if permission_type == "admin" else PermissionType.MEMBER
)
for filter_ in handler.event_filters:
if isinstance(filter_, PermissionTypeFilter):
filter_.permission_type = target_perm_type
found_permission_filter = True
break
if not found_permission_filter:
handler.event_filters.insert(0, PermissionTypeFilter(target_perm_type))
# Re-build descriptor to reflect changes
return _build_descriptor(handler) or descriptor
async def list_commands() -> list[dict[str, Any]]: async def list_commands() -> list[dict[str, Any]]:
descriptors = _collect_descriptors(include_sub_commands=True) descriptors = _collect_descriptors(include_sub_commands=True)
config_records = await db_helper.get_command_configs() config_records = await db_helper.get_command_configs()
+22
View File
@@ -10,6 +10,9 @@ from astrbot.core.star.command_management import (
from astrbot.core.star.command_management import ( from astrbot.core.star.command_management import (
toggle_command as toggle_command_service, toggle_command as toggle_command_service,
) )
from astrbot.core.star.command_management import (
update_command_permission as update_command_permission_service,
)
from .route import Response, Route, RouteContext from .route import Response, Route, RouteContext
@@ -22,6 +25,7 @@ class CommandRoute(Route):
"/commands/conflicts": ("GET", self.get_conflicts), "/commands/conflicts": ("GET", self.get_conflicts),
"/commands/toggle": ("POST", self.toggle_command), "/commands/toggle": ("POST", self.toggle_command),
"/commands/rename": ("POST", self.rename_command), "/commands/rename": ("POST", self.rename_command),
"/commands/permission": ("POST", self.update_permission),
} }
self.register_routes() self.register_routes()
@@ -74,6 +78,24 @@ class CommandRoute(Route):
payload = await _get_command_payload(handler_full_name) payload = await _get_command_payload(handler_full_name)
return Response().ok(payload).__dict__ return Response().ok(payload).__dict__
async def update_permission(self):
data = await request.get_json()
handler_full_name = data.get("handler_full_name")
permission = data.get("permission")
if not handler_full_name or not permission:
return (
Response().error("handler_full_name 与 permission 均为必填。").__dict__
)
try:
await update_command_permission_service(handler_full_name, permission)
except ValueError as exc:
return Response().error(str(exc)).__dict__
payload = await _get_command_payload(handler_full_name)
return Response().ok(payload).__dict__
async def _get_command_payload(handler_full_name: str): async def _get_command_payload(handler_full_name: str):
commands = await list_commands() commands = await list_commands()
@@ -18,6 +18,7 @@ const emit = defineEmits<{
(e: 'toggle-command', cmd: CommandItem): void; (e: 'toggle-command', cmd: CommandItem): void;
(e: 'rename', cmd: CommandItem): void; (e: 'rename', cmd: CommandItem): void;
(e: 'view-details', cmd: CommandItem): void; (e: 'view-details', cmd: CommandItem): void;
(e: 'update-permission', cmd: CommandItem, permission: 'admin' | 'member'): void;
}>(); }>();
// 表格表头 // 表格表头
@@ -146,9 +147,36 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
</template> </template>
<template v-slot:item.permission="{ item }"> <template v-slot:item.permission="{ item }">
<v-chip :color="getPermissionColor(item.permission)" size="small" class="font-weight-medium"> <v-menu location="bottom">
{{ getPermissionLabel(item.permission) }} <template v-slot:activator="{ props }">
</v-chip> <v-chip
v-bind="props"
:color="getPermissionColor(item.permission)"
size="small"
class="font-weight-medium cursor-pointer"
link
>
{{ getPermissionLabel(item.permission) }}
<v-icon end size="14">mdi-chevron-down</v-icon>
</v-chip>
</template>
<v-list density="compact">
<v-list-item
:value="'member'"
@click="$emit('update-permission', item, 'member')"
:active="item.permission !== 'admin'"
>
<v-list-item-title>{{ tm('permission.everyone') }}</v-list-item-title>
</v-list-item>
<v-list-item
:value="'admin'"
@click="$emit('update-permission', item, 'admin')"
:active="item.permission === 'admin'"
>
<v-list-item-title>{{ tm('permission.admin') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template> </template>
<template v-slot:item.enabled="{ item }"> <template v-slot:item.enabled="{ item }">
@@ -253,5 +281,9 @@ code.sub-command-code {
.v-data-table .sub-command-row:hover { .v-data-table .sub-command-row:hover {
background-color: rgba(var(--v-theme-info), 0.08) !important; background-color: rgba(var(--v-theme-info), 0.08) !important;
} }
.cursor-pointer {
cursor: pointer;
}
</style> </style>
@@ -28,8 +28,8 @@ export function useCommandActions(
* 切换指令启用/禁用状态 * 切换指令启用/禁用状态
*/ */
const toggleCommand = async ( const toggleCommand = async (
cmd: CommandItem, cmd: CommandItem,
successMessage: string, successMessage: string,
errorMessage: string errorMessage: string
) => { ) => {
try { try {
@@ -131,7 +131,7 @@ export function useCommandActions(
* 获取状态显示信息 * 获取状态显示信息
*/ */
const getStatusInfo = ( const getStatusInfo = (
cmd: CommandItem, cmd: CommandItem,
translations: { conflict: string; enabled: string; disabled: string } translations: { conflict: string; enabled: string; disabled: string }
): StatusInfo => { ): StatusInfo => {
if (cmd.has_conflict) { if (cmd.has_conflict) {
@@ -160,13 +160,39 @@ export function useCommandActions(
return classes.length > 0 ? { class: classes.join(' ') } : {}; return classes.length > 0 ? { class: classes.join(' ') } : {};
}; };
/**
* 更新指令权限
*/
const updatePermission = async (
cmd: CommandItem,
permission: 'admin' | 'member',
successMessage: string,
errorMessage: string
) => {
try {
const res = await axios.post('/api/commands/permission', {
handler_full_name: cmd.handler_full_name,
permission: permission
});
if (res.data.status === 'ok') {
toast(successMessage, 'success');
await fetchCommands();
} else {
toast(res.data.message || errorMessage, 'error');
}
} catch (err: any) {
toast(err?.message || errorMessage, 'error');
}
};
return { return {
// 状态 // 状态
renameDialog, renameDialog,
detailsDialog, detailsDialog,
// 方法 // 方法
toggleCommand, toggleCommand,
updatePermission,
openRenameDialog, openRenameDialog,
confirmRename, confirmRename,
openDetailsDialog, openDetailsDialog,
@@ -76,6 +76,7 @@ const {
renameDialog, renameDialog,
detailsDialog, detailsDialog,
toggleCommand, toggleCommand,
updatePermission,
openRenameDialog, openRenameDialog,
confirmRename, confirmRename,
openDetailsDialog openDetailsDialog
@@ -95,6 +96,10 @@ const handleToggleCommand = async (cmd: CommandItem) => {
await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed')); await toggleCommand(cmd, tm('messages.toggleSuccess'), tm('messages.toggleFailed'));
}; };
const handleUpdatePermission = async (cmd: CommandItem, permission: 'admin' | 'member') => {
await updatePermission(cmd, permission, tm('messages.updateSuccess'), tm('messages.updateFailed'));
};
const handleToggleTool = async (tool: ToolItem) => { const handleToggleTool = async (tool: ToolItem) => {
const previous = tool.active; const previous = tool.active;
tool.active = !tool.active; tool.active = !tool.active;
@@ -240,6 +245,7 @@ watch(viewMode, async (mode) => {
@toggle-command="handleToggleCommand" @toggle-command="handleToggleCommand"
@rename="openRenameDialog" @rename="openRenameDialog"
@view-details="openDetailsDialog" @view-details="openDetailsDialog"
@update-permission="handleUpdatePermission"
/> />
</div> </div>
@@ -69,7 +69,9 @@
"toggleFailed": "Failed to update command status", "toggleFailed": "Failed to update command status",
"renameSuccess": "Command renamed", "renameSuccess": "Command renamed",
"renameFailed": "Rename failed", "renameFailed": "Rename failed",
"loadFailed": "Failed to load commands" "loadFailed": "Failed to load commands",
"updateSuccess": "Updated successfully",
"updateFailed": "Update failed"
}, },
"search": { "search": {
"placeholder": "Search commands..." "placeholder": "Search commands..."
@@ -69,7 +69,9 @@
"toggleFailed": "更新指令状态失败", "toggleFailed": "更新指令状态失败",
"renameSuccess": "指令已重命名", "renameSuccess": "指令已重命名",
"renameFailed": "重命名失败", "renameFailed": "重命名失败",
"loadFailed": "加载指令列表失败" "loadFailed": "加载指令列表失败",
"updateSuccess": "更新成功",
"updateFailed": "更新失败"
}, },
"search": { "search": {
"placeholder": "搜索指令..." "placeholder": "搜索指令..."