80b89fd2ea
move mcp management to plugin managemanet page * feat: 新增命令配置数据库模型 * feat: 实现核心命令管理系统 * feat: 将命令管理集成到 Star 框架 * feat: 新增命令管理后台 API * feat: 新增命令管理界面页面 * feat: 新增命令管理国际化支持 * test: 新增命令管理相关测试 * refactor(command): 移除指令重命名时的别名功能 * fix(command): 修正指令冲突检测逻辑 * fix(command): 排除已禁用指令的冲突检测 - 只有 `effective_command` 存在且 `enabled` 为 `True` 的指令才会被纳入冲突检测范围。 * feat(command): 优化指令冲突显示与提示 - 【功能】新增指令冲突警告提示,当检测到冲突时显示详细信息及解决方案。 - 【优化】调整指令列表排序逻辑,将冲突指令优先显示并分组。 - 【样式】为冲突指令行添加专属高亮样式,提升视觉识别度。 - 【国际化】更新英文和中文多语言文件,增加指令冲突警告相关的翻译文本。 * chore(command-page): 禁用命令表格部分列的排序功能 * style(command-page): 调整命令页面表格样式和图标大小 * refactor(command): 优化指令页面布局并更新冲突警告 - 【布局优化】重新组织指令管理页面布局,将筛选器移至顶部独立行 - 【信息展示】将搜索栏与总指令数、已禁用指令数合并显示,提升页面空间利用率 - 【视觉更新】更新指令冲突警告样式 * style: UI 细节 * refactor(command): 调整指令管理中的成员权限显示与筛选 - 更新指令筛选逻辑,当选择“所有人”权限筛选时,将同时包含 `everyone` 和 `member` 权限的指令。 * feat(command-management): 新增指令层级管理与UI展示 - 【后端】 - `CommandDescriptor` 新增 `parent_group_handler` 和 `sub_commands` 字段,支持指令层级结构定义。 - `list_commands` 函数重构,实现指令的层级收集与构建,将子指令正确挂载到其父指令组下。 - 新增 `_collect_all_descriptors` 和 `_find_parent_group_handler` 辅助函数,用于全面收集指令并定位父指令组。 - `_build_descriptor` 优化指令类型判断逻辑,明确区分普通指令、指令组和子指令。 - `_descriptor_to_dict` 递归处理子指令,确保 API 返回完整的指令层级数据。 - 【前端】 - 指令管理页面 (`CommandPage.vue`) 增加指令类型筛选器,并支持指令组的展开/折叠功能。 - 表格展示优化,为指令组和子指令添加不同的样式和缩进,提升层级结构的视觉可读性。 - 指令详情对话框新增指令类型、所属指令组和子指令列表的展示。 - 更新 `CommandItem` 接口,以适配后端提供的层级数据结构。 - 【i18n】 - 新增指令类型(指令、指令组、子指令)的国际化文本。 - 更新指令管理相关 UI 文本,包括表格头部、详情对话框字段和筛选器选项。 * style(command): 优化指令组子指令数量显示UI * refactor(command): 修改指令列表排序逻辑 * style(command-page): 优化命令列表UI * feat(command): 添加系统插件指令过滤与冲突处理 * refactor(command): 更新指令数展示逻辑 * style(command): 更新空状态描述 * feat(extension): 添加插件指令冲突检测与提示 - 在插件安装或启用后,自动检测并提示指令冲突。 - 当检测到指令冲突时,显示警告对话框,告知用户冲突数量及可能的影响。 * refactor(command): 移除指令表格内部加载指示器 * style(extension): 文案修改 * refactor(command): 模块化指令管理面板前端代码 * refactor(commandPanel): 重命名指令模块目录为 commandPanel * style(commandPanel): 微调指令面板UI * fix(command): 确保新命令配置的事务提交 * fix(sidebar): 补全新增侧边栏项后的侧边栏位追加逻辑 * refactor(commands): 重构/help指令以动态显示实际命令并补充部分命令描述 * style(builtin_commands): 补充命令描述 * refactor(commandPanel): 移除未使用的 filterState 常量 * perf(dashboard): 删除多余的CommandPage.vue文件(已被模块化引用) * perf(command): 优化命令冲突计数逻辑 * perf(command): 优化指令管理辅助函数和配置绑定逻辑 * perf(db): 优化重构command相关数据库操作 * refactor(sidebar): 提取侧边栏项目解析逻辑到工具函数复用 * refactor: move mcp and command page to extension page * refactor: remove unused imports in component panel * fix: update terminology for handler management in extension localization --------- Co-authored-by: Soulter <905617992@qq.com>
450 lines
16 KiB
Python
450 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from astrbot.core import db_helper
|
|
from astrbot.core.db.po import CommandConfig
|
|
from astrbot.core.star.filter.command import CommandFilter
|
|
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
|
from astrbot.core.star.filter.permission import PermissionType, PermissionTypeFilter
|
|
from astrbot.core.star.star import star_map
|
|
from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry
|
|
|
|
|
|
@dataclass
|
|
class CommandDescriptor:
|
|
handler: StarHandlerMetadata = field(repr=False)
|
|
filter_ref: CommandFilter | CommandGroupFilter | None = field(
|
|
default=None,
|
|
repr=False,
|
|
)
|
|
handler_full_name: str = ""
|
|
handler_name: str = ""
|
|
plugin_name: str = ""
|
|
plugin_display_name: str | None = None
|
|
module_path: str = ""
|
|
description: str = ""
|
|
command_type: str = "command" # "command" | "group" | "sub_command"
|
|
raw_command_name: str | None = None
|
|
current_fragment: str | None = None
|
|
parent_signature: str = ""
|
|
parent_group_handler: str = ""
|
|
original_command: str | None = None
|
|
effective_command: str | None = None
|
|
aliases: list[str] = field(default_factory=list)
|
|
permission: str = "everyone"
|
|
enabled: bool = True
|
|
is_group: bool = False
|
|
is_sub_command: bool = False
|
|
reserved: bool = False
|
|
config: CommandConfig | None = None
|
|
has_conflict: bool = False
|
|
sub_commands: list[CommandDescriptor] = field(default_factory=list)
|
|
|
|
|
|
async def sync_command_configs() -> None:
|
|
"""同步指令配置,清理过期配置。"""
|
|
descriptors = _collect_descriptors(include_sub_commands=False)
|
|
config_records = await db_helper.get_command_configs()
|
|
config_map = _bind_configs_to_descriptors(descriptors, config_records)
|
|
live_handlers = {desc.handler_full_name for desc in descriptors}
|
|
|
|
stale_configs = [key for key in config_map if key not in live_handlers]
|
|
if stale_configs:
|
|
await db_helper.delete_command_configs(stale_configs)
|
|
|
|
|
|
async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescriptor:
|
|
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
|
if not descriptor:
|
|
raise ValueError("指定的处理函数不存在或不是指令。")
|
|
|
|
existing_cfg = await db_helper.get_command_config(handler_full_name)
|
|
config = await db_helper.upsert_command_config(
|
|
handler_full_name=handler_full_name,
|
|
plugin_name=descriptor.plugin_name or "",
|
|
module_path=descriptor.module_path,
|
|
original_command=descriptor.original_command or descriptor.handler_name,
|
|
resolved_command=(
|
|
existing_cfg.resolved_command
|
|
if existing_cfg
|
|
else descriptor.current_fragment
|
|
),
|
|
enabled=enabled,
|
|
keep_original_alias=False,
|
|
conflict_key=existing_cfg.conflict_key
|
|
if existing_cfg and existing_cfg.conflict_key
|
|
else descriptor.original_command,
|
|
resolution_strategy=existing_cfg.resolution_strategy if existing_cfg else None,
|
|
note=existing_cfg.note if existing_cfg else None,
|
|
extra_data=existing_cfg.extra_data if existing_cfg else None,
|
|
auto_managed=False,
|
|
)
|
|
_bind_descriptor_with_config(descriptor, config)
|
|
await sync_command_configs()
|
|
return descriptor
|
|
|
|
|
|
async def rename_command(
|
|
handler_full_name: str,
|
|
new_fragment: str,
|
|
) -> CommandDescriptor:
|
|
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
|
if not descriptor:
|
|
raise ValueError("指定的处理函数不存在或不是指令。")
|
|
|
|
new_fragment = new_fragment.strip()
|
|
if not new_fragment:
|
|
raise ValueError("指令名不能为空。")
|
|
|
|
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
|
if _is_command_in_use(handler_full_name, candidate_full):
|
|
raise ValueError("新的指令名已被其他指令占用,请换一个名称。")
|
|
|
|
config = await db_helper.upsert_command_config(
|
|
handler_full_name=handler_full_name,
|
|
plugin_name=descriptor.plugin_name or "",
|
|
module_path=descriptor.module_path,
|
|
original_command=descriptor.original_command or descriptor.handler_name,
|
|
resolved_command=new_fragment,
|
|
enabled=True if descriptor.enabled else False,
|
|
keep_original_alias=False,
|
|
conflict_key=descriptor.original_command,
|
|
resolution_strategy="manual_rename",
|
|
note=None,
|
|
extra_data=None,
|
|
auto_managed=False,
|
|
)
|
|
_bind_descriptor_with_config(descriptor, config)
|
|
|
|
await sync_command_configs()
|
|
return descriptor
|
|
|
|
|
|
async def list_commands() -> list[dict[str, Any]]:
|
|
descriptors = _collect_descriptors(include_sub_commands=True)
|
|
config_records = await db_helper.get_command_configs()
|
|
_bind_configs_to_descriptors(descriptors, config_records)
|
|
|
|
conflict_groups = _group_conflicts(descriptors)
|
|
conflict_handler_names: set[str] = {
|
|
d.handler_full_name for group in conflict_groups.values() for d in group
|
|
}
|
|
|
|
# 分类,设置冲突标志,将子指令挂载到父指令组
|
|
group_map: dict[str, CommandDescriptor] = {}
|
|
sub_commands: list[CommandDescriptor] = []
|
|
root_commands: list[CommandDescriptor] = []
|
|
|
|
for desc in descriptors:
|
|
desc.has_conflict = desc.handler_full_name in conflict_handler_names
|
|
if desc.is_group:
|
|
group_map[desc.handler_full_name] = desc
|
|
elif desc.is_sub_command:
|
|
sub_commands.append(desc)
|
|
else:
|
|
root_commands.append(desc)
|
|
|
|
for sub in sub_commands:
|
|
if sub.parent_group_handler and sub.parent_group_handler in group_map:
|
|
group_map[sub.parent_group_handler].sub_commands.append(sub)
|
|
else:
|
|
root_commands.append(sub)
|
|
|
|
# 指令组 + 普通指令,按 effective_command 字母排序
|
|
all_commands = list(group_map.values()) + root_commands
|
|
all_commands.sort(key=lambda d: (d.effective_command or "").lower())
|
|
|
|
result = [_descriptor_to_dict(desc) for desc in all_commands]
|
|
return result
|
|
|
|
|
|
async def list_command_conflicts() -> list[dict[str, Any]]:
|
|
"""列出所有冲突的指令组。"""
|
|
descriptors = _collect_descriptors(include_sub_commands=False)
|
|
config_records = await db_helper.get_command_configs()
|
|
_bind_configs_to_descriptors(descriptors, config_records)
|
|
|
|
conflict_groups = _group_conflicts(descriptors)
|
|
details = [
|
|
{
|
|
"conflict_key": key,
|
|
"handlers": [
|
|
{
|
|
"handler_full_name": item.handler_full_name,
|
|
"plugin": item.plugin_name,
|
|
"current_name": item.effective_command,
|
|
}
|
|
for item in group
|
|
],
|
|
}
|
|
for key, group in conflict_groups.items()
|
|
]
|
|
return details
|
|
|
|
|
|
# Internal helpers ----------------------------------------------------------
|
|
|
|
|
|
def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]:
|
|
"""收集指令,按需包含子指令。"""
|
|
descriptors: list[CommandDescriptor] = []
|
|
for handler in star_handlers_registry:
|
|
desc = _build_descriptor(handler)
|
|
if not desc:
|
|
continue
|
|
if not include_sub_commands and desc.is_sub_command:
|
|
continue
|
|
descriptors.append(desc)
|
|
return descriptors
|
|
|
|
|
|
def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None:
|
|
filter_ref = _locate_primary_filter(handler)
|
|
if filter_ref is None:
|
|
return None
|
|
|
|
plugin_meta = star_map.get(handler.handler_module_path)
|
|
plugin_name = (
|
|
plugin_meta.name if plugin_meta else None
|
|
) or handler.handler_module_path
|
|
plugin_display = plugin_meta.display_name if plugin_meta else None
|
|
|
|
is_sub_command = bool(handler.extras_configs.get("sub_command"))
|
|
parent_group_handler = ""
|
|
|
|
if isinstance(filter_ref, CommandFilter):
|
|
raw_fragment = getattr(
|
|
filter_ref, "_original_command_name", filter_ref.command_name
|
|
)
|
|
current_fragment = filter_ref.command_name
|
|
parent_signature = (filter_ref.parent_command_names or [""])[0].strip()
|
|
# 如果是子指令,尝试找到父指令组的 handler_full_name
|
|
if is_sub_command and parent_signature:
|
|
parent_group_handler = _find_parent_group_handler(
|
|
handler.handler_module_path, parent_signature
|
|
)
|
|
else:
|
|
raw_fragment = getattr(
|
|
filter_ref, "_original_group_name", filter_ref.group_name
|
|
)
|
|
current_fragment = filter_ref.group_name
|
|
parent_signature = _resolve_group_parent_signature(filter_ref)
|
|
|
|
original_command = _compose_command(parent_signature, raw_fragment)
|
|
effective_command = _compose_command(parent_signature, current_fragment)
|
|
|
|
# 确定 command_type
|
|
if isinstance(filter_ref, CommandGroupFilter):
|
|
command_type = "group"
|
|
elif is_sub_command:
|
|
command_type = "sub_command"
|
|
else:
|
|
command_type = "command"
|
|
|
|
descriptor = CommandDescriptor(
|
|
handler=handler,
|
|
filter_ref=filter_ref,
|
|
handler_full_name=handler.handler_full_name,
|
|
handler_name=handler.handler_name,
|
|
plugin_name=plugin_name,
|
|
plugin_display_name=plugin_display,
|
|
module_path=handler.handler_module_path,
|
|
description=handler.desc or "",
|
|
command_type=command_type,
|
|
raw_command_name=raw_fragment,
|
|
current_fragment=current_fragment,
|
|
parent_signature=parent_signature,
|
|
parent_group_handler=parent_group_handler,
|
|
original_command=original_command,
|
|
effective_command=effective_command,
|
|
aliases=sorted(getattr(filter_ref, "alias", set())),
|
|
permission=_determine_permission(handler),
|
|
enabled=handler.enabled,
|
|
is_group=isinstance(filter_ref, CommandGroupFilter),
|
|
is_sub_command=is_sub_command,
|
|
reserved=plugin_meta.reserved if plugin_meta else False,
|
|
)
|
|
return descriptor
|
|
|
|
|
|
def _build_descriptor_by_full_name(full_name: str) -> CommandDescriptor | None:
|
|
handler = star_handlers_registry.get_handler_by_full_name(full_name)
|
|
if not handler:
|
|
return None
|
|
return _build_descriptor(handler)
|
|
|
|
|
|
def _locate_primary_filter(
|
|
handler: StarHandlerMetadata,
|
|
) -> CommandFilter | CommandGroupFilter | None:
|
|
for filter_ref in handler.event_filters:
|
|
if isinstance(filter_ref, (CommandFilter, CommandGroupFilter)):
|
|
return filter_ref
|
|
return None
|
|
|
|
|
|
def _determine_permission(handler: StarHandlerMetadata) -> str:
|
|
for filter_ref in handler.event_filters:
|
|
if isinstance(filter_ref, PermissionTypeFilter):
|
|
return (
|
|
"admin"
|
|
if filter_ref.permission_type == PermissionType.ADMIN
|
|
else "member"
|
|
)
|
|
return "everyone"
|
|
|
|
|
|
def _resolve_group_parent_signature(group_filter: CommandGroupFilter) -> str:
|
|
signatures: list[str] = []
|
|
parent = group_filter.parent_group
|
|
while parent:
|
|
signatures.append(getattr(parent, "_original_group_name", parent.group_name))
|
|
parent = parent.parent_group
|
|
return " ".join(reversed(signatures)).strip()
|
|
|
|
|
|
def _find_parent_group_handler(module_path: str, parent_signature: str) -> str:
|
|
"""根据模块路径和父级签名,找到对应的指令组 handler_full_name。"""
|
|
parent_sig_normalized = parent_signature.strip()
|
|
for handler in star_handlers_registry:
|
|
if handler.handler_module_path != module_path:
|
|
continue
|
|
filter_ref = _locate_primary_filter(handler)
|
|
if not isinstance(filter_ref, CommandGroupFilter):
|
|
continue
|
|
# 检查该指令组的完整指令名是否匹配 parent_signature
|
|
group_names = filter_ref.get_complete_command_names()
|
|
if parent_sig_normalized in group_names:
|
|
return handler.handler_full_name
|
|
return ""
|
|
|
|
|
|
def _compose_command(parent_signature: str, fragment: str | None) -> str:
|
|
fragment = (fragment or "").strip()
|
|
parent_signature = parent_signature.strip()
|
|
if not parent_signature:
|
|
return fragment
|
|
if not fragment:
|
|
return parent_signature
|
|
return f"{parent_signature} {fragment}"
|
|
|
|
|
|
def _bind_descriptor_with_config(
|
|
descriptor: CommandDescriptor,
|
|
config: CommandConfig,
|
|
) -> None:
|
|
_apply_config_to_descriptor(descriptor, config)
|
|
_apply_config_to_runtime(descriptor, config)
|
|
|
|
|
|
def _apply_config_to_descriptor(
|
|
descriptor: CommandDescriptor,
|
|
config: CommandConfig,
|
|
) -> None:
|
|
descriptor.config = config
|
|
descriptor.enabled = config.enabled
|
|
|
|
if config.original_command:
|
|
descriptor.original_command = config.original_command
|
|
|
|
new_fragment = config.resolved_command or descriptor.current_fragment
|
|
descriptor.current_fragment = new_fragment
|
|
descriptor.effective_command = _compose_command(
|
|
descriptor.parent_signature,
|
|
new_fragment,
|
|
)
|
|
|
|
|
|
def _apply_config_to_runtime(
|
|
descriptor: CommandDescriptor,
|
|
config: CommandConfig,
|
|
) -> None:
|
|
descriptor.handler.enabled = config.enabled
|
|
if descriptor.filter_ref and descriptor.current_fragment:
|
|
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
|
|
|
|
|
def _bind_configs_to_descriptors(
|
|
descriptors: list[CommandDescriptor],
|
|
config_records: list[CommandConfig],
|
|
) -> dict[str, CommandConfig]:
|
|
config_map = {cfg.handler_full_name: cfg for cfg in config_records}
|
|
for desc in descriptors:
|
|
if cfg := config_map.get(desc.handler_full_name):
|
|
_bind_descriptor_with_config(desc, cfg)
|
|
return config_map
|
|
|
|
|
|
def _group_conflicts(
|
|
descriptors: list[CommandDescriptor],
|
|
) -> dict[str, list[CommandDescriptor]]:
|
|
conflicts: dict[str, list[CommandDescriptor]] = defaultdict(list)
|
|
for desc in descriptors:
|
|
if desc.effective_command and desc.enabled:
|
|
conflicts[desc.effective_command].append(desc)
|
|
return {k: v for k, v in conflicts.items() if len(v) > 1}
|
|
|
|
|
|
def _set_filter_fragment(
|
|
filter_ref: CommandFilter | CommandGroupFilter,
|
|
fragment: str,
|
|
) -> None:
|
|
attr = (
|
|
"group_name" if isinstance(filter_ref, CommandGroupFilter) else "command_name"
|
|
)
|
|
current_value = getattr(filter_ref, attr)
|
|
if fragment == current_value:
|
|
return
|
|
setattr(filter_ref, attr, fragment)
|
|
if hasattr(filter_ref, "_cmpl_cmd_names"):
|
|
filter_ref._cmpl_cmd_names = None
|
|
|
|
|
|
def _is_command_in_use(
|
|
target_handler_full_name: str,
|
|
candidate_full_command: str,
|
|
) -> bool:
|
|
candidate = candidate_full_command.strip()
|
|
for handler in star_handlers_registry:
|
|
if handler.handler_full_name == target_handler_full_name:
|
|
continue
|
|
filter_ref = _locate_primary_filter(handler)
|
|
if not filter_ref:
|
|
continue
|
|
names = {name.strip() for name in filter_ref.get_complete_command_names()}
|
|
if candidate in names:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
|
|
result = {
|
|
"handler_full_name": desc.handler_full_name,
|
|
"handler_name": desc.handler_name,
|
|
"plugin": desc.plugin_name,
|
|
"plugin_display_name": desc.plugin_display_name,
|
|
"module_path": desc.module_path,
|
|
"description": desc.description,
|
|
"type": desc.command_type,
|
|
"parent_signature": desc.parent_signature,
|
|
"parent_group_handler": desc.parent_group_handler,
|
|
"original_command": desc.original_command,
|
|
"current_fragment": desc.current_fragment,
|
|
"effective_command": desc.effective_command,
|
|
"aliases": desc.aliases,
|
|
"permission": desc.permission,
|
|
"enabled": desc.enabled,
|
|
"is_group": desc.is_group,
|
|
"has_conflict": desc.has_conflict,
|
|
"reserved": desc.reserved,
|
|
}
|
|
# 如果是指令组,包含子指令列表
|
|
if desc.is_group and desc.sub_commands:
|
|
result["sub_commands"] = [_descriptor_to_dict(sub) for sub in desc.sub_commands]
|
|
else:
|
|
result["sub_commands"] = []
|
|
return result
|