diff --git a/README.md b/README.md index 76e3394..14d0f89 100644 --- a/README.md +++ b/README.md @@ -589,6 +589,7 @@ bash "$HOME/zk-data-agent/scripts/install-from-git.sh" OPENAI_API_KEY OPENAI_BASE_URL OPENAI_MODEL +OPENAI_TIMEOUT_SECONDS CLAW_BACKEND_HOST CLAW_BACKEND_PORT CLAW_FRONTEND_HOST @@ -730,6 +731,7 @@ cat .env.deploy - `OPENAI_API_KEY` - `OPENAI_BASE_URL` - `OPENAI_MODEL` +- `OPENAI_TIMEOUT_SECONDS` 然后看后端日志: diff --git a/backend/api/server.py b/backend/api/server.py index c371858..8d51f52 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -278,6 +278,7 @@ class AgentInstanceConfig: model: str base_url: str api_key: str + timeout_seconds: float allow_shell: bool allow_write: bool @@ -483,6 +484,7 @@ class AgentState: model: str, base_url: str, api_key: str, + timeout_seconds: float, allow_shell: bool, allow_write: bool, session_directory: Path, @@ -498,6 +500,7 @@ class AgentState: model=model, base_url=base_url, api_key=api_key, + timeout_seconds=timeout_seconds, allow_shell=allow_shell, allow_write=allow_write, ) @@ -772,6 +775,7 @@ class AgentState: model=config.model, base_url=config.base_url, api_key=config.api_key, + timeout_seconds=config.timeout_seconds, ) return LocalCodingAgent( model_config=model_config, @@ -1753,6 +1757,9 @@ def _interrupted_session_message(status: str) -> str: def _runtime_event_stage(event: dict[str, object]) -> str: event_type = event.get('type') if event_type == 'tool_start': + stage_note = str(event.get('assistant_content') or '').strip() + if stage_note.startswith(('进度:', '进度:')): + return stage_note tool_name = str(event.get('tool_name') or '').strip() return f'调用工具 {tool_name}' if tool_name else '正在调用工具' if event_type == 'tool_result': diff --git a/frontend/app/app/api/chat/route.ts b/frontend/app/app/api/chat/route.ts index 2f5f0e2..b4c1e8b 100644 --- a/frontend/app/app/api/chat/route.ts +++ b/frontend/app/app/api/chat/route.ts @@ -15,7 +15,7 @@ import { } from "@/lib/claw-auth"; export const runtime = "nodejs"; -export const maxDuration = 300; +export const maxDuration = 3600; type ClawChatResponse = { final_output?: string; diff --git a/frontend/app/components/assistant-ui/thread-list.tsx b/frontend/app/components/assistant-ui/thread-list.tsx index ed7217d..ee8a601 100644 --- a/frontend/app/components/assistant-ui/thread-list.tsx +++ b/frontend/app/components/assistant-ui/thread-list.tsx @@ -566,8 +566,16 @@ function summarizeRunEvents(runStatus: ClawActiveRunStatus) { lines.push("后端已开始执行本轮任务。"); } if (event.type === "tool_start") { + const stageNote = + typeof event.assistant_content === "string" + ? event.assistant_content.trim() + : ""; lines.push( - event.tool_name ? `调用工具 ${event.tool_name}` : "正在调用工具", + stageNote.startsWith("进度:") || stageNote.startsWith("进度:") + ? stageNote + : event.tool_name + ? `调用工具 ${event.tool_name}` + : "正在调用工具", ); } if (event.type === "tool_result") { @@ -636,7 +644,6 @@ function getReplayMessages( const visibleMessages = isActiveRunStatus(runStatus) ? messages.filter((message) => !isRunStatusMessage(message)) : [...messages]; - if (isActiveRunStatus(runStatus)) return visibleMessages; return trimIncompleteReplayTail(visibleMessages); } diff --git a/frontend/app/components/assistant-ui/thread.tsx b/frontend/app/components/assistant-ui/thread.tsx index 91afd11..f91115e 100644 --- a/frontend/app/components/assistant-ui/thread.tsx +++ b/frontend/app/components/assistant-ui/thread.tsx @@ -1601,6 +1601,17 @@ function formatActivityDuration(ms: number) { function compactActivityLabel(label: string, maxLength = 15) { const normalized = label.replace(/\s+/g, " ").trim(); + const progress = normalized.match( + /^进度[::]\s*批\s*(\d+)\s*\/\s*(\d+)(?:[,,]\s*已生成\s*(\d+)\s*\/\s*(\d+))?/, + ); + if (progress) { + const batch = `批${progress[1]}/${progress[2]}`; + const count = + progress[3] && progress[4] ? ` 已${progress[3]}/${progress[4]}` : ""; + const compactProgress = `${batch}${count}`; + if (compactProgress.length <= maxLength) return compactProgress; + return batch; + } if (normalized.length <= maxLength) return normalized; return `${normalized.slice(0, Math.max(1, maxLength - 1))}…`; } diff --git a/scripts/deploy-ubuntu.sh b/scripts/deploy-ubuntu.sh index 46479c3..24d162d 100755 --- a/scripts/deploy-ubuntu.sh +++ b/scripts/deploy-ubuntu.sh @@ -161,6 +161,7 @@ write_env_file() { export OPENAI_API_KEY=$(printf "%q" "${api_key}") export OPENAI_BASE_URL=$(printf "%q" "${base_url}") export OPENAI_MODEL=$(printf "%q" "${model}") +export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}" export CLAW_DEPLOY_INSTANCE=$(printf "%q" "${DEPLOY_INSTANCE}") export CLAW_BACKEND_HOST=$(printf "%q" "${backend_host}") @@ -184,6 +185,7 @@ ensure_env_file() { echo "CLAW_DEPLOY_INSTANCE=${DEPLOY_INSTANCE}" echo "OPENAI_BASE_URL=${OPENAI_BASE_URL:-}" echo "OPENAI_MODEL=${OPENAI_MODEL:-}" + echo "OPENAI_TIMEOUT_SECONDS=${OPENAI_TIMEOUT_SECONDS:-3600}" echo "OPENAI_API_KEY=已配置" return fi diff --git a/scripts/start-backend.sh b/scripts/start-backend.sh index 9760cc5..c8a4cc1 100755 --- a/scripts/start-backend.sh +++ b/scripts/start-backend.sh @@ -19,6 +19,7 @@ fi export OPENAI_API_KEY export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}" export OPENAI_MODEL="${OPENAI_MODEL:-xiaomi/mimo-v2-flash}" +export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}" # 后端也会调用 Node 生态工具,例如 feishu-mcp-pro。systemd 不读取交互式 # shell 的 nvm 配置,所以需要把部署时记录的 Node 路径显式传给 Python 进程。 @@ -38,6 +39,7 @@ exec "${PYTHON_BIN}" -m src.gui \ --host "${BACKEND_HOST}" \ --port "${BACKEND_PORT}" \ --cwd "${ROOT_DIR}" \ + --timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \ --session-dir "${ROOT_DIR}/.port_sessions/agent" \ --allow-write \ --allow-shell \ diff --git a/scripts/start-webui.sh b/scripts/start-webui.sh index c06bfe5..5ca425e 100755 --- a/scripts/start-webui.sh +++ b/scripts/start-webui.sh @@ -141,6 +141,7 @@ fi export OPENAI_API_KEY="${OPENAI_API_KEY:-}" export OPENAI_BASE_URL="${OPENAI_BASE_URL:-http://model.mify.ai.srv/v1}" export OPENAI_MODEL="${OPENAI_MODEL:-xiaomi/mimo-v2-flash}" +export OPENAI_TIMEOUT_SECONDS="${OPENAI_TIMEOUT_SECONDS:-3600}" BACKEND_LOG="${RUN_DIR}/webui-backend.log" FRONTEND_LOG="${RUN_DIR}/webui-frontend.log" @@ -151,6 +152,7 @@ start_detached_process "backend" "${RUN_DIR}/webui-backend.pid" "${BACKEND_LOG}" --host "${BACKEND_HOST}" \ --port "${BACKEND_PORT}" \ --cwd "${ROOT_DIR}" \ + --timeout-seconds "${OPENAI_TIMEOUT_SECONDS}" \ --session-dir "${RUN_DIR}/agent" \ --allow-write \ --allow-shell \ diff --git a/skills/product-data/README.md b/skills/product-data/README.md index 52ed636..910844a 100644 --- a/skills/product-data/README.md +++ b/skills/product-data/README.md @@ -66,4 +66,10 @@ cat input.json | python skills/product-data/scripts/.py 最后从 `scratchpad/normalized_records.jsonl` 统一校验和导出。 -为避免长时间连续生成导致模型请求超时,单轮最多连续生成 3 批;超过 24 条时先阶段性停顿,用户回复“继续生成”后先统计 `scratchpad/normalized_records.jsonl` 已有行数,再从下一批 append。 +用户确认计划后,生成任务应在同一轮内尽量跑完整个批次链路;不要因为超过 24 条就主动停顿等待继续。每批工具调用前用固定格式输出进度,方便 Web 摘要行展示: + +```text +进度:批 i/n,已生成 x/y,主题 +``` + +如果确实因为超时、取消或工具错误中断,用户回复继续后先统计 `scratchpad/normalized_records.jsonl` 已有行数,再从下一批 append。 diff --git a/skills/product-data/SKILL.md b/skills/product-data/SKILL.md index 3513c6c..35158fa 100644 --- a/skills/product-data/SKILL.md +++ b/skills/product-data/SKILL.md @@ -171,7 +171,7 @@ review 展示必须简短清晰,不要重复解释工具和流程。每次 rev 11. 分批生成 dataset draft text v1,每批最多 8 条,不要用 `write_file` 保存 `draft_part*.txt` 这类中间草稿。 12. 如果是继续一个已确认但中断/超时的生成任务,先检查 `scratchpad/normalized_records.jsonl` 是否已存在;存在时先用 `python_exec` 统计已有记录数,然后从下一批继续追加,禁止覆盖已有记录。 13. 每生成一批,立刻使用 `python_exec` 的 `script_path` 模式执行 `skills/product-data/scripts/normalize_dataset_draft.py`:通过 `stdin` 传入小批量 JSON,必须带 `confirmed_plan_id`、`records_output_path="scratchpad/normalized_records.jsonl"`、`append=true`、`return_records=false`;draft 中每条 case 都必须有 `complex: true/false`。 -14. 单轮最多连续生成并 normalize 3 批;如果计划还没完成,先停止并汇报“已生成 X/Y 条,回复继续生成可从下一批继续”,不要在同一轮里继续生成第 4 批。 +14. 批次数超过 1 时,每批工具调用前必须用一句阶段说明标记进度,格式固定为:`进度:批 i/n,已生成 x/y,主题`。例如:`进度:批 3/7,已生成 16/50,路线偏好追加`。摘要行会优先展示这个格式。 15. 所有批次 normalize 完成后,使用 `python_exec` 执行 `skills/product-data/scripts/validate_dataset_records.py`,传 `records_path="scratchpad/normalized_records.jsonl"`。 16. 如果用户要求落盘 canonical records,使用 `python_exec` 执行 `skills/product-data/scripts/export_dataset_records.py`,逻辑输出固定为 `output/records.jsonl`,默认导出紧凑 JSONL,并同时生成同目录 `records.csv` 表格,不要用 `write_file` 手写 JSON 或 CSV。 17. 如果用户已经明确要训练数据,使用 `python_exec` 执行 `skills/product-data/scripts/export_training_jsonl.py`,优先传 `records_path`,输出固定为 `output/training.jsonl`。 @@ -181,7 +181,8 @@ review 展示必须简短清晰,不要重复解释工具和流程。每次 rev 生成阶段要避免一次性把大量数据塞进工具参数: - 单次 `normalize_dataset_draft.py` 最多处理 8 条 case;计划数量更多时,分批生成、分批 normalize 到同一个 `scratchpad/normalized_records.jsonl`,再统一校验和导出。 -- 单轮最多推进 3 批,超过 24 条的数据要做阶段性停顿,等待用户回复“继续生成”再继续;这是为了避免模型长时间连续生成导致请求超时。 +- 不要因为批次数多就中途停止等待用户继续;用户确认计划后,除非遇到工具错误、模型错误、用户取消或标签/边界歧义,否则在同一轮里把所有批次生成、校验和导出跑完。 +- 每个批次工具调用前的自然语言进度必须使用 `进度:批 i/n,已生成 x/y,主题`,让 Web 摘要行能展示当前进度。 - 继续生成前必须先统计 `scratchpad/normalized_records.jsonl` 的现有行数;如果已有记录,后续 normalize 必须 `append=true`,不要重新从第 1 批覆盖。 - dataset draft 中的 Agent target 推荐写成单引号形式,例如 `target: Agent(tag='地图导航')`。工具会规范化为 `Agent(tag="地图导航")`,这样可以降低 tool call JSON 里双引号转义失败的概率。 - `complex:` 独立写一行,不要写进 `target:`;工具会在导出训练数据和流转表格时自动组合成 `complex=false\nAgent(...)`。 diff --git a/src/gui/__main__.py b/src/gui/__main__.py index 4f3fff8..d32b8b3 100644 --- a/src/gui/__main__.py +++ b/src/gui/__main__.py @@ -38,6 +38,12 @@ def main() -> None: '--api-key', default=os.environ.get('OPENAI_API_KEY', ''), ) + parser.add_argument( + '--timeout-seconds', + type=float, + default=float(os.environ.get('OPENAI_TIMEOUT_SECONDS', '3600')), + help='Timeout for each model API request in seconds.', + ) parser.add_argument( '--session-dir', default=str(DEFAULT_AGENT_SESSION_DIR), @@ -58,6 +64,7 @@ def main() -> None: model=args.model, base_url=args.base_url, api_key=args.api_key, + timeout_seconds=args.timeout_seconds, allow_shell=args.allow_shell, allow_write=args.allow_write, session_directory=Path(args.session_dir), @@ -69,6 +76,7 @@ def main() -> None: print(f' cwd : {state.cwd}') print(f' model : {state.model}') print(f' base-url : {state.base_url}') + print(f' timeout : {args.timeout_seconds:g}s') print(f' sessions : {state.session_directory}') print(f' shell : {"on" if state.allow_shell else "off"}') print(f' write : {"on" if state.allow_write else "off"}') diff --git a/tests/test_feishu_integration.py b/tests/test_feishu_integration.py index 7360c79..b09e3f3 100644 --- a/tests/test_feishu_integration.py +++ b/tests/test_feishu_integration.py @@ -20,6 +20,7 @@ def _build_client(tmp: Path) -> tuple[TestClient, AgentState]: model='test-model', base_url='http://127.0.0.1:8000/v1', api_key='local-token', + timeout_seconds=120.0, allow_shell=False, allow_write=False, session_directory=tmp / 'sessions', diff --git a/tests/test_gui_server.py b/tests/test_gui_server.py index 36131d1..0971a7c 100644 --- a/tests/test_gui_server.py +++ b/tests/test_gui_server.py @@ -37,6 +37,7 @@ def _build_client(tmp: Path) -> tuple[TestClient, AgentState]: model='test-model', base_url='http://127.0.0.1:8000/v1', api_key='local-token', + timeout_seconds=120.0, allow_shell=False, allow_write=False, session_directory=tmp / 'sessions', diff --git a/tests/test_model_list.py b/tests/test_model_list.py index bbcad24..d8876d6 100644 --- a/tests/test_model_list.py +++ b/tests/test_model_list.py @@ -115,6 +115,7 @@ class ModelListTests(unittest.TestCase): model='xiaomi/mimo-v2-flash', base_url='http://model.example/v1', api_key='token', + timeout_seconds=120.0, allow_shell=False, allow_write=False, session_directory=Path(tmp_dir) / 'sessions',