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 @@