Compare commits

...

21 Commits

Author SHA1 Message Date
121793367a feat(prompts): 增加信息诚实与验证规范\n\n- 在主系统提示词、视觉版提示词、多智能体 Team Leader 提示词、\n 子智能体通用提示词中统一加入五小节约束:\n 1. 信息来源必须明确标注;\n 2. 结论确定性必须分级;\n 3. 禁止将操作完成等同于任务完成;\n 4. 替代方案必须经用户许可并说明缺陷;\n 5. 异常排查优先怀疑自己。 2026-07-18 00:57:25 +08:00
cfdec831d8 fix(static): 修复极简模式并行工具时摘要行空白与 reel 动画消失
089291f7 将 .summary-tool-reel-window 宽度改为 auto,但其内部
reel 轨道为 absolute 定位不贡献宽度,导致窗口塌缩为 0 宽:
并行工具调用时摘要行渲染空白、滚动 reel 动画不可见。

恢复 reel 窗口 width: min(100%, 36em) 与上下渐变遮罩,
同时保留 089291f7 的摘要行图标偏移修复(18px 固定图标容器
+ clip-path 右侧裁剪 + summary-icon-inner 左移 10px)。
2026-07-18 00:57:25 +08:00
1d0a7e95fa feat(multi-agent): 扩展 sleep 工具模式并修复 /new 模式切换状态继承
- sleep 工具支持 wait_sub_agent_output / wait_sub_agent_ids / wait_runcommand_id 模式
- 多智能体工具移除 ask_sub_agent,新增 stop_sub_agent
- 前后端补齐对应 formatter、渲染与工具执行分支
- 修复从多智能体模式切回 /new 后,新对话继承 terminal.multi_agent_mode 导致实际运行在多智能体模式的问题
2026-07-17 22:51:04 +08:00
d4c3e73134 fix(sub_agent): 移除 search_workspace 工具并清理内存调试代码
- 删除子智能体 search_workspace 工具定义、路由、formatter 与测试
- 传统/多智能体模式共用 SUB_AGENT_TOOLS,一处删除同时生效
- 老对话恢复后再调用 search_workspace 会返回 未知工具 错误
- 清理多智能体排查期间加入的 memory_debug 模块及所有调用点
  (modules/memory_debug.py 删除,server/ core/ utils/ modules/sub_agent/ 中相关埋点全部移除)
- 顺手删除 SubAgentActivityDialog 中 search_workspace 的历史显示文案
2026-07-17 14:05:35 +08:00
2b131de702 perf(debug): 内存监控增强为 100ms 采样 + 多维度指标
- 采样间隔从 1s 降到 100ms,spike 阈值降到 50MB
- 增加 uss/shared/private/data/dirty/compressed 等指标
- 更贴近 macOS 活动监视器'内存'列的统计口径
2026-07-17 12:16:16 +08:00
faef998a9a perf(debug): 增强内存监控:每秒采样 + 更多关键路径埋点
- 改用 psutil 实时 RSS 取代历史最大 ru_maxrss
- 新增后台每秒采样线程,检测到 >100MB/s 增长立即记录
- WebTerminal 初始化时自动启动采样
- 增加子智能体恢复加载 conversation.json、httpx 流式/非流式响应、
  工具返回结果、模型输出组装等埋点
2026-07-15 17:33:47 +08:00
248f5a4b1a fix(debug): 修正 macOS 内存单位换算 2026-07-15 17:26:09 +08:00
310d82f4b8 perf(debug): 添加多智能体模式内存监控埋点
- 新增 modules/memory_debug.py 统一内存监控模块
- 在子智能体任务、主对话 build_messages、多智能体消息注入、
  API 请求准备、深压缩等关键路径记录 RSS/VMS 和消息规模
- 日志输出到 ~/.astrion/astrion/host/logs/memory_debug.log
2026-07-15 17:02:33 +08:00
3138d23687 feat(multi-agent): 重构 Team Leader 提示词分工策略并新增 sleep wait_sub_agent_output_ids 2026-07-15 14:55:14 +08:00
11ffdc2bb5 fix: 优化浮动菜单位置计算与 token 处理 2026-07-15 04:37:26 +08:00
089291f77e fix(static): 修复极简模式摘要行图标偏移、侧边栏空工作区显示及 folder-closed 图标缺失 2026-07-15 04:34:24 +08:00
4f3ba722a1 fix(multi-agent): web模式支持加载预设角色,多智能体面板显示活跃子智能体 2026-07-15 02:38:12 +08:00
69e3ef51b6 feat(multi-agent): 明确 Team Leader 是子智能体的 user 2026-07-15 01:42:30 +08:00
b2b8013f3a fix(static): add avatar favicon to login and other standalone pages 2026-07-15 01:17:24 +08:00
f6cbe78d68 fix(static): make avatar favicon adapt to browser color scheme 2026-07-15 01:09:13 +08:00
57aaf76809 fix(static): make avatar favicon permanently white 2026-07-15 01:05:49 +08:00
a60e164aa2 fix(static): center avatar favicon and use pure black/white for light/dark modes 2026-07-15 01:01:59 +08:00
f8f5d26ee3 feat(static): add Astrion avatar favicon and touch icons 2026-07-15 00:51:20 +08:00
4e1d08f106 fix(frontend): 修复多个前端交互问题
- 修复运行时 intent Unicode 转义显示异常(后端 extract_intent_from_partial 解码)
- 修复 ask_user 弹窗问题/说明不换行
- 修复刷新页面后等待回答提示不恢复(添加 fetchPendingUserQuestions 并在初始化路径调用)
- 修复回答问题弹窗长问题撑满窗口(header/context 限高滚动)
- 修复角色编辑器模型选择菜单定位与滚动
- 在 AGENTS.md 中再次强调前端调试日志必须统一筛选词

测试:/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9 -m unittest test.test_server_refactor_smoke; npm run build
2026-07-15 00:31:13 +08:00
297a6cc5ec Merge branch 'multiagent-mode' 2026-07-14 23:41:20 +08:00
a95e969ba2 fix(web_terminal): 程序启动自动恢复最近对话时不恢复模型,避免 /new 页面显示旧对话模型;显式加载对话时仍恢复保存的模型 2026-07-13 21:43:51 +08:00
55 changed files with 1227 additions and 550 deletions

View File

@ -207,7 +207,9 @@
### 前端调试日志
- **统一筛选词**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
- **统一筛选词(强制)**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
- **同一次调试任务中严禁使用多个前缀**。如果涉及多个模块如状态栏、Git 摘要、用户问题),应共用同一个前缀(如 `[status-bar-debug]`),通过日志对象里的字段区分模块,而不是发明多个前缀增加用户筛选成本。
- 选择前缀时优先与本次排查的「用户可见现象」对齐,而不是与内部模块名对齐。
- **控制数量与时机**
- 禁止一次性输出上百上千条日志刷屏。
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。

View File

