修复了一些已知问题

This commit is contained in:
advent259141
2026-01-26 17:22:20 +08:00
parent 6b39717695
commit 6d47663842
6 changed files with 100 additions and 25 deletions
+10 -1
View File
@@ -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,
)
+8 -2
View File
@@ -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"
+17 -13
View File
@@ -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__)
+2 -1
View File
@@ -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"]
+14
View File
@@ -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',
+49 -8
View File
@@ -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 @@
</v-row>
<v-textarea
v-model="agent.description"
label="SubAgent 描述 / 指令"
v-model="agent.public_description"
label="对主 LLM 的描述(用于决定是否 handoff)"
variant="outlined"
rows="3"
auto-grow
hint="主 LLM 主要通过这里的描述来决定是否 handoff 到该 SubAgent。"
hint="这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
persistent-hint
/>
<v-textarea
v-model="agent.system_prompt"
label="SubAgent System Prompt(该 SubAgent 自己的指令)"
variant="outlined"
rows="4"
auto-grow
hint="这段只给该 SubAgent 自己作为 system prompt 使用,可以更长、更严格。"
persistent-hint
class="mt-3"
/>
<div class="mt-3">
<div class="text-caption text-medium-emphasis">预览 LLM 将看到的 handoff 工具</div>
<div class="d-flex align-center" style="gap: 8px; flex-wrap: wrap;">
@@ -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;
}
</style>
<style>
/*
Vuetify renders selected chips inside the input control and will grow the
field height as chips wrap. For subagent tool assignment this quickly becomes
unwieldy, so we cap the chip area height and allow scrolling.
Note: this must be a non-scoped style so it can reach Vuetify's internal
elements.
*/
.subagent-tools .v-field__input {
max-height: 160px;
overflow-y: auto;
align-content: flex-start;
}
/* Small breathing room so the scrollbar doesn't overlap chip close icons. */
.subagent-tools .v-field__input {
padding-right: 6px;
}
</style>