From adae1f3598925ac6942a043691aafbf06ffb19bb Mon Sep 17 00:00:00 2001 From: elecvoid243 <67492363+elecvoid243@users.noreply.github.com> Date: Fri, 29 May 2026 01:17:32 +0800 Subject: [PATCH] fix(command-suggestion): support custom wake-up words & hover information (#8353) * feature: add command suggestion for ChatUI * feat(chat): add focus functionality to chat input after sending messages * feat(llm): add error handling for LLM provider selection failures * feat(command-suggestion): enhance command filtering and sorting logic * fix(command-suggestion): support custom wake-up words & hover information * ruff --------- Co-authored-by: Soulter <905617992@qq.com> --- astrbot/dashboard/routes/command.py | 16 ++++- astrbot/dashboard/server.py | 2 +- dashboard/src/components/chat/ChatInput.vue | 56 ++++++++++++--- .../src/components/chat/CommandSuggestion.vue | 70 ++++++++++++++++++- 4 files changed, 130 insertions(+), 14 deletions(-) diff --git a/astrbot/dashboard/routes/command.py b/astrbot/dashboard/routes/command.py index cbc565c47..1921fa4a4 100644 --- a/astrbot/dashboard/routes/command.py +++ b/astrbot/dashboard/routes/command.py @@ -18,8 +18,9 @@ from .route import Response, Route, RouteContext class CommandRoute(Route): - def __init__(self, context: RouteContext) -> None: + def __init__(self, context: RouteContext, core_lifecycle=None) -> None: super().__init__(context) + self.core_lifecycle = core_lifecycle self.routes = { "/commands": ("GET", self.get_commands), "/commands/conflicts": ("GET", self.get_conflicts), @@ -36,7 +37,18 @@ class CommandRoute(Route): "disabled": len([cmd for cmd in commands if not cmd["enabled"]]), "conflicts": len([cmd for cmd in commands if cmd.get("has_conflict")]), } - return Response().ok({"items": commands, "summary": summary}).__dict__ + # 优先从指定 config_id 的配置中读取唤醒词,否则使用默认配置 + config_id = request.args.get("config_id", "").strip() + wake_prefix = self.config.get("wake_prefix", ["/"]) + if config_id and self.core_lifecycle: + acm = getattr(self.core_lifecycle, "astrbot_config_mgr", None) + if acm and config_id in acm.confs: + wake_prefix = acm.confs[config_id].get("wake_prefix", wake_prefix) + return ( + Response() + .ok({"items": commands, "summary": summary, "wake_prefix": wake_prefix}) + .__dict__ + ) async def get_conflicts(self): conflicts = await list_command_conflicts() diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index a39337b5f..c21d74524 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -154,7 +154,7 @@ class AstrBotDashboard: core_lifecycle, core_lifecycle.plugin_manager, ) - self.command_route = CommandRoute(self.context) + self.command_route = CommandRoute(self.context, core_lifecycle) self.cr = ConfigRoute(self.context, core_lifecycle) self.lr = LogRoute(self.context, core_lifecycle.log_broker) self.sfr = StaticFileRoute(self.context) diff --git a/dashboard/src/components/chat/ChatInput.vue b/dashboard/src/components/chat/ChatInput.vue index e4657c6f6..4fb59b22d 100644 --- a/dashboard/src/components/chat/ChatInput.vue +++ b/dashboard/src/components/chat/ChatInput.vue @@ -406,15 +406,36 @@ const allCommands = ref([]); const showCommandSuggestion = ref(false); const selectedCommandIndex = ref(0); const commandSuggestionLoading = ref(false); +const wakePrefixes = ref(["/"]); +const currentConfigId = ref((props.configId as string) || "default"); + +/** 检查文本是否以任意一个唤醒词前缀开头 */ +function hasWakePrefix(text: string): boolean { + return wakePrefixes.value.some((p) => text.startsWith(p)); +} + +/** 去掉文本开头匹配的任意唤醒词前缀,返回剥离后的文本 */ +function stripWakePrefix(text: string): string { + let result = text; + for (const p of wakePrefixes.value) { + if (result.startsWith(p)) { + result = result.slice(p.length); + break; // 只剥离第一个匹配的前缀 + } + } + return result; +} function normalizeCommandSearchText(value: string) { - return value.trim().replace(/^\/+/, "").toLowerCase(); + return stripWakePrefix(value.trim()).toLowerCase(); } /** 从所有指令中展平获取启用的普通指令和子指令 */ const enabledCommands = computed(() => { const result: SuggestionCommand[] = []; const seen = new Set(); + // 使用第一个唤醒词前缀作为指令的展示前缀 + const displayPrefix = wakePrefixes.value[0] || "/"; function addCommand(cmd: CommandItem) { if (!cmd.enabled) return; @@ -423,10 +444,10 @@ const enabledCommands = computed(() => { cmd.sub_commands?.forEach(addCommand); return; } - // 统一添加 / 前缀(子命令的 effective_command 如 "music play" 需要变成 "/music play") - const displayCmd = cmd.effective_command.startsWith("/") + // 统一添加唤醒词前缀(子命令的 effective_command 如 "music play" 需要变成 "/music play") + const displayCmd = hasWakePrefix(cmd.effective_command) ? cmd.effective_command - : `/${cmd.effective_command}`; + : `${displayPrefix}${cmd.effective_command}`; if (!seen.has(displayCmd)) { seen.add(displayCmd); result.push({ @@ -438,14 +459,14 @@ const enabledCommands = computed(() => { reserved: cmd.reserved, }); } - // 同时加入别名(别名也需要加上 / 前缀) + // 同时加入别名(别名也需要加上唤醒词前缀) cmd.aliases?.forEach((alias) => { const aliasBase = cmd.parent_signature ? `${cmd.parent_signature} ${alias}` : alias; - const aliasKey = aliasBase.startsWith("/") + const aliasKey = hasWakePrefix(aliasBase) ? aliasBase - : `/${aliasBase}`; + : `${displayPrefix}${aliasBase}`; if (!seen.has(aliasKey)) { seen.add(aliasKey); result.push({ @@ -471,7 +492,7 @@ function sortSystemPluginCommandsFirst(commands: SuggestionCommand[]) { /** 根据当前输入过滤候选指令 */ const filteredCommands = computed(() => { const text = props.prompt; - if (!text || !text.startsWith("/")) return []; + if (!text || !hasWakePrefix(text)) return []; const query = normalizeCommandSearchText(text); if (!query) return sortSystemPluginCommandsFirst(enabledCommands.value); @@ -689,7 +710,7 @@ function handleKeyDown(e: KeyboardEvent) { /** 处理输入变化,控制命令提示显示 */ function handleInput() { const text = props.prompt; - if (text && text.startsWith("/") && !isComposing.value) { + if (text && hasWakePrefix(text) && !isComposing.value) { showCommandSuggestion.value = filteredCommands.value.length > 0; selectedCommandIndex.value = 0; } else { @@ -721,9 +742,19 @@ async function fetchCommands() { if (commandSuggestionLoading.value) return; commandSuggestionLoading.value = true; try { - const res = await axios.get("/api/commands"); + const params: Record = {}; + const cid = currentConfigId.value; + if (cid && cid !== "default") { + params.config_id = cid; + } + const res = await axios.get("/api/commands", { params }); if (res.data.status === "ok") { allCommands.value = res.data.data.items || []; + // 读取当前配置的唤醒词列表,用于指令候选的触发前缀 + const prefixes: string[] = res.data.data.wake_prefix; + if (prefixes && prefixes.length > 0) { + wakePrefixes.value = prefixes; + } } } catch (err) { // 静默失败,不影响聊天功能 @@ -826,6 +857,11 @@ function handleConfigChange(payload: { const runnerType = (payload.agentRunnerType || "").toLowerCase(); const isInternal = runnerType === "internal" || runnerType === "local"; showProviderSelector.value = isInternal; + // 配置切换后重新获取指令列表和唤醒词 + if (payload.configId && payload.configId !== currentConfigId.value) { + currentConfigId.value = payload.configId; + fetchCommands(); + } } function getCurrentSelection() { diff --git a/dashboard/src/components/chat/CommandSuggestion.vue b/dashboard/src/components/chat/CommandSuggestion.vue index 02bfaaf53..87035ef08 100644 --- a/dashboard/src/components/chat/CommandSuggestion.vue +++ b/dashboard/src/components/chat/CommandSuggestion.vue @@ -13,6 +13,8 @@ :class="{ active: index === selectedIndex }" @click="handleSelect(index)" @mouseenter="handleMouseEnter(index)" + @mousemove="handleMouseMove" + @mouseleave="handleMouseLeave" >
{{ cmd.effective_command }} @@ -31,10 +33,21 @@ Esc {{ tm("commandSuggestion.close") }}
+ + +
+ {{ tooltip.text }} +
+
@@ -203,3 +246,28 @@ function handleMouseEnter(index: number) { white-space: nowrap; } + + +