diff --git a/astrbot/core/agent/handoff.py b/astrbot/core/agent/handoff.py index 85276540b..511fb5399 100644 --- a/astrbot/core/agent/handoff.py +++ b/astrbot/core/agent/handoff.py @@ -15,10 +15,19 @@ class HandoffTool(FunctionTool, Generic[TContext]): **kwargs, ): self.agent = agent + + # Avoid passing duplicate `description` to the FunctionTool dataclass. + # Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs + # to override what the main agent sees, while we also compute a default + # description here. + description = kwargs.pop( + "description", + agent.instructions or self.default_description(agent.name), + ) super().__init__( name=f"transfer_to_{agent.name}", parameters=parameters or self.default_parameters(), - description=agent.instructions or self.default_description(agent.name), + description=description, **kwargs, ) diff --git a/astrbot/core/subagent_orchestrator.py b/astrbot/core/subagent_orchestrator.py index f16067222..d565aa7c5 100644 --- a/astrbot/core/subagent_orchestrator.py +++ b/astrbot/core/subagent_orchestrator.py @@ -15,7 +15,10 @@ class SubAgentConfig: """Runtime representation of a configured subagent.""" name: str + # Instructions are used as the subagent's system prompt. instructions: str + # Public description is what the main LLM sees for transfer_to_* tool description. + public_description: str tools: list[str] enabled: bool = True @@ -63,7 +66,8 @@ class SubAgentOrchestrator: if not name: continue - instructions = str(item.get("description", "")).strip() + instructions = str(item.get("system_prompt", "")).strip() + public_description = str(item.get("public_description", "")).strip() tools = item.get("tools", []) if not isinstance(tools, list): tools = [] @@ -74,7 +78,9 @@ class SubAgentOrchestrator: instructions=instructions, tools=tools, ) - handoff = HandoffTool(agent=agent) + # The tool description should be a short description for the main LLM, + # while the subagent system prompt can be longer/more specific. + handoff = HandoffTool(agent=agent, description=public_description or None) # Mark as dynamic so we can replace/remove later. handoff.handler_module_path = "core.subagent_orchestrator" diff --git a/astrbot/dashboard/routes/subagent.py b/astrbot/dashboard/routes/subagent.py index 08bbb8dda..24cb2fcef 100644 --- a/astrbot/dashboard/routes/subagent.py +++ b/astrbot/dashboard/routes/subagent.py @@ -1,6 +1,7 @@ import traceback from quart import request +from quart import jsonify from astrbot.core import logger from astrbot.core.core_lifecycle import AstrBotCoreLifecycle @@ -16,11 +17,13 @@ class SubAgentRoute(Route): ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle - self.routes = { - "/subagent/config": ("GET", self.get_config), - "/subagent/config": ("POST", self.update_config), - "/subagent/available-tools": ("GET", self.get_available_tools), - } + # NOTE: dict cannot hold duplicate keys; use list form to register multiple + # methods for the same path. + self.routes = [ + ("/subagent/config", ("GET", self.get_config)), + ("/subagent/config", ("POST", self.update_config)), + ("/subagent/available-tools", ("GET", self.get_available_tools)), + ] self.register_routes() async def get_config(self): @@ -45,16 +48,16 @@ class SubAgentRoute(Route): data.setdefault("main_enable", False) data.setdefault("main_tools_policy", "handoff_only") data.setdefault("agents", []) - return Response().ok(data=data).__dict__ + return jsonify(Response().ok(data=data).__dict__) except Exception as e: logger.error(traceback.format_exc()) - return Response().error(f"获取 subagent 配置失败: {e!s}").__dict__ + return jsonify(Response().error(f"获取 subagent 配置失败: {e!s}").__dict__) async def update_config(self): try: data = await request.json if not isinstance(data, dict): - return Response().error("配置必须为 JSON 对象").__dict__ + return jsonify(Response().error("配置必须为 JSON 对象").__dict__) cfg = self.core_lifecycle.astrbot_config provider_settings = cfg.get("provider_settings", {}) @@ -62,17 +65,18 @@ class SubAgentRoute(Route): cfg["provider_settings"] = provider_settings # Persist to cmd_config.json - self.core_lifecycle.astrbot_config_mgr.save(cfg) + # AstrBotConfigManager does not expose a `save()` method; persist via AstrBotConfig. + cfg.save_config() # Reload dynamic handoff tools if orchestrator exists orch = getattr(self.core_lifecycle, "subagent_orchestrator", None) if orch is not None: orch.reload_from_config(provider_settings) - return Response().ok(message="保存成功").__dict__ + return jsonify(Response().ok(message="保存成功").__dict__) except Exception as e: logger.error(traceback.format_exc()) - return Response().error(f"保存 subagent 配置失败: {e!s}").__dict__ + return jsonify(Response().error(f"保存 subagent 配置失败: {e!s}").__dict__) async def get_available_tools(self): """Return all registered tools (name/description/parameters/active/origin). @@ -92,7 +96,7 @@ class SubAgentRoute(Route): "handler_module_path": tool.handler_module_path, } ) - return Response().ok(data=tools_dict).__dict__ + return jsonify(Response().ok(data=tools_dict).__dict__) except Exception as e: logger.error(traceback.format_exc()) - return Response().error(f"获取可用工具失败: {e!s}").__dict__ + return jsonify(Response().error(f"获取可用工具失败: {e!s}").__dict__) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 5b5abdfeb..6f1386d09 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -132,7 +132,8 @@ class AstrBotDashboard: r = jsonify(Response().error("未授权").__dict__) r.status_code = 401 return r - token = token.removeprefix("Bearer ") + # Be tolerant of different header casing / formatting. + token = token.strip().removeprefix("Bearer ").strip() try: payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"]) g.username = payload["username"] diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index 958eded22..305c7644b 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -61,6 +61,20 @@ axios.interceptors.request.use((config) => { return config; }); +// Keep fetch() calls consistent with axios by automatically attaching the JWT. +// Some parts of the UI use fetch directly; without this, those requests will 401. +const _origFetch = window.fetch.bind(window); +window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + const token = localStorage.getItem('token'); + if (!token) return _origFetch(input, init); + + const headers = new Headers(init?.headers || (typeof input !== 'string' && 'headers' in input ? (input as Request).headers : undefined)); + if (!headers.has('Authorization')) { + headers.set('Authorization', `Bearer ${token}`); + } + return _origFetch(input, { ...init, headers }); +}; + loader.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs', diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index 625fcb0ea..359828c57 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -116,9 +116,12 @@ label="分配工具(多选)" variant="outlined" density="comfortable" + class="subagent-tools" multiple chips closable-chips + :menu-props="{ maxHeight: 320 }" + :max-chips="8" :loading="toolsLoading" :disabled="toolsLoading" clearable @@ -127,15 +130,26 @@ + +
预览:主 LLM 将看到的 handoff 工具
@@ -175,7 +189,8 @@ type ToolOption = { title: string; value: string } type SubAgentItem = { __key: string name: string - description: string + public_description: string + system_prompt: string tools: string[] enabled: boolean } @@ -221,14 +236,16 @@ function normalizeConfig(raw: any): SubAgentConfig { const agents: SubAgentItem[] = agentsRaw.map((a: any, i: number) => { const name = (a?.name ?? '').toString() - const description = (a?.description ?? '').toString() + const public_description = (a?.public_description ?? '').toString() + const system_prompt = (a?.system_prompt ?? '').toString() const tools = Array.isArray(a?.tools) ? a.tools.map((x: any) => String(x)) : [] const enabled = a?.enabled !== false return { __key: `${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`, name, - description, + public_description, + system_prompt, tools, enabled } @@ -296,7 +313,8 @@ function addAgent() { cfg.value.agents.push({ __key: `${Date.now()}_${Math.random().toString(16).slice(2)}`, name: '', - description: '', + public_description: '', + system_prompt: '', tools: [], enabled: true }) @@ -316,7 +334,8 @@ async function save() { main_tools_policy: 'handoff_only', agents: cfg.value.agents.map(a => ({ name: a.name, - description: a.description, + public_description: a.public_description, + system_prompt: a.system_prompt, tools: a.tools, enabled: a.enabled })) @@ -351,3 +370,25 @@ onMounted(() => { padding-bottom: 40px; } + +