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:
@@ -4,6 +4,7 @@ from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from astrbot.api import sp
|
||||
from astrbot.core import db_helper, logger
|
||||
from astrbot.core.db.po import CommandConfig
|
||||
from astrbot.core.star.filter.command import CommandFilter
|
||||
@@ -139,6 +140,51 @@ async def rename_command(
|
||||
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]]:
|
||||
descriptors = _collect_descriptors(include_sub_commands=True)
|
||||
config_records = await db_helper.get_command_configs()
|
||||
|
||||
@@ -10,6 +10,9 @@ from astrbot.core.star.command_management import (
|
||||
from astrbot.core.star.command_management import (
|
||||
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
|
||||
|
||||
@@ -22,6 +25,7 @@ class CommandRoute(Route):
|
||||
"/commands/conflicts": ("GET", self.get_conflicts),
|
||||
"/commands/toggle": ("POST", self.toggle_command),
|
||||
"/commands/rename": ("POST", self.rename_command),
|
||||
"/commands/permission": ("POST", self.update_permission),
|
||||
}
|
||||
self.register_routes()
|
||||
|
||||
@@ -74,6 +78,24 @@ class CommandRoute(Route):
|
||||
payload = await _get_command_payload(handler_full_name)
|
||||
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):
|
||||
commands = await list_commands()
|
||||
|
||||
@@ -18,6 +18,7 @@ const emit = defineEmits<{
|
||||
(e: 'toggle-command', cmd: CommandItem): void;
|
||||
(e: 'rename', 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 v-slot:item.permission="{ item }">
|
||||
<v-chip :color="getPermissionColor(item.permission)" size="small" class="font-weight-medium">
|
||||
{{ getPermissionLabel(item.permission) }}
|
||||
</v-chip>
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<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 v-slot:item.enabled="{ item }">
|
||||
@@ -253,5 +281,9 @@ code.sub-command-code {
|
||||
.v-data-table .sub-command-row:hover {
|
||||
background-color: rgba(var(--v-theme-info), 0.08) !important;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ export function useCommandActions(
|
||||
* 切换指令启用/禁用状态
|
||||
*/
|
||||
const toggleCommand = async (
|
||||
cmd: CommandItem,
|
||||
successMessage: string,
|
||||
cmd: CommandItem,
|
||||
successMessage: string,
|
||||
errorMessage: string
|
||||
) => {
|
||||
try {
|
||||
@@ -131,7 +131,7 @@ export function useCommandActions(
|
||||
* 获取状态显示信息
|
||||
*/
|
||||
const getStatusInfo = (
|
||||
cmd: CommandItem,
|
||||
cmd: CommandItem,
|
||||
translations: { conflict: string; enabled: string; disabled: string }
|
||||
): StatusInfo => {
|
||||
if (cmd.has_conflict) {
|
||||
@@ -160,13 +160,39 @@ export function useCommandActions(
|
||||
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 {
|
||||
// 状态
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
|
||||
|
||||
// 方法
|
||||
toggleCommand,
|
||||
updatePermission,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog,
|
||||
|
||||
@@ -76,6 +76,7 @@ const {
|
||||
renameDialog,
|
||||
detailsDialog,
|
||||
toggleCommand,
|
||||
updatePermission,
|
||||
openRenameDialog,
|
||||
confirmRename,
|
||||
openDetailsDialog
|
||||
@@ -95,6 +96,10 @@ const handleToggleCommand = async (cmd: CommandItem) => {
|
||||
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 previous = tool.active;
|
||||
tool.active = !tool.active;
|
||||
@@ -240,6 +245,7 @@ watch(viewMode, async (mode) => {
|
||||
@toggle-command="handleToggleCommand"
|
||||
@rename="openRenameDialog"
|
||||
@view-details="openDetailsDialog"
|
||||
@update-permission="handleUpdatePermission"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
"toggleFailed": "Failed to update command status",
|
||||
"renameSuccess": "Command renamed",
|
||||
"renameFailed": "Rename failed",
|
||||
"loadFailed": "Failed to load commands"
|
||||
"loadFailed": "Failed to load commands",
|
||||
"updateSuccess": "Updated successfully",
|
||||
"updateFailed": "Update failed"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search commands..."
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
"toggleFailed": "更新指令状态失败",
|
||||
"renameSuccess": "指令已重命名",
|
||||
"renameFailed": "重命名失败",
|
||||
"loadFailed": "加载指令列表失败"
|
||||
"loadFailed": "加载指令列表失败",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateFailed": "更新失败"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索指令..."
|
||||
|
||||
Reference in New Issue
Block a user