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 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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,31 @@ 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,
|
||||||
@@ -167,6 +192,7 @@ export function useCommandActions(
|
|||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
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": "搜索指令..."
|
||||||
|
|||||||
Reference in New Issue
Block a user