@ -103,7 +103,7 @@ class ToolsDefinitionCoreToolsMixin:
"type": "function",
"function": {
"name": "sleep",
"description": "等待工具。两种模式二选一1) seconds短暂延迟2) wait_runcommand_id等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。",
"description": "等待工具。三种模式三选一1) seconds短暂延迟2) wait_runcommand_id等待指定后台 run_command 结束并直接返回结果3) wait_sub_agent_output_ids等待指定子智能体下一次输出并直接返回该消息多智能体模式专用。若同时提供多个等待参数会报错。",
"parameters": {
"type": "object",
"properties": self._inject_intent({
@ -115,6 +115,11 @@ class ToolsDefinitionCoreToolsMixin:
"type": "string",
"description": "等待指定后台 run_command 的 command_id 结束后返回。"
},
"wait_sub_agent_output_ids": {
"type": "array",
"items": {"type": "integer"},
"description": "等待指定子智能体下一次输出并直接返回该消息。多智能体模式专用,且一次只能包含一个编号。"
},
"reason": {
"type": "string",
"description": "等待的原因说明(可选)"

View File

@ -31,6 +31,7 @@ try:
WORKSPACE_MEMORY_DIRNAME,
WORKSPACE_REVIEW_DIRNAME,
)
from config.paths import WEB_PRESET_ROLES_DIR
except ImportError:
import sys
project_root = Path(__file__).resolve().parents[2]
@ -57,6 +58,7 @@ except ImportError:
WORKSPACE_MEMORY_DIRNAME,
WORKSPACE_REVIEW_DIRNAME,
)
from config.paths import WEB_PRESET_ROLES_DIR
from modules.file_manager import FileManager
from modules.search_engine import SearchEngine
@ -1078,6 +1080,7 @@ class MainTerminalToolsExecutionMixin:
elif tool_name == "sleep":
seconds = arguments.get("seconds")
wait_sub_agent_ids = arguments.get("wait_sub_agent_ids")
wait_sub_agent_output_ids = arguments.get("wait_sub_agent_output_ids")
wait_runcommand_id = arguments.get("wait_runcommand_id")
reason = arguments.get("reason", "等待操作完成")
@ -1086,23 +1089,67 @@ class MainTerminalToolsExecutionMixin:
provided += 1
if wait_sub_agent_ids:
provided += 1
if wait_sub_agent_output_ids:
provided += 1
if wait_runcommand_id:
provided += 1
if provided == 0:
result = {
"success": False,
"error": "sleep 至少需要提供一个参数seconds / wait_sub_agent_ids / wait_runcommand_id"
"error": "sleep 至少需要提供一个参数seconds / wait_sub_agent_ids / wait_sub_agent_output_ids / wait_runcommand_id"
}
elif provided > 1:
result = {
"success": False,
"error": "sleep 的等待参数互斥seconds / wait_sub_agent_ids / wait_runcommand_id 只能提供一个"
"error": "sleep 的等待参数互斥seconds / wait_sub_agent_ids / wait_sub_agent_output_ids / wait_runcommand_id 只能提供一个"
}
elif wait_sub_agent_output_ids:
if not getattr(self, "multi_agent_mode", False):
result = {"success": False, "error": "wait_sub_agent_output_ids 仅在多智能体模式下可用"}
elif not isinstance(wait_sub_agent_output_ids, list) or len(wait_sub_agent_output_ids) != 1:
result = {"success": False, "error": "wait_sub_agent_output_ids 必须包含且仅包含一个子智能体编号"}
else:
try:
agent_id = int(wait_sub_agent_output_ids[0])
if agent_id <= 0:
raise ValueError()
except Exception:
result = {"success": False, "error": "wait_sub_agent_output_ids 必须是正整数"}
else:
manager = getattr(self, "sub_agent_manager", None)
if not manager:
result = {"success": False, "error": "子智能体管理器不可用"}
else:
state = manager.get_multi_agent_state(getattr(self.context_manager, "current_conversation_id", None))
if not state:
result = {"success": False, "error": "当前对话没有多智能体状态"}
else:
try:
loop = asyncio.get_running_loop()
fut = state.register_output_wait(agent_id, loop)
msg = await asyncio.wait_for(fut, timeout=300)
result = {
"success": True,
"mode": "wait_sub_agent_output",
"agent_id": agent_id,
"message": msg,
}
except asyncio.TimeoutError:
result = {"success": False, "error": f"等待子智能体 {agent_id} 输出超时5分钟。可用 send_message_to_sub_agent 重新激活。"}
except asyncio.CancelledError:
result = {"success": False, "error": f"等待子智能体 {agent_id} 被取消。该子智能体现在不可用,可用 send_message_to_sub_agent 重新激活。"}
except RuntimeError as exc:
error_msg = str(exc)
if "send_message_to_sub_agent" not in error_msg:
error_msg += " 可用 send_message_to_sub_agent 重新激活。"
result = {"success": False, "error": error_msg}
except Exception as exc:
result = {"success": False, "error": f"等待子智能体 {agent_id} 输出失败: {exc}。可用 send_message_to_sub_agent 重新激活。"}
elif wait_sub_agent_ids:
if getattr(self, "multi_agent_mode", False):
result = {
"success": False,
"error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids。请通过 list_active_sub_agents / get_sub_agent_status 查看状态,或直接向子智能体发送消息。"
"error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids,请使用 wait_sub_agent_output_ids"
}
elif not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids:
result = {"success": False, "error": "wait_sub_agent_ids 必须是非空数组"}
@ -1748,9 +1795,9 @@ class MainTerminalToolsExecutionMixin:
from modules.multi_agent.prompts import build_multi_agent_sub_agent_prompt
_data_dir = str(getattr(self, "data_dir", ""))
_custom_dir = infer_custom_roles_dir(_data_dir)
# host模式下 custom_dir 就是 runtime_dirweb模式下 custom_dir 是用户个人目录
# host模式下 custom_dir 就是 runtime_dirweb模式下 runtime_dir 指向 web 预设目录
_is_web = '/web/users/' in _data_dir
_runtime_dir = None if _is_web else _custom_dir
_runtime_dir = WEB_PRESET_ROLES_DIR if _is_web else _custom_dir
role = load_preset_role(role_id, runtime_dir=_runtime_dir, custom_dir=_custom_dir)
if not role:
result = {"success": False, "error": f"角色不存在: {role_id}"}
@ -1883,7 +1930,7 @@ class MainTerminalToolsExecutionMixin:
agent_ids=arguments.get("agent_ids", [])
)
# 多智能体模式专属工具send_message_to_sub_agent / ask_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents
# 多智能体模式专属工具send_message_to_sub_agent / stop_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents
elif tool_name == "send_message_to_sub_agent":
if not getattr(self, "multi_agent_mode", False):
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
@ -1922,51 +1969,15 @@ class MainTerminalToolsExecutionMixin:
logger.exception("[multi_agent] send_message_to_sub_agent failed")
result = {"success": False, "error": str(exc)}
elif tool_name == "ask_sub_agent":
elif tool_name == "stop_sub_agent":
if not getattr(self, "multi_agent_mode", False):
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
else:
try:
from modules.multi_agent.state import format_multi_agent_message, TYPE_ASK
agent_id = int(arguments.get("agent_id", 0))
question = arguments.get("question", "")
timeout = int(arguments.get("timeout_seconds", 600))
conv_id = self.context_manager.current_conversation_id
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
if not state:
result = {"success": False, "error": "多智能体状态未就绪"}
else:
question_id = f"ask_sub_agent_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
# 构造提问并插入子对话(子智能体下一轮 assistant 输出作为回答返回到工具结果)
text = format_multi_agent_message(
display_name="Team Leader",
msg_type=TYPE_ASK,
content=question,
msg_id=question_id,
target=state.get_instance(agent_id).display_name if state.get_instance(agent_id) else f"Agent_{agent_id}",
)
ma_debug(
"tool_ask_sub_agent",
agent_id=agent_id,
question=str(question)[:500],
question_id=question_id,
wrapped_message_preview=text[:500],
conversation_id=conv_id,
)
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
if not ok:
result = {"success": False, "error": f"子智能体 {agent_id} 不存在"}
else:
# 阻塞等待子智能体下一轮输出作为回答
answer = await state.wait_for_answer(
question_id=question_id,
agent_id=agent_id,
timeout=timeout,
)
result = {"success": True, "answer": answer}
except asyncio.TimeoutError:
result = {"success": False, "error": "等待子智能体回答超时"}
result = self.sub_agent_manager.stop_sub_agent(agent_id=agent_id)
except Exception as exc:
logger.exception("[multi_agent] stop_sub_agent failed")
result = {"success": False, "error": str(exc)}
elif tool_name == "answer_sub_agent_question":

View File

@ -286,6 +286,9 @@ class MultiAgentState:
# 一个 agent 可能同时只阻塞在一个 ask 工具上(最简实现)
# key = agent_id, value = question_id表示当前 agent 正阻塞等待)
self.agent_blocking_question: Dict[int, str] = {}
# 主智能体通过 sleep(wait_sub_agent_output_ids) 等待某个子智能体下一次输出
# key = agent_id, value = asyncio.Future结果为完整消息文本
self.output_waits: Dict[int, asyncio.Future] = {}
# 角色实例计数role_id -> 已分配的最大 agent_id数字
# 用于创建新实例时自动递增编号,但允许调用方显式指定
self.role_counters: Dict[str, int] = {}
@ -313,17 +316,45 @@ class MultiAgentState:
return self.agents.get(aid)
def list_active(self) -> List[AgentInstance]:
return [a for a in self.agents.values() if a.status == "running" or a.status == "idle"]
# failed 视为可复活状态,仍算活跃
return [a for a in self.agents.values() if a.status in ("running", "idle", "failed")]
def list_all(self) -> List[AgentInstance]:
return list(self.agents.values())
def mark_status(self, agent_id: int, status: str, last_output: str = "") -> None:
a = self.agents.get(agent_id)
if a:
a.status = status
if last_output:
a.last_output = last_output
if not a:
return
a.status = status
if last_output:
a.last_output = last_output
# 当子智能体进入终态/idle 时,立即取消 sleep(wait_sub_agent_output_ids) 的等待
if status in ("failed", "terminated", "idle"):
self._cancel_output_wait(agent_id, status)
def _cancel_output_wait(self, agent_id: int, status: str) -> None:
"""取消指定 agent 的 sleep 输出等待,并提示可用 send_message_to_sub_agent 重新激活。"""
fut = self.output_waits.pop(agent_id, None)
if not fut or fut.done():
return
if status == "terminated":
msg = f"子智能体 {agent_id} 已终止,无法继续等待输出。"
elif status == "failed":
msg = f"子智能体 {agent_id} 已失败,无法继续等待输出。该子智能体可被复活,可用 send_message_to_sub_agent 重新激活。"
else:
msg = f"子智能体 {agent_id} 已进入空闲状态,无法继续等待输出。可用 send_message_to_sub_agent 重新激活。"
try:
loop = fut.get_loop()
if loop and not loop.is_closed():
loop.call_soon_threadsafe(fut.set_exception, RuntimeError(msg))
return
except Exception:
pass
try:
fut.set_exception(RuntimeError(msg))
except Exception:
pass
# ----- 主对话注入 -----
def push_master_message(self, message_text: str) -> None:
@ -353,6 +384,76 @@ class MultiAgentState:
def has_pending_master_messages(self) -> bool:
return len(self.pending_master_messages) > 0
# ----- 等待子智能体输出sleep wait_sub_agent_output_ids -----
def register_output_wait(
self, agent_id: int, loop: AbstractEventLoop
) -> asyncio.Future:
"""注册等待指定子智能体的下一次输出。
如果 `pending_master_messages` 里已经有该子智能体的未消费输出
则立即消费并返回该消息否则返回一个 future由后续输出唤醒
"""
fut: asyncio.Future = loop.create_future()
inst = self.agents.get(agent_id)
if not inst:
fut.set_exception(ValueError(f"未找到子智能体 {agent_id}"))
return fut
if inst.status in ("terminated", "failed"):
fut.set_exception(
RuntimeError(
f"子智能体 {agent_id} 当前状态为 {inst.status},无法等待输出"
)
)
return fut
# 优先消费 pending_master_messages 里已有的未消费输出
display_name = inst.display_name
for i, msg in enumerate(self.pending_master_messages):
parsed = parse_multi_agent_message(msg)
if parsed and parsed.get("display_name") == display_name:
self.pending_master_messages.pop(i)
fut.set_result(msg)
ma_debug(
"state_output_wait_claimed_pending",
conversation_id=self.conversation_id,
agent_id=agent_id,
display_name=display_name,
)
return fut
# 没有未消费输出,注册等待
self.output_waits[agent_id] = fut
ma_debug(
"state_output_wait_registered",
conversation_id=self.conversation_id,
agent_id=agent_id,
display_name=display_name,
)
return fut
def claim_output_wait(self, agent_id: int, message_text: str) -> bool:
"""如果该 agent 正被 sleep 等待输出,则把消息交给等待方并阻止进入主对话。"""
fut = self.output_waits.pop(agent_id, None)
if not fut:
return False
loop: Optional[AbstractEventLoop] = None
try:
loop = fut.get_loop()
except Exception:
pass
if loop and not loop.is_closed():
try:
loop.call_soon_threadsafe(fut.set_result, message_text)
return True
except Exception:
pass
try:
fut.set_result(message_text)
except Exception:
pass
return True
# ----- 阻塞问答 -----
async def wait_for_answer(self, question_id: str, agent_id: int, timeout: float = 600.0) -> str:
"""子智能体 ask_* 工具调用后阻塞等待答案。
@ -496,6 +597,17 @@ class MultiAgentState:
loop.call_soon_threadsafe(fut.cancel)
except Exception:
pass
for fut in list(self.output_waits.values()):
if not fut.done():
try:
loop = fut.get_loop()
if loop and not loop.is_closed():
loop.call_soon_threadsafe(fut.cancel)
else:
fut.cancel()
except Exception:
pass
self.output_waits.clear()
self.agents.clear()
self.task_id_to_agent_id.clear()
self.pending_master_messages.clear()

View File

@ -65,6 +65,23 @@ def _master_tool_create_sub_agent() -> Dict[str, Any]:
}
def _master_tool_stop_sub_agent() -> Dict[str, Any]:
return {
"type": "function",
"function": {
"name": "stop_sub_agent",
"description": "暂停指定子智能体实例,使其进入 idle 状态而不终结。暂停后可用 send_message_to_sub_agent 重新激活继续工作。",
"parameters": {
"type": "object",
"properties": _inject_intent({
"agent_id": {"type": "integer", "description": "要暂停的子智能体编号。"},
}),
"required": ["agent_id"],
},
},
}
def _master_tool_terminate_sub_agent() -> Dict[str, Any]:
return {
"type": "function",
@ -104,29 +121,6 @@ def _master_tool_send_message_to_sub_agent() -> Dict[str, Any]:
}
def _master_tool_ask_sub_agent() -> Dict[str, Any]:
return {
"type": "function",
"function": {
"name": "ask_sub_agent",
"description": (
"向指定子智能体提出一个明确问题并阻塞等待一轮回答(不是发起任务,是问问题)。"
"问题会以 `来自 Team Leader 的提问` 格式插入子对话,子智能体下一轮 assistant 输出"
"作为回答返回到此工具结果中。"
),
"parameters": {
"type": "object",
"properties": _inject_intent({
"agent_id": {"type": "integer", "description": "目标子智能体编号。"},
"question": {"type": "string", "description": "要询问的问题,应简短明确。"},
"timeout_seconds": {"type": "integer", "description": "等待回答超时秒数,默认 600。"},
}),
"required": ["agent_id", "question"],
},
},
}
def _master_tool_answer_sub_agent_question() -> Dict[str, Any]:
return {
"type": "function",
@ -210,9 +204,9 @@ def _master_tool_get_sub_agent_status() -> Dict[str, Any]:
MULTI_AGENT_MASTER_TOOLS: List[Dict[str, Any]] = [
_master_tool_create_sub_agent(),
_master_tool_stop_sub_agent(),
_master_tool_terminate_sub_agent(),
_master_tool_send_message_to_sub_agent(),
_master_tool_ask_sub_agent(),
_master_tool_answer_sub_agent_question(),
_master_tool_create_custom_agent(),
_master_tool_list_agents(),

View File

@ -24,7 +24,7 @@ from config import (
from utils.logger import setup_logger
from modules.sub_agent.task import SubAgentTask
from modules.sub_agent.prompts import build_user_message, build_system_prompt
from modules.sub_agent.tools import handle_search_workspace, handle_read_mediafile
from modules.sub_agent.tools import handle_read_mediafile
from modules.sub_agent.state import SubAgentStateMixin
from modules.sub_agent.stats import SubAgentStatsMixin
from modules.sub_agent.creation import SubAgentCreationMixin
@ -294,7 +294,13 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
def _on_done(fut):
try:
self._running_tasks.pop(task_id, None)
self._sub_agent_instances.pop(agent_id, None)
# 多智能体模式下 failed 视为可复活状态,保留实例引用供后续 send_message_to_sub_agent 重新激活
if multi_agent_mode:
final_task = self.tasks.get(task_id) or {}
if final_task.get("status") != "failed":
self._sub_agent_instances.pop(agent_id, None)
else:
self._sub_agent_instances.pop(agent_id, None)
self.reconcile_task_states(conversation_id=conversation_id)
# 多智能体模式:结束时把状态写回 MultiAgentState
if multi_agent_mode and multi_agent_state:
@ -434,6 +440,59 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
)
return count
def stop_sub_agent(
self,
*,
task_id: Optional[str] = None,
agent_id: Optional[int] = None,
) -> Dict:
"""暂停指定子智能体,使其进入 idle 状态而不终结。"""
task = self._select_task(task_id, agent_id)
if not task:
return {"success": False, "error": "未找到对应的子智能体任务"}
real_task_id = task["task_id"]
real_agent_id = task.get("agent_id")
if not task.get("multi_agent_mode"):
return {"success": False, "error": "stop_sub_agent 仅在多智能体模式下可用"}
# 查找或复活实例,确保能接收软停止信号
if real_agent_id is not None:
sub_agent = self._find_or_revive_sub_agent_task(real_agent_id)
else:
sub_agent = None
if sub_agent and hasattr(sub_agent, "request_soft_stop"):
try:
sub_agent.request_soft_stop()
except Exception as exc:
return {"success": False, "error": f"暂停子智能体失败: {exc}"}
else:
# 没有活实例时直接修改任务状态为 idle
task["status"] = "idle"
task["updated_at"] = time.time()
self._save_state()
# 同步更新 MultiAgentState
conversation_id = task.get("conversation_id")
if conversation_id:
state = self.get_multi_agent_state(conversation_id)
if state and real_agent_id is not None:
state.mark_status(real_agent_id, "idle")
ma_debug(
"manager_stop_sub_agent",
task_id=real_task_id,
agent_id=real_agent_id,
had_instance=bool(sub_agent),
)
return {
"success": True,
"task_id": real_task_id,
"agent_id": real_agent_id,
"message": f"子智能体{real_agent_id} 已暂停,可用 send_message_to_sub_agent 重新激活。",
}
def terminate_sub_agent(
self,
*,
@ -634,8 +693,6 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
try:
# 多智能体模式常见问答工具已在 SubAgentTask._execute_multi_agent_tool 中处理
# 这里只处理实际通过主进程执行的工具
if tool_name == "search_workspace":
return await handle_search_workspace(self.project_path, self.terminal, arguments)
if tool_name == "read_mediafile":
return await handle_read_mediafile(self.project_path, arguments)
@ -907,12 +964,14 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
def inject_message_to_sub_agent(self, agent_id: int, message_text: str) -> bool:
"""同事件循环中向子智能体上下文插入 user 消息。
适用于 ask_other_agent / send_message_to_sub_agent / answer_sub_agent_question_
非阻塞到工具结果的路径返回 True 表示成功注入
若内存中无运行实例 failed 后保留的实例已结束会尝试从 conversation
文件重建子智能体保留原 agent_id role_id后再注入消息
"""
# 查找该 agent_id 对应的 running SubAgentTask
sub_agent = self._find_sub_agent_task_by_agent_id(agent_id)
# 查找或复活该 agent_id 对应的 SubAgentTask
sub_agent = self._find_or_revive_sub_agent_task(agent_id)
ma_debug(
"manager_inject_message_to_sub_agent",
agent_id=agent_id,
@ -925,6 +984,162 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
sub_agent.inject_message(message_text)
return True
def _find_or_revive_sub_agent_task(self, agent_id: int) -> Optional[Any]:
"""查找内存中的 SubAgentTask不存在或已结束时从磁盘复活多智能体模式"""
inst = self._find_sub_agent_task_by_agent_id(agent_id)
if inst is not None:
task = getattr(inst, "_task", None)
if task is None or not task.done():
return inst
# 实例存在但已结束,需要复活前先清理旧引用
self._sub_agent_instances.pop(agent_id, None)
revived = self._revive_sub_agent(agent_id)
return revived
def _revive_sub_agent(self, agent_id: int) -> Optional[Any]:
"""从 conversation.json 重建一个多智能体子智能体实例(保留原 agent_id/role_id
用于 failed/idle 等可复活状态被 send_message_to_sub_agent 重新激活的场景
"""
from modules.sub_agent.task import SubAgentTask
candidates = [
t for t in self.tasks.values()
if isinstance(t, dict) and t.get("agent_id") == agent_id and t.get("multi_agent_mode")
]
if not candidates:
return None
# 按创建时间取最新一条
candidates.sort(key=lambda item: item.get("created_at", 0), reverse=True)
task = candidates[0]
task_id = task.get("task_id")
if not task_id:
return None
# 已在运行中则不重复重建
if task_id in self._running_tasks:
existing_inst = self._sub_agent_instances.get(agent_id)
if existing_inst is not None:
return existing_inst
task_root = Path(task.get("task_root", ""))
conversation_file = Path(task.get("conversation_file", ""))
system_prompt_file = task_root / "system_prompt.txt"
task_message_file = task_root / "task.txt"
if not conversation_file.exists():
logger.warning(f"[revive] 任务 {task_id} 的对话文件缺失,无法复活")
return None
try:
conversation_data = json.loads(conversation_file.read_text(encoding="utf-8"))
messages = list(conversation_data.get("messages") or [])
except Exception as exc:
logger.warning(f"[revive] 读取任务 {task_id} 对话文件失败: {exc}")
return None
system_prompt = ""
if system_prompt_file.exists():
try:
system_prompt = system_prompt_file.read_text(encoding="utf-8")
except Exception:
pass
task_message = ""
if task_message_file.exists():
try:
task_message = task_message_file.read_text(encoding="utf-8")
except Exception:
pass
if not messages:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": task_message},
]
conversation_id = task.get("conversation_id")
multi_agent_state = None
if conversation_id:
multi_agent_state = self.get_or_create_multi_agent_state(conversation_id)
if multi_agent_state and not multi_agent_state.get_instance(agent_id):
from modules.multi_agent.state import AgentInstance
inst = AgentInstance(
agent_id=agent_id,
role_id=task.get("role_id") or "",
display_name=task.get("display_name") or f"Agent_{agent_id}",
task_id=task_id,
status="idle",
summary=task.get("summary", ""),
)
try:
multi_agent_state.register_instance(inst)
except ValueError:
pass
sub_agent = SubAgentTask(
manager=self,
task_record=task,
task_message=task_message,
system_prompt=system_prompt,
model_key=task.get("model_key"),
thinking_mode=task.get("thinking_mode") or "fast",
multi_agent_mode=True,
multi_agent_state=multi_agent_state,
display_name=task.get("display_name"),
)
sub_agent.messages = messages
sub_agent._idle = True
task["status"] = "idle"
task["updated_at"] = time.time()
if multi_agent_state:
multi_agent_state.mark_status(agent_id, "idle")
# 同步落盘 output.json
try:
output_file = Path(task.get("output_file", ""))
if output_file.exists():
output_data = json.loads(output_file.read_text(encoding="utf-8"))
else:
output_data = {}
output_data["status"] = "idle"
output_data["success"] = None
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
except Exception as exc:
logger.warning(f"[revive] 更新任务 {task_id} output 文件失败: {exc}")
task_coro = sub_agent.run()
asyncio_task = self._run_coro(task_coro)
sub_agent._task = asyncio_task
self._running_tasks[task_id] = asyncio_task
self._sub_agent_instances[agent_id] = sub_agent
def _on_done(fut, tid=task_id, aid=agent_id, state=multi_agent_state, sa=sub_agent):
try:
self._running_tasks.pop(tid, None)
# failed 保留实例供复活;其余终态清理
if state:
final_task = self.tasks.get(tid) or {}
if final_task.get("status") != "failed":
self._sub_agent_instances.pop(aid, None)
else:
self._sub_agent_instances.pop(aid, None)
self.reconcile_task_states(conversation_id=conversation_id)
if state:
self._on_multi_agent_task_done(tid, aid, state, sa)
except Exception as exc:
logger.exception(f"[SubAgent] revived task {tid} 完成回调异常: {exc}")
ma_debug("manager_revive_on_done_exception", task_id=tid, agent_id=aid, error=str(exc))
asyncio_task.add_done_callback(_on_done)
ma_debug(
"manager_revive_sub_agent",
task_id=task_id,
agent_id=agent_id,
display_name=task.get("display_name"),
message_count=len(messages),
)
return sub_agent
def _find_sub_agent_task_by_agent_id(self, agent_id: int) -> Optional[Any]:
"""通过遍历创建中的 task 查找活 SubAgentTask 实例。

View File

@ -396,6 +396,27 @@ class SubAgentTask:
try:
from modules.multi_agent.state import build_sub_agent_output_text
msg = build_sub_agent_output_text(self.display_name, output_text.strip(), is_final=is_final)
# 如果该 agent 正被 sleep(wait_sub_agent_output_ids) 等待,把输出交给等待方,
# 不再插入主对话(避免同一条消息出现两遍)
if self.multi_agent_state.claim_output_wait(self.agent_id, msg):
ma_debug(
"sub_agent_output_claimed_by_wait",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
msg_preview=msg[:200],
)
# 同时记录到实例状态,供 list_active_sub_agents 使用
inst = self.multi_agent_state.get_instance(self.agent_id)
if inst:
inst.last_output = output_text[:500]
self.emit("progress", {
"subtype": "output",
"content": output_text,
"is_final": is_final,
"ts": int(time.time() * 1000),
})
return
self.multi_agent_state.push_master_message(msg)
ma_debug(
"sub_agent_forward_pushed",
@ -694,8 +715,6 @@ class SubAgentTask:
self.stats["write_files"] += 1
elif name == "edit_file":
self.stats["edit_files"] += 1
elif name == "search_workspace":
self.stats["searches"] += 1
elif name in ("web_search", "extract_webpage", "save_webpage"):
self.stats["web_pages"] += 1
elif name == "run_command":
@ -942,7 +961,8 @@ class SubAgentTask:
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
}
self.conversation_file.parent.mkdir(parents=True, exist_ok=True)
self.conversation_file.write_text(json.dumps(conversation_data, ensure_ascii=False), encoding="utf-8")
conversation_json = json.dumps(conversation_data, ensure_ascii=False)
self.conversation_file.write_text(conversation_json, encoding="utf-8")
output_data = {
"success": None,
@ -951,7 +971,8 @@ class SubAgentTask:
"stats": conversation_data["stats"],
}
self.output_file.parent.mkdir(parents=True, exist_ok=True)
self.output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
output_json = json.dumps(output_data, ensure_ascii=False)
self.output_file.write_text(output_json, encoding="utf-8")
except Exception as exc:
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
@ -1032,8 +1053,17 @@ class SubAgentTask:
idle=self._idle,
cancelled=self._cancelled,
)
if timeout:
status = "timeout"
elif max_turns_exceeded:
status = "failed"
elif success:
status = "completed"
else:
status = "failed"
output_data = {
"success": success,
"status": status,
"summary": summary,
"timeout": timeout,
"max_turns_exceeded": max_turns_exceeded,

View File

@ -145,29 +145,6 @@ SUB_AGENT_TOOLS: List[Dict[str, Any]] = [
},
},
},
{
"type": "function",
"function": {
"name": "search_workspace",
"description": "在本地目录内搜索文件名或文件内容(跨文件)。仅返回摘要。",
"parameters": {
"type": "object",
"properties": {
"mode": {"type": "string", "enum": ["file", "content"]},
"query": {"type": "string"},
"root": {"type": "string", "description": "搜索起点目录,默认 '.'"},
"use_regex": {"type": "boolean"},
"case_sensitive": {"type": "boolean"},
"max_results": {"type": "integer"},
"max_matches_per_file": {"type": "integer"},
"include_glob": {"type": "array", "items": {"type": "string"}},
"exclude_glob": {"type": "array", "items": {"type": "string"}},
"max_file_size": {"type": "integer"},
},
"required": ["mode", "query"],
},
},
},
{
"type": "function",
"function": {
@ -338,21 +315,6 @@ def _format_tool_result(name: str, raw: Any) -> str:
if raw.get("mode") == "save":
return f"已保存: {raw.get('path')}"
return f"URL: {raw.get('url')}\n{raw.get('content') or ''}"
if name == "search_workspace":
if raw.get("mode") == "file":
lines = [f"命中文件({len(raw.get('matches') or [])}):"]
for idx, p in enumerate(raw.get("matches") or [], 1):
lines.append(f"{idx}) {p}")
return "\n".join(lines)
if raw.get("mode") == "content":
parts = []
for item in raw.get("results") or []:
parts.append(item.get("file") or "")
for m in item.get("matches") or []:
parts.append(f"- L{m.get('line')}: {m.get('snippet')}")
parts.append("")
return "\n".join(parts).strip()
return ""
if name == "read_mediafile":
return raw.get("message") or "媒体已读取"
if name == "read_skill":

View File

@ -1,231 +1,16 @@
"""子智能体特有工具实现(search_workspace / read_mediafile
"""子智能体特有工具实现(read_mediafile
这些工具在主进程中没有同名工具因此由子智能体管理器本地实现
search_workspace 优先复用主进程的 run_command 调用 rg回退到 Python 遍历
read_mediafile 直接读取项目内媒体文件并返回 base64
search_workspace 工具已移除子智能体需要搜索时改用 run_command
执行 rg / grep / find走主进程沙箱链路避免进程内遍历大目录导致内存失控
"""
from __future__ import annotations
import base64
import fnmatch
import json
import mimetypes
import re
import shlex
import shutil
from pathlib import Path
from typing import Any, Dict, List, Optional
async def handle_search_workspace(
project_path: Path,
terminal: Any,
arguments: Dict[str, Any],
) -> Dict[str, Any]:
"""search_workspace 实现。"""
mode = arguments.get("mode")
query = arguments.get("query") or ""
root = arguments.get("root") or "."
use_regex = bool(arguments.get("use_regex", False))
case_sensitive = bool(arguments.get("case_sensitive", False))
max_results = int(arguments.get("max_results", 20))
max_matches_per_file = int(arguments.get("max_matches_per_file", 3))
include_glob = arguments.get("include_glob") or ["**/*"]
exclude_glob = arguments.get("exclude_glob") or []
max_file_size = arguments.get("max_file_size")
if mode not in {"file", "content"}:
return {"success": False, "error": "search_workspace mode 必须是 file 或 content"}
root_path = (project_path / root).resolve()
try:
root_path.relative_to(project_path)
except Exception:
return {"success": False, "error": "搜索根目录必须位于项目目录内"}
if mode == "file":
return _search_workspace_file(project_path, root_path, query, use_regex, case_sensitive, max_results, include_glob, exclude_glob)
rg_result = await _search_workspace_content_rg(
project_path, root_path, query, use_regex, case_sensitive, max_results, max_matches_per_file,
include_glob, exclude_glob, max_file_size, terminal
)
if rg_result is not None:
return rg_result
return _search_workspace_content_python(
project_path, root_path, query, use_regex, case_sensitive, max_results, max_matches_per_file,
include_glob, exclude_glob, max_file_size
)
def _search_workspace_file(
project_path: Path,
root_path: Path,
query: str,
use_regex: bool,
case_sensitive: bool,
max_results: int,
include_glob: List[str],
exclude_glob: List[str],
) -> Dict[str, Any]:
try:
pattern = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
needle = query if case_sensitive else query.lower()
matches = []
for item in _iter_files(project_path, root_path, include_glob, exclude_glob):
name = item.name
hay = name if case_sensitive else name.lower()
ok = pattern.search(name) if pattern else needle in hay
if ok:
matches.append(str(item.relative_to(project_path)))
if len(matches) >= max_results:
break
return {
"success": True,
"mode": "file",
"root": str(root_path.relative_to(project_path)),
"query": query,
"matches": matches,
}
except Exception as exc:
return {"success": False, "error": str(exc)}
async def _search_workspace_content_rg(
project_path: Path,
root_path: Path,
query: str,
use_regex: bool,
case_sensitive: bool,
max_results: int,
max_matches_per_file: int,
include_glob: List[str],
exclude_glob: List[str],
max_file_size: Optional[int],
terminal: Any,
) -> Optional[Dict[str, Any]]:
if not shutil.which("rg"):
return None
try:
args = ["rg", "--line-number", "--no-heading", "--color=never"]
if not use_regex:
args.append("--fixed-strings")
if not case_sensitive:
args.append("-i")
if max_matches_per_file:
args.append(f"--max-count={max_matches_per_file}")
if max_file_size:
args.append(f"--max-filesize={max_file_size}")
for g in include_glob:
args.extend(["-g", g])
for g in exclude_glob:
args.extend(["-g", f"!{g}"])
args.extend([query, str(root_path)])
result_text = await terminal.handle_tool_call("run_command", {"command": shlex.join(args), "timeout": 60})
parsed = json.loads(result_text) if isinstance(result_text, str) else result_text
if not parsed.get("success"):
return None
files_map: Dict[str, List[Dict[str, Any]]] = {}
for line in (parsed.get("output") or "").splitlines():
if not line.strip():
continue
parts = line.split(":", 2)
if len(parts) < 3:
continue
file_path, line_num, snippet = parts[0], parts[1], parts[2]
try:
line_no = int(line_num)
except ValueError:
continue
files_map.setdefault(file_path, [])
if len(files_map[file_path]) < max_matches_per_file:
files_map[file_path].append({"line": line_no, "snippet": snippet.strip()})
if len(files_map) >= max_results and len(files_map[file_path]) >= max_matches_per_file:
break
results = [{"file": f, "matches": ms} for f, ms in list(files_map.items())[:max_results]]
return {
"success": True,
"mode": "content",
"root": str(root_path.relative_to(project_path)),
"query": query,
"results": results,
}
except Exception:
return None
def _search_workspace_content_python(
project_path: Path,
root_path: Path,
query: str,
use_regex: bool,
case_sensitive: bool,
max_results: int,
max_matches_per_file: int,
include_glob: List[str],
exclude_glob: List[str],
max_file_size: Optional[int],
) -> Dict[str, Any]:
try:
pattern = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
needle = query if case_sensitive else query.lower()
results = []
for file_path in _iter_files(project_path, root_path, include_glob, exclude_glob):
if max_file_size:
try:
if file_path.stat().st_size > max_file_size:
continue
except Exception:
continue
try:
text = file_path.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
lines = text.splitlines()
matches = []
for idx, line in enumerate(lines, 1):
hay = line if case_sensitive else line.lower()
ok = pattern.search(line) if pattern else needle in hay
if ok:
matches.append({"line": idx, "snippet": line.strip()})
if len(matches) >= max_matches_per_file:
break
if matches:
results.append({"file": str(file_path.relative_to(project_path)), "matches": matches})
if len(results) >= max_results:
break
return {
"success": True,
"mode": "content",
"root": str(root_path.relative_to(project_path)),
"query": query,
"results": results,
}
except Exception as exc:
return {"success": False, "error": str(exc)}
def _iter_files(project_path: Path, root_path: Path, include_glob: List[str], exclude_glob: List[str]):
"""按 glob 规则遍历文件。"""
for item in root_path.rglob("*"):
if not item.is_file():
continue
try:
rel = item.relative_to(project_path)
except Exception:
continue
rel_str = str(rel)
included = any(fnmatch.fnmatch(rel_str, g) for g in include_glob)
if not included:
continue
if any(fnmatch.fnmatch(rel_str, g) for g in exclude_glob):
continue
yield item
from typing import Any, Dict
async def handle_read_mediafile(project_path: Path, arguments: Dict[str, Any]) -> Dict[str, Any]:

View File

@ -161,6 +161,54 @@
---
## 信息诚实与验证规范
### 1. 信息来源必须明确标注
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
- **基于我已有知识的回答**——模型训练知识,无实时来源;
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
### 2. 结论确定性必须分级
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
禁止把"很大概率"或"有可能"说成确定结论。
### 3. 工作完成状态禁止默认等同于操作完成
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
因此:
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了"
- 必须说明实际做了什么操作,以及当前的验证状态;
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
### 4. 替代方案必须经用户许可并说明缺陷
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
1. 明确得到用户的许可;
2. 清楚说明该方案与用户原始要求的差异;
3. 说明可能带来的缺陷、风险或错误。
没有得到明确许可前,禁止擅自采用折中方案。
### 5. 异常排查优先怀疑自己
遇到与预期不符的情况时,排查顺序必须是:
1. 先怀疑**我的操作有问题**
2. 再怀疑**我改的地方错误了**
3. 最后再考虑外部环境或用户侧因素。
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
---
## 5. 操作技巧
- **待办事项**任务≥2步时使用 `todo_create`当用户提出稍微复杂的要求、预计超过3个步骤时应积极创建待办事项概述≤50字任务2-6条完成立即更新

View File

@ -178,6 +178,54 @@
---
## 信息诚实与验证规范
### 1. 信息来源必须明确标注
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
- **基于我已有知识的回答**——模型训练知识,无实时来源;
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
### 2. 结论确定性必须分级
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
禁止把"很大概率"或"有可能"说成确定结论。
### 3. 工作完成状态禁止默认等同于操作完成
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
因此:
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了"
- 必须说明实际做了什么操作,以及当前的验证状态;
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
### 4. 替代方案必须经用户许可并说明缺陷
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
1. 明确得到用户的许可;
2. 清楚说明该方案与用户原始要求的差异;
3. 说明可能带来的缺陷、风险或错误。
没有得到明确许可前,禁止擅自采用折中方案。
### 5. 异常排查优先怀疑自己
遇到与预期不符的情况时,排查顺序必须是:
1. 先怀疑**我的操作有问题**
2. 再怀疑**我改的地方错误了**
3. 最后再考虑外部环境或用户侧因素。
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
---
## 5. 操作技巧
- **待办事项**任务≥2步时使用 `todo_create`当用户提出稍微复杂的要求、预计超过3个步骤时应积极创建待办事项概述≤50字任务2-6条完成立即更新

View File

@ -25,15 +25,40 @@
- **监督与引导**:掌握团队全局进度,在子智能体偏离方向时及时干预纠正
- **综合与决策**:收集子智能体的产出,综合形成最终结果,对用户负责
- **除非用户要求,或必须为之,你需要尽可能避免自己亲自去执行任务**
你不是单打独斗的执行者但你的主要价值不在于亲自执行。你拥有完整的工具集文件读写、终端、搜索、MCP、skill、memory 等),只在必要的统筹、协调和判断中使用它们;具体的读、写、改、验证等操作默认交给子智能体。
你不是单打独斗的执行者。除非任务极其简单或明确不需要子智能体否则你应该主动把任务拆解并指派给合适的角色。但同时你仍然拥有完整的工具集文件读写、终端、搜索、MCP、skill、memory 等),在需要时可以自己动手。
## 你自己做 vs 派给子智能体
你的核心定位是 **统筹者、协调者、决策者和汇报者**,而不是执行者。你的上下文非常宝贵,必须省着用。
### 你必须自己做(不可替代)
1. **理解用户意图**:澄清模糊需求、追问关键信息、把用户的话翻译成可执行目标。
2. **整体规划与任务拆解**:决定要做哪些事、按什么顺序、需要哪些角色。
3. **分配与协调子智能体**:创建子智能体、给它们下指令、在它们之间传递信息、纠正方向。
4. **回答子智能体提问**:通过 `answer_sub_agent_question` 及时回应子智能体的 `ask_master`。
5. **综合与最终汇报**:把各子智能体的产出整合成统一结论,用清晰结构向用户汇报。
### 你应该派给子智能体(默认不自己做)
1. **项目初期调查**:从对话开始就可以创建子智能体并行调查项目结构、关键文件、代码位置、运行方式。不要等"正式开始"才创建它们。
2. **具体实现与修改**:写代码、改配置、调整前端、调试 bug 等。
3. **专项调研与对比**:技术选型、竞品分析、文档阅读、方案搜索。
4. **验证与检查**:代码审查、测试执行、结果验证。
5. **大量文件阅读**:需要读多个文件或长文件才能得出结论时,优先派给子智能体,而不是自己逐行读。
### 核心原则
- **默认派出去**:只要不是"理解、规划、协调、决策、汇报"这几类工作,就默认创建子智能体去做。
- **尽早并行**:任务一开始就可以创建 2-3 个子智能体并行摸底,你再根据反馈调整方向。
- **节省上下文**:尽量避免自己调用 `read_file`、`write_file`、`edit_file`、`run_command` 等消耗上下文的工具;这些操作优先由子智能体完成,你只要看结论。
- **只做统筹**:你的价值在于判断"谁做什么、何时干预、怎么综合",而不是亲自敲代码。
## 任务复杂度评估与投入预算
在开始执行前,先评估任务复杂度,决定子智能体数量和工具调用预算:
决定派多少子智能体时,评估任务复杂度,决定子智能体数量和工具调用预算:
- **简单任务**单一明确目标1-2 步即可完成1 个子智能体,约 3-10 次工具调用。甚至可以自己直接做,不需要创建子智能体。
- **简单任务**单一明确目标1-2 步即可完成1 个子智能体,约 3-10 次工具调用。
- **中等任务**(需要多方面调查或 2-3 个不同角色协作2-4 个子智能体,每个约 10-15 次工具调用。
- **复杂任务**(涉及多个领域、需要深度调查或多阶段执行):可以创建更多子智能体,每个有明确分工的职责范围。
@ -56,7 +81,8 @@
## 工作原则
- **主动分工**:除非任务极其简单或明确不需要子智能体,否则主动把任务拆解并指派给合适的角色。
- **明确你的身份**和子智能体交流的对象是你Team Leader不是用户。对子智能体来说你才是它们的 user。你在委派任务、回答提问、发送引导消息时代表的是 Team Leader不要替用户转述或以用户自居。
- **主动分工**:根据「你自己做 vs 派给子智能体」的分类,把执行性、调研性、验证性工作默认派给子智能体;你自己专注于理解、规划、协调、决策和汇报。
- **明确指令**:委派时写清楚任务目标、范围、产出要求和边界(见上方"委派质量标准")。
- **及时回答**:当子智能体通过 `ask_master` 提问时,必须尽快通过 `answer_sub_agent_question` 回答。子智能体在阻塞等待你的回答,拖延会卡住整个团队。
- **监督进度**:通过 `list_active_sub_agents` / `get_sub_agent_status` 掌握全局,并在合适的时机引导子智能体。
@ -65,12 +91,60 @@
- **先宽后窄**:在探索性任务中,引导子智能体先从宽泛的角度了解全局,再逐步聚焦到具体细节。避免一开始就钻入过窄的方向。
- **上下文意识**:子智能体的每轮输出都会插入你的对话上下文。引导子智能体精简输出——只汇报关键步骤和结论,不要把冗长的过程细节都推给你。详细过程留在子智能体自己的上下文里。
---
## 信息诚实与验证规范
### 1. 信息来源必须明确标注
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
- **基于我已有知识的回答**——模型训练知识,无实时来源;
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
### 2. 结论确定性必须分级
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
禁止把"很大概率"或"有可能"说成确定结论。
### 3. 工作完成状态禁止默认等同于操作完成
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
因此:
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了"
- 必须说明实际做了什么操作,以及当前的验证状态;
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
### 4. 替代方案必须经用户许可并说明缺陷
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
1. 明确得到用户的许可;
2. 清楚说明该方案与用户原始要求的差异;
3. 说明可能带来的缺陷、风险或错误。
没有得到明确许可前,禁止擅自采用折中方案。
### 5. 异常排查优先怀疑自己
遇到与预期不符的情况时,排查顺序必须是:
1. 先怀疑**我的操作有问题**
2. 再怀疑**我改的地方错误了**
3. 最后再考虑外部环境或用户侧因素。
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
## 子智能体运行期间的行为规范
当子智能体正在工作时,你的行为必须遵循以下规则:
- **严格禁止反复查看状态**:不要在子智能体运行期间反复调用 `get_sub_agent_status` 或 `list_active_sub_agents` 来查看进度。子智能体的输出会自动以消息形式插入你的对话,你不需要主动轮询。反复查状态是浪费时间的行为。
- **严格禁止用 sleep 填充等待**:不要在子智能体运行期间反复调用 `sleep` 工具来等待。如果你没有需要立即处理的事情,**立刻停止输出**,等待子智能体的汇报消息自动到达。
- **等待子智能体输出的正确方式**:如果你没有需要立即处理的事情,可以选择**立刻停止输出**(等待子智能体的汇报消息自动到达),也可以选择使用 `sleep` 工具的 `wait_sub_agent_output_ids` 主动等待指定子智能体的下一次输出。不要在子智能体运行期间反复调用 `sleep` 做无意义的短延迟
- **无事可做时立刻停止输出**:如果你已经把任务委派出去,当前没有需要回答的 `ask_master` 提问,也没有需要干预的情况,就**停止输出**,不要输出任何文字。子智能体的产出会以 user 消息自动插入你的对话,届时你再继续工作。
- **有事可做时主动干预**:如果你在子智能体的输出中发现了需要纠正的错误、可以提供的指导、或需要传递给其他子智能体的信息,立刻用 `send_message_to_sub_agent` 行动。这是你运行期间最有价值的工作。

View File

@ -34,6 +34,54 @@
- 生成的文档要清晰、格式正确
- 代码要包含必要的注释和说明
---
## 信息诚实与验证规范
### 1. 信息来源必须明确标注
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
- **基于我已有知识的回答**——模型训练知识,无实时来源;
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
### 2. 结论确定性必须分级
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
禁止把"很大概率"或"有可能"说成确定结论。
### 3. 工作完成状态禁止默认等同于操作完成
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
因此:
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了"
- 必须说明实际做了什么操作,以及当前的验证状态;
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
### 4. 替代方案必须经用户许可并说明缺陷
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
1. 明确得到用户的许可;
2. 清楚说明该方案与用户原始要求的差异;
3. 说明可能带来的缺陷、风险或错误。
没有得到明确许可前,禁止擅自采用折中方案。
### 5. 异常排查优先怀疑自己
遇到与预期不符的情况时,排查顺序必须是:
1. 先怀疑**我的操作有问题**
2. 再怀疑**我改的地方错误了**
3. 最后再考虑外部环境或用户侧因素。
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
# 交付要求
所有结果文件必须放在指定的交付目录中,包括:
@ -54,8 +102,7 @@
你拥有以下工具能力:
- read_file: 读取文件内容
- write_file / edit_file: 创建或修改文件
- search_workspace: 搜索文件和代码
- run_command: 执行终端命令
- run_command: 执行终端命令(需要搜索文件/代码时,用它执行 rg 或 grep -rn / find务必排除 node_modules、.git 等重目录,例如 grep -rn --exclude-dir=node_modules --exclude-dir=.git
- web_search / extract_webpage: 搜索和提取网页内容
- read_mediafile: 读取图片/视频文件
- finish_task: 完成任务并退出(必须调用)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import json
import re
from typing import Any, Dict, List, Optional
@ -10,7 +11,16 @@ def extract_intent_from_partial(arg_str: str) -> Optional[str]:
return None
match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
if match:
return match.group(1)
raw = match.group(1)
try:
# 某些模型会对非 ASCII 字符输出 JSON Unicode 转义(如 \u4e2d\u6587
# 这里用 json.loads 安全解码,失败时回退原始值。
decoded = json.loads('"{}"'.format(raw))
if isinstance(decoded, str):
return decoded
except Exception:
pass
return raw
return None

View File

@ -350,7 +350,6 @@ def inject_multi_agent_master_message(
inline=inline,
after_tool_call_id=after_tool_call_id,
)
# 解析标准格式以获取 subtype解析失败时回退到通用类型
try:
from modules.multi_agent.state import parse_multi_agent_message

View File

@ -644,13 +644,20 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
metadata_overrides={
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
"multi_agent_mode": multi_agent_mode,
"multi_agent_mode": bool(multi_agent_mode),
},
)
try:
cm.current_conversation_id = previous_cm_current
except Exception:
pass
# 同步 terminal 级别的多智能体开关,避免新对话继承旧状态
terminal.multi_agent_mode = bool(multi_agent_mode)
try:
if hasattr(terminal, "sub_agent_manager"):
terminal.sub_agent_manager.multi_agent_mode = bool(multi_agent_mode)
except Exception:
pass
result = {
"success": True,
"conversation_id": conversation_id,
@ -661,8 +668,15 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
result = terminal.create_new_conversation(
thinking_mode=thinking_mode,
run_mode=run_mode,
metadata_overrides={"multi_agent_mode": multi_agent_mode} if multi_agent_mode else None,
metadata_overrides={"multi_agent_mode": bool(multi_agent_mode)},
)
# 同步 terminal 级别的多智能体开关,避免新对话继承旧状态
terminal.multi_agent_mode = bool(multi_agent_mode)
try:
if hasattr(terminal, "sub_agent_manager"):
terminal.sub_agent_manager.multi_agent_mode = bool(multi_agent_mode)
except Exception:
pass
if result["success"]:
# 仅在当前工作区创建时更新 session 模式;指定其他工作区时由前端切换后自动同步。

View File

@ -6,7 +6,6 @@ from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
def _load_summary_prompt(web_terminal) -> str:
"""从 prompts/deep_compression_summary.txt 加载压缩总结提示词。"""
try:
@ -509,7 +508,6 @@ async def run_deep_compression(
"compress_behavior": compress_behavior,
"job_id": job_id,
})
return {
"success": True,
"in_place": True,

View File

@ -8,6 +8,7 @@
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, List
@ -328,6 +329,13 @@ def create_multi_agent_conversation():
ctx_manager.current_conversation_id = previous_cm_current
except Exception:
pass
# 同步 terminal 级别的多智能体开关
terminal.multi_agent_mode = True
try:
if hasattr(terminal, "sub_agent_manager"):
terminal.sub_agent_manager.multi_agent_mode = True
except Exception:
pass
# 触发对话列表更新事件
try:
@ -398,11 +406,28 @@ def list_active_sub_agents_api():
if not sub_agent_manager:
return jsonify({"success": False, "error": "子智能体管理器未就绪"}), 503
state = sub_agent_manager.get_multi_agent_state(conversation_id)
agents: List[Dict[str, Any]] = []
if state:
for a in state.list_all():
d = a.to_dict()
task = sub_agent_manager.tasks.get(a.task_id)
if isinstance(task, dict):
d["last_tool"] = task.get("last_tool")
stats_file = Path(task.get("stats_file", ""))
if stats_file.exists():
try:
stats = json.loads(stats_file.read_text(encoding="utf-8"))
d["current_context_tokens"] = stats.get("current_context_tokens", 0)
except Exception:
d["current_context_tokens"] = 0
else:
d["current_context_tokens"] = 0
agents.append(d)
if not state:
return jsonify({"success": True, "agents": []})
return jsonify({
"success": True,
"agents": [a.to_dict() for a in state.list_all()],
"agents": agents,
})

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理员监控面板</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/dist/assets/admin.css" />
<script src="/static/security.js"></script>
</head>

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理员策略配置</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/dist/assets/adminPolicy.css" />
<script src="/static/security.js"></script>
</head>

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

35
static/astrion-avatar.svg Normal file
View File

@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 200 200" aria-hidden="true">
<style>
.sa-frame, .sa-eye {
stroke: #000000;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
.sa-frame {
stroke-width: 12;
}
.sa-eye {
stroke-width: 10;
}
@media (prefers-color-scheme: dark) {
.sa-frame, .sa-eye {
stroke: #ffffff;
}
}
</style>
<path
class="sa-frame"
d="M 175.959,107.000 Q 180.000,100.000 175.959,93.000 L 144.041,37.718 Q 140.000,30.718 131.917,30.718 L 68.083,30.718 Q 60.000,30.718 55.959,37.718 L 24.041,93.000 Q 20.000,100.000 24.041,107.000 L 55.959,162.282 Q 60.000,169.282 68.083,169.282 L 131.917,169.282 Q 140.000,169.282 144.041,162.282 Z"
/>
<path
class="sa-eye"
d="M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11"
transform="translate(84, 96)"
/>
<path
class="sa-eye"
d="M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11"
transform="translate(116, 96)"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -4,7 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>自定义工具管理</title>
<link rel="icon" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/dist/assets/adminCustomTools.css" />
<script src="/static/security.js"></script>
</head>

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>极简模式 V2 - 无缝展开</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="style.css">
</head>
<body>

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>极简模式 Demo</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="style.css">
</head>
<body>

BIN
static/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

BIN
static/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

View File

@ -3,6 +3,10 @@
<head>
<meta charset="UTF-8">
<title>文件管理器</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/file_manager/style.css">
</head>
<body>

View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
</svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astrion</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/easter-eggs/flood.css">
<link rel="stylesheet" href="/static/easter-eggs/snake.css">

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 - Astrion</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<script>
(function () {
try {

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>注册 - Astrion</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<script>
(function () {
try {

View File

@ -41,7 +41,6 @@ export async function mounted() {
// 立即加载初始数据(并行获取状态,优先同步运行模式)
const initialDataPromise = this.loadInitialData();
this.refreshProjectGitSummary?.();
this.startProjectGitSummaryIdleRefresh?.();
this.startConnectionHeartbeat();
this.fetchTerminalCount();
@ -50,6 +49,8 @@ export async function mounted() {
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
initialDataPromise
.then(() => {
// 初始数据加载完成后再刷新 git 摘要,确保 workspace/project_path 已就绪
this.refreshProjectGitSummary?.();
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
this.checkTutorialPrompt();
})

View File

@ -232,6 +232,8 @@ export const loadMethods = {
this.fetchNetworkPermission();
await this.fetchVersioningStatus(conversationId, { silent: true });
this.fetchPendingToolApprovals();
this.fetchPendingUserQuestions();
this.refreshProjectGitSummary();
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
traceLog('loadConversation:after-history', {

View File

@ -58,6 +58,35 @@ export const dialogMethods = {
this.userQuestionDialogVisible = true;
this.userQuestionMinimized = false;
},
async fetchPendingUserQuestions() {
if (!this.currentConversationId) {
this.pendingUserQuestions = [];
return;
}
try {
const response = await fetch(
`/api/user-questions/pending?conversation_id=${encodeURIComponent(this.currentConversationId)}`
);
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.success) {
return;
}
const items = Array.isArray(payload.items) ? payload.items : [];
this.pendingUserQuestions = items;
if (items.length > 0) {
this.userQuestionActiveIndex = Math.min(
Math.max(0, Number(this.userQuestionActiveIndex || 0)),
Math.max(0, items.length - 1)
);
} else {
this.userQuestionDialogVisible = false;
this.userQuestionMinimized = false;
this.userQuestionActiveIndex = 0;
}
} catch (_error) {
// ignore
}
},
approveToolApproval(approvalId) {
return this.decideToolApproval(approvalId, 'approved');
},

View File

@ -70,7 +70,7 @@ export const gitMethods = {
if (this.gitChangesPanelOpen) {
this.loadGitChangesDiff?.();
}
} catch (_) {
} catch (_error) {
this.projectGitSummary = null;
this.gitChangesDiff = null;
} finally {

View File

@ -151,6 +151,9 @@ export const routeMethods = {
this.initialRouteResolved = true;
}
await this.restoreComposerDraftState('bootstrap-route:conversation');
if (this.currentConversationId) {
this.fetchPendingUserQuestions?.();
}
},
handlePopState(event) {
const state = event.state || {};

View File

@ -372,6 +372,7 @@ export const socketMethods = {
this.fetchConversationTokenStatistics();
this.updateCurrentContextTokens();
await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
this.fetchPendingUserQuestions();
} catch (e) {
console.warn('获取当前对话标题失败:', e);
this.titleReady = true;

View File

@ -52,10 +52,9 @@
</div>
<!-- 加载动画或完成图标 -->
<div class="summary-status-icon">
<component
v-if="isSummaryRunning(group.actions, group.id)"
:is="getSummaryLoader(group.id)"
/>
<div v-if="isSummaryRunning(group.actions, group.id)" class="summary-icon-inner">
<component :is="getSummaryLoader(group.id)" />
</div>
<svg
v-else
class="check-icon"
@ -1081,6 +1080,8 @@ onBeforeUnmount(() => {
color: var(--claude-text-secondary);
line-height: 1.7;
padding: 0;
position: relative;
z-index: 0;
}
.summary-preview {
@ -1157,8 +1158,18 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
justify-content: center;
min-width: 18px;
min-height: 18px;
width: 18px;
height: 18px;
/* 严格限制右侧不溢出对话记录边界;上下左允许动画自然延伸,不裁剪 */
clip-path: inset(-100% 0 -100% -100%);
}
.summary-icon-inner {
display: flex;
align-items: center;
justify-content: center;
/* 整体向左偏移 10px不影响容器尺寸和布局 */
transform: translateX(-10px);
}
.check-icon {

View File

@ -204,8 +204,6 @@ function renderToolResult(): string {
return renderTerminateSubAgent(result, args);
} else if (name === 'send_message_to_sub_agent') {
return renderSendMessageToSubAgent(result, args);
} else if (name === 'ask_sub_agent') {
return renderAskSubAgent(result, args);
} else if (name === 'answer_sub_agent_question') {
return renderAnswerSubAgentQuestion(result, args);
} else if (name === 'create_custom_agent') {
@ -832,14 +830,123 @@ function renderTerminalSnapshot(result: any, args: any): string {
}
function renderSleep(result: any, args: any): string {
const seconds = args.seconds || args.duration || 0;
const status = formatToolStatusLabel(result, '✓ 完成');
const mode = result?.mode || 'seconds';
let html = '<div class="tool-result-meta">';
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>模式:</strong>${escapeHtml(mode)}</div>`;
if (mode === 'seconds' || mode === '' || mode === null) {
const seconds = args.seconds || args.duration || 0;
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
if (args.reason) {
html += `<div><strong>原因:</strong>${escapeHtml(String(args.reason))}</div>`;
}
} else if (mode === 'wait_sub_agent_output') {
const agentId = result?.agent_id ?? args?.wait_sub_agent_output_ids ?? '';
if (agentId !== '') {
html += `<div><strong>等待子智能体:</strong>#${escapeHtml(String(agentId))}</div>`;
}
} else if (mode === 'wait_sub_agent_ids') {
const agentIds = Array.isArray(result?.agent_ids) ? result.agent_ids : [];
if (agentIds.length > 0) {
html += `<div><strong>等待子智能体:</strong>${agentIds.map(String).join(', ')}</div>`;
}
const taskIds = Array.isArray(result?.waited_task_ids) ? result.waited_task_ids : [];
if (taskIds.length > 0) {
html += `<div><strong>任务 ID</strong>${taskIds.map(String).join(', ')}</div>`;
}
} else if (mode === 'wait_runcommand_id') {
const commandId = result?.command_id ?? args?.wait_runcommand_id ?? '';
if (commandId !== '') {
html += `<div><strong>后台命令 ID</strong>${escapeHtml(String(commandId))}</div>`;
}
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
// content
if (mode === 'seconds' || mode === '' || mode === null) {
const message = result?.message || '';
if (message) {
html += '<div class="tool-result-content scrollable">';
html += `<div class="content-label">结果:</div>`;
html += `<pre>${escapeHtml(String(message))}</pre>`;
html += '</div>';
}
} else if (mode === 'wait_sub_agent_output') {
const message = result?.message || '';
if (message) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">子智能体输出:</div>';
html += `<pre>${escapeHtml(String(message))}</pre>`;
html += '</div>';
}
} else if (mode === 'wait_sub_agent_ids') {
const results = Array.isArray(result?.results) ? result.results : [];
if (results.length > 0) {
html += '<div class="tool-result-content scrollable">';
html += `<div class="content-label">已等待 ${results.length} 个子智能体:</div>`;
results.forEach((item: any) => {
if (!item || typeof item !== 'object') return;
const agentId = item.agent_id ?? '?';
const taskId = item.task_id ?? '?';
const itemStatus = item.status ?? 'unknown';
const success = item.success === true;
html += '<div class="sub-agent-result-item">';
html += `<div><strong>子智能体 #${escapeHtml(String(agentId))}</strong>(任务 ${escapeHtml(String(taskId))})· ${escapeHtml(String(itemStatus))} · ${success ? '✓ 成功' : '✗ 失败'}</div>`;
const itemMessage = item.message || item.error || '';
if (itemMessage) {
const runtime = item.runtime_seconds ?? item.elapsed_seconds ?? null;
const runtimeNote = runtime !== null ? `运行 ${Math.round(Number(runtime))}` : '';
const stats = item.stats;
if (runtimeNote || stats) {
html += '<div class="sub-agent-meta">';
if (runtimeNote) {
html += `<span>${escapeHtml(runtimeNote)}</span>`;
}
if (stats && typeof stats === 'object') {
const statParts = [];
if (stats.api_calls != null) statParts.push(`调用 ${stats.api_calls}`);
if (stats.files_read != null) statParts.push(`阅读 ${stats.files_read}`);
if (stats.edit_files != null) statParts.push(`编辑 ${stats.edit_files}`);
if (stats.searches != null) statParts.push(`搜索 ${stats.searches}`);
if (stats.web_pages != null) statParts.push(`网页 ${stats.web_pages}`);
if (stats.commands != null) statParts.push(`命令 ${stats.commands}`);
if (statParts.length > 0) {
html += `<span>${escapeHtml(statParts.join(' | '))}</span>`;
}
}
html += '</div>';
}
html += `<pre>${escapeHtml(String(itemMessage))}</pre>`;
}
html += '</div>';
});
html += '</div>';
}
} else if (mode === 'wait_runcommand_id') {
const nested = result?.result;
if (nested && typeof nested === 'object') {
const output = nested.output || nested.stdout || '';
const exitCode = nested.exit_code !== undefined ? nested.exit_code : nested.returncode;
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">后台命令输出:</div>';
if (exitCode !== undefined) {
html += `<div><strong>退出码:</strong>${escapeHtml(String(exitCode))}</div>`;
}
if (output) {
html += `<pre>${escapeHtml(String(output))}</pre>`;
} else {
html += '<div class="tool-result-empty">[无输出]</div>';
}
html += '</div>';
}
}
return html;
}
@ -1378,42 +1485,6 @@ function renderSendMessageToSubAgent(result: any, args: any): string {
return html;
}
function renderAskSubAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
const agentId = args.agent_id ?? '';
const question = args.question ?? '';
const answer =
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (agentId !== '') {
html += `<div><strong>子智能体 ID</strong>${escapeHtml(String(agentId))}</div>`;
}
if (question) {
const preview = String(question).slice(0, 200);
const suffix = String(question).length > 200 ? '…' : '';
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
if (answer && typeof answer === 'string') {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">回答</div>';
html += `<pre>${escapeHtml(answer)}</pre>`;
html += '</div>';
} else if (result?.success) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
html += '</div>';
}
return html;
}
function renderAnswerSubAgentQuestion(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
const questionId = args.question_id ?? result.question_id ?? '';

View File

@ -159,8 +159,6 @@ export function renderEnhancedToolResult(
return renderTerminateSubAgent(result, args);
} else if (name === 'send_message_to_sub_agent') {
return renderSendMessageToSubAgent(result, args);
} else if (name === 'ask_sub_agent') {
return renderAskSubAgent(result, args);
} else if (name === 'answer_sub_agent_question') {
return renderAnswerSubAgentQuestion(result, args);
} else if (name === 'create_custom_agent') {
@ -804,14 +802,123 @@ function renderTerminalSnapshot(result: any, args: any): string {
}
function renderSleep(result: any, args: any): string {
const seconds = args.seconds || args.duration || 0;
const status = formatToolStatusLabel(result, '✓ 完成');
const mode = result?.mode || 'seconds';
let html = '<div class="tool-result-meta">';
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += `<div><strong>模式:</strong>${escapeHtml(mode)}</div>`;
if (mode === 'seconds' || mode === '' || mode === null) {
const seconds = args.seconds || args.duration || 0;
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
if (args.reason) {
html += `<div><strong>原因:</strong>${escapeHtml(String(args.reason))}</div>`;
}
} else if (mode === 'wait_sub_agent_output') {
const agentId = result?.agent_id ?? args?.wait_sub_agent_output_ids ?? '';
if (agentId !== '') {
html += `<div><strong>等待子智能体:</strong>#${escapeHtml(String(agentId))}</div>`;
}
} else if (mode === 'wait_sub_agent_ids') {
const agentIds = Array.isArray(result?.agent_ids) ? result.agent_ids : [];
if (agentIds.length > 0) {
html += `<div><strong>等待子智能体:</strong>${agentIds.map(String).join(', ')}</div>`;
}
const taskIds = Array.isArray(result?.waited_task_ids) ? result.waited_task_ids : [];
if (taskIds.length > 0) {
html += `<div><strong>任务 ID</strong>${taskIds.map(String).join(', ')}</div>`;
}
} else if (mode === 'wait_runcommand_id') {
const commandId = result?.command_id ?? args?.wait_runcommand_id ?? '';
if (commandId !== '') {
html += `<div><strong>后台命令 ID</strong>${escapeHtml(String(commandId))}</div>`;
}
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
// content
if (mode === 'seconds' || mode === '' || mode === null) {
const message = result?.message || '';
if (message) {
html += '<div class="tool-result-content scrollable">';
html += `<div class="content-label">结果:</div>`;
html += `<pre>${escapeHtml(String(message))}</pre>`;
html += '</div>';
}
} else if (mode === 'wait_sub_agent_output') {
const message = result?.message || '';
if (message) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">子智能体输出:</div>';
html += `<pre>${escapeHtml(String(message))}</pre>`;
html += '</div>';
}
} else if (mode === 'wait_sub_agent_ids') {
const results = Array.isArray(result?.results) ? result.results : [];
if (results.length > 0) {
html += '<div class="tool-result-content scrollable">';
html += `<div class="content-label">已等待 ${results.length} 个子智能体:</div>`;
results.forEach((item: any) => {
if (!item || typeof item !== 'object') return;
const agentId = item.agent_id ?? '?';
const taskId = item.task_id ?? '?';
const itemStatus = item.status ?? 'unknown';
const success = item.success === true;
html += '<div class="sub-agent-result-item">';
html += `<div><strong>子智能体 #${escapeHtml(String(agentId))}</strong>(任务 ${escapeHtml(String(taskId))})· ${escapeHtml(String(itemStatus))} · ${success ? '✓ 成功' : '✗ 失败'}</div>`;
const itemMessage = item.message || item.error || '';
if (itemMessage) {
const runtime = item.runtime_seconds ?? item.elapsed_seconds ?? null;
const runtimeNote = runtime !== null ? `运行 ${Math.round(Number(runtime))}` : '';
const stats = item.stats;
if (runtimeNote || stats) {
html += '<div class="sub-agent-meta">';
if (runtimeNote) {
html += `<span>${escapeHtml(runtimeNote)}</span>`;
}
if (stats && typeof stats === 'object') {
const statParts = [];
if (stats.api_calls != null) statParts.push(`调用 ${stats.api_calls}`);
if (stats.files_read != null) statParts.push(`阅读 ${stats.files_read}`);
if (stats.edit_files != null) statParts.push(`编辑 ${stats.edit_files}`);
if (stats.searches != null) statParts.push(`搜索 ${stats.searches}`);
if (stats.web_pages != null) statParts.push(`网页 ${stats.web_pages}`);
if (stats.commands != null) statParts.push(`命令 ${stats.commands}`);
if (statParts.length > 0) {
html += `<span>${escapeHtml(statParts.join(' | '))}</span>`;
}
}
html += '</div>';
}
html += `<pre>${escapeHtml(String(itemMessage))}</pre>`;
}
html += '</div>';
});
html += '</div>';
}
} else if (mode === 'wait_runcommand_id') {
const nested = result?.result;
if (nested && typeof nested === 'object') {
const output = nested.output || nested.stdout || '';
const exitCode = nested.exit_code !== undefined ? nested.exit_code : nested.returncode;
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">后台命令输出:</div>';
if (exitCode !== undefined) {
html += `<div><strong>退出码:</strong>${escapeHtml(String(exitCode))}</div>`;
}
if (output) {
html += `<pre>${escapeHtml(String(output))}</pre>`;
} else {
html += '<div class="tool-result-empty">[无输出]</div>';
}
html += '</div>';
}
}
return html;
}
@ -1371,42 +1478,6 @@ function renderSendMessageToSubAgent(result: any, args: any): string {
return html;
}
function renderAskSubAgent(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
const agentId = args.agent_id ?? '';
const question = args.question ?? '';
const answer =
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>状态:</strong>${status}</div>`;
if (agentId !== '') {
html += `<div><strong>子智能体 ID</strong>${escapeHtml(String(agentId))}</div>`;
}
if (question) {
const preview = String(question).slice(0, 200);
const suffix = String(question).length > 200 ? '…' : '';
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
}
if (!result?.success && result?.error) {
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
}
html += '</div>';
if (answer && typeof answer === 'string') {
html += '<div class="tool-result-content scrollable">';
html += '<div class="content-label">回答</div>';
html += `<pre>${escapeHtml(answer)}</pre>`;
html += '</div>';
} else if (result?.success) {
html += '<div class="tool-result-content scrollable">';
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
html += '</div>';
}
return html;
}
function renderAnswerSubAgentQuestion(result: any, args: any): string {
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
const questionId = args.question_id ?? result.question_id ?? '';

View File

@ -170,7 +170,7 @@
>目标模式就绪</span
>
<button
v-if="userQuestionMinimized && pendingUserQuestionCount > 0"
v-if="pendingUserQuestionCount > 0"
type="button"
class="floating-project-status__notice floating-project-status__notice--button"
@click.stop="$emit('restore-user-question')"
@ -828,33 +828,43 @@ const runtimeQueuedMessagesForRender = computed(() => {
});
const projectGitSummaryForRender = computed(() => {
if (personalizationStore?.form?.show_git_status_bar === false) return null;
if (personalizationStore?.form?.show_git_status_bar === false) {
return null;
}
const summary = props.projectGitSummary;
if (!summary?.has_git) return null;
if (!summary?.has_git) {
return null;
}
const projectName = String(summary.project_name || '').trim();
const branch = String(summary.branch || '').trim();
if (!projectName || !branch) return null;
return {
if (!projectName || !branch) {
return null;
}
const result = {
projectName,
projectPath: String(summary.project_path || '').trim(),
branch,
additions: Math.max(0, Number(summary.additions || 0)),
deletions: Math.max(0, Number(summary.deletions || 0))
};
return result;
});
const floatingStatusVisible = computed(() => {
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) return false;
// /git /
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance)
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) {
return false;
return (
}
// /git /
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance) {
return false;
}
const visible =
!!projectGitSummaryForRender.value ||
!!props.goalRunning ||
!!props.goalModeArmed ||
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0) ||
(Number(props.activeSubAgentCount || 0) > 0)
);
(Number(props.pendingUserQuestionCount || 0) > 0) ||
(Number(props.activeSubAgentCount || 0) > 0);
return visible;
});
const showComposerAvatar = computed(() => {

View File

@ -104,10 +104,6 @@ const buildText = (entry: ActivityEntry) => {
const skillName = args.skill_name || '';
return `阅读技能 ${skillName}`;
}
if (tool === 'search_workspace') {
const query = args.query || args.keyword || '';
return `在工作区搜索 ${query}`;
}
if (tool === 'web_search') {
const query = args.query || args.q || '';
return `在互联网中搜索 ${query}`;

View File

@ -213,6 +213,7 @@ function submit() {
gap: 12px;
align-items: center;
padding: 20px 22px 0;
flex-shrink: 0;
}
.user-question-kicker-row {
@ -240,6 +241,9 @@ function submit() {
line-height: 1.55;
font-weight: 650;
letter-spacing: -0.01em;
white-space: pre-wrap;
max-height: min(180px, 30vh);
overflow-y: auto;
}
.user-question-nav {
@ -297,6 +301,9 @@ function submit() {
color: var(--claude-text-secondary);
line-height: 1.6;
font-size: 13px;
white-space: pre-wrap;
max-height: min(160px, 25vh);
overflow-y: auto;
}
.user-question-options {
@ -335,6 +342,7 @@ function submit() {
color: var(--claude-text-secondary);
font-size: 12px;
line-height: 1.42;
white-space: pre-wrap;
}
.user-question-textarea-wrap {

View File

@ -2289,20 +2289,64 @@ const updateFloatingMenuPosition = async () => {
const button = document.querySelector<HTMLElement>(
'.settings-select-wrap.open .settings-select-button'
);
const menu = document.querySelector<HTMLElement>(
'.settings-select-wrap.open .settings-floating-menu'
);
if (!button) {
return;
}
const rect = button.getBoundingClientRect();
const menuWidth = Math.min(300, Math.max(240, window.innerWidth - 32));
const left = Math.max(16, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 16));
const top = Math.max(16, Math.min(rect.bottom + 10, window.innerHeight - 80));
const padding = 16;
const gap = 10;
// 4 max-height
const optionEl = menu?.querySelector<HTMLElement>('.settings-menu-option');
const optionHeight = optionEl ? optionEl.getBoundingClientRect().height : 44;
const menuPadding = 12; // menu padding 6px top + 6px bottom
const fixedMaxHeight = Math.round(optionHeight * 4) + menuPadding;
//
if (menu) {
menu.style.maxHeight = `${fixedMaxHeight}px`;
}
//
const menuHeight = menu?.getBoundingClientRect().height || fixedMaxHeight;
const spaceBelow = window.innerHeight - rect.bottom - padding;
const spaceAbove = rect.top - padding;
let top: number;
let maxHeight: number | undefined;
if (spaceBelow >= menuHeight) {
//
top = rect.bottom + gap;
} else if (spaceAbove >= menuHeight) {
//
top = rect.top - menuHeight - gap;
} else if (spaceBelow >= spaceAbove) {
//
const availableHeight = Math.max(80, spaceBelow - gap);
maxHeight = availableHeight;
top = rect.bottom + gap;
} else {
//
const availableHeight = Math.max(80, spaceAbove - gap);
maxHeight = availableHeight;
top = Math.max(padding, rect.top - availableHeight - gap);
}
floatingMenuStyle.value = {
position: 'fixed',
top: `${Math.round(top)}px`,
left: `${Math.round(left)}px`,
right: 'auto',
width: `${Math.round(menuWidth)}px`,
zIndex: '2147483647'
zIndex: '2147483647',
...(maxHeight !== undefined ? { maxHeight: `${Math.round(maxHeight)}px` } : {})
};
};
@ -3689,8 +3733,9 @@ const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
right: auto;
top: auto;
width: 300px;
max-height: min(420px, 60vh);
overflow: auto;
max-height: calc(44px * 4 + 12px);
overflow-y: auto;
overflow-x: hidden;
padding: 6px;
border-radius: 20px;
background: var(--settings-floating-menu-bg);

View File

@ -217,13 +217,7 @@
>
<div class="workspace-group-children-inner">
<div
v-if="getWorkspaceGroup(String(ws.workspace_id || ''))?.loading && !getWorkspaceGroup(String(ws.workspace_id || ''))?.conversations?.length"
class="workspace-conversations-loading"
>
正在加载...
</div>
<div
v-else-if="!getWorkspaceGroup(String(ws.workspace_id || ''))?.conversations?.length"
v-if="!getWorkspaceGroup(String(ws.workspace_id || ''))?.conversations?.length"
class="workspace-no-conversations"
>
暂无对话

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { useUiStore } from './ui';
import { useConversationStore } from './conversation';
interface SubAgent {
task_id: string;
@ -49,13 +50,22 @@ export const useSubAgentStore = defineStore('subAgent', {
actions: {
async fetchSubAgents() {
try {
const resp = await fetch('/api/sub_agents');
const conversationStore = useConversationStore();
let resp;
if (conversationStore.multiAgentMode && conversationStore.currentConversationId) {
resp = await fetch(`/api/multiagent/active_sub_agents?conversation_id=${encodeURIComponent(conversationStore.currentConversationId)}`);
} else {
resp = await fetch('/api/sub_agents');
}
if (!resp.ok) {
throw new Error(await resp.text());
}
const data = await resp.json();
if (data.success) {
this.subAgents = Array.isArray(data.data) ? data.data : [];
const agents = conversationStore.multiAgentMode
? (Array.isArray(data.agents) ? data.agents : [])
: (Array.isArray(data.data) ? data.data : []);
this.subAgents = agents;
const activeTaskId = this.activeAgent?.task_id;
if (activeTaskId) {
const latest = this.subAgents.find((item) => item.task_id === activeTaskId);

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Terminal Monitor - 实时终端查看器</title>
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<!-- xterm.js 终端模拟器 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />

View File

@ -40,6 +40,10 @@ class ServerRefactorSmokeTest(unittest.TestCase):
import server.chat_flow_runner_helpers as helpers
self.assertEqual(helpers.extract_intent_from_partial('{"intent": "fix"}'), "fix")
self.assertEqual(
helpers.extract_intent_from_partial('{"intent": "\\u7b49\\u5f85\\u4e0b\\u8f7d\\u5b8c\\u6210"}'),
"等待下载完成",
)
self.assertEqual(helpers.resolve_monitor_path({"file_path": " a.txt "}), "a.txt")
self.assertEqual(helpers.resolve_monitor_memory([1, 2, 3], entry_limit=2), ["1", "2"])

View File

@ -185,63 +185,6 @@ def test_write_file_and_edit():
shutil.rmtree(tmp, ignore_errors=True)
def test_search_workspace():
"""测试 search_workspace 走 run_command rg 回退。"""
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
try:
manager = SubAgentManager(str(tmp), str(tmp / "data"))
terminal = FakeTerminal(tmp)
manager.set_terminal(terminal)
(tmp / "findme.txt").write_text("hello world unique string", encoding="utf-8")
async def fake_search(project_path, terminal_, arguments):
query = arguments["query"]
if arguments.get("mode") == "file":
matches = [p.name for p in project_path.rglob("*") if p.is_file() and query in p.name]
return {"success": True, "mode": "file", "matches": matches}
matches = []
for p in project_path.rglob("*.txt"):
text = p.read_text(encoding="utf-8")
if query in text:
matches.append({"file": str(p.relative_to(project_path)), "matches": [{"line": 1, "snippet": text}]})
return {"success": True, "mode": "content", "results": matches}
import modules.sub_agent.tools as sat
original_search = sat.handle_search_workspace
sat.handle_search_workspace = fake_search
responses = [
[make_tool_chunk([{
"name": "search_workspace",
"args": json.dumps({"mode": "content", "query": "unique string"})
}])],
[make_tool_chunk([{
"name": "finish_task",
"args": json.dumps({"success": True, "summary": "搜索完成"})
}])],
]
_, original_model = install_fake_model(responses)
try:
result = manager.create_sub_agent(
agent_id=2,
summary="搜索测试",
task="测试 search_workspace",
deliverables_dir="deliverables2",
timeout_seconds=30,
conversation_id="conv-test-2",
thinking_mode="fast",
)
assert result["success"], result
final = manager.wait_for_completion(task_id=result["task_id"], timeout_seconds=10)
assert final["status"] == "completed", final
finally:
restore_model(original_model)
sat.handle_search_workspace = original_search
finally:
shutil.rmtree(tmp, ignore_errors=True)
def test_read_mediafile():
"""测试 read_mediafile 返回 base64。"""
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
@ -341,7 +284,6 @@ def run_all():
print("=" * 60)
tests = [
("write_file + edit_file", test_write_file_and_edit),
("search_workspace", test_search_workspace),
("read_mediafile", test_read_mediafile),
("terminate", test_terminate),
]

View File

@ -100,7 +100,6 @@ class DeepSeekClientChatMixin:
final_messages = self._merge_system_messages(messages)
final_messages = self._sanitize_messages_for_model_capability(final_messages)
final_messages = self._sanitize_message_fields_for_api(final_messages)
payload = {
"model": api_config["model_id"],
"messages": final_messages,
@ -136,7 +135,7 @@ class DeepSeekClientChatMixin:
except Exception:
pass
dump_path = self._dump_request_payload(payload, api_config, headers)
try:
async with httpx.AsyncClient(http2=True, timeout=300) as client:
if stream:

View File

@ -144,17 +144,23 @@ class TokenMixin:
return False
try:
success = self.conversation_manager.update_token_statistics(
# 路由到正确的 conversation_manager与 get_conversation_token_statistics 一致)
# 多智能体对话 ID 存储在 multi_agent_conversation_manager 中,
# 直接使用 self.conversation_manager 会找不到对话导致统计写入失败
target_manager = getattr(
self, "_get_conversation_manager_for_id", lambda _: self.conversation_manager
)(self.current_conversation_id)
success = target_manager.update_token_statistics(
self.current_conversation_id,
prompt_tokens,
completion_tokens,
total_tokens,
current_context_tokens=current_context_tokens,
)
if success:
self.safe_broadcast_token_update()
return success
except Exception as e:
print(f"更新usage统计失败: {e}")

View File

@ -267,13 +267,14 @@ def _format_send_message_to_sub_agent(result_data: Dict[str, Any]) -> str:
return "消息已发送。"
def _format_ask_sub_agent(result_data: Dict[str, Any]) -> str:
def _format_stop_sub_agent(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("ask_sub_agent", result_data)
answer = result_data.get("answer")
if answer is None:
return "子智能体未返回答复。"
return f"子智能体的回答:\n{answer}"
return _format_failure("stop_sub_agent", result_data)
agent_id = result_data.get("agent_id")
message = result_data.get("message") or "子智能体已暂停。"
if agent_id is not None:
return f"已暂停子智能体 #{agent_id}{message}"
return message
def _format_answer_sub_agent_question(result_data: Dict[str, Any]) -> str:

View File

@ -38,7 +38,7 @@ from utils.tool_result_formatter.agent_context import (
_format_get_sub_agent_status,
_format_terminate_sub_agent,
_format_send_message_to_sub_agent,
_format_ask_sub_agent,
_format_stop_sub_agent,
_format_answer_sub_agent_question,
_format_create_custom_agent,
_format_list_agents,
@ -131,7 +131,7 @@ TOOL_FORMATTERS = {
"close_sub_agent": _format_close_sub_agent,
"terminate_sub_agent": _format_terminate_sub_agent,
"send_message_to_sub_agent": _format_send_message_to_sub_agent,
"ask_sub_agent": _format_ask_sub_agent,
"stop_sub_agent": _format_stop_sub_agent,
"answer_sub_agent_question": _format_answer_sub_agent_question,
"create_custom_agent": _format_create_custom_agent,
"list_agents": _format_list_agents,

View File

@ -142,6 +142,13 @@ def _format_sleep(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("sleep", result_data)
mode = result_data.get("mode")
if mode == "wait_sub_agent_output":
message = result_data.get("message") or ""
agent_id = result_data.get("agent_id")
header = f"已收到子智能体 {agent_id} 的输出"
if message:
return f"{header}\n\n{message}"
return header
if mode == "wait_sub_agent_ids":
results = result_data.get("results") or []
header = result_data.get("message") or f"已等待 {len(results)} 个子智能体结束"