From 824f7465713847144cc0ab580f5272e53c17c63e Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 8 Jun 2026 09:52:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(progress):=20=E5=81=9C=E5=8E=8B=20task=5Fpr?= =?UTF-8?q?ogress=20=E5=8F=82=E6=95=B0=E4=BF=AE=E8=BF=9B=E5=BA=A6=E8=BF=98?= =?UTF-8?q?=E5=8E=9F=20+=20=E8=BF=9B=E5=BA=A6=E5=8C=BA=E7=A7=BB=E5=88=B0?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=8C=BA=E9=A1=B6=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题1(进度不对): 上下文压缩把旧 task_progress tool_call 参数换成 {"_compacted":true,"step_id":"sX"} 这种像合法调用的标记, 既毒化模型让它 照抄出残废 update_step(丢 step.status)入库, 又让前端 applyProgressAction 读不到 args.step → 步骤永停 pending。修复: task_progress 参数一律不压缩。 问题2(没像 codex 在顶部): 删掉每条消息卡内联进度块, 进度统一只在对话区 顶部单一 dock 实时显示(钉顶不滚); 全部完成时折叠成一行摘要。prompt/tool 描述改为跑完标 completed 而非 clear, 留住全绿收尾。 校验: unittest test_context_compaction/test_task_progress_tool 12 过; node --test frontend_task_progress 2 过。 Co-Authored-By: Claude Opus 4.8 (1M context) --- PROGRESS.md | 3 +- core/context.py | 32 ++++----------------- prompts/system/general_v1.md | 1 + tests/test_context_compaction.py | 16 +++++------ tools/task_progress.py | 5 ++-- web/static/dev.html | 16 ++++++++--- web/static/js/chat.js | 49 +++++++++++++------------------- 7 files changed, 50 insertions(+), 72 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 7c0eb71..18581a0 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-06-08(task_progress 进度工具 + 前端固定进度区 + 静态资源 no-cache) +最后更新:2026-06-08(进度还原修复:停压 task_progress 参数 + 进度区移到对话区顶部 + 完成态折叠) --- @@ -23,6 +23,7 @@ ### 2026-06-08 +- **修进度还原错乱 + 进度区移到对话区顶部(codex 式)**:根因(查 DB 实锤)= 上下文压缩把旧 `task_progress` tool_call 参数换成 `{"_compacted":true,"step_id":"sX"}` 这种"看着像合法调用"的标记,① 毒化模型让它后续照抄出残废 `update_step`(丢了 `step.status`)并入库,② 残废格式前端 `applyProgressAction` 读不到 `args.step` → s4/s5 永停 pending → 进度显示不对。修复:`context.py` 对 `task_progress` 参数**一律不压缩**(参数本就小,压缩省不了几个 token 却两头坏事);旧的 `_compact_task_progress_arguments` 整个删除。**进度展示重构**:删掉每条消息卡内联进度块(`renderProgressHtml`/`renderProgressInto` 移除),进度统一只在**对话区顶部**单一 `#task-progress-dock`(从 composer 上方移到 `chat-stream` 之上、`flex-shrink:0` 钉顶不滚)实时显示;**完成态折叠**——全部步骤 completed 时 dock 自动收成一行 `✓ 全部完成 · N/N 步`(`
` 点开看清单)。prompt + tool 描述改为"跑完把最后一步标 `completed`、不要 `clear`",留住全绿收尾。校验:`python -m unittest tests.test_context_compaction tests.test_task_progress_tool`(12 过,改写 `test_keeps_old_task_progress_arguments_intact` 断言参数原样保留);`node --test tests/frontend_task_progress.test.mjs`(2 过)。 - **修登录无反应(`$ is not defined`)+ 补 favicon 消 404**:`newtask.js` 用了 DOM 简写 `$`(`dom.js` 导出的 `getElementById`)却漏 import,模块加载到顶层 `$("hd-new").onclick` 即抛 `ReferenceError: $ is not defined`,中断 newtask 全部绑定及其 import 的 auth/chat 链路 → 点登录无反应。补 `import { $ } from "./dom.js"` 与其余模块对齐。另在 `dev.html` `` 加内联 SVG data-URI ``(蓝底白机器人),浏览器不再请求根 `/favicon.ico`,消掉 404;选内联 SVG 而非新增 `.ico` 文件 / 服务端路由,零新增文件零 app.py 改动。 - **新增 Codex 式 `task_progress` 进度工具 + Web 固定进度区**:`TaskProgressTool` 默认注册到 agent,支持 `set_plan/update_step/clear`,返回极短 UI-only 结果;上下文压缩对旧 `task_progress` tool_call/result 做专门折叠,避免进度历史长期占 prompt。前端新增 `progress.js` 做 task 级进度状态合并,修复 `update_step` 只带 `{id,status}` 时因缺标题不显示的问题;当前进度显示从助手消息内提升到 `#task-progress-dock`(对话流下方、输入框上方),历史消息内仍保留进度块作记录。system prompt + coding/ppt/proposal/analyze skill 加轻量使用约定,要求只在多步骤关键阶段少量更新。**部署侧补静态资源 no-cache**:`NoCacheStaticFiles` 替换默认 `StaticFiles`,让浏览器重新校验 `/static/*.js` 等资源,避免前端修复已部署但旧 `chat.js` 仍被缓存导致看不到进度区。校验:`pytest tests/test_context_compaction.py tests/test_task_progress_tool.py tests/test_executor_docker.py tests/test_static_vendor.py -v` 相关集通过;`node --test tests/frontend_task_progress.test.mjs` 2 过;`node --check web/static/js/chat.js web/static/js/progress.js` 过。 diff --git a/core/context.py b/core/context.py index 44fc20b..ae811ae 100644 --- a/core/context.py +++ b/core/context.py @@ -45,35 +45,13 @@ def _message_chars(msg: dict[str, Any]) -> int: return len(str(msg)) -def _compact_task_progress_arguments(raw: Any) -> tuple[Any, bool]: - if not isinstance(raw, str): - return raw, False - try: - parsed = json.loads(raw) - except Exception: - parsed = {} - if not isinstance(parsed, dict): - parsed = {} - marker: dict[str, Any] = { - "_compacted": True, - "tool": "task_progress", - "note": "old UI progress details omitted from context", - } - action = parsed.get("action") - if isinstance(action, str) and action: - marker["action"] = action - steps = parsed.get("steps") - if isinstance(steps, list): - marker["step_count"] = len(steps) - step = parsed.get("step") - if isinstance(step, dict) and isinstance(step.get("id"), str): - marker["step_id"] = step["id"] - return json.dumps(marker, ensure_ascii=False), True - - def _compact_tool_call_arguments(raw: Any, max_chars: int, tool_name: str = "") -> tuple[Any, bool]: + # task_progress 参数本就很小(3-7 个短步骤),压缩省的 token 微乎其微,但把它换成 + # `{"_compacted":true,"step_id":...}` 这种"看起来像合法调用"的标记会:① 毒化模型, + # 让它照葫芦画瓢生成残废的 update_step(丢了 step.status)入库;② 残废格式前端 + # applyProgressAction 读不到 args.step → 进度还原错乱。故 task_progress 一律不压缩参数。 if tool_name == "task_progress": - return _compact_task_progress_arguments(raw) + return raw, False if not isinstance(raw, str) or len(raw) <= max_chars: return raw, False marker: dict[str, Any] = { diff --git a/prompts/system/general_v1.md b/prompts/system/general_v1.md index daa50fa..a4a15a9 100644 --- a/prompts/system/general_v1.md +++ b/prompts/system/general_v1.md @@ -12,6 +12,7 @@ - 多步骤任务开始后,用 `task_progress(action="set_plan", steps=[...])` 发布一份简短计划。 - 步骤标题面向用户,写“理解需求 / 实现功能 / 运行测试 / 输出结果”这类阶段,不要写每个文件读写或每次 shell 命令。 - 只在关键状态变化时用 `update_step`:当前步骤进入 `in_progress`,完成后改 `completed`。不要高频更新,不要把普通 tool 调用都转成进度步骤。 +- 任务全部做完时,把最后一步标成 `completed`(让用户在顶部进度面板看到"全绿"收尾),**不要用 `clear`**;`clear` 只在计划被推翻、不再相关时才用。 - 简单问答、单次文件读取、很小的改动不需要调用 `task_progress`。 ## 媒体生成工具(按需可用,未配置 ARK_API_KEY 时该工具不会出现) diff --git a/tests/test_context_compaction.py b/tests/test_context_compaction.py index 68164d2..b68fe95 100644 --- a/tests/test_context_compaction.py +++ b/tests/test_context_compaction.py @@ -150,7 +150,9 @@ class ContextCompactionTests(unittest.TestCase): self.assertNotIn("A" * 100, tc["function"]["arguments"]) self.assertEqual(stats["compacted_tool_call_arguments"], 1) - def test_compacts_old_task_progress_arguments_to_plan_marker(self) -> None: + def test_keeps_old_task_progress_arguments_intact(self) -> None: + # task_progress 参数本就很小,且压成 `{"_compacted":...,"step_id":...}` 这种"像合法调用" + # 的标记会毒化模型 + 毁掉前端进度还原。故旧的 task_progress 调用参数必须原样保留。 args = json.dumps({ "action": "set_plan", "steps": [ @@ -173,13 +175,11 @@ class ContextCompactionTests(unittest.TestCase): prepared, stats = prepare_messages_with_stats(messages) - compacted_args = json.loads(prepared[1]["tool_calls"][0]["function"]["arguments"]) - self.assertTrue(compacted_args["_compacted"]) - self.assertEqual(compacted_args["tool"], "task_progress") - self.assertEqual(compacted_args["action"], "set_plan") - self.assertEqual(compacted_args["step_count"], 2) - self.assertNotIn("理解需求", prepared[1]["tool_calls"][0]["function"]["arguments"]) - self.assertEqual(stats["compacted_tool_call_arguments"], 1) + kept_args = json.loads(prepared[1]["tool_calls"][0]["function"]["arguments"]) + self.assertNotIn("_compacted", kept_args) + self.assertEqual(kept_args["action"], "set_plan") + self.assertEqual(kept_args["steps"][0]["title"], "理解需求") + self.assertEqual(stats["compacted_tool_call_arguments"], 0) def test_old_task_progress_tool_result_uses_tiny_marker(self) -> None: messages = [ diff --git a/tools/task_progress.py b/tools/task_progress.py index 09894a6..0c90c75 100644 --- a/tools/task_progress.py +++ b/tools/task_progress.py @@ -17,8 +17,9 @@ class TaskProgressTool(Tool): description = ( "Publish or update a concise user-visible progress checklist for the current task. " "Use only for meaningful multi-step work: set the plan once, update a step when it " - "starts or completes, and clear it only when it is no longer relevant. This is a UI " - "progress signal, not a work product." + "starts or completes, and when all work is done mark the final step completed (do NOT " + "clear). Use clear only when the plan is no longer relevant. This is a UI progress " + "signal, not a work product." ) parameters = { "type": "object", diff --git a/web/static/dev.html b/web/static/dev.html index 01367bc..1b8a419 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -445,9 +445,17 @@ border: 1px solid var(--border-soft); border-radius: var(--r-md); background: #fafafa; font-size: 12px; } - .task-progress .tp-title { - margin-bottom: 5px; color: var(--muted); font-family: var(--mono); font-size: 11px; + .task-progress > .tp-summary { + cursor: pointer; list-style: none; color: var(--muted); + font-family: var(--mono); font-size: 11px; user-select: none; } + .task-progress > .tp-summary::-webkit-details-marker { display: none; } + .task-progress > .tp-summary::before { + content: "▸"; display: inline-block; margin-right: 5px; transition: transform .12s; + } + .task-progress[open] > .tp-summary::before { transform: rotate(90deg); } + .task-progress > .tp-summary.tp-done { color: var(--c-green); } + .task-progress[open] > .tp-summary { margin-bottom: 5px; } .task-progress .tp-list { display: grid; gap: 4px; } .task-progress .tp-step { display: grid; grid-template-columns: 18px minmax(0, 1fr); @@ -469,7 +477,7 @@ .task-progress .tp-step.completed .tp-text { color: var(--muted); text-decoration: line-through; } #task-progress-dock { flex-shrink: 0; display: none; - padding: 8px 12px; border-top: 1px solid var(--border); + padding: 8px 12px; border-bottom: 1px solid var(--border); background: #fff; } #task-progress-dock.show { display: block; } @@ -1040,8 +1048,8 @@
(未选中任务)
-
请在左侧选一个任务
+
请在左侧选一个任务