fix: unify background run_command notifications with sub-agent flow
This commit is contained in:
parent
cf6f056a93
commit
a04eca3aab
103
agentskills/run-command-guide/SKILL.md
Normal file
103
agentskills/run-command-guide/SKILL.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: run-command-guide
|
||||||
|
description: run_command 工具使用指南。介绍前台与后台模式、后台模式的等待方式(command_id + sleep(wait_runcommand_id))、互斥参数约束以及常见坑与示例。
|
||||||
|
---
|
||||||
|
|
||||||
|
# run_command 使用指南
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
- `run_command` 是 **一次性执行** 的终端命令,适合查询文件信息(`ls`、`file`、`grep -n`)、触发 CLI 工具、做简单的数据转换或运行非交互脚本。它**不能**用于启动交互式程序(如 `python` REPL、`vim`、`top`)。
|
||||||
|
- 每次调用必须提供 `timeout`(单位秒,必须大于 0),系统会在超时后强制打断命令并返回当前输出。
|
||||||
|
- 输出内容有字符数限制(默认 10000 字符),超过时会被截断或遭拒,因此尽量控制命令输出量,必要时拆分多个命令或保存到文件再读取。
|
||||||
|
- 如果需要持续交互、长时间运行、反复检查进度,优先使用 `terminal_session`/`terminal_input`;`run_command` 用于“一次性”场景。
|
||||||
|
|
||||||
|
## 执行模式:前台 vs 后台
|
||||||
|
|
||||||
|
### 前台模式(默认,`run_in_background=false`)
|
||||||
|
|
||||||
|
- timeout 上限 30 秒。命令在工具完成前会阻塞整个模型线程,返回后即可马上读取 `output`、`return_code` 等字段。
|
||||||
|
- 适合“快照式”场景:查看目录、检查某个文件、运行一条短命令并立刻需要结果。
|
||||||
|
- 要求命令在 30 秒内完成,否则会中断并返回 `status: "timeout"`。
|
||||||
|
|
||||||
|
### 后台模式(`run_in_background=true`)
|
||||||
|
|
||||||
|
- timeout 最长 3600 秒。命令在后台继续执行,工具会等约 5 秒收集当前输出并返回,然后 **立即释放控制权**,让你继续处理其他任务。
|
||||||
|
- 返回值里会包含 `command_id`,`status` 可能是 `running_background`。你会拿到已经产生的输出片段,但最终结果要靠系统通知或手动等待。
|
||||||
|
- 成功或失败完成后,系统会自动发送一条 `system` 消息(内容类似 `后台命令已完成(completed)`,并附带 `command_id`、`command`、`output` 摘要)通知你。不要再依赖 `wait_sub_agent`/手动轮询。
|
||||||
|
- 适合长时间任务(大规模日志分析、编译/安装、数据处理),或者你想发起命令后继续进行其他工作。
|
||||||
|
- 返回的 `command_id` 需要保留,用于后续通过 `sleep(wait_runcommand_id=...)` 等待结果或根据通知定位输出。
|
||||||
|
|
||||||
|
## 等待后台命令完成:command_id + sleep(wait_runcommand_id)
|
||||||
|
|
||||||
|
1. 调用后台命令后记录 `command_id`(形如 `cmd_<时间戳>_<8位hash>`);
|
||||||
|
2. 当你确实需要知道最终输出/返回码时,调用 `sleep(wait_runcommand_id=command_id)`;
|
||||||
|
3. `sleep` 工具会阻塞直到命令完成,返回值的 `result` 字段就是后台命令的最终 payload,`success`/`return_code` 等都可以直接使用;
|
||||||
|
4. `sleep` 默认会验证该 `command_id` 是否属于当前对话;跨会话调用会报错。
|
||||||
|
|
||||||
|
注意:即使你不调用 `sleep`,后台命令完成后也会有系统通知,只要收到消息就可以去交叉检查输出或 `sleep` 结果。
|
||||||
|
|
||||||
|
## `sleep` 工具的互斥参数规则
|
||||||
|
|
||||||
|
- `sleep` 工具支持三种等待方式:`seconds`(纯等待)、`wait_sub_agent_ids`(等待子智能体)、`wait_runcommand_id`(等待后台 `run_command`)。
|
||||||
|
- **只能选择其中一个等待参数**,否则会报错 “sleep 的等待参数互斥”;
|
||||||
|
- 如果你需要短暂停顿而非等待任务完成,传 `seconds` 并可选填 `reason`;
|
||||||
|
- 需要等待后台命令完成时仅填写 `wait_runcommand_id`,其余两个参数必须为 `null`/不传。
|
||||||
|
- `wait_runcommand_id` 返回:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"mode": "wait_runcommand_id",
|
||||||
|
"command_id": "...",
|
||||||
|
"result": {...}, # 与 run_command 结果结构一致
|
||||||
|
"message": "后台 run_command 等待完成"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见坑与注意事项
|
||||||
|
|
||||||
|
1. **忘记设置 `timeout` 或给值 ≤ 0**:前台/后台都会立即报错。后台模式还有限制(>0 且 ≤3600)。
|
||||||
|
2. **误以为后台模式会自动输出最终结果**:后台只返回 `running_background` 的片段,必须通过系统通知或 `sleep(wait_runcommand_id=...)` 才能拿到完整状态。
|
||||||
|
3. **没有保存 `command_id` 即失去追踪**:`command_id` 是等待、复查、排错的唯一标识符,出错后可直接用它查询后台管理器。
|
||||||
|
4. **在 wait/run_command 之间同时传多个 `sleep` 参数**:会被客户端拒绝,报错明确指出互斥约束。
|
||||||
|
5. **命令输出过大(超过 10000 字符)**:会被截断并提示降低输出量,可考虑写入文件再用 `read_file` 读取,或分步执行。
|
||||||
|
6. **尝试运行交互式程序**:`run_command` 明确禁止,命令会在 `_validate_command` 阶段失败。
|
||||||
|
7. **后台命令属于别的对话**:`sleep(wait_runcommand_id=...)` 会报 “该后台命令不属于当前对话”,只能在原 conversation 中等待。
|
||||||
|
8. **误以为 `sleep` 会自动拼接 system message**:`sleep` 只是等待,系统通知仍旧按背景轮询插入,你收到 `system` 消息即可确认状态。
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### 前台快速查询
|
||||||
|
|
||||||
|
```python
|
||||||
|
run_command(command="ls -a docs | head", timeout=5)
|
||||||
|
# 立即得到输出和 return_code
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后台安装并等待
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 发起后台安装
|
||||||
|
bg = run_command(
|
||||||
|
command="python -m pip install -r requirements.txt",
|
||||||
|
timeout=600,
|
||||||
|
run_in_background=True
|
||||||
|
)
|
||||||
|
command_id = bg.get("command_id")
|
||||||
|
|
||||||
|
# 可先去做别的事情,或立即等待最终状态
|
||||||
|
sleep(wait_runcommand_id=command_id, reason="等待 pip 安装完成")
|
||||||
|
# 返回 result["result"]["output"], return_code 等最终内容
|
||||||
|
```
|
||||||
|
|
||||||
|
### 结合系统通知与 sleep
|
||||||
|
|
||||||
|
- 系统会插入类似 `后台命令已完成(completed)` 的 `system` 消息,包含 `command_id`。
|
||||||
|
- 如果你优先希望第一时间处理输出,可以在收到消息后再 `sleep(wait_runcommand_id=command_id)` 来确认成功状态(即使已有通知,也可再次 `sleep` 获得 `result` 结构)。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
- 前台模式适用于“立刻需要结果”的单步命令,后台模式适合“发起后继续干别的”的长任务。
|
||||||
|
- 后台结果需要 `command_id + sleep(wait_runcommand_id=...)` 或系统通知来确认最终状态。
|
||||||
|
- `sleep` 的等待参数互斥,务必只传一个。
|
||||||
|
- 避免交互式命令、超出字符/超时限制,必要时将输出先写到文件再读。
|
||||||
@ -121,6 +121,10 @@ create_sub_agent(
|
|||||||
- 子智能体在后台运行,完成后系统会**自动插入一条 user 消息**通知你
|
- 子智能体在后台运行,完成后系统会**自动插入一条 user 消息**通知你
|
||||||
- 你会在对话中看到类似:"这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成..."
|
- 你会在对话中看到类似:"这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成..."
|
||||||
|
|
||||||
|
**完成通知机制说明:**
|
||||||
|
- 后台子智能体完成的通知是系统自动插入的 user/system 消息,不依赖手动调用 `wait_sub_agent` 或其它轮询接口。
|
||||||
|
- 你只需在收到通知后直接查看交付目录即可,系统会把所有必需信息一并带上。
|
||||||
|
|
||||||
**何时使用:**
|
**何时使用:**
|
||||||
- ✅ 耗时任务(预计 > 1 分钟)
|
- ✅ 耗时任务(预计 > 1 分钟)
|
||||||
- ✅ 多个并行任务
|
- ✅ 多个并行任务
|
||||||
@ -160,6 +164,7 @@ create_sub_agent(agent_id=1, task="大规模日志分析", run_in_background=tru
|
|||||||
- 系统会自动处理通知,你只需要在收到通知消息后处理结果即可
|
- 系统会自动处理通知,你只需要在收到通知消息后处理结果即可
|
||||||
- 如果你正在执行工具调用,系统会延迟通知,避免打断你的工作流
|
- 如果你正在执行工具调用,系统会延迟通知,避免打断你的工作流
|
||||||
- 通知类型取决于你是否继续工作:继续工作时收到 system 消息,停止输出时收到 user 消息
|
- 通知类型取决于你是否继续工作:继续工作时收到 system 消息,停止输出时收到 user 消息
|
||||||
|
- 后台完成由上述系统通知负责,不要再依赖 `wait_sub_agent` 或类似的手动等待流,系统会在能插入消息时自动通知你。
|
||||||
|
|
||||||
### 模式选择决策树
|
### 模式选择决策树
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ from modules.memory_manager import MemoryManager
|
|||||||
from modules.terminal_manager import TerminalManager
|
from modules.terminal_manager import TerminalManager
|
||||||
from modules.todo_manager import TodoManager
|
from modules.todo_manager import TodoManager
|
||||||
from modules.sub_agent_manager import SubAgentManager
|
from modules.sub_agent_manager import SubAgentManager
|
||||||
|
from modules.background_command_manager import BackgroundCommandManager
|
||||||
from modules.ocr_client import OCRClient
|
from modules.ocr_client import OCRClient
|
||||||
from modules.easter_egg_manager import EasterEggManager
|
from modules.easter_egg_manager import EasterEggManager
|
||||||
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
|
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
|
||||||
@ -127,6 +128,9 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
data_dir=str(self.data_dir),
|
data_dir=str(self.data_dir),
|
||||||
container_session=container_session,
|
container_session=container_session,
|
||||||
)
|
)
|
||||||
|
self.background_command_manager = BackgroundCommandManager(
|
||||||
|
project_path=self.project_path,
|
||||||
|
)
|
||||||
self.easter_egg_manager = EasterEggManager()
|
self.easter_egg_manager = EasterEggManager()
|
||||||
self._announced_sub_agent_tasks = set()
|
self._announced_sub_agent_tasks = set()
|
||||||
self.silent_tool_disable = False # 是否静默工具禁用提示
|
self.silent_tool_disable = False # 是否静默工具禁用提示
|
||||||
@ -168,6 +172,7 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
# Skill 强约束开关(按个人空间配置)
|
# Skill 强约束开关(按个人空间配置)
|
||||||
self.skill_strict_terminal_enabled: bool = False
|
self.skill_strict_terminal_enabled: bool = False
|
||||||
self.skill_strict_sub_agent_enabled: bool = False
|
self.skill_strict_sub_agent_enabled: bool = False
|
||||||
|
self.skill_strict_run_command_background_enabled: bool = False
|
||||||
# 当前生效的启用 skills(用于强约束判定)
|
# 当前生效的启用 skills(用于强约束判定)
|
||||||
self.enabled_skill_ids: Set[str] = set()
|
self.enabled_skill_ids: Set[str] = set()
|
||||||
self.apply_personalization_preferences()
|
self.apply_personalization_preferences()
|
||||||
|
|||||||
@ -190,22 +190,31 @@ class MainTerminalToolsDefinitionMixin:
|
|||||||
tools = [
|
tools = [
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "sleep",
|
"name": "sleep",
|
||||||
"description": "等待指定的秒数,用于短暂延迟/节奏控制(例如让终端产生更多输出、或在两次快照之间留出间隔)。命令是否完成必须用 terminal_snapshot 确认;需要强制超时终止请使用 run_command。",
|
"description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_sub_agent_ids:等待指定子智能体全部结束并直接返回结果;3) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": self._inject_intent({
|
"properties": self._inject_intent({
|
||||||
"seconds": {
|
"seconds": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "等待的秒数,可以是小数(如0.2秒)。建议范围:0.1-10秒"
|
"description": "等待的秒数,可以是小数(如0.2秒)。建议范围:0.1-10秒"
|
||||||
},
|
},
|
||||||
"reason": {
|
"wait_sub_agent_ids": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "等待的原因说明(可选)"
|
"items": {"type": "integer"},
|
||||||
}
|
"description": "等待这些子智能体全部结束后返回(可提供一个或多个编号)。"
|
||||||
|
},
|
||||||
|
"wait_runcommand_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "等待指定后台 run_command 的 command_id 结束后返回。"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "等待的原因说明(可选)"
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
"required": ["seconds"]
|
"required": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -584,16 +593,20 @@ class MainTerminalToolsDefinitionMixin:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "run_command",
|
"name": "run_command",
|
||||||
"description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序;对已聚焦文件仅允许使用 grep -n 等定位命令。必须提供 timeout(最长30秒);一旦超时,命令**一定会被打断**且无法继续执行(需要重新运行),并返回已捕获输出;输出超过10000字符将被截断或拒绝。",
|
"description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序;对已聚焦文件仅允许使用 grep -n 等定位命令。必须提供 timeout。前台模式(run_in_background=false,默认)上限30秒,超时会打断;后台模式(run_in_background=true)上限3600秒,会先等待5秒返回已有输出并继续在后台运行,完成后由系统通知。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": self._inject_intent({
|
"properties": self._inject_intent({
|
||||||
"command": {"type": "string", "description": "终端命令"},
|
"command": {"type": "string", "description": "终端命令"},
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "超时时长(秒),必填,最大30"
|
"description": "超时时长(秒),必填。前台最大30;后台最大3600。"
|
||||||
|
},
|
||||||
|
"run_in_background": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否后台运行。true 时先等待5秒返回已有输出,并在后台持续执行直至结束。"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"required": ["command", "timeout"]
|
"required": ["command", "timeout"]
|
||||||
|
|||||||
@ -160,6 +160,8 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
self.context_manager._set_meta_flag(self._skill_meta_key("terminal-guide"), True)
|
self.context_manager._set_meta_flag(self._skill_meta_key("terminal-guide"), True)
|
||||||
if normalized.endswith("skills/sub-agent-guide/skill.md"):
|
if normalized.endswith("skills/sub-agent-guide/skill.md"):
|
||||||
self.context_manager._set_meta_flag(self._skill_meta_key("sub-agent-guide"), True)
|
self.context_manager._set_meta_flag(self._skill_meta_key("sub-agent-guide"), True)
|
||||||
|
if normalized.endswith("skills/run-command-guide/skill.md"):
|
||||||
|
self.context_manager._set_meta_flag(self._skill_meta_key("run-command-guide"), True)
|
||||||
|
|
||||||
def _get_visited_read_files(self) -> Set[str]:
|
def _get_visited_read_files(self) -> Set[str]:
|
||||||
raw = self.context_manager._get_meta_flag(self._file_read_meta_key(), [])
|
raw = self.context_manager._get_meta_flag(self._file_read_meta_key(), [])
|
||||||
@ -258,15 +260,21 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _required_skill_for_tool(self, tool_name: str) -> Optional[str]:
|
def _required_skill_for_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||||||
if tool_name in self._TERMINAL_SERIES_TOOLS and bool(getattr(self, "skill_strict_terminal_enabled", False)):
|
if tool_name in self._TERMINAL_SERIES_TOOLS and bool(getattr(self, "skill_strict_terminal_enabled", False)):
|
||||||
return "terminal-guide"
|
return "terminal-guide"
|
||||||
if tool_name in self._SUB_AGENT_SERIES_TOOLS and bool(getattr(self, "skill_strict_sub_agent_enabled", False)):
|
if tool_name in self._SUB_AGENT_SERIES_TOOLS and bool(getattr(self, "skill_strict_sub_agent_enabled", False)):
|
||||||
return "sub-agent-guide"
|
return "sub-agent-guide"
|
||||||
|
if (
|
||||||
|
tool_name == "run_command"
|
||||||
|
and bool(getattr(self, "skill_strict_run_command_background_enabled", False))
|
||||||
|
and bool((arguments or {}).get("run_in_background", False))
|
||||||
|
):
|
||||||
|
return "run-command-guide"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _check_skill_strict_prerequisite(self, tool_name: str) -> Optional[Dict[str, Any]]:
|
def _check_skill_strict_prerequisite(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
||||||
skill_id = self._required_skill_for_tool(tool_name)
|
skill_id = self._required_skill_for_tool(tool_name, arguments)
|
||||||
if not skill_id:
|
if not skill_id:
|
||||||
return None
|
return None
|
||||||
# 仅当该 skill 在个人空间中启用时才执行强约束
|
# 仅当该 skill 在个人空间中启用时才执行强约束
|
||||||
@ -343,7 +351,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
custom_tool = self.custom_tool_registry.get_tool(tool_name)
|
custom_tool = self.custom_tool_registry.get_tool(tool_name)
|
||||||
|
|
||||||
# Skill 强约束:未阅读对应 SKILL.md 时,阻止 terminal/sub-agent 系列工具执行
|
# Skill 强约束:未阅读对应 SKILL.md 时,阻止 terminal/sub-agent 系列工具执行
|
||||||
strict_error = self._check_skill_strict_prerequisite(tool_name)
|
strict_error = self._check_skill_strict_prerequisite(tool_name, arguments)
|
||||||
if strict_error:
|
if strict_error:
|
||||||
return json.dumps(strict_error, ensure_ascii=False)
|
return json.dumps(strict_error, ensure_ascii=False)
|
||||||
|
|
||||||
@ -480,39 +488,155 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
|
|
||||||
# sleep工具
|
# sleep工具
|
||||||
elif tool_name == "sleep":
|
elif tool_name == "sleep":
|
||||||
seconds = arguments.get("seconds", 1)
|
seconds = arguments.get("seconds")
|
||||||
|
wait_sub_agent_ids = arguments.get("wait_sub_agent_ids")
|
||||||
|
wait_runcommand_id = arguments.get("wait_runcommand_id")
|
||||||
reason = arguments.get("reason", "等待操作完成")
|
reason = arguments.get("reason", "等待操作完成")
|
||||||
|
|
||||||
# 限制最大等待时间
|
provided = 0
|
||||||
max_sleep = 3600 # 最多等待3600秒(1小时)
|
if seconds is not None:
|
||||||
if seconds > max_sleep:
|
provided += 1
|
||||||
|
if wait_sub_agent_ids:
|
||||||
|
provided += 1
|
||||||
|
if wait_runcommand_id:
|
||||||
|
provided += 1
|
||||||
|
if provided == 0:
|
||||||
result = {
|
result = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"等待时间过长,最多允许 {max_sleep} 秒",
|
"error": "sleep 至少需要提供一个参数:seconds / wait_sub_agent_ids / wait_runcommand_id"
|
||||||
"suggestion": f"建议分多次等待或减少等待时间"
|
|
||||||
}
|
}
|
||||||
|
elif provided > 1:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error": "sleep 的等待参数互斥:seconds / wait_sub_agent_ids / wait_runcommand_id 只能提供一个"
|
||||||
|
}
|
||||||
|
elif wait_sub_agent_ids:
|
||||||
|
if not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids:
|
||||||
|
result = {"success": False, "error": "wait_sub_agent_ids 必须是非空数组"}
|
||||||
|
else:
|
||||||
|
normalized_ids = []
|
||||||
|
invalid = []
|
||||||
|
for item in wait_sub_agent_ids:
|
||||||
|
try:
|
||||||
|
agent_id = int(item)
|
||||||
|
if agent_id <= 0:
|
||||||
|
raise ValueError()
|
||||||
|
normalized_ids.append(agent_id)
|
||||||
|
except Exception:
|
||||||
|
invalid.append(item)
|
||||||
|
if invalid:
|
||||||
|
result = {"success": False, "error": f"wait_sub_agent_ids 含非法值: {invalid}"}
|
||||||
|
else:
|
||||||
|
manager = getattr(self, "sub_agent_manager", None)
|
||||||
|
if not manager:
|
||||||
|
result = {"success": False, "error": "子智能体管理器不可用"}
|
||||||
|
else:
|
||||||
|
task_ids = []
|
||||||
|
missing = []
|
||||||
|
for aid in normalized_ids:
|
||||||
|
task = manager.lookup_task(agent_id=aid)
|
||||||
|
if not task:
|
||||||
|
missing.append(aid)
|
||||||
|
else:
|
||||||
|
task_ids.append(task.get("task_id"))
|
||||||
|
if missing:
|
||||||
|
result = {"success": False, "error": f"未找到对应子智能体: {missing}"}
|
||||||
|
else:
|
||||||
|
wait_results = []
|
||||||
|
waited_task_ids = []
|
||||||
|
for tid in task_ids:
|
||||||
|
wait_result = await asyncio.to_thread(
|
||||||
|
manager.wait_for_completion,
|
||||||
|
task_id=tid,
|
||||||
|
timeout_seconds=None,
|
||||||
|
)
|
||||||
|
wait_results.append(wait_result)
|
||||||
|
waited_task_ids.append(tid)
|
||||||
|
try:
|
||||||
|
task = manager.tasks.get(tid)
|
||||||
|
if isinstance(task, dict):
|
||||||
|
task["notified"] = True
|
||||||
|
task["updated_at"] = time.time()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
manager._save_state()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"mode": "wait_sub_agent_ids",
|
||||||
|
"agent_ids": normalized_ids,
|
||||||
|
"waited_task_ids": waited_task_ids,
|
||||||
|
"results": wait_results,
|
||||||
|
"message": f"已等待 {len(normalized_ids)} 个子智能体结束"
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
if not hasattr(self, "_announced_sub_agent_tasks"):
|
||||||
|
self._announced_sub_agent_tasks = set()
|
||||||
|
for tid in waited_task_ids:
|
||||||
|
if tid:
|
||||||
|
self._announced_sub_agent_tasks.add(tid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif wait_runcommand_id:
|
||||||
|
bg_manager = getattr(self, "background_command_manager", None)
|
||||||
|
if not bg_manager:
|
||||||
|
result = {"success": False, "error": "后台命令管理器不可用"}
|
||||||
|
else:
|
||||||
|
rec = bg_manager.get_record(str(wait_runcommand_id))
|
||||||
|
current_conv = getattr(self.context_manager, "current_conversation_id", None)
|
||||||
|
if not rec:
|
||||||
|
result = {"success": False, "error": f"未找到后台命令: {wait_runcommand_id}"}
|
||||||
|
elif rec.get("conversation_id") and current_conv and rec.get("conversation_id") != current_conv:
|
||||||
|
result = {"success": False, "error": "该后台命令不属于当前对话"}
|
||||||
|
else:
|
||||||
|
wait_result = await asyncio.to_thread(
|
||||||
|
bg_manager.wait_for_completion,
|
||||||
|
str(wait_runcommand_id),
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
bg_manager.mark_claimed(str(wait_runcommand_id))
|
||||||
|
result = {
|
||||||
|
"success": bool(wait_result.get("success", False)),
|
||||||
|
"mode": "wait_runcommand_id",
|
||||||
|
"command_id": str(wait_runcommand_id),
|
||||||
|
"result": wait_result,
|
||||||
|
"message": "后台 run_command 等待完成"
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
# 确保秒数为正数
|
max_sleep = 3600 # 最多等待3600秒(1小时)
|
||||||
if seconds <= 0:
|
seconds_parse_ok = True
|
||||||
|
try:
|
||||||
|
seconds = float(seconds)
|
||||||
|
except Exception:
|
||||||
|
result = {"success": False, "error": "seconds 必须是数字"}
|
||||||
|
seconds_parse_ok = False
|
||||||
|
seconds = 0.0
|
||||||
|
if seconds_parse_ok and seconds > max_sleep:
|
||||||
result = {
|
result = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "等待时间必须大于0"
|
"error": f"等待时间过长,最多允许 {max_sleep} 秒",
|
||||||
|
"suggestion": f"建议分多次等待或减少等待时间"
|
||||||
}
|
}
|
||||||
else:
|
elif seconds_parse_ok:
|
||||||
print(f"{OUTPUT_FORMATS['info']} 等待 {seconds} 秒: {reason}")
|
# 确保秒数为正数
|
||||||
|
if seconds <= 0:
|
||||||
# 执行等待
|
result = {
|
||||||
import asyncio
|
"success": False,
|
||||||
await asyncio.sleep(seconds)
|
"error": "等待时间必须大于0"
|
||||||
|
}
|
||||||
result = {
|
else:
|
||||||
"success": True,
|
print(f"{OUTPUT_FORMATS['info']} 等待 {seconds} 秒: {reason}")
|
||||||
"message": f"已等待 {seconds} 秒",
|
await asyncio.sleep(seconds)
|
||||||
"reason": reason,
|
result = {
|
||||||
"timestamp": datetime.now().isoformat()
|
"success": True,
|
||||||
}
|
"message": f"已等待 {seconds} 秒",
|
||||||
|
"reason": reason,
|
||||||
print(f"{OUTPUT_FORMATS['success']} 等待完成")
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
print(f"{OUTPUT_FORMATS['success']} 等待完成")
|
||||||
|
|
||||||
elif tool_name == "create_file":
|
elif tool_name == "create_file":
|
||||||
result = self.file_manager.create_file(
|
result = self.file_manager.create_file(
|
||||||
@ -768,22 +892,59 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif tool_name == "run_command":
|
elif tool_name == "run_command":
|
||||||
result = await self.terminal_ops.run_command(
|
run_in_background = bool(arguments.get("run_in_background", False))
|
||||||
arguments["command"],
|
timeout_value = arguments.get("timeout")
|
||||||
timeout=arguments.get("timeout")
|
if run_in_background:
|
||||||
)
|
if timeout_value is None or float(timeout_value) <= 0:
|
||||||
|
|
||||||
# 字符数检查
|
|
||||||
if result.get("success") and "output" in result:
|
|
||||||
char_count = len(result["output"])
|
|
||||||
if char_count > MAX_RUN_COMMAND_CHARS:
|
|
||||||
result = {
|
result = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"结果内容过大,有{char_count}字符,请使用限制字符数的获取内容方式,根据程度选择10k以内的数",
|
"error": "后台模式下 timeout 参数必填且需大于0"
|
||||||
"char_count": char_count,
|
|
||||||
"limit": MAX_RUN_COMMAND_CHARS,
|
|
||||||
"command": arguments["command"]
|
|
||||||
}
|
}
|
||||||
|
elif float(timeout_value) > 3600:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error": "后台模式下 timeout 最大为 3600 秒"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
bg_manager = getattr(self, "background_command_manager", None)
|
||||||
|
if not bg_manager:
|
||||||
|
result = {"success": False, "error": "后台命令管理器不可用"}
|
||||||
|
else:
|
||||||
|
result = bg_manager.create_background_command(
|
||||||
|
terminal_ops=self.terminal_ops,
|
||||||
|
command=arguments["command"],
|
||||||
|
timeout=timeout_value,
|
||||||
|
conversation_id=getattr(self.context_manager, "current_conversation_id", None),
|
||||||
|
wait_seconds=5.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if timeout_value is None or float(timeout_value) <= 0:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error": "timeout 参数必填且需大于0"
|
||||||
|
}
|
||||||
|
elif float(timeout_value) > 30:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error": "前台模式下 timeout 最大为 30 秒"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result = await self.terminal_ops.run_command(
|
||||||
|
arguments["command"],
|
||||||
|
timeout=timeout_value
|
||||||
|
)
|
||||||
|
|
||||||
|
# 字符数检查
|
||||||
|
if result.get("success") and "output" in result:
|
||||||
|
char_count = len(result["output"])
|
||||||
|
if char_count > MAX_RUN_COMMAND_CHARS:
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"error": f"结果内容过大,有{char_count}字符,请使用限制字符数的获取内容方式,根据程度选择10k以内的数",
|
||||||
|
"char_count": char_count,
|
||||||
|
"limit": MAX_RUN_COMMAND_CHARS,
|
||||||
|
"command": arguments["command"]
|
||||||
|
}
|
||||||
|
|
||||||
elif tool_name == "update_memory":
|
elif tool_name == "update_memory":
|
||||||
operation = arguments["operation"]
|
operation = arguments["operation"]
|
||||||
|
|||||||
@ -97,6 +97,9 @@ class MainTerminalToolsPolicyMixin:
|
|||||||
# Skill 强约束开关
|
# Skill 强约束开关
|
||||||
self.skill_strict_terminal_enabled = bool(effective_config.get("skill_strict_terminal_enabled", False))
|
self.skill_strict_terminal_enabled = bool(effective_config.get("skill_strict_terminal_enabled", False))
|
||||||
self.skill_strict_sub_agent_enabled = bool(effective_config.get("skill_strict_sub_agent_enabled", False))
|
self.skill_strict_sub_agent_enabled = bool(effective_config.get("skill_strict_sub_agent_enabled", False))
|
||||||
|
self.skill_strict_run_command_background_enabled = bool(
|
||||||
|
effective_config.get("skill_strict_run_command_background_enabled", False)
|
||||||
|
)
|
||||||
|
|
||||||
# 解析当前启用的 skills(用于强约束“仅对已启用 skill 生效”)
|
# 解析当前启用的 skills(用于强约束“仅对已启用 skill 生效”)
|
||||||
try:
|
try:
|
||||||
|
|||||||
500
modules/background_command_manager.py
Normal file
500
modules/background_command_manager.py
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
"""后台 run_command 任务管理。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from config import MAX_RUN_COMMAND_CHARS
|
||||||
|
|
||||||
|
|
||||||
|
TERMINAL_STATUSES = {"completed", "failed", "timeout", "cancelled"}
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundCommandManager:
|
||||||
|
"""管理 run_command 的后台执行、轮询与等待。"""
|
||||||
|
|
||||||
|
def __init__(self, project_path: str):
|
||||||
|
self.project_path = Path(project_path).resolve()
|
||||||
|
self._records: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._cv = threading.Condition(self._lock)
|
||||||
|
|
||||||
|
def create_background_command(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
terminal_ops,
|
||||||
|
command: str,
|
||||||
|
timeout: Optional[int],
|
||||||
|
conversation_id: Optional[str],
|
||||||
|
wait_seconds: float = 5.0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""启动后台命令;先等待一小段时间返回已有输出。"""
|
||||||
|
if timeout is None or float(timeout) <= 0:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "timeout 参数必填且需大于0",
|
||||||
|
"status": "error",
|
||||||
|
"output": "timeout 参数缺失",
|
||||||
|
"return_code": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout_value = min(int(timeout), 3600)
|
||||||
|
if timeout_value <= 0:
|
||||||
|
timeout_value = 1
|
||||||
|
|
||||||
|
session_override = None
|
||||||
|
if not getattr(terminal_ops, "container_session", None):
|
||||||
|
session_override = terminal_ops._resolve_active_container_session()
|
||||||
|
|
||||||
|
execution_in_container = terminal_ops._will_use_container(session_override)
|
||||||
|
python_rewrite = terminal_ops.container_python_cmd if execution_in_container else terminal_ops.python_cmd
|
||||||
|
pip_rewrite = terminal_ops._derive_pip_from_python(python_rewrite)
|
||||||
|
final_command = command
|
||||||
|
if re.search(r"\bpython3?\b", final_command):
|
||||||
|
final_command = re.sub(r"\bpython3?\b", python_rewrite, final_command)
|
||||||
|
if re.search(r"(?<![/.\w-])pip3?\b", final_command):
|
||||||
|
final_command = re.sub(r"(?<![/.\w-])pip3?\b", pip_rewrite, final_command)
|
||||||
|
|
||||||
|
valid, error = terminal_ops._validate_command(final_command)
|
||||||
|
if not valid:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error,
|
||||||
|
"status": "error",
|
||||||
|
"output": "",
|
||||||
|
"return_code": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
work_path = terminal_ops._resolve_work_path(None)
|
||||||
|
except ValueError:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "工作目录必须在项目文件夹内",
|
||||||
|
"status": "error",
|
||||||
|
"output": "",
|
||||||
|
"return_code": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
command_id = f"cmd_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||||
|
now = time.time()
|
||||||
|
with self._lock:
|
||||||
|
self._records[command_id] = {
|
||||||
|
"command_id": command_id,
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"status": "running",
|
||||||
|
"command": final_command,
|
||||||
|
"timeout": timeout_value,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
"finished_at": None,
|
||||||
|
"stdout_chunks": [],
|
||||||
|
"stderr_chunks": [],
|
||||||
|
"truncated": False,
|
||||||
|
"result": None,
|
||||||
|
"notified": False,
|
||||||
|
"claimed_by_sleep": False,
|
||||||
|
"pid": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self._run_command_thread,
|
||||||
|
kwargs={
|
||||||
|
"command_id": command_id,
|
||||||
|
"command": final_command,
|
||||||
|
"work_path": work_path,
|
||||||
|
"timeout": timeout_value,
|
||||||
|
"session": session_override or getattr(terminal_ops, "container_session", None),
|
||||||
|
"python_env": getattr(terminal_ops, "_python_env", None) or {},
|
||||||
|
},
|
||||||
|
name=f"bg-run-command-{command_id}",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
wait_seconds = max(0.1, min(float(wait_seconds or 5.0), 5.0))
|
||||||
|
deadline = time.time() + wait_seconds
|
||||||
|
with self._cv:
|
||||||
|
while time.time() < deadline:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
break
|
||||||
|
if rec.get("status") in TERMINAL_STATUSES:
|
||||||
|
break
|
||||||
|
remaining = deadline - time.time()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
self._cv.wait(timeout=remaining)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"status": "error",
|
||||||
|
"error": "后台任务记录丢失",
|
||||||
|
"output": "",
|
||||||
|
"return_code": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rec.get("status") in TERMINAL_STATUSES and isinstance(rec.get("result"), dict):
|
||||||
|
rec["claimed_by_sleep"] = True
|
||||||
|
rec["notified"] = True
|
||||||
|
rec["updated_at"] = time.time()
|
||||||
|
result = dict(rec["result"])
|
||||||
|
result["command_id"] = command_id
|
||||||
|
result["run_in_background"] = True
|
||||||
|
result["background_task_created"] = False
|
||||||
|
result["message"] = "命令在5秒内完成,未创建后台任务"
|
||||||
|
return result
|
||||||
|
|
||||||
|
output = self._build_current_output(rec)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": "running_background",
|
||||||
|
"command_id": command_id,
|
||||||
|
"command": final_command,
|
||||||
|
"message": "后台命令已创建;以下为当前已捕获输出。",
|
||||||
|
"output": output,
|
||||||
|
"return_code": None,
|
||||||
|
"timeout": timeout_value,
|
||||||
|
"elapsed_ms": int((time.time() - now) * 1000),
|
||||||
|
"run_in_background": True,
|
||||||
|
"background_task_created": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _run_command_thread(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
command_id: str,
|
||||||
|
command: str,
|
||||||
|
work_path: Path,
|
||||||
|
timeout: int,
|
||||||
|
session,
|
||||||
|
python_env: Dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
start_ts = time.time()
|
||||||
|
process: Optional[subprocess.Popen] = None
|
||||||
|
stdout_buf: List[str] = []
|
||||||
|
stderr_buf: List[str] = []
|
||||||
|
status = "failed"
|
||||||
|
return_code = -1
|
||||||
|
message: Optional[str] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
exec_cmd: Optional[List[str]] = None
|
||||||
|
use_shell = True
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.setdefault("PYTHONUNBUFFERED", "1")
|
||||||
|
if python_env:
|
||||||
|
env.update(python_env)
|
||||||
|
|
||||||
|
if session and getattr(session, "mode", None) == "docker":
|
||||||
|
container_name = getattr(session, "container_name", None)
|
||||||
|
mount_path = getattr(session, "mount_path", "/workspace") or "/workspace"
|
||||||
|
docker_bin = shutil.which("docker") or "docker"
|
||||||
|
if not container_name:
|
||||||
|
raise RuntimeError("容器模式下缺少 container_name")
|
||||||
|
try:
|
||||||
|
relative = work_path.relative_to(self.project_path).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
relative = ""
|
||||||
|
container_workdir = mount_path.rstrip("/")
|
||||||
|
if relative:
|
||||||
|
container_workdir = f"{container_workdir}/{relative}"
|
||||||
|
exec_cmd = [
|
||||||
|
docker_bin,
|
||||||
|
"exec",
|
||||||
|
"-e",
|
||||||
|
"PATH=/opt/agent-venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
|
"-e",
|
||||||
|
"VIRTUAL_ENV=/opt/agent-venv",
|
||||||
|
"-w",
|
||||||
|
container_workdir,
|
||||||
|
container_name,
|
||||||
|
"/bin/bash",
|
||||||
|
"-lc",
|
||||||
|
command,
|
||||||
|
]
|
||||||
|
use_shell = False
|
||||||
|
|
||||||
|
if use_shell:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
cwd=str(work_path),
|
||||||
|
shell=True,
|
||||||
|
env=env,
|
||||||
|
start_new_session=True,
|
||||||
|
text=True,
|
||||||
|
errors="replace",
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
exec_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
env=env,
|
||||||
|
start_new_session=True,
|
||||||
|
text=True,
|
||||||
|
errors="replace",
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if rec is not None:
|
||||||
|
rec["pid"] = process.pid
|
||||||
|
rec["updated_at"] = time.time()
|
||||||
|
|
||||||
|
def _reader(stream, collector, rec_key: str):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = stream.readline()
|
||||||
|
if line == "":
|
||||||
|
break
|
||||||
|
collector.append(line)
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if rec is not None:
|
||||||
|
rec[rec_key].append(line)
|
||||||
|
rec["updated_at"] = time.time()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
t_out = threading.Thread(target=_reader, args=(process.stdout, stdout_buf, "stdout_chunks"), daemon=True)
|
||||||
|
t_err = threading.Thread(target=_reader, args=(process.stderr, stderr_buf, "stderr_chunks"), daemon=True)
|
||||||
|
t_out.start()
|
||||||
|
t_err.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
process.wait(timeout=timeout)
|
||||||
|
return_code = process.returncode if process.returncode is not None else -1
|
||||||
|
if return_code == 0:
|
||||||
|
status = "completed"
|
||||||
|
else:
|
||||||
|
status = "failed"
|
||||||
|
message = f"命令执行失败 (返回码: {return_code})"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
status = "timeout"
|
||||||
|
message = f"命令执行超时 ({timeout}秒)"
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGINT)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
process.wait(timeout=2)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
try:
|
||||||
|
os.killpg(process.pid, signal.SIGKILL)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
process.wait(timeout=2)
|
||||||
|
return_code = process.returncode if process.returncode is not None else -1
|
||||||
|
|
||||||
|
t_out.join(timeout=1)
|
||||||
|
t_err.join(timeout=1)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
status = "failed"
|
||||||
|
message = f"执行失败: {exc}"
|
||||||
|
finally:
|
||||||
|
combined_output = "".join(stdout_buf + stderr_buf)
|
||||||
|
truncated = False
|
||||||
|
if MAX_RUN_COMMAND_CHARS and len(combined_output) > MAX_RUN_COMMAND_CHARS:
|
||||||
|
combined_output = combined_output[-MAX_RUN_COMMAND_CHARS:]
|
||||||
|
truncated = True
|
||||||
|
|
||||||
|
success = status == "completed"
|
||||||
|
result = {
|
||||||
|
"success": success,
|
||||||
|
"status": status,
|
||||||
|
"command": command,
|
||||||
|
"output": combined_output,
|
||||||
|
"return_code": return_code,
|
||||||
|
"truncated": truncated,
|
||||||
|
"timeout": timeout,
|
||||||
|
"elapsed_ms": int((time.time() - start_ts) * 1000),
|
||||||
|
"command_id": command_id,
|
||||||
|
"run_in_background": True,
|
||||||
|
}
|
||||||
|
if message:
|
||||||
|
result["message"] = message
|
||||||
|
|
||||||
|
with self._cv:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if rec is not None:
|
||||||
|
rec["status"] = status
|
||||||
|
rec["result"] = result
|
||||||
|
rec["truncated"] = truncated
|
||||||
|
rec["updated_at"] = time.time()
|
||||||
|
rec["finished_at"] = time.time()
|
||||||
|
self._cv.notify_all()
|
||||||
|
|
||||||
|
def _build_current_output(self, rec: Dict[str, Any]) -> str:
|
||||||
|
output = "".join((rec.get("stdout_chunks") or []) + (rec.get("stderr_chunks") or []))
|
||||||
|
if MAX_RUN_COMMAND_CHARS and len(output) > MAX_RUN_COMMAND_CHARS:
|
||||||
|
return output[-MAX_RUN_COMMAND_CHARS:]
|
||||||
|
return output
|
||||||
|
|
||||||
|
def wait_for_completion(self, command_id: str, timeout_seconds: Optional[float] = None, claim: bool = False) -> Dict[str, Any]:
|
||||||
|
"""阻塞等待后台命令完成。"""
|
||||||
|
with self._cv:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
return {"success": False, "error": f"未找到后台命令: {command_id}", "status": "error"}
|
||||||
|
|
||||||
|
status = rec.get("status")
|
||||||
|
if status in TERMINAL_STATUSES and isinstance(rec.get("result"), dict):
|
||||||
|
if claim:
|
||||||
|
rec["claimed_by_sleep"] = True
|
||||||
|
return dict(rec["result"])
|
||||||
|
|
||||||
|
wait_limit = timeout_seconds
|
||||||
|
if wait_limit is None:
|
||||||
|
created = float(rec.get("created_at") or time.time())
|
||||||
|
timeout_val = float(rec.get("timeout") or 0)
|
||||||
|
if timeout_val > 0:
|
||||||
|
wait_limit = max(1.0, (created + timeout_val + 5.0) - time.time())
|
||||||
|
else:
|
||||||
|
wait_limit = 3605.0
|
||||||
|
|
||||||
|
deadline = time.time() + max(0.1, float(wait_limit))
|
||||||
|
while time.time() < deadline:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
return {"success": False, "error": f"未找到后台命令: {command_id}", "status": "error"}
|
||||||
|
status = rec.get("status")
|
||||||
|
if status in TERMINAL_STATUSES and isinstance(rec.get("result"), dict):
|
||||||
|
if claim:
|
||||||
|
rec["claimed_by_sleep"] = True
|
||||||
|
return dict(rec["result"])
|
||||||
|
self._cv.wait(timeout=min(0.5, deadline - time.time()))
|
||||||
|
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
return {"success": False, "error": f"未找到后台命令: {command_id}", "status": "error"}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"status": "timeout",
|
||||||
|
"command_id": command_id,
|
||||||
|
"message": "等待后台命令完成超时",
|
||||||
|
"output": self._build_current_output(rec),
|
||||||
|
"return_code": rec.get("result", {}).get("return_code") if isinstance(rec.get("result"), dict) else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll_updates(self, conversation_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""获取未通知且未被 sleep 领取的已完成任务。"""
|
||||||
|
updates: List[Dict[str, Any]] = []
|
||||||
|
with self._lock:
|
||||||
|
for rec in self._records.values():
|
||||||
|
if conversation_id and rec.get("conversation_id") != conversation_id:
|
||||||
|
continue
|
||||||
|
if rec.get("status") not in TERMINAL_STATUSES:
|
||||||
|
continue
|
||||||
|
if rec.get("notified") or rec.get("claimed_by_sleep"):
|
||||||
|
continue
|
||||||
|
payload = rec.get("result")
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
updates.append(dict(payload))
|
||||||
|
|
||||||
|
updates.sort(key=lambda item: self._records.get(item.get("command_id"), {}).get("updated_at", 0))
|
||||||
|
return updates
|
||||||
|
|
||||||
|
def mark_notified(self, command_id: str):
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if rec:
|
||||||
|
rec["notified"] = True
|
||||||
|
rec["updated_at"] = time.time()
|
||||||
|
|
||||||
|
def mark_claimed(self, command_id: str):
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if rec:
|
||||||
|
rec["claimed_by_sleep"] = True
|
||||||
|
rec["updated_at"] = time.time()
|
||||||
|
|
||||||
|
def get_record(self, command_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
return None
|
||||||
|
return dict(rec)
|
||||||
|
|
||||||
|
def get_record_with_output(self, command_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""获取单条后台命令记录,并附带当前可读输出。"""
|
||||||
|
with self._lock:
|
||||||
|
rec = self._records.get(command_id)
|
||||||
|
if not rec:
|
||||||
|
return None
|
||||||
|
payload = dict(rec)
|
||||||
|
payload["output"] = self._build_current_output(rec)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def list_records(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
conversation_id: Optional[str] = None,
|
||||||
|
limit: int = 200,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""列出后台命令记录(按创建时间倒序)。"""
|
||||||
|
with self._lock:
|
||||||
|
items: List[Dict[str, Any]] = []
|
||||||
|
for rec in self._records.values():
|
||||||
|
if conversation_id and rec.get("conversation_id") != conversation_id:
|
||||||
|
continue
|
||||||
|
item = dict(rec)
|
||||||
|
item["output"] = self._build_current_output(rec)
|
||||||
|
items.append(item)
|
||||||
|
items.sort(key=lambda x: float(x.get("created_at") or 0), reverse=True)
|
||||||
|
max_limit = max(1, min(int(limit or 200), 1000))
|
||||||
|
return items[:max_limit]
|
||||||
|
|
||||||
|
def has_pending_for_conversation(self, conversation_id: Optional[str]) -> bool:
|
||||||
|
if not conversation_id:
|
||||||
|
return False
|
||||||
|
with self._lock:
|
||||||
|
for rec in self._records.values():
|
||||||
|
if rec.get("conversation_id") != conversation_id:
|
||||||
|
continue
|
||||||
|
if rec.get("status") == "running":
|
||||||
|
return True
|
||||||
|
if rec.get("status") in TERMINAL_STATUSES and (not rec.get("notified")) and (not rec.get("claimed_by_sleep")):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_waiting_items(self, conversation_id: Optional[str]) -> List[Dict[str, Any]]:
|
||||||
|
items: List[Dict[str, Any]] = []
|
||||||
|
if not conversation_id:
|
||||||
|
return items
|
||||||
|
with self._lock:
|
||||||
|
for rec in self._records.values():
|
||||||
|
if rec.get("conversation_id") != conversation_id:
|
||||||
|
continue
|
||||||
|
if rec.get("status") == "running" or (
|
||||||
|
rec.get("status") in TERMINAL_STATUSES
|
||||||
|
and not rec.get("notified")
|
||||||
|
and not rec.get("claimed_by_sleep")
|
||||||
|
):
|
||||||
|
items.append({
|
||||||
|
"command_id": rec.get("command_id"),
|
||||||
|
"command": rec.get("command"),
|
||||||
|
"status": rec.get("status"),
|
||||||
|
})
|
||||||
|
items.sort(key=lambda x: x.get("command_id") or "")
|
||||||
|
return items
|
||||||
@ -55,6 +55,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
|
|||||||
"skill_hints_enabled": False, # Skill 提示系统开关(默认关闭)
|
"skill_hints_enabled": False, # Skill 提示系统开关(默认关闭)
|
||||||
"skill_strict_terminal_enabled": False, # 强约束:terminal 系列工具需先阅读 terminal-guide
|
"skill_strict_terminal_enabled": False, # 强约束:terminal 系列工具需先阅读 terminal-guide
|
||||||
"skill_strict_sub_agent_enabled": False, # 强约束:子智能体系列工具需先阅读 sub-agent-guide
|
"skill_strict_sub_agent_enabled": False, # 强约束:子智能体系列工具需先阅读 sub-agent-guide
|
||||||
|
"skill_strict_run_command_background_enabled": False, # 强约束:run_command 后台模式需先阅读 run-command-guide
|
||||||
"default_model": "kimi-k2.5",
|
"default_model": "kimi-k2.5",
|
||||||
"image_compression": "original", # original / 1080p / 720p / 540p
|
"image_compression": "original", # original / 1080p / 720p / 540p
|
||||||
"auto_shallow_compress_enabled": False,
|
"auto_shallow_compress_enabled": False,
|
||||||
@ -187,6 +188,13 @@ def sanitize_personalization_payload(
|
|||||||
else:
|
else:
|
||||||
base["skill_strict_sub_agent_enabled"] = bool(base.get("skill_strict_sub_agent_enabled", False))
|
base["skill_strict_sub_agent_enabled"] = bool(base.get("skill_strict_sub_agent_enabled", False))
|
||||||
|
|
||||||
|
if "skill_strict_run_command_background_enabled" in data:
|
||||||
|
base["skill_strict_run_command_background_enabled"] = bool(data.get("skill_strict_run_command_background_enabled"))
|
||||||
|
else:
|
||||||
|
base["skill_strict_run_command_background_enabled"] = bool(
|
||||||
|
base.get("skill_strict_run_command_background_enabled", False)
|
||||||
|
)
|
||||||
|
|
||||||
if "disabled_tool_categories" in data:
|
if "disabled_tool_categories" in data:
|
||||||
base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories)
|
base["disabled_tool_categories"] = _sanitize_tool_categories(data.get("disabled_tool_categories"), allowed_tool_categories)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -127,11 +127,98 @@ from .chat_flow_runtime import (
|
|||||||
detect_malformed_tool_call,
|
detect_malformed_tool_call,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .chat_flow_task_support import process_sub_agent_updates
|
from .chat_flow_task_support import process_sub_agent_updates, process_background_command_updates
|
||||||
from .chat_flow_tool_loop import execute_tool_calls
|
from .chat_flow_tool_loop import execute_tool_calls
|
||||||
from .chat_flow_stream_loop import run_streaming_attempts
|
from .chat_flow_stream_loop import run_streaming_attempts
|
||||||
from .deep_compression import run_deep_compression
|
from .deep_compression import run_deep_compression
|
||||||
|
|
||||||
|
async def _dispatch_completion_user_notice(
|
||||||
|
*,
|
||||||
|
web_terminal,
|
||||||
|
workspace,
|
||||||
|
sender,
|
||||||
|
client_sid: str,
|
||||||
|
username: str,
|
||||||
|
conversation_id: str,
|
||||||
|
user_message: str,
|
||||||
|
extra_payload: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
"""复用子智能体完成后的 user 代发机制。"""
|
||||||
|
extra_payload = extra_payload or {}
|
||||||
|
try:
|
||||||
|
from .tasks import task_manager
|
||||||
|
workspace_id = getattr(workspace, "workspace_id", None) or "default"
|
||||||
|
session_data = {
|
||||||
|
"username": username,
|
||||||
|
"role": getattr(web_terminal, "user_role", "user"),
|
||||||
|
"is_api_user": getattr(web_terminal, "user_role", "") == "api",
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"run_mode": getattr(web_terminal, "run_mode", None),
|
||||||
|
"thinking_mode": getattr(web_terminal, "thinking_mode", None),
|
||||||
|
"model_key": getattr(web_terminal, "model_key", None),
|
||||||
|
}
|
||||||
|
# 关键:通知类后台任务需要把 user_message 写入任务事件流,
|
||||||
|
# 否则前端轮询只会看到 AI/tool 事件,看不到 user_message。
|
||||||
|
session_data["auto_user_message_event"] = True
|
||||||
|
session_data["auto_user_message_payload"] = dict(extra_payload or {})
|
||||||
|
rec = task_manager.create_chat_task(
|
||||||
|
username,
|
||||||
|
workspace_id,
|
||||||
|
user_message,
|
||||||
|
[],
|
||||||
|
conversation_id,
|
||||||
|
model_key=session_data.get("model_key"),
|
||||||
|
thinking_mode=session_data.get("thinking_mode"),
|
||||||
|
run_mode=session_data.get("run_mode"),
|
||||||
|
session_data=session_data,
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
'message': user_message,
|
||||||
|
'conversation_id': conversation_id,
|
||||||
|
'task_id': rec.task_id,
|
||||||
|
}
|
||||||
|
payload.update(extra_payload)
|
||||||
|
sender('user_message', payload)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"[CompletionNotice] 创建后台消息任务失败,回退直接执行: {e}")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'message': user_message,
|
||||||
|
'conversation_id': conversation_id,
|
||||||
|
}
|
||||||
|
payload.update(extra_payload)
|
||||||
|
sender('user_message', payload)
|
||||||
|
try:
|
||||||
|
task_handle = asyncio.create_task(handle_task_with_sender(
|
||||||
|
terminal=web_terminal,
|
||||||
|
workspace=workspace,
|
||||||
|
message=user_message,
|
||||||
|
images=[],
|
||||||
|
sender=sender,
|
||||||
|
client_sid=client_sid,
|
||||||
|
username=username,
|
||||||
|
videos=[]
|
||||||
|
))
|
||||||
|
await task_handle
|
||||||
|
except Exception as inner_exc:
|
||||||
|
debug_log(f"[CompletionNotice] 回退处理 user_message 失败: {inner_exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_shared_waiting_payload(items: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""构建与子智能体一致的 waiting 事件载荷结构。"""
|
||||||
|
normalized = []
|
||||||
|
for item in items or []:
|
||||||
|
summary = item.get('summary') or item.get('command') or '后台任务'
|
||||||
|
normalized.append({
|
||||||
|
'agent_id': item.get('agent_id') or item.get('command_id') or item.get('task_id'),
|
||||||
|
'summary': summary,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'count': len(normalized),
|
||||||
|
'tasks': normalized,
|
||||||
|
}
|
||||||
|
|
||||||
async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, client_sid, username):
|
async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, client_sid, username):
|
||||||
"""后台轮询子智能体完成状态,完成后触发新一轮对话"""
|
"""后台轮询子智能体完成状态,完成后触发新一轮对话"""
|
||||||
from .extensions import socketio
|
from .extensions import socketio
|
||||||
@ -251,65 +338,36 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id,
|
|||||||
]
|
]
|
||||||
remaining_count = len(running_tasks) + len(pending_notice_tasks)
|
remaining_count = len(running_tasks) + len(pending_notice_tasks)
|
||||||
has_remaining = remaining_count > 0
|
has_remaining = remaining_count > 0
|
||||||
|
await _dispatch_completion_user_notice(
|
||||||
# 注册为后台任务,确保刷新后可恢复轮询
|
web_terminal=web_terminal,
|
||||||
from .tasks import task_manager
|
workspace=workspace,
|
||||||
workspace_id = getattr(workspace, "workspace_id", None) or "default"
|
sender=sender,
|
||||||
session_data = {
|
client_sid=client_sid,
|
||||||
"username": username,
|
username=username,
|
||||||
"role": getattr(web_terminal, "user_role", "user"),
|
conversation_id=conversation_id,
|
||||||
"is_api_user": getattr(web_terminal, "user_role", "") == "api",
|
user_message=user_message,
|
||||||
"workspace_id": workspace_id,
|
extra_payload={
|
||||||
"run_mode": getattr(web_terminal, "run_mode", None),
|
'sub_agent_notice': True,
|
||||||
"thinking_mode": getattr(web_terminal, "thinking_mode", None),
|
'has_running_sub_agents': has_remaining,
|
||||||
"model_key": getattr(web_terminal, "model_key", None),
|
'remaining_count': remaining_count,
|
||||||
}
|
},
|
||||||
rec = task_manager.create_chat_task(
|
|
||||||
username,
|
|
||||||
workspace_id,
|
|
||||||
user_message,
|
|
||||||
[],
|
|
||||||
conversation_id,
|
|
||||||
model_key=session_data.get("model_key"),
|
|
||||||
thinking_mode=session_data.get("thinking_mode"),
|
|
||||||
run_mode=session_data.get("run_mode"),
|
|
||||||
session_data=session_data,
|
|
||||||
)
|
)
|
||||||
debug_log(f"[SubAgent] 已创建后台任务: task_id={rec.task_id}")
|
|
||||||
sender('user_message', {
|
|
||||||
'message': user_message,
|
|
||||||
'conversation_id': conversation_id,
|
|
||||||
'task_id': rec.task_id,
|
|
||||||
'sub_agent_notice': True,
|
|
||||||
'has_running_sub_agents': has_remaining,
|
|
||||||
'remaining_count': remaining_count
|
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_log(f"[SubAgent] 创建后台任务失败,回退直接执行: {e}")
|
debug_log(f"[SubAgent] 创建后台任务失败,回退直接执行: {e}")
|
||||||
sender('user_message', {
|
await _dispatch_completion_user_notice(
|
||||||
'message': user_message,
|
web_terminal=web_terminal,
|
||||||
'conversation_id': conversation_id,
|
workspace=workspace,
|
||||||
'sub_agent_notice': True,
|
sender=sender,
|
||||||
'has_running_sub_agents': has_remaining,
|
client_sid=client_sid,
|
||||||
'remaining_count': remaining_count
|
username=username,
|
||||||
})
|
conversation_id=conversation_id,
|
||||||
try:
|
user_message=user_message,
|
||||||
task = asyncio.create_task(handle_task_with_sender(
|
extra_payload={
|
||||||
terminal=web_terminal,
|
'sub_agent_notice': True,
|
||||||
workspace=workspace,
|
'has_running_sub_agents': has_remaining,
|
||||||
message=user_message,
|
'remaining_count': remaining_count,
|
||||||
images=[],
|
},
|
||||||
sender=sender,
|
)
|
||||||
client_sid=client_sid,
|
|
||||||
username=username,
|
|
||||||
videos=[]
|
|
||||||
))
|
|
||||||
await task
|
|
||||||
debug_log(f"[SubAgent] process_message_task 调用成功")
|
|
||||||
except Exception as inner_exc:
|
|
||||||
debug_log(f"[SubAgent] process_message_task 失败: {inner_exc}")
|
|
||||||
import traceback
|
|
||||||
debug_log(f"[SubAgent] 错误堆栈: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
return # 只处理第一个完成的子智能体
|
return # 只处理第一个完成的子智能体
|
||||||
|
|
||||||
@ -420,6 +478,77 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id,
|
|||||||
|
|
||||||
debug_log("[SubAgent] 后台轮询结束")
|
debug_log("[SubAgent] 后台轮询结束")
|
||||||
|
|
||||||
|
|
||||||
|
async def poll_background_command_completion(*, web_terminal, workspace, conversation_id: str, client_sid: str, username: str):
|
||||||
|
"""后台轮询 run_command 后台任务并在主流程结束后代发 user 通知。"""
|
||||||
|
from .extensions import socketio
|
||||||
|
|
||||||
|
manager = getattr(web_terminal, "background_command_manager", None)
|
||||||
|
if not manager:
|
||||||
|
debug_log("[BgCommand] poll_background_command_completion: manager 不存在")
|
||||||
|
return
|
||||||
|
|
||||||
|
max_wait_time = 3600
|
||||||
|
start_wait = time.time()
|
||||||
|
debug_log(f"[BgCommand] 开始后台轮询,conversation_id={conversation_id}, username={username}")
|
||||||
|
|
||||||
|
def sender(event_type, data):
|
||||||
|
try:
|
||||||
|
socketio.emit(event_type, data, room=f"user_{username}")
|
||||||
|
except Exception as e:
|
||||||
|
debug_log(f"[BgCommand] 发送事件失败: {event_type}, 错误: {e}")
|
||||||
|
|
||||||
|
while (time.time() - start_wait) < max_wait_time:
|
||||||
|
client_stop_info = get_stop_flag(client_sid, username)
|
||||||
|
if client_stop_info:
|
||||||
|
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
|
||||||
|
if stop_requested:
|
||||||
|
debug_log("[BgCommand] 用户请求停止,终止轮询")
|
||||||
|
break
|
||||||
|
|
||||||
|
if getattr(web_terminal, "_tool_loop_active", False):
|
||||||
|
debug_log("[BgCmdDebug] tool_loop_active=True, 延迟 user 代发轮询")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
updates = manager.poll_updates(conversation_id=conversation_id)
|
||||||
|
debug_log(f"[BgCmdDebug] background polling updates={len(updates)} conv={conversation_id}")
|
||||||
|
for update in updates:
|
||||||
|
command_id = update.get("command_id")
|
||||||
|
output = update.get("output") or ""
|
||||||
|
content = "[后台 run_command 完成]\n" + (output if output else "[no_output]")
|
||||||
|
prefix = "这是一句系统自动发送的user消息,用于通知你后台run_command已经运行完成"
|
||||||
|
user_message = f"{prefix}\n\n{content}"
|
||||||
|
debug_log(f"[BgCmdDebug] preparing user notice command_id={command_id} output_len={len(output)}")
|
||||||
|
manager.mark_notified(str(command_id))
|
||||||
|
has_remaining = manager.has_pending_for_conversation(conversation_id)
|
||||||
|
await _dispatch_completion_user_notice(
|
||||||
|
web_terminal=web_terminal,
|
||||||
|
workspace=workspace,
|
||||||
|
sender=sender,
|
||||||
|
client_sid=client_sid,
|
||||||
|
username=username,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
user_message=user_message,
|
||||||
|
extra_payload={
|
||||||
|
# 与子智能体完成通知完全复用同一前端通道/处理逻辑
|
||||||
|
'sub_agent_notice': True,
|
||||||
|
'remaining_count': 1 if has_remaining else 0,
|
||||||
|
'has_running_sub_agents': has_remaining,
|
||||||
|
'background_command_notice': True,
|
||||||
|
'has_running_background_commands': has_remaining,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
debug_log(f"[BgCmdDebug] user notice dispatched command_id={command_id} has_remaining={has_remaining}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not manager.has_pending_for_conversation(conversation_id):
|
||||||
|
debug_log("[BgCmdDebug] no pending background commands, stop polling")
|
||||||
|
break
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
debug_log("[BgCommand] 后台轮询结束")
|
||||||
|
|
||||||
async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspace, message, images, sender, client_sid, username: str, videos=None):
|
async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspace, message, images, sender, client_sid, username: str, videos=None):
|
||||||
"""处理任务并发送消息 - 集成token统计版本"""
|
"""处理任务并发送消息 - 集成token统计版本"""
|
||||||
from .extensions import socketio
|
from .extensions import socketio
|
||||||
@ -962,6 +1091,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
last_tool_call_time=last_tool_call_time,
|
last_tool_call_time=last_tool_call_time,
|
||||||
process_sub_agent_updates=process_sub_agent_updates,
|
process_sub_agent_updates=process_sub_agent_updates,
|
||||||
|
process_background_command_updates=process_background_command_updates,
|
||||||
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
|
maybe_mark_failure_from_message=maybe_mark_failure_from_message,
|
||||||
mark_force_thinking=mark_force_thinking,
|
mark_force_thinking=mark_force_thinking,
|
||||||
get_stop_flag=get_stop_flag,
|
get_stop_flag=get_stop_flag,
|
||||||
@ -1007,6 +1137,8 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
# 检查是否有后台运行的子智能体或待通知的完成任务
|
# 检查是否有后台运行的子智能体或待通知的完成任务
|
||||||
manager = getattr(web_terminal, "sub_agent_manager", None)
|
manager = getattr(web_terminal, "sub_agent_manager", None)
|
||||||
has_running_sub_agents = False
|
has_running_sub_agents = False
|
||||||
|
bg_manager = getattr(web_terminal, "background_command_manager", None)
|
||||||
|
has_running_background_commands = False
|
||||||
if manager:
|
if manager:
|
||||||
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
|
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
|
||||||
web_terminal._announced_sub_agent_tasks = set()
|
web_terminal._announced_sub_agent_tasks = set()
|
||||||
@ -1052,12 +1184,41 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac
|
|||||||
|
|
||||||
socketio.start_background_task(run_poll)
|
socketio.start_background_task(run_poll)
|
||||||
|
|
||||||
# 发送完成事件(如果有子智能体在运行,前端会保持等待状态)
|
# 检查是否有后台 run_command 或待通知任务
|
||||||
if not has_running_sub_agents:
|
if bg_manager and conversation_id:
|
||||||
|
waiting_items = bg_manager.list_waiting_items(conversation_id)
|
||||||
|
if waiting_items:
|
||||||
|
has_running_background_commands = True
|
||||||
|
# 与子智能体完全复用同一 waiting 事件(前端已有稳定处理链路)
|
||||||
|
sender('sub_agent_waiting', _build_shared_waiting_payload(waiting_items))
|
||||||
|
|
||||||
|
def run_bg_poll():
|
||||||
|
import asyncio
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(poll_background_command_completion(
|
||||||
|
web_terminal=web_terminal,
|
||||||
|
workspace=workspace,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
client_sid=client_sid,
|
||||||
|
username=username
|
||||||
|
))
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
socketio.start_background_task(run_bg_poll)
|
||||||
|
|
||||||
|
has_running_completion_jobs = has_running_sub_agents or has_running_background_commands
|
||||||
|
|
||||||
|
# 发送完成事件(如果有后台完成任务在运行,前端会保持等待状态)
|
||||||
|
if not has_running_completion_jobs:
|
||||||
finalize_user_work_timer()
|
finalize_user_work_timer()
|
||||||
sender('task_complete', {
|
sender('task_complete', {
|
||||||
'total_iterations': total_iterations,
|
'total_iterations': total_iterations,
|
||||||
'total_tool_calls': total_tool_calls,
|
'total_tool_calls': total_tool_calls,
|
||||||
'auto_fix_attempts': auto_fix_attempts,
|
'auto_fix_attempts': auto_fix_attempts,
|
||||||
'has_running_sub_agents': has_running_sub_agents
|
# 沿用子智能体字段,确保前端直接走已验证通路
|
||||||
|
'has_running_sub_agents': has_running_completion_jobs,
|
||||||
|
'has_running_background_commands': has_running_background_commands,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,6 +7,40 @@ from typing import Dict, List, Optional
|
|||||||
from modules.sub_agent_manager import TERMINAL_STATUSES
|
from modules.sub_agent_manager import TERMINAL_STATUSES
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_completion_notice_message(
|
||||||
|
*,
|
||||||
|
messages: List[Dict],
|
||||||
|
after_tool_call_id: Optional[str],
|
||||||
|
message: str,
|
||||||
|
inline: bool,
|
||||||
|
sender,
|
||||||
|
metadata: Optional[Dict] = None,
|
||||||
|
sub_agent_notice: bool = False,
|
||||||
|
):
|
||||||
|
"""统一插入完成通知(system/user)并发送事件。"""
|
||||||
|
insert_index = len(messages)
|
||||||
|
if after_tool_call_id:
|
||||||
|
for idx, msg in enumerate(messages):
|
||||||
|
if msg.get("role") == "tool" and msg.get("tool_call_id") == after_tool_call_id:
|
||||||
|
insert_index = idx + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
insert_role = "system" if inline else "user"
|
||||||
|
meta = dict(metadata or {})
|
||||||
|
meta["inline"] = inline
|
||||||
|
messages.insert(insert_index, {
|
||||||
|
"role": insert_role,
|
||||||
|
"content": message,
|
||||||
|
"metadata": meta
|
||||||
|
})
|
||||||
|
|
||||||
|
payload = {'content': message, 'inline': inline}
|
||||||
|
if sub_agent_notice:
|
||||||
|
payload['sub_agent_notice'] = True
|
||||||
|
payload.update({k: v for k, v in meta.items() if k in {"command_id", "task_id", "background_command_notice"}})
|
||||||
|
sender('system_message', payload)
|
||||||
|
|
||||||
|
|
||||||
async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message):
|
async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message):
|
||||||
"""轮询子智能体任务并通知前端,并把结果插入当前对话上下文。"""
|
"""轮询子智能体任务并通知前端,并把结果插入当前对话上下文。"""
|
||||||
manager = getattr(web_terminal, "sub_agent_manager", None)
|
manager = getattr(web_terminal, "sub_agent_manager", None)
|
||||||
@ -75,6 +109,9 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
|
|||||||
if task_id and task_id in web_terminal._announced_sub_agent_tasks:
|
if task_id and task_id in web_terminal._announced_sub_agent_tasks:
|
||||||
debug_log(f"[SubAgent] 任务 {task_id} 已通知过,跳过")
|
debug_log(f"[SubAgent] 任务 {task_id} 已通知过,跳过")
|
||||||
continue
|
continue
|
||||||
|
if isinstance(task_info, dict) and task_info.get("notified"):
|
||||||
|
debug_log(f"[SubAgent] 任务 {task_id} notified=true,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
message = update.get("system_message")
|
message = update.get("system_message")
|
||||||
if not message:
|
if not message:
|
||||||
@ -107,34 +144,89 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
|
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
|
||||||
|
|
||||||
debug_log(f"[SubAgent] 计算插入位置")
|
|
||||||
|
|
||||||
insert_index = len(messages)
|
|
||||||
if after_tool_call_id:
|
|
||||||
for idx, msg in enumerate(messages):
|
|
||||||
if msg.get("role") == "tool" and msg.get("tool_call_id") == after_tool_call_id:
|
|
||||||
insert_index = idx + 1
|
|
||||||
break
|
|
||||||
|
|
||||||
# 运行中插入 system 消息,避免触发新的 user 轮次;非运行中保持 user 通知
|
# 运行中插入 system 消息,避免触发新的 user 轮次;非运行中保持 user 通知
|
||||||
insert_role = "system" if inline else "user"
|
|
||||||
if not inline:
|
if not inline:
|
||||||
prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成"
|
prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成"
|
||||||
if not message.startswith(prefix):
|
if not message.startswith(prefix):
|
||||||
message = f"{prefix}\n\n{message}"
|
message = f"{prefix}\n\n{message}"
|
||||||
messages.insert(insert_index, {
|
_insert_completion_notice_message(
|
||||||
"role": insert_role,
|
messages=messages,
|
||||||
"content": message,
|
after_tool_call_id=after_tool_call_id,
|
||||||
"metadata": {"sub_agent_notice": True, "inline": inline, "task_id": task_id}
|
message=message,
|
||||||
})
|
inline=inline,
|
||||||
|
sender=sender,
|
||||||
|
metadata={"sub_agent_notice": True, "task_id": task_id},
|
||||||
|
sub_agent_notice=True,
|
||||||
|
)
|
||||||
if inline:
|
if inline:
|
||||||
web_terminal._inline_sub_agent_notified.add(inline_key)
|
web_terminal._inline_sub_agent_notified.add(inline_key)
|
||||||
debug_log(f"[SubAgent] 插入子智能体通知位置: {insert_index} role={insert_role} after_tool_call_id={after_tool_call_id}")
|
debug_log(f"[SubAgent] 插入子智能体通知 after_tool_call_id={after_tool_call_id} inline={inline}")
|
||||||
sender('system_message', {
|
maybe_mark_failure_from_message(web_terminal, message)
|
||||||
'content': message,
|
|
||||||
'inline': inline,
|
|
||||||
'sub_agent_notice': True
|
async def process_background_command_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message):
|
||||||
})
|
"""轮询后台 run_command 完成状态并发送 system 消息通知。"""
|
||||||
|
manager = getattr(web_terminal, "background_command_manager", None)
|
||||||
|
if not manager:
|
||||||
|
debug_log("[BgCmdDebug] process_background_command_updates skipped: manager 不存在")
|
||||||
|
return
|
||||||
|
|
||||||
|
conversation_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None)
|
||||||
|
debug_log(f"[BgCmdDebug] process_background_command_updates start inline={inline} after_tool_call_id={after_tool_call_id} conv={conversation_id}")
|
||||||
|
try:
|
||||||
|
updates = manager.poll_updates(conversation_id=conversation_id)
|
||||||
|
debug_log(f"[BgCmdDebug] poll_updates returned {len(updates)} updates")
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[BgCommand] poll 更新失败: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
debug_log("[BgCmdDebug] no background command updates")
|
||||||
|
return
|
||||||
|
|
||||||
|
for update in updates:
|
||||||
|
command_id = update.get("command_id")
|
||||||
|
output = update.get("output") or ""
|
||||||
|
message = "[后台 run_command 完成]\n" + (output if output else "[no_output]")
|
||||||
|
status = update.get("status")
|
||||||
|
debug_log(
|
||||||
|
f"[BgCmdDebug] emit inline notice command_id={command_id} status={status} "
|
||||||
|
f"output_len={len(output)} inline={inline}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 与子智能体 inline 通知一致:写入对话历史,确保前端/历史都可见
|
||||||
|
try:
|
||||||
|
web_terminal.context_manager.add_conversation(
|
||||||
|
"system",
|
||||||
|
message,
|
||||||
|
metadata={
|
||||||
|
"background_command_notice": True,
|
||||||
|
"inline": inline,
|
||||||
|
"command_id": command_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
debug_log(f"[BgCmdDebug] persisted system notice command_id={command_id}")
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[BgCmdDebug] persist system notice failed command_id={command_id}: {exc}")
|
||||||
|
|
||||||
|
_insert_completion_notice_message(
|
||||||
|
messages=messages,
|
||||||
|
after_tool_call_id=after_tool_call_id,
|
||||||
|
message=message,
|
||||||
|
inline=inline,
|
||||||
|
sender=sender,
|
||||||
|
metadata={
|
||||||
|
"background_command_notice": True,
|
||||||
|
"command_id": command_id,
|
||||||
|
},
|
||||||
|
sub_agent_notice=False,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
manager.mark_notified(str(command_id))
|
||||||
|
debug_log(f"[BgCmdDebug] mark_notified success command_id={command_id}")
|
||||||
|
except Exception:
|
||||||
|
debug_log(f"[BgCmdDebug] mark_notified failed command_id={command_id}")
|
||||||
|
pass
|
||||||
maybe_mark_failure_from_message(web_terminal, message)
|
maybe_mark_failure_from_message(web_terminal, message)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from modules.personalization_manager import load_personalization_config, resolve
|
|||||||
from .deep_compression import run_deep_compression
|
from .deep_compression import run_deep_compression
|
||||||
|
|
||||||
|
|
||||||
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag, workspace=None):
|
async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, process_background_command_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag, workspace=None):
|
||||||
previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False)
|
previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False)
|
||||||
web_terminal._tool_loop_active = True
|
web_terminal._tool_loop_active = True
|
||||||
# 执行每个工具
|
# 执行每个工具
|
||||||
@ -337,6 +337,17 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
'inline': False
|
'inline': False
|
||||||
})
|
})
|
||||||
maybe_mark_failure_from_message(web_terminal, system_msg)
|
maybe_mark_failure_from_message(web_terminal, system_msg)
|
||||||
|
if function_name == "sleep":
|
||||||
|
try:
|
||||||
|
if isinstance(result_data, dict) and result_data.get("mode") == "wait_sub_agent_ids":
|
||||||
|
waited_task_ids = result_data.get("waited_task_ids") or []
|
||||||
|
if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
|
||||||
|
web_terminal._announced_sub_agent_tasks = set()
|
||||||
|
for tid in waited_task_ids:
|
||||||
|
if tid:
|
||||||
|
web_terminal._announced_sub_agent_tasks.add(str(tid))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
monitor_snapshot_after = None
|
monitor_snapshot_after = None
|
||||||
if function_name in MONITOR_FILE_TOOLS:
|
if function_name in MONITOR_FILE_TOOLS:
|
||||||
result_path = None
|
result_path = None
|
||||||
@ -572,6 +583,8 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
|
|
||||||
debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates")
|
debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates")
|
||||||
await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message)
|
await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message)
|
||||||
|
debug_log(f"[BgCmdDebug] after tool={function_name} call_id={tool_call_id} -> poll background command updates (inline)")
|
||||||
|
await process_background_command_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message)
|
||||||
|
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
|||||||
@ -730,6 +730,81 @@ def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserW
|
|||||||
return jsonify({"success": False, "error": str(exc)}), 500
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@conversation_bp.route('/api/background_commands', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
@with_terminal
|
||||||
|
def list_background_commands(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
|
"""返回当前对话的后台 run_command 列表。"""
|
||||||
|
manager = getattr(terminal, "background_command_manager", None)
|
||||||
|
if not manager:
|
||||||
|
return jsonify({"success": True, "data": []})
|
||||||
|
try:
|
||||||
|
conversation_id = terminal.context_manager.current_conversation_id
|
||||||
|
limit_raw = request.args.get("limit", "200")
|
||||||
|
try:
|
||||||
|
limit_num = max(1, min(int(limit_raw), 1000))
|
||||||
|
except Exception:
|
||||||
|
limit_num = 200
|
||||||
|
|
||||||
|
records = manager.list_records(conversation_id=conversation_id, limit=limit_num)
|
||||||
|
data = []
|
||||||
|
for rec in records:
|
||||||
|
result = rec.get("result") if isinstance(rec.get("result"), dict) else {}
|
||||||
|
data.append({
|
||||||
|
"command_id": rec.get("command_id"),
|
||||||
|
"status": rec.get("status"),
|
||||||
|
"command": rec.get("command"),
|
||||||
|
"conversation_id": rec.get("conversation_id"),
|
||||||
|
"created_at": rec.get("created_at"),
|
||||||
|
"updated_at": rec.get("updated_at"),
|
||||||
|
"finished_at": rec.get("finished_at"),
|
||||||
|
"timeout": rec.get("timeout"),
|
||||||
|
"return_code": result.get("return_code"),
|
||||||
|
"run_in_background": True,
|
||||||
|
})
|
||||||
|
return jsonify({"success": True, "data": data})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@conversation_bp.route('/api/background_commands/<command_id>', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
@with_terminal
|
||||||
|
def get_background_command_detail(command_id: str, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
|
"""返回指定后台 run_command 的详情(含实时输出)。"""
|
||||||
|
manager = getattr(terminal, "background_command_manager", None)
|
||||||
|
if not manager:
|
||||||
|
return jsonify({"success": False, "error": "后台命令管理器不可用"}), 404
|
||||||
|
try:
|
||||||
|
rec = manager.get_record_with_output(command_id)
|
||||||
|
if not rec:
|
||||||
|
return jsonify({"success": False, "error": "未找到对应后台命令"}), 404
|
||||||
|
|
||||||
|
current_conv_id = terminal.context_manager.current_conversation_id
|
||||||
|
rec_conv_id = rec.get("conversation_id")
|
||||||
|
if current_conv_id and rec_conv_id and rec_conv_id != current_conv_id:
|
||||||
|
return jsonify({"success": False, "error": "无权限访问该后台命令"}), 403
|
||||||
|
|
||||||
|
result = rec.get("result") if isinstance(rec.get("result"), dict) else {}
|
||||||
|
payload = {
|
||||||
|
"command_id": rec.get("command_id"),
|
||||||
|
"status": rec.get("status"),
|
||||||
|
"command": rec.get("command"),
|
||||||
|
"conversation_id": rec.get("conversation_id"),
|
||||||
|
"created_at": rec.get("created_at"),
|
||||||
|
"updated_at": rec.get("updated_at"),
|
||||||
|
"finished_at": rec.get("finished_at"),
|
||||||
|
"timeout": rec.get("timeout"),
|
||||||
|
"return_code": result.get("return_code"),
|
||||||
|
"message": result.get("message"),
|
||||||
|
"output": rec.get("output") or "",
|
||||||
|
"run_in_background": True,
|
||||||
|
}
|
||||||
|
return jsonify({"success": True, "data": payload})
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"success": False, "error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
@conversation_bp.route('/api/conversations/<conversation_id>/duplicate', methods=['POST'])
|
@conversation_bp.route('/api/conversations/<conversation_id>/duplicate', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@with_terminal
|
||||||
|
|||||||
@ -252,6 +252,23 @@ class TaskManager:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise RuntimeError(f"对话加载失败: {exc}") from exc
|
raise RuntimeError(f"对话加载失败: {exc}") from exc
|
||||||
|
|
||||||
|
# 仅对“后台通知触发的新任务”补发 user_message 事件到任务事件流。
|
||||||
|
# 这样前端轮询能即时看到这条 user 消息,而不是刷新后才从历史中看到。
|
||||||
|
try:
|
||||||
|
if bool((rec.session_data or {}).get("auto_user_message_event")):
|
||||||
|
extra_payload = (rec.session_data or {}).get("auto_user_message_payload") or {}
|
||||||
|
if not isinstance(extra_payload, dict):
|
||||||
|
extra_payload = {}
|
||||||
|
payload = {
|
||||||
|
"message": rec.message,
|
||||||
|
"conversation_id": rec.conversation_id,
|
||||||
|
"task_id": rec.task_id,
|
||||||
|
}
|
||||||
|
payload.update(extra_payload)
|
||||||
|
self._append_event(rec, "user_message", payload)
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[Task] 注入 user_message 事件失败: {exc}")
|
||||||
|
|
||||||
def sender(event_type, data):
|
def sender(event_type, data):
|
||||||
# 记录事件
|
# 记录事件
|
||||||
self._append_event(rec, event_type, data)
|
self._append_event(rec, event_type, data)
|
||||||
|
|||||||
@ -351,6 +351,7 @@
|
|||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<SubAgentActivityDialog />
|
<SubAgentActivityDialog />
|
||||||
|
<BackgroundCommandDialog />
|
||||||
<TutorialOverlay />
|
<TutorialOverlay />
|
||||||
<NewUserTutorialPrompt
|
<NewUserTutorialPrompt
|
||||||
:visible="tutorialPromptVisible"
|
:visible="tutorialPromptVisible"
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { useResourceStore } from './stores/resource';
|
|||||||
import { useUploadStore } from './stores/upload';
|
import { useUploadStore } from './stores/upload';
|
||||||
import { useFileStore } from './stores/file';
|
import { useFileStore } from './stores/file';
|
||||||
import { useSubAgentStore } from './stores/subAgent';
|
import { useSubAgentStore } from './stores/subAgent';
|
||||||
|
import { useBackgroundCommandStore } from './stores/backgroundCommand';
|
||||||
import { useFocusStore } from './stores/focus';
|
import { useFocusStore } from './stores/focus';
|
||||||
import { usePersonalizationStore } from './stores/personalization';
|
import { usePersonalizationStore } from './stores/personalization';
|
||||||
import { useModelStore } from './stores/model';
|
import { useModelStore } from './stores/model';
|
||||||
@ -182,6 +183,11 @@ const appOptions = {
|
|||||||
subAgentStartPolling: 'startPolling',
|
subAgentStartPolling: 'startPolling',
|
||||||
subAgentStopPolling: 'stopPolling'
|
subAgentStopPolling: 'stopPolling'
|
||||||
}),
|
}),
|
||||||
|
...mapActions(useBackgroundCommandStore, {
|
||||||
|
backgroundCommandFetch: 'fetchCommands',
|
||||||
|
backgroundCommandStartPolling: 'startPolling',
|
||||||
|
backgroundCommandStopPolling: 'stopPolling'
|
||||||
|
}),
|
||||||
...mapActions(useFocusStore, {
|
...mapActions(useFocusStore, {
|
||||||
focusFetchFiles: 'fetchFocusedFiles',
|
focusFetchFiles: 'fetchFocusedFiles',
|
||||||
focusSetFiles: 'setFocusedFiles'
|
focusSetFiles: 'setFocusedFiles'
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import AppShell from '../components/shell/AppShell.vue';
|
|||||||
import ImagePicker from '../components/overlay/ImagePicker.vue';
|
import ImagePicker from '../components/overlay/ImagePicker.vue';
|
||||||
import ConversationReviewDialog from '../components/overlay/ConversationReviewDialog.vue';
|
import ConversationReviewDialog from '../components/overlay/ConversationReviewDialog.vue';
|
||||||
import SubAgentActivityDialog from '../components/overlay/SubAgentActivityDialog.vue';
|
import SubAgentActivityDialog from '../components/overlay/SubAgentActivityDialog.vue';
|
||||||
|
import BackgroundCommandDialog from '../components/overlay/BackgroundCommandDialog.vue';
|
||||||
import TutorialOverlay from '../components/overlay/TutorialOverlay.vue';
|
import TutorialOverlay from '../components/overlay/TutorialOverlay.vue';
|
||||||
import NewUserTutorialPrompt from '../components/overlay/NewUserTutorialPrompt.vue';
|
import NewUserTutorialPrompt from '../components/overlay/NewUserTutorialPrompt.vue';
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ export const appComponents = {
|
|||||||
ImagePicker,
|
ImagePicker,
|
||||||
ConversationReviewDialog,
|
ConversationReviewDialog,
|
||||||
SubAgentActivityDialog,
|
SubAgentActivityDialog,
|
||||||
|
BackgroundCommandDialog,
|
||||||
TutorialOverlay,
|
TutorialOverlay,
|
||||||
NewUserTutorialPrompt
|
NewUserTutorialPrompt
|
||||||
};
|
};
|
||||||
|
|||||||
@ -75,6 +75,8 @@ export async function mounted() {
|
|||||||
|
|
||||||
this.subAgentFetch();
|
this.subAgentFetch();
|
||||||
this.subAgentStartPolling();
|
this.subAgentStartPolling();
|
||||||
|
this.backgroundCommandFetch();
|
||||||
|
this.backgroundCommandStartPolling();
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.autoResizeInput();
|
this.autoResizeInput();
|
||||||
@ -104,6 +106,7 @@ export function beforeUnmount() {
|
|||||||
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
||||||
this.teardownMobileViewportWatcher();
|
this.teardownMobileViewportWatcher();
|
||||||
this.subAgentStopPolling();
|
this.subAgentStopPolling();
|
||||||
|
this.backgroundCommandStopPolling();
|
||||||
this.resourceStopContainerStatsPolling();
|
this.resourceStopContainerStatsPolling();
|
||||||
this.resourceStopProjectStoragePolling();
|
this.resourceStopProjectStoragePolling();
|
||||||
this.resourceStopUsageQuotaPolling();
|
this.resourceStopUsageQuotaPolling();
|
||||||
|
|||||||
@ -277,10 +277,11 @@ export const conversationMethods = {
|
|||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
const hasActiveTask = taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent;
|
const hasActiveTask = taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent;
|
||||||
if (hasActiveTask) {
|
if (hasActiveTask) {
|
||||||
|
const waitingLabel = this.waitingForBackgroundCommand ? '后台指令' : '后台子智能体';
|
||||||
const confirmed = await this.confirmAction({
|
const confirmed = await this.confirmAction({
|
||||||
title: '切换对话',
|
title: '切换对话',
|
||||||
message: this.waitingForSubAgent
|
message: this.waitingForSubAgent
|
||||||
? '后台子智能体正在运行,切换对话后将不会自动接收完成提示。确定要切换吗?'
|
? `${waitingLabel}正在运行,切换对话后将不会自动接收完成提示。确定要切换吗?`
|
||||||
: '当前有任务正在执行,切换对话后任务会停止。确定要切换吗?',
|
: '当前有任务正在执行,切换对话后任务会停止。确定要切换吗?',
|
||||||
confirmText: '切换',
|
confirmText: '切换',
|
||||||
cancelText: '取消'
|
cancelText: '取消'
|
||||||
@ -301,6 +302,7 @@ export const conversationMethods = {
|
|||||||
|
|
||||||
if (this.waitingForSubAgent && !taskStore.hasActiveTask) {
|
if (this.waitingForSubAgent && !taskStore.hasActiveTask) {
|
||||||
this.waitingForSubAgent = false;
|
this.waitingForSubAgent = false;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
@ -455,12 +457,13 @@ export const conversationMethods = {
|
|||||||
|
|
||||||
if (taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent) {
|
if (taskStore.hasActiveTask || this.taskInProgress || this.waitingForSubAgent) {
|
||||||
hasActiveTask = true;
|
hasActiveTask = true;
|
||||||
|
const waitingLabel = this.waitingForBackgroundCommand ? '后台指令' : '后台子智能体';
|
||||||
|
|
||||||
// 显示提示
|
// 显示提示
|
||||||
const confirmed = await this.confirmAction({
|
const confirmed = await this.confirmAction({
|
||||||
title: '创建新对话',
|
title: '创建新对话',
|
||||||
message: this.waitingForSubAgent
|
message: this.waitingForSubAgent
|
||||||
? '后台子智能体正在运行,创建新对话后将不会自动接收完成提示。确定要创建吗?'
|
? `${waitingLabel}正在运行,创建新对话后将不会自动接收完成提示。确定要创建吗?`
|
||||||
: '当前有任务正在执行,创建新对话后任务会停止。确定要创建吗?',
|
: '当前有任务正在执行,创建新对话后任务会停止。确定要创建吗?',
|
||||||
confirmText: '创建',
|
confirmText: '创建',
|
||||||
cancelText: '取消'
|
cancelText: '取消'
|
||||||
@ -487,6 +490,7 @@ export const conversationMethods = {
|
|||||||
this.stopRequested = false;
|
this.stopRequested = false;
|
||||||
if (this.waitingForSubAgent && !taskStore.hasActiveTask) {
|
if (this.waitingForSubAgent && !taskStore.hasActiveTask) {
|
||||||
this.waitingForSubAgent = false;
|
this.waitingForSubAgent = false;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog('[创建新对话] 任务已停止');
|
debugLog('[创建新对话] 任务已停止');
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
|
|
||||||
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
||||||
|
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
|
||||||
|
|
||||||
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
||||||
const content = (rawContent || '').toString().trim();
|
const content = (rawContent || '').toString().trim();
|
||||||
@ -11,6 +12,17 @@ function parseSubAgentDoneLabel(rawContent: any): string | null {
|
|||||||
return `子智能体${match[1]} 任务完成`;
|
return `子智能体${match[1]} 任务完成`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBackgroundRunCommandDoneLabel(rawContent: any): string | null {
|
||||||
|
const content = (rawContent || '').toString().trim();
|
||||||
|
if (!content) return null;
|
||||||
|
if (!BG_RUN_COMMAND_DONE_PREFIX_RE.test(content)) return null;
|
||||||
|
return '后台 run_command 完成';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSystemNoticeLabel(rawContent: any): string | null {
|
||||||
|
return parseSubAgentDoneLabel(rawContent) || parseBackgroundRunCommandDoneLabel(rawContent);
|
||||||
|
}
|
||||||
|
|
||||||
export const historyMethods = {
|
export const historyMethods = {
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 关键功能:获取并显示历史对话内容
|
// 关键功能:获取并显示历史对话内容
|
||||||
@ -327,10 +339,10 @@ export const historyMethods = {
|
|||||||
}
|
}
|
||||||
if (message.role === 'system') {
|
if (message.role === 'system') {
|
||||||
const rawContent = message.content || '';
|
const rawContent = message.content || '';
|
||||||
const label = parseSubAgentDoneLabel(rawContent);
|
const label = parseSystemNoticeLabel(rawContent);
|
||||||
console.log('[DEBUG_SYSTEM][history] 命中 role=system 历史消息', {
|
console.log('[DEBUG_SYSTEM][history] 命中 role=system 历史消息', {
|
||||||
rawContent,
|
rawContent,
|
||||||
matchedSubAgentDone: !!label
|
matchedDoneNotice: !!label
|
||||||
});
|
});
|
||||||
if (label) {
|
if (label) {
|
||||||
// 历史中的 system 通知转换为 assistant system action,避免被 role=system 过滤掉
|
// 历史中的 system 通知转换为 assistant system action,避免被 role=system 过滤掉
|
||||||
|
|||||||
@ -1,11 +1,118 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
|
|
||||||
|
const debugNotifyLog = (..._args: any[]) => {};
|
||||||
|
const keyNotifyLog = (...args: any[]) => {
|
||||||
|
console.log(...args);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务轮询事件处理器
|
* 任务轮询事件处理器
|
||||||
* 将从 REST API 轮询获取的事件转换为前端状态更新
|
* 将从 REST API 轮询获取的事件转换为前端状态更新
|
||||||
*/
|
*/
|
||||||
export const taskPollingMethods = {
|
export const taskPollingMethods = {
|
||||||
|
stopWaitingTaskProbe() {
|
||||||
|
if (this.waitingTaskProbeTimer) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] stopWaitingTaskProbe');
|
||||||
|
clearInterval(this.waitingTaskProbeTimer);
|
||||||
|
this.waitingTaskProbeTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async tryResumeWaitingNoticeTask() {
|
||||||
|
if (!this.waitingForSubAgent || !this.currentConversationId) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:skip', {
|
||||||
|
waitingForSubAgent: this.waitingForSubAgent,
|
||||||
|
currentConversationId: this.currentConversationId
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
if (taskStore.hasActiveTask) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:already-active', {
|
||||||
|
taskId: taskStore.currentTaskId,
|
||||||
|
status: taskStore.taskStatus
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch('/api/tasks');
|
||||||
|
if (!resp.ok) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:tasks-api-not-ok', {
|
||||||
|
status: resp.status
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const result = await resp.json().catch(() => ({}));
|
||||||
|
const tasks = Array.isArray(result?.data) ? result.data : [];
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:tasks', {
|
||||||
|
currentConversationId: this.currentConversationId,
|
||||||
|
count: tasks.length,
|
||||||
|
running: tasks
|
||||||
|
.filter((t: any) => t?.status === 'running')
|
||||||
|
.map((t: any) => ({
|
||||||
|
task_id: t?.task_id,
|
||||||
|
conversation_id: t?.conversation_id,
|
||||||
|
status: t?.status
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
const runningTask = tasks.find((task: any) =>
|
||||||
|
task?.status === 'running' && task?.conversation_id === this.currentConversationId
|
||||||
|
);
|
||||||
|
if (!runningTask?.task_id) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:no-matched-task');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:resume', {
|
||||||
|
task_id: runningTask.task_id
|
||||||
|
});
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] tryResumeWaitingNoticeTask:resume', {
|
||||||
|
task_id: runningTask.task_id,
|
||||||
|
conversationId: this.currentConversationId
|
||||||
|
});
|
||||||
|
// 关键修复:切到新的通知任务前,清空旧任务事件去重索引。
|
||||||
|
// 否则新任务 event idx 与旧任务重叠时会被误判为“重复事件”而丢失 user_message。
|
||||||
|
if (typeof this.clearProcessedEvents === 'function') {
|
||||||
|
this.clearProcessedEvents();
|
||||||
|
}
|
||||||
|
taskStore.resumeTask(runningTask.task_id, {
|
||||||
|
status: 'running',
|
||||||
|
resetOffset: true,
|
||||||
|
eventHandler: (event: any) => this.handleTaskEvent(event)
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[TaskPolling] 接管等待任务失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startWaitingTaskProbe() {
|
||||||
|
if (this.waitingTaskProbeTimer) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] startWaitingTaskProbe:already-started');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] startWaitingTaskProbe');
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] startWaitingTaskProbe', {
|
||||||
|
conversationId: this.currentConversationId
|
||||||
|
});
|
||||||
|
this.waitingTaskProbeTimer = setInterval(async () => {
|
||||||
|
if (!this.waitingForSubAgent) {
|
||||||
|
this.stopWaitingTaskProbe();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resumed = await this.tryResumeWaitingNoticeTask();
|
||||||
|
if (resumed) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] waitingTaskProbe:resumed-and-stop');
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] waitingTaskProbe:resumed-and-stop');
|
||||||
|
this.stopWaitingTaskProbe();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理任务事件(从轮询获取)
|
* 处理任务事件(从轮询获取)
|
||||||
*/
|
*/
|
||||||
@ -22,11 +129,19 @@ export const taskPollingMethods = {
|
|||||||
eventType === 'sub_agent_waiting' ||
|
eventType === 'sub_agent_waiting' ||
|
||||||
(eventType === 'user_message' && eventData?.sub_agent_notice)
|
(eventType === 'user_message' && eventData?.sub_agent_notice)
|
||||||
) {
|
) {
|
||||||
console.log('[DEBUG_SYSTEM][event] 捕获关键事件', {
|
debugNotifyLog('[DEBUG_NOTIFY][event] 捕获关键事件', {
|
||||||
eventType,
|
eventType,
|
||||||
eventIdx,
|
eventIdx,
|
||||||
eventData
|
eventData
|
||||||
});
|
});
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][event] key-event', {
|
||||||
|
eventType,
|
||||||
|
idx: eventIdx,
|
||||||
|
task_id: eventData?.task_id,
|
||||||
|
sub_agent_notice: !!eventData?.sub_agent_notice,
|
||||||
|
has_running_sub_agents: eventData?.has_running_sub_agents,
|
||||||
|
has_running_background_commands: eventData?.has_running_background_commands
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件去重检查
|
// 事件去重检查
|
||||||
@ -167,6 +282,10 @@ export const taskPollingMethods = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user_message':
|
case 'user_message':
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][event] dispatch:user_message', {
|
||||||
|
idx: eventIdx,
|
||||||
|
data: eventData
|
||||||
|
});
|
||||||
this.handleUserMessage(eventData, eventIdx);
|
this.handleUserMessage(eventData, eventIdx);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -712,6 +831,7 @@ export const taskPollingMethods = {
|
|||||||
|
|
||||||
handleTaskComplete(data: any) {
|
handleTaskComplete(data: any) {
|
||||||
const hasRunningSubAgents = !!data?.has_running_sub_agents;
|
const hasRunningSubAgents = !!data?.has_running_sub_agents;
|
||||||
|
const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
|
||||||
if (hasRunningSubAgents) {
|
if (hasRunningSubAgents) {
|
||||||
debugLog('[TaskPolling] 任务完成,但仍有后台子智能体运行');
|
debugLog('[TaskPolling] 任务完成,但仍有后台子智能体运行');
|
||||||
} else {
|
} else {
|
||||||
@ -728,9 +848,15 @@ export const taskPollingMethods = {
|
|||||||
if (hasRunningSubAgents) {
|
if (hasRunningSubAgents) {
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
this.waitingForSubAgent = true;
|
this.waitingForSubAgent = true;
|
||||||
|
this.waitingForBackgroundCommand = hasRunningBackgroundCommands;
|
||||||
|
// 关键修复:主任务结束后将切到新的通知任务,先清空旧任务事件索引去重表。
|
||||||
|
this.clearProcessedEvents();
|
||||||
|
this.startWaitingTaskProbe();
|
||||||
} else {
|
} else {
|
||||||
this.taskInProgress = false;
|
this.taskInProgress = false;
|
||||||
this.waitingForSubAgent = false;
|
this.waitingForSubAgent = false;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
|
this.stopWaitingTaskProbe();
|
||||||
this.clearTaskState(); // 清理任务状态
|
this.clearTaskState(); // 清理任务状态
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -753,6 +879,8 @@ export const taskPollingMethods = {
|
|||||||
this.taskInProgress = false;
|
this.taskInProgress = false;
|
||||||
this.stopRequested = false;
|
this.stopRequested = false;
|
||||||
this.waitingForSubAgent = false;
|
this.waitingForSubAgent = false;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
|
this.stopWaitingTaskProbe();
|
||||||
|
|
||||||
if (typeof this.clearPendingTools === 'function') {
|
if (typeof this.clearPendingTools === 'function') {
|
||||||
this.clearPendingTools('task_stopped');
|
this.clearPendingTools('task_stopped');
|
||||||
@ -764,6 +892,7 @@ export const taskPollingMethods = {
|
|||||||
|
|
||||||
// 新增统一清理方法
|
// 新增统一清理方法
|
||||||
clearTaskState() {
|
clearTaskState() {
|
||||||
|
this.stopWaitingTaskProbe();
|
||||||
(async () => {
|
(async () => {
|
||||||
const { useTaskStore } = await import('../../stores/task');
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
@ -778,9 +907,24 @@ export const taskPollingMethods = {
|
|||||||
handleUserMessage(data: any) {
|
handleUserMessage(data: any) {
|
||||||
const message = (data?.message || data?.content || '').trim();
|
const message = (data?.message || data?.content || '').trim();
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] handleUserMessage:empty', { data });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debugLog('[TaskPolling] 收到用户消息事件');
|
debugLog('[TaskPolling] 收到用户消息事件');
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][ui] handleUserMessage', {
|
||||||
|
messagePreview: message.slice(0, 120),
|
||||||
|
sub_agent_notice: !!data?.sub_agent_notice,
|
||||||
|
background_command_notice: !!data?.background_command_notice,
|
||||||
|
task_id: data?.task_id,
|
||||||
|
has_running_sub_agents: data?.has_running_sub_agents,
|
||||||
|
has_running_background_commands: data?.has_running_background_commands
|
||||||
|
});
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] handleUserMessage', {
|
||||||
|
messagePreview: message.slice(0, 80),
|
||||||
|
task_id: data?.task_id,
|
||||||
|
sub_agent_notice: !!data?.sub_agent_notice,
|
||||||
|
background_command_notice: !!data?.background_command_notice
|
||||||
|
});
|
||||||
this.chatAddUserMessage(message, data?.images || [], data?.videos || []);
|
this.chatAddUserMessage(message, data?.images || [], data?.videos || []);
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
@ -791,6 +935,16 @@ export const taskPollingMethods = {
|
|||||||
} else if (typeof data?.remaining_count === 'number') {
|
} else if (typeof data?.remaining_count === 'number') {
|
||||||
this.waitingForSubAgent = data.remaining_count > 0;
|
this.waitingForSubAgent = data.remaining_count > 0;
|
||||||
}
|
}
|
||||||
|
if (typeof data?.has_running_background_commands === 'boolean') {
|
||||||
|
this.waitingForBackgroundCommand = data.has_running_background_commands;
|
||||||
|
} else if (data?.background_command_notice) {
|
||||||
|
this.waitingForBackgroundCommand = this.waitingForSubAgent;
|
||||||
|
}
|
||||||
|
if (this.waitingForSubAgent) {
|
||||||
|
this.startWaitingTaskProbe();
|
||||||
|
} else {
|
||||||
|
this.stopWaitingTaskProbe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
this.conditionalScrollToBottom();
|
this.conditionalScrollToBottom();
|
||||||
@ -1363,9 +1517,11 @@ export const taskPollingMethods = {
|
|||||||
pendingTasks: pendingNotice.map((task: any) => ({ task_id: task?.task_id, status: task?.status }))
|
pendingTasks: pendingNotice.map((task: any) => ({ task_id: task?.task_id, status: task?.status }))
|
||||||
});
|
});
|
||||||
this.waitingForSubAgent = true;
|
this.waitingForSubAgent = true;
|
||||||
|
this.waitingForBackgroundCommand = false;
|
||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
this.stopRequested = false;
|
this.stopRequested = false;
|
||||||
|
this.startWaitingTaskProbe();
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
import { debugLog } from './common';
|
import { debugLog } from './common';
|
||||||
|
|
||||||
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
||||||
|
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
|
||||||
|
|
||||||
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
function parseSubAgentDoneLabel(rawContent: any): string | null {
|
||||||
const content = (rawContent || '').toString().trim();
|
const content = (rawContent || '').toString().trim();
|
||||||
@ -41,6 +42,21 @@ function parseSubAgentDoneLabel(rawContent: any): string | null {
|
|||||||
return `子智能体${agentId} 任务完成`;
|
return `子智能体${agentId} 任务完成`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBackgroundRunCommandDoneLabel(rawContent: any): string | null {
|
||||||
|
const content = (rawContent || '').toString().trim();
|
||||||
|
if (!content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!BG_RUN_COMMAND_DONE_PREFIX_RE.test(content)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return '后台 run_command 完成';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSystemNoticeLabel(rawContent: any): string | null {
|
||||||
|
return parseSubAgentDoneLabel(rawContent) || parseBackgroundRunCommandDoneLabel(rawContent);
|
||||||
|
}
|
||||||
|
|
||||||
export const uiMethods = {
|
export const uiMethods = {
|
||||||
ensureScrollListener() {
|
ensureScrollListener() {
|
||||||
if (this._scrollListenerReady) {
|
if (this._scrollListenerReady) {
|
||||||
@ -248,6 +264,12 @@ export const uiMethods = {
|
|||||||
// 切换到子智能体面板时立即刷新,避免等待轮询间隔
|
// 切换到子智能体面板时立即刷新,避免等待轮询间隔
|
||||||
this.subAgentFetch();
|
this.subAgentFetch();
|
||||||
this.subAgentStartPolling();
|
this.subAgentStartPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === 'backgroundCommands') {
|
||||||
|
// 切换到后台指令面板时立即刷新,避免等待轮询间隔
|
||||||
|
this.backgroundCommandFetch();
|
||||||
|
this.backgroundCommandStartPolling();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -873,17 +895,17 @@ export const uiMethods = {
|
|||||||
|
|
||||||
addSystemMessage(content) {
|
addSystemMessage(content) {
|
||||||
console.log('[DEBUG_SYSTEM][ui] addSystemMessage 入参', { content });
|
console.log('[DEBUG_SYSTEM][ui] addSystemMessage 入参', { content });
|
||||||
const subAgentDoneLabel = parseSubAgentDoneLabel(content);
|
const systemNoticeLabel = parseSystemNoticeLabel(content);
|
||||||
if (!subAgentDoneLabel) {
|
if (!systemNoticeLabel) {
|
||||||
// 其他 system 消息全部隐藏,且不进入渲染链路,避免出现空白块
|
// 其他 system 消息全部隐藏,且不进入渲染链路,避免出现空白块
|
||||||
console.log('[DEBUG_SYSTEM][ui] addSystemMessage 被拦截(非子智能体完成)', { content });
|
console.log('[DEBUG_SYSTEM][ui] addSystemMessage 被拦截(非完成通知)', { content });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[DEBUG_SYSTEM][ui] addSystemMessage 通过,写入 action', {
|
console.log('[DEBUG_SYSTEM][ui] addSystemMessage 通过,写入 action', {
|
||||||
original: content,
|
original: content,
|
||||||
normalizedLabel: subAgentDoneLabel
|
normalizedLabel: systemNoticeLabel
|
||||||
});
|
});
|
||||||
this.chatAddSystemMessage(subAgentDoneLabel, { variant: 'sub_agent_done' });
|
this.chatAddSystemMessage(systemNoticeLabel, { variant: 'sub_agent_done' });
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
this.conditionalScrollToBottom();
|
this.conditionalScrollToBottom();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -11,6 +11,8 @@ export function dataState() {
|
|||||||
usePollingMode: true,
|
usePollingMode: true,
|
||||||
// 后台子智能体等待状态
|
// 后台子智能体等待状态
|
||||||
waitingForSubAgent: false,
|
waitingForSubAgent: false,
|
||||||
|
// 后台 run_command 等待状态(用于文案区分)
|
||||||
|
waitingForBackgroundCommand: false,
|
||||||
// 是否在对话区展示 system 消息
|
// 是否在对话区展示 system 消息
|
||||||
hideSystemMessages: true,
|
hideSystemMessages: true,
|
||||||
|
|
||||||
@ -21,6 +23,8 @@ export function dataState() {
|
|||||||
toolStacks: new Map(),
|
toolStacks: new Map(),
|
||||||
// 当前任务是否仍在进行中(用于保持输入区的"停止"状态)
|
// 当前任务是否仍在进行中(用于保持输入区的"停止"状态)
|
||||||
taskInProgress: false,
|
taskInProgress: false,
|
||||||
|
// 等待后台通知期间,用于发现并接管新建任务的轮询定时器
|
||||||
|
waitingTaskProbeTimer: null,
|
||||||
// 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次
|
// 记录上一次成功加载历史的对话ID,防止初始化阶段重复加载导致动画播放两次
|
||||||
lastHistoryLoadedConversationId: null,
|
lastHistoryLoadedConversationId: null,
|
||||||
|
|
||||||
|
|||||||
@ -449,6 +449,7 @@ const filteredMessages = computed(() => {
|
|||||||
const latestMessageIndex = computed(() => filteredMessages.value.length - 1);
|
const latestMessageIndex = computed(() => filteredMessages.value.length - 1);
|
||||||
const SUB_AGENT_DONE_LABEL_RE = /^子智能体\d+\s*任务完成$/;
|
const SUB_AGENT_DONE_LABEL_RE = /^子智能体\d+\s*任务完成$/;
|
||||||
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[::]/;
|
||||||
|
const BG_RUN_COMMAND_DONE_LABEL_RE = /^(?:\[)?后台\s*run_command\s*完成(?:\])?$/;
|
||||||
const RENDERABLE_ACTION_TYPES = new Set(['thinking', 'text', 'tool', 'append', 'append_payload', 'system']);
|
const RENDERABLE_ACTION_TYPES = new Set(['thinking', 'text', 'tool', 'append', 'append_payload', 'system']);
|
||||||
const nowMs = ref(Date.now());
|
const nowMs = ref(Date.now());
|
||||||
let timerHandle: number | null = null;
|
let timerHandle: number | null = null;
|
||||||
@ -524,7 +525,7 @@ function getSubAgentSystemNoticeLabel(action: any): string | null {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (action.variant === 'sub_agent_done' && SUB_AGENT_DONE_LABEL_RE.test(content)) {
|
if (action.variant === 'sub_agent_done' && (SUB_AGENT_DONE_LABEL_RE.test(content) || BG_RUN_COMMAND_DONE_LABEL_RE.test(content))) {
|
||||||
if (!debugLoggedSystemActionKeys.has(`${key}-variant-pass`)) {
|
if (!debugLoggedSystemActionKeys.has(`${key}-variant-pass`)) {
|
||||||
console.log('[DEBUG_SYSTEM][render] 命中 variant 渲染', { action, label: content });
|
console.log('[DEBUG_SYSTEM][render] 命中 variant 渲染', { action, label: content });
|
||||||
debugLoggedSystemActionKeys.add(`${key}-variant-pass`);
|
debugLoggedSystemActionKeys.add(`${key}-variant-pass`);
|
||||||
|
|||||||
47
static/src/components/overlay/BackgroundCommandDialog.vue
Normal file
47
static/src/components/overlay/BackgroundCommandDialog.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="overlay-fade">
|
||||||
|
<div v-if="activeCommand" class="subagent-activity-overlay" @click.self="close">
|
||||||
|
<div class="subagent-activity-modal bg-command-modal">
|
||||||
|
<div class="subagent-activity-header">
|
||||||
|
<div class="subagent-activity-title">
|
||||||
|
后台指令 {{ activeCommand.command_id }}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="subagent-activity-close" @click="close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="subagent-activity-meta">
|
||||||
|
<span class="subagent-activity-status" :class="activeDetail?.status || activeCommand.status || ''">
|
||||||
|
{{ activeDetail?.status || activeCommand.status || 'running' }}
|
||||||
|
</span>
|
||||||
|
<span class="subagent-activity-summary" v-if="activeDetail?.command || activeCommand.command">
|
||||||
|
{{ activeDetail?.command || activeCommand.command }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="subagent-activity-body">
|
||||||
|
<div v-if="detailError" class="subagent-activity-error">{{ detailError }}</div>
|
||||||
|
<div v-else-if="detailLoading && !displayOutput" class="subagent-activity-empty">
|
||||||
|
正在读取后台指令输出...
|
||||||
|
</div>
|
||||||
|
<pre v-else class="bg-command-output">{{ displayOutput || '[no_output]' }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
||||||
|
|
||||||
|
const commandStore = useBackgroundCommandStore();
|
||||||
|
const { activeCommand, activeDetail, detailLoading, detailError } = storeToRefs(commandStore);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
commandStore.closeCommand();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayOutput = computed(() => {
|
||||||
|
const text = (activeDetail.value?.output || '').toString();
|
||||||
|
return text;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -71,6 +71,14 @@
|
|||||||
>
|
>
|
||||||
<span class="icon icon-md" :style="iconStyle('bot')" aria-hidden="true"></span>
|
<span class="icon icon-md" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ active: panelMode === 'backgroundCommands' }"
|
||||||
|
@click.stop="$emit('select-panel', 'backgroundCommands')"
|
||||||
|
title="后台指令"
|
||||||
|
>
|
||||||
|
<span class="icon icon-md" :style="iconStyle('terminal')" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@ -91,8 +99,14 @@
|
|||||||
<span>待办列表</span>
|
<span>待办列表</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="icon-label">
|
<span v-else class="icon-label">
|
||||||
<span class="icon icon-sm" :style="iconStyle('bot')" aria-hidden="true"></span>
|
<template v-if="panelMode === 'subAgents'">
|
||||||
<span>子智能体</span>
|
<span class="icon icon-sm" :style="iconStyle('bot')" aria-hidden="true"></span>
|
||||||
|
<span>子智能体</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="icon icon-sm" :style="iconStyle('terminal')" aria-hidden="true"></span>
|
||||||
|
<span>后台指令</span>
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -123,6 +137,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="panelMode === 'backgroundCommands'" class="sub-agent-panel">
|
||||||
|
<div v-if="!backgroundCommands.length" class="sub-agent-empty">暂无后台指令</div>
|
||||||
|
<div v-else class="sub-agent-cards">
|
||||||
|
<div
|
||||||
|
class="sub-agent-card"
|
||||||
|
v-for="command in backgroundCommands"
|
||||||
|
:key="command.command_id"
|
||||||
|
@click="openBackgroundCommand(command)"
|
||||||
|
>
|
||||||
|
<div class="sub-agent-header">
|
||||||
|
<span class="sub-agent-id">{{ command.command_id }}</span>
|
||||||
|
<span class="sub-agent-status" :class="command.status">{{ command.status || 'running' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sub-agent-summary">{{ command.command || '(空命令)' }}</div>
|
||||||
|
<div class="sub-agent-tool">
|
||||||
|
{{ formatBackgroundCommandMeta(command) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="file-tree" @contextmenu.prevent>
|
<div v-else class="file-tree" @contextmenu.prevent>
|
||||||
<div v-if="fileTreeUnavailable" class="file-tree-empty">{{ fileTreeMessage }}</div>
|
<div v-if="fileTreeUnavailable" class="file-tree-empty">{{ fileTreeMessage }}</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -149,6 +183,7 @@ import { storeToRefs } from 'pinia';
|
|||||||
import FileNode from '@/components/files/FileNode.vue';
|
import FileNode from '@/components/files/FileNode.vue';
|
||||||
import { useFileStore } from '@/stores/file';
|
import { useFileStore } from '@/stores/file';
|
||||||
import { useSubAgentStore } from '@/stores/subAgent';
|
import { useSubAgentStore } from '@/stores/subAgent';
|
||||||
|
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
||||||
import statusLogoSvgRaw from '../../../logo/logo.svg?raw';
|
import statusLogoSvgRaw from '../../../logo/logo.svg?raw';
|
||||||
|
|
||||||
defineOptions({ name: 'LeftPanel' });
|
defineOptions({ name: 'LeftPanel' });
|
||||||
@ -161,14 +196,14 @@ const props = defineProps<{
|
|||||||
thinkingMode: boolean;
|
thinkingMode: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
panelMenuOpen: boolean;
|
panelMenuOpen: boolean;
|
||||||
panelMode: 'files' | 'todo' | 'subAgents';
|
panelMode: 'files' | 'todo' | 'subAgents' | 'backgroundCommands';
|
||||||
runMode: 'fast' | 'thinking' | 'deep';
|
runMode: 'fast' | 'thinking' | 'deep';
|
||||||
fileManagerDisabled?: boolean;
|
fileManagerDisabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(event: 'toggle-panel-menu'): void;
|
(event: 'toggle-panel-menu'): void;
|
||||||
(event: 'select-panel', mode: 'files' | 'todo' | 'subAgents'): void;
|
(event: 'select-panel', mode: 'files' | 'todo' | 'subAgents' | 'backgroundCommands'): void;
|
||||||
(event: 'open-file-manager'): void;
|
(event: 'open-file-manager'): void;
|
||||||
(event: 'toggle-thinking-mode'): void;
|
(event: 'toggle-thinking-mode'): void;
|
||||||
}>();
|
}>();
|
||||||
@ -230,13 +265,27 @@ const modeIndicatorTitle = computed(() => {
|
|||||||
});
|
});
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
const subAgentStore = useSubAgentStore();
|
const subAgentStore = useSubAgentStore();
|
||||||
|
const backgroundCommandStore = useBackgroundCommandStore();
|
||||||
const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } = storeToRefs(fileStore);
|
const { fileTree, expandedFolders, todoList, fileTreeUnavailable, fileTreeMessage } = storeToRefs(fileStore);
|
||||||
const { subAgents } = storeToRefs(subAgentStore);
|
const { subAgents } = storeToRefs(subAgentStore);
|
||||||
|
const { commands: backgroundCommands } = storeToRefs(backgroundCommandStore);
|
||||||
|
|
||||||
const openSubAgent = (agent: any) => {
|
const openSubAgent = (agent: any) => {
|
||||||
subAgentStore.openSubAgent(agent);
|
subAgentStore.openSubAgent(agent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openBackgroundCommand = (command: any) => {
|
||||||
|
backgroundCommandStore.openCommand(command);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatBackgroundCommandMeta = (command: any) => {
|
||||||
|
const code = command?.return_code;
|
||||||
|
if (code === null || code === undefined) {
|
||||||
|
return '点击查看实时输出';
|
||||||
|
}
|
||||||
|
return `返回码:${code}`;
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
panelMenuWrapper
|
panelMenuWrapper
|
||||||
});
|
});
|
||||||
|
|||||||
@ -749,6 +749,23 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>子智能体系列工具</span>
|
<span>子智能体系列工具</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.skill_strict_run_command_background_enabled"
|
||||||
|
@change="personalization.updateField({ key: 'skill_strict_run_command_background_enabled', value: $event.target.checked })"
|
||||||
|
/>
|
||||||
|
<span class="fancy-check" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 64 64">
|
||||||
|
<path
|
||||||
|
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||||||
|
pathLength="575.0541381835938"
|
||||||
|
class="fancy-path"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>run_command 后台模式</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="behavior-field">
|
<div class="behavior-field">
|
||||||
|
|||||||
136
static/src/stores/backgroundCommand.ts
Normal file
136
static/src/stores/backgroundCommand.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useUiStore } from './ui';
|
||||||
|
|
||||||
|
interface BackgroundCommand {
|
||||||
|
command_id: string;
|
||||||
|
status?: string;
|
||||||
|
command?: string;
|
||||||
|
created_at?: number;
|
||||||
|
updated_at?: number;
|
||||||
|
finished_at?: number | null;
|
||||||
|
timeout?: number;
|
||||||
|
return_code?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackgroundCommandDetail extends BackgroundCommand {
|
||||||
|
output?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackgroundCommandState {
|
||||||
|
commands: BackgroundCommand[];
|
||||||
|
pollTimer: ReturnType<typeof setInterval> | null;
|
||||||
|
detailPollTimer: ReturnType<typeof setInterval> | null;
|
||||||
|
activeCommand: BackgroundCommand | null;
|
||||||
|
activeDetail: BackgroundCommandDetail | null;
|
||||||
|
detailLoading: boolean;
|
||||||
|
detailError: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'timeout', 'cancelled']);
|
||||||
|
|
||||||
|
export const useBackgroundCommandStore = defineStore('backgroundCommand', {
|
||||||
|
state: (): BackgroundCommandState => ({
|
||||||
|
commands: [],
|
||||||
|
pollTimer: null,
|
||||||
|
detailPollTimer: null,
|
||||||
|
activeCommand: null,
|
||||||
|
activeDetail: null,
|
||||||
|
detailLoading: false,
|
||||||
|
detailError: null
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async fetchCommands() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/background_commands?limit=200');
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(await resp.text());
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.commands = Array.isArray(data.data) ? data.data : [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取后台指令列表失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startPolling() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
this.pollTimer = setInterval(() => {
|
||||||
|
if (uiStore.panelMode === 'backgroundCommands') {
|
||||||
|
this.fetchCommands();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
stopPolling() {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCommand(command: BackgroundCommand) {
|
||||||
|
if (!command || !command.command_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activeCommand = command;
|
||||||
|
this.activeDetail = null;
|
||||||
|
this.detailError = null;
|
||||||
|
this.fetchCommandDetail(command.command_id);
|
||||||
|
this.startDetailPolling();
|
||||||
|
},
|
||||||
|
closeCommand() {
|
||||||
|
this.stopDetailPolling();
|
||||||
|
this.activeCommand = null;
|
||||||
|
this.activeDetail = null;
|
||||||
|
this.detailError = null;
|
||||||
|
this.detailLoading = false;
|
||||||
|
},
|
||||||
|
startDetailPolling() {
|
||||||
|
if (this.detailPollTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.detailPollTimer = setInterval(() => {
|
||||||
|
const commandId = this.activeCommand?.command_id;
|
||||||
|
if (commandId) {
|
||||||
|
this.fetchCommandDetail(commandId);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
stopDetailPolling() {
|
||||||
|
if (this.detailPollTimer) {
|
||||||
|
clearInterval(this.detailPollTimer);
|
||||||
|
this.detailPollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchCommandDetail(commandId: string) {
|
||||||
|
if (!commandId) return;
|
||||||
|
this.detailLoading = true;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/api/background_commands/${encodeURIComponent(commandId)}`);
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(await resp.text());
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data && data.success && data.data) {
|
||||||
|
this.activeDetail = data.data;
|
||||||
|
const current = this.commands.find((item) => item.command_id === commandId);
|
||||||
|
if (current) {
|
||||||
|
Object.assign(current, data.data);
|
||||||
|
}
|
||||||
|
const status = (data.data.status || '').toString();
|
||||||
|
if (TERMINAL_STATUSES.has(status)) {
|
||||||
|
this.stopDetailPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.detailError = error?.message || String(error);
|
||||||
|
console.error('获取后台指令详情失败:', error);
|
||||||
|
} finally {
|
||||||
|
this.detailLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -10,6 +10,7 @@ interface PersonalForm {
|
|||||||
skill_hints_enabled: boolean;
|
skill_hints_enabled: boolean;
|
||||||
skill_strict_terminal_enabled: boolean;
|
skill_strict_terminal_enabled: boolean;
|
||||||
skill_strict_sub_agent_enabled: boolean;
|
skill_strict_sub_agent_enabled: boolean;
|
||||||
|
skill_strict_run_command_background_enabled: boolean;
|
||||||
silent_tool_disable: boolean;
|
silent_tool_disable: boolean;
|
||||||
enhanced_tool_display: boolean;
|
enhanced_tool_display: boolean;
|
||||||
enabled_skills: string[];
|
enabled_skills: string[];
|
||||||
@ -72,6 +73,7 @@ const defaultForm = (): PersonalForm => ({
|
|||||||
skill_hints_enabled: false,
|
skill_hints_enabled: false,
|
||||||
skill_strict_terminal_enabled: false,
|
skill_strict_terminal_enabled: false,
|
||||||
skill_strict_sub_agent_enabled: false,
|
skill_strict_sub_agent_enabled: false,
|
||||||
|
skill_strict_run_command_background_enabled: false,
|
||||||
silent_tool_disable: false,
|
silent_tool_disable: false,
|
||||||
enhanced_tool_display: true,
|
enhanced_tool_display: true,
|
||||||
enabled_skills: [],
|
enabled_skills: [],
|
||||||
@ -210,6 +212,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
skill_hints_enabled: !!data.skill_hints_enabled,
|
skill_hints_enabled: !!data.skill_hints_enabled,
|
||||||
skill_strict_terminal_enabled: !!data.skill_strict_terminal_enabled,
|
skill_strict_terminal_enabled: !!data.skill_strict_terminal_enabled,
|
||||||
skill_strict_sub_agent_enabled: !!data.skill_strict_sub_agent_enabled,
|
skill_strict_sub_agent_enabled: !!data.skill_strict_sub_agent_enabled,
|
||||||
|
skill_strict_run_command_background_enabled: !!data.skill_strict_run_command_background_enabled,
|
||||||
silent_tool_disable: !!data.silent_tool_disable,
|
silent_tool_disable: !!data.silent_tool_disable,
|
||||||
enhanced_tool_display: data.enhanced_tool_display !== false,
|
enhanced_tool_display: data.enhanced_tool_display !== false,
|
||||||
enabled_skills: Array.isArray(data.enabled_skills)
|
enabled_skills: Array.isArray(data.enabled_skills)
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { debugLog } from '../app/methods/common';
|
import { debugLog } from '../app/methods/common';
|
||||||
|
|
||||||
|
const debugNotifyLog = (..._args: any[]) => {};
|
||||||
|
const keyNotifyLog = (...args: any[]) => {
|
||||||
|
console.log(...args);
|
||||||
|
};
|
||||||
|
|
||||||
export const useTaskStore = defineStore('task', {
|
export const useTaskStore = defineStore('task', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
currentTaskId: null as string | null,
|
currentTaskId: null as string | null,
|
||||||
@ -109,6 +114,25 @@ export const useTaskStore = defineStore('task', {
|
|||||||
console.error('[Task] 处理事件失败:', err, event);
|
console.error('[Task] 处理事件失败:', err, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const interesting = data.events.filter((e: any) =>
|
||||||
|
['user_message', 'sub_agent_waiting', 'task_complete', 'task_stopped', 'error'].includes(e?.type)
|
||||||
|
);
|
||||||
|
if (interesting.length > 0) {
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] events', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
from: this.lastEventIndex,
|
||||||
|
nextOffset: data.next_offset,
|
||||||
|
count: interesting.length,
|
||||||
|
items: interesting.map((e: any) => ({
|
||||||
|
idx: e?.idx,
|
||||||
|
type: e?.type,
|
||||||
|
sub_agent_notice: !!e?.data?.sub_agent_notice,
|
||||||
|
has_running_sub_agents: e?.data?.has_running_sub_agents,
|
||||||
|
has_running_background_commands: e?.data?.has_running_background_commands,
|
||||||
|
task_id: e?.data?.task_id
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.lastEventIndex = data.next_offset;
|
this.lastEventIndex = data.next_offset;
|
||||||
}
|
}
|
||||||
@ -145,6 +169,12 @@ export const useTaskStore = defineStore('task', {
|
|||||||
this.pollingError = error.message;
|
this.pollingError = error.message;
|
||||||
|
|
||||||
console.error('[Task] 轮询失败:', error);
|
console.error('[Task] 轮询失败:', error);
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] pollTaskEvents:error', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
from: this.lastEventIndex,
|
||||||
|
error: error?.message || String(error),
|
||||||
|
pollingErrorCount: this.pollingErrorCount
|
||||||
|
});
|
||||||
|
|
||||||
// 连续失败 5 次后停止
|
// 连续失败 5 次后停止
|
||||||
if (this.pollingErrorCount >= 5) {
|
if (this.pollingErrorCount >= 5) {
|
||||||
@ -165,6 +195,10 @@ export const useTaskStore = defineStore('task', {
|
|||||||
startPolling(eventHandler?: (event: any) => void) {
|
startPolling(eventHandler?: (event: any) => void) {
|
||||||
if (this.isPolling) {
|
if (this.isPolling) {
|
||||||
debugLog('[Task] 轮询已在运行');
|
debugLog('[Task] 轮询已在运行');
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling:already-running', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
lastEventIndex: this.lastEventIndex
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +208,10 @@ export const useTaskStore = defineStore('task', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debugLog('[Task] 启动轮询:', this.currentTaskId);
|
debugLog('[Task] 启动轮询:', this.currentTaskId);
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling', {
|
||||||
|
taskId: this.currentTaskId,
|
||||||
|
lastEventIndex: this.lastEventIndex
|
||||||
|
});
|
||||||
this.isPolling = true;
|
this.isPolling = true;
|
||||||
|
|
||||||
// 如果没有传入 eventHandler,从根实例获取
|
// 如果没有传入 eventHandler,从根实例获取
|
||||||
@ -198,6 +236,16 @@ export const useTaskStore = defineStore('task', {
|
|||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
debugNotifyLog('[DEBUG_NOTIFY][task] resumeTask', {
|
||||||
|
incomingTaskId: taskId,
|
||||||
|
currentTaskId: this.currentTaskId,
|
||||||
|
options
|
||||||
|
});
|
||||||
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][task] resumeTask', {
|
||||||
|
incomingTaskId: taskId,
|
||||||
|
currentTaskId: this.currentTaskId,
|
||||||
|
resetOffset: options?.resetOffset !== false
|
||||||
|
});
|
||||||
if (this.currentTaskId && this.currentTaskId !== taskId) {
|
if (this.currentTaskId && this.currentTaskId !== taskId) {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const DEFAULT_STEPS: TutorialStep[] = [
|
|||||||
{ id: 'sidebar-workspace-toggle', title: '工作区折叠', description: '显示/隐藏左侧工作区面板。', target: '[data-tutorial="workspace-toggle"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
{ id: 'sidebar-workspace-toggle', title: '工作区折叠', description: '显示/隐藏左侧工作区面板。', target: '[data-tutorial="workspace-toggle"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
||||||
{ id: 'sidebar-monitor-toggle', title: '虚拟显示器', description: '切换到虚拟显示器模式。', target: '[data-tutorial="monitor-toggle"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
{ id: 'sidebar-monitor-toggle', title: '虚拟显示器', description: '切换到虚拟显示器模式。', target: '[data-tutorial="monitor-toggle"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
||||||
{ id: 'workspace-panel-switch', title: '工作区面板切换', description: '下一步会自动展开面板切换菜单。', target: '[data-tutorial="panel-menu-toggle"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
{ id: 'workspace-panel-switch', title: '工作区面板切换', description: '下一步会自动展开面板切换菜单。', target: '[data-tutorial="panel-menu-toggle"]', mode: 'info', autoClick: true, placement: 'right', condition: 'not_mobile_viewport' },
|
||||||
{ id: 'workspace-panel-options', title: '三合一面板', description: '这里可以切换文件 / 待办 / 子智能体。', target: '[data-tutorial="panel-menu"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
{ id: 'workspace-panel-options', title: '四合一面板', description: '这里可以切换文件 / 待办 / 子智能体 / 后台指令。', target: '[data-tutorial="panel-menu"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
||||||
{ id: 'workspace-mode-indicator', title: '思考模式', description: '点击切换快速 / 思考 / 深度思考。', target: '[data-tutorial="run-mode-indicator"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
{ id: 'workspace-mode-indicator', title: '思考模式', description: '点击切换快速 / 思考 / 深度思考。', target: '[data-tutorial="run-mode-indicator"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
||||||
{ id: 'workspace-connection-indicator', title: '连接状态指示灯', description: '绿色表示连接正常;红色表示与后端断开连接。', target: '[data-tutorial="connection-indicator"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
{ id: 'workspace-connection-indicator', title: '连接状态指示灯', description: '绿色表示连接正常;红色表示与后端断开连接。', target: '[data-tutorial="connection-indicator"]', mode: 'info', placement: 'right', condition: 'not_mobile_viewport' },
|
||||||
{ id: 'header-model-selector', title: '模型与模式选择', description: '下一步会自动展开模型与运行模式弹窗。', target: '[data-tutorial="header-model-selector"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'not_mobile_viewport' },
|
{ id: 'header-model-selector', title: '模型与模式选择', description: '下一步会自动展开模型与运行模式弹窗。', target: '[data-tutorial="header-model-selector"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'not_mobile_viewport' },
|
||||||
@ -58,7 +58,7 @@ const DEFAULT_STEPS: TutorialStep[] = [
|
|||||||
{ id: 'mobile-menu-open-workspace', title: '再次打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-menu-open-workspace', title: '再次打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
||||||
{ id: 'mobile-menu-workspace', title: '工作文件', description: '下一步会自动进入工作文件。', target: '[data-tutorial="mobile-menu-workspace"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-menu-workspace', title: '工作文件', description: '下一步会自动进入工作文件。', target: '[data-tutorial="mobile-menu-workspace"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
||||||
{ id: 'mobile-workspace-panel-switch', title: '工作区面板切换', description: '下一步会自动展开切换弹窗。', target: '[data-tutorial="panel-menu-toggle"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-workspace-panel-switch', title: '工作区面板切换', description: '下一步会自动展开切换弹窗。', target: '[data-tutorial="panel-menu-toggle"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
||||||
{ id: 'mobile-workspace-panel-options', title: '切换弹窗选项', description: '这里可切换文件 / 待办 / 子智能体。', target: '[data-tutorial="panel-menu"]', mode: 'info', placement: 'bottom', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-workspace-panel-options', title: '切换弹窗选项', description: '这里可切换文件 / 待办 / 子智能体 / 后台指令。', target: '[data-tutorial="panel-menu"]', mode: 'info', placement: 'bottom', condition: 'is_mobile_viewport' },
|
||||||
{ id: 'mobile-workspace-close', title: '关闭工作文件', description: '下一步会自动关闭工作文件面板。', target: '[data-tutorial="mobile-workspace-close"]', mode: 'info', autoClick: true, placement: 'right', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-workspace-close', title: '关闭工作文件', description: '下一步会自动关闭工作文件面板。', target: '[data-tutorial="mobile-workspace-close"]', mode: 'info', autoClick: true, placement: 'right', condition: 'is_mobile_viewport' },
|
||||||
{ id: 'mobile-menu-open-newchat', title: '再次打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-menu-open-newchat', title: '再次打开菜单', description: '下一步会自动打开菜单。', target: '[data-tutorial="mobile-menu-trigger"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
||||||
{ id: 'mobile-menu-new-chat', title: '新建对话', description: '下一步会自动新建对话。', target: '[data-tutorial="mobile-menu-new-chat"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
{ id: 'mobile-menu-new-chat', title: '新建对话', description: '下一步会自动新建对话。', target: '[data-tutorial="mobile-menu-new-chat"]', mode: 'info', autoClick: true, placement: 'bottom', condition: 'is_mobile_viewport' },
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
type PanelMode = 'files' | 'todo' | 'subAgents';
|
type PanelMode = 'files' | 'todo' | 'subAgents' | 'backgroundCommands';
|
||||||
type ResizingPanel = 'left' | 'right' | null;
|
type ResizingPanel = 'left' | 'right' | null;
|
||||||
type MobileOverlayTarget = 'conversation' | 'workspace' | 'focus' | null;
|
type MobileOverlayTarget = 'conversation' | 'workspace' | 'focus' | null;
|
||||||
|
|
||||||
|
|||||||
@ -2050,6 +2050,26 @@
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-command-modal {
|
||||||
|
width: min(980px, 92vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-command-output {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--theme-surface-soft);
|
||||||
|
color: var(--claude-text);
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.confirm-dialog-fade-enter-active,
|
.confirm-dialog-fade-enter-active,
|
||||||
.confirm-dialog-fade-leave-active {
|
.confirm-dialog-fade-leave-active {
|
||||||
|
|||||||
@ -424,6 +424,31 @@ def _format_terminal_input(result_data: Dict[str, Any]) -> str:
|
|||||||
def _format_sleep(result_data: Dict[str, Any]) -> str:
|
def _format_sleep(result_data: Dict[str, Any]) -> str:
|
||||||
if not result_data.get("success"):
|
if not result_data.get("success"):
|
||||||
return _format_failure("sleep", result_data)
|
return _format_failure("sleep", result_data)
|
||||||
|
mode = result_data.get("mode")
|
||||||
|
if mode == "wait_sub_agent_ids":
|
||||||
|
results = result_data.get("results") or []
|
||||||
|
header = result_data.get("message") or f"已等待 {len(results)} 个子智能体结束"
|
||||||
|
detail_blocks: List[str] = []
|
||||||
|
for item in results:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
# 优先复用子智能体原始 system_message,确保与系统通知内容一致
|
||||||
|
sys_msg = item.get("system_message")
|
||||||
|
if isinstance(sys_msg, str) and sys_msg.strip():
|
||||||
|
detail_blocks.append(sys_msg.strip())
|
||||||
|
else:
|
||||||
|
detail_blocks.append(_format_wait_sub_agent(item))
|
||||||
|
if detail_blocks:
|
||||||
|
return "\n\n".join([header] + detail_blocks)
|
||||||
|
return header
|
||||||
|
if mode == "wait_runcommand_id":
|
||||||
|
nested = result_data.get("result")
|
||||||
|
if isinstance(nested, dict):
|
||||||
|
output = nested.get("output") or ""
|
||||||
|
if output:
|
||||||
|
return f"[后台 run_command 完成]\n{output}"
|
||||||
|
return "[后台 run_command 完成]\n[no_output]"
|
||||||
|
return "[后台 run_command 完成]"
|
||||||
reason = result_data.get("reason")
|
reason = result_data.get("reason")
|
||||||
timestamp = result_data.get("timestamp")
|
timestamp = result_data.get("timestamp")
|
||||||
message = result_data.get("message") or "等待完成"
|
message = result_data.get("message") or "等待完成"
|
||||||
@ -436,8 +461,26 @@ def _format_sleep(result_data: Dict[str, Any]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _format_run_command(result_data: Dict[str, Any]) -> str:
|
def _format_run_command(result_data: Dict[str, Any]) -> str:
|
||||||
|
status = str(result_data.get("status") or "").lower()
|
||||||
|
if result_data.get("background_task_created") is False:
|
||||||
|
body = _plain_command_output(result_data)
|
||||||
|
if not body:
|
||||||
|
body = "[no_output]"
|
||||||
|
return "[命令在5秒内完成,未创建后台任务]\n" + body
|
||||||
|
|
||||||
|
if status == "running_background":
|
||||||
|
command_id = result_data.get("command_id") or "-"
|
||||||
|
message = result_data.get("message") or "后台命令已创建;以下为当前已捕获输出。"
|
||||||
|
output = result_data.get("output") or ""
|
||||||
|
lines = [f"[{command_id}]", f"[{message}]"]
|
||||||
|
if output:
|
||||||
|
lines.append(output)
|
||||||
|
else:
|
||||||
|
lines.append("[no_output]")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
text = _plain_command_output(result_data)
|
text = _plain_command_output(result_data)
|
||||||
if (result_data.get("status") or "").lower() == "timeout":
|
if status == "timeout":
|
||||||
suggestion = "建议:在持久终端中直接运行该命令(terminal_session + terminal_input),或缩短命令执行时间。"
|
suggestion = "建议:在持久终端中直接运行该命令(terminal_session + terminal_input),或缩短命令执行时间。"
|
||||||
text = f"{text}\n{suggestion}" if text else suggestion
|
text = f"{text}\n{suggestion}" if text else suggestion
|
||||||
return text
|
return text
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user