diff --git a/PROGRESS.md b/PROGRESS.md index b6d40b9..4bc69a5 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。 -最后更新:2026-05-18(`cli.py` 改名 `main.py`(入口);原 `main.py` 挪到 `core/agent_builder.py`(装配 lib);CLI REPL `chat / tasks / export` 删,入口只剩 `web / db / probe`;§7 E CLI 双模式路线撤) +最后更新:2026-05-18(dev SPA 全套 UI 中文化:菜单/按钮/状态提示/role 标签/弹窗文案全部本地化;状态码 `active/completed/abandoned` 显示为「进行中/已完成/已废弃」) --- @@ -21,6 +21,7 @@ ## 已完成关键能力 +- **05-18 / dev SPA 全套 UI 中文化**:用户反馈"web 页面菜单按钮啥的改为中文"。`web/static/dev.html` 静态部分(login overlay / header / 三栏 pane-head label / chat 操作按钮 / new task modal)+ JS 动态部分(状态文案 / role 标签 / confirm/alert 文案 / 状态 badge / SSE 流式提示)全面本地化。**静态文案**:`zcbot dev login → zcbot 登录` / `+ new task → + 新建任务` / `logout → 退出登录` / `tasks/chat/files → 任务/对话/文件` / 状态 select `(all)/active/completed/abandoned → (全部)/进行中/已完成/已废弃` / `export .docx/done/abandon/delete → 导出 docx/完成/废弃/删除` / `stop/send → 停止/发送` / `ready/sending/streaming/cancelling → 就绪/发送中/接收中/停止中` / `(no task selected) → (未选中任务)` / `select a task on the left → 请在左侧选一个任务` / `loading… → 加载中…` / `load failed → 加载失败` / `(no tasks) → (暂无任务)` / `(no messages yet) → (暂无消息 · 在下方输入开始对话)` / `(unnamed) → (未命名)` / `(user root) → (根目录)`。**动态文案**:`renderTaskList` / `renderChatMeta` 加 `statusLabels` map(`active→进行中`等),task list 计数 `msg → 条`;消息卡 role 标签 `user/assistant/error → 我/助手/错误`,`tool · name → 工具调用 · name`,`result (N chars) → 结果(N 字符)`,SSE 流式 `tool_call:/tool_result → 工具调用:/工具结果`;`cancelled` badge `已停止(stopped by user) → 已停止`(更简洁)。**弹窗 / 错文案**:`确认置为 status? → 确认置为「中文 label」?` / `delete failed → 删除失败:` / `download failed → 下载失败:` / `upload failed → 上传失败:` / `export failed → 导出失败:` / 删 task confirm 文案改"任务「项目名」(N 条消息)" / `任务名 必填 → 任务名为必填项`。**modal**:`新建 task → 新建任务` / 各 label "必填"/"可选" 加括号统一 / `留空 fallback 用任务名 → 留空则用任务名` / `N 个 task → N 个任务`。**Smoke**(in-process TestClient 拉 `/static/dev.html`):assert 13 个中文标签全在 + 8 个原英文按钮文案全无残留。**没动**:技术字段(`user_id` / `platform_key` / `UUID` / `tok` token 简称)、CSS class(`badge active` 等仍是英文 class,但显示文本走 statusLabels)、SSE event 名(`text/tool_call/tool_result/done/error/cancelled`)、API 字段名 — 都是 schema 层,不影响 UI 中文。 - **05-18 / 入口归位:`cli.py` 改名 `main.py`、原 `main.py` 挪 `core/agent_builder.py`,删 CLI REPL `chat/tasks/export`,§7 E 双模式路线撤**:接 0004 schema 大瘦身后又一轮架构清理。用户复盘"§7 E `--remote` 是不是可以移除""有 dev SPA 后 CLI REPL 还需要吗""统一到 main.py 是否合理"——一连串问题指向同一个底层:`cli.py`(CLI 入口)+ `main.py`(装配 lib)+ `chat / tasks / export` REPL 子命令是历史多形态共存遗留,在"UI 由 platform 实现 + dev SPA 是开发主路径"的新形态下都是冗余。**架构判断**:`main.py` 此前混三角色(装配 lib + 路径/验证 utility + 被 cli+web 共同 import 的事实入口),按 §5 Less Scaffolding + SoC 应该拆;直接答案是 `cli.py` 改名 `main.py`(入口),原 `main.py` 改 `core/agent_builder.py`(装配 lib),单一职责对齐 Python 社区惯例(入口叫 main.py,lib 在子模块)。**改动**:① `git mv main.py core/agent_builder.py`;② `git mv cli.py main.py`(覆盖);③ 5 处 `web/app.py::from main import xxx` → `from core.agent_builder import xxx`(`build_agent / sync_task_tokens / working_dir_from_name / resolve_workspace / user_root / InvalidTaskName / validate_task_name`);④ 新 `main.py` 自指 `from main import` 改 `from core.agent_builder import`;⑤ 删 `chat / tasks / export` 三个 click 命令 + REPL 内部 helpers(`_cleanup_if_empty / _delete_task_db_row / _task_has_messages / _list_task_rows` 共 ~110 行)+ REPL 主循环(`/exit /reset /new /resume /id /status /done /abandon /desc /export` 共 ~200 行)+ `--name --working-dir --skill --desc --resume --model` CLI 选项 + `tasks` 列表渲染 + `export` 命令 — 共 ~400 行;新 `main.py` ~180 行(`db {upgrade,downgrade,current}` + `probe` + `web` 三命令组);⑥ `core/agent_builder.py` 顺手清:删 `_resolve_uuid_or_prefix` 函数(web 端只传完整 UUID,前缀匹配无 caller)+ `resolve_task_id` 内 `task_id_arg in (None, "", "last")` 分支(web 不传 "last"),resume 直接 `UUID(task_id_arg)`;模块 docstring "本地 CLI user_id = SENTINEL" → "所有入口走 web `/v1` + JWT;dev SPA 默认填 SENTINEL 走同一条路径"。**Smoke 6 case 全绿**(in-process TestClient + 子进程跑 `python main.py db current`):① `/healthz` 200 ② POST /v1/tasks → GET → POST messages(返 `events_url` 无 run_id)→ cancel → DELETE 全链路 ③ `/v1/folders` 走 `core.agent_builder.user_root` 路径 ④ `/v1/files` 走 `_load_user_root` ⑤ `resolve_task_id` 完整 UUID resume(去前缀匹配后用 `UUID(...)` 直接解析;非 UUID 字符串 ValueError;ghost UUID `empty working_dir` ValueError)⑥ `subprocess.run([sys.executable, "main.py", "db", "current"])` 子进程跑通 + stdout 含 `0004 (head)`(验证 click 入口、alembic config 路径、ROOT 解析都没坏)。**文档同步**:DESIGN §1 形态兼容(删 `--remote`,讲"无 CLI / in-process 分叉")/ §2 目录树(`{main.py, cli.py}` → `core/agent_builder.py + main.py`)/ §3.3 `cli.py probe` → `main.py probe` / §3.6 "REPL 内 task 切换"段改"Task 切换 / 软删 走 dev SPA + /v1" + "入口"段讲 `python main.py web` / §7.0 共享差别表入口列改 `python main.py web` + auth 行讲"dev SPA 填 sentinel + 本地 key" / §7.6 #8 标"已撤" / §7.7 E 阶段标"撤" / §7.8 风险表"CLI 双模式分叉"行融合进"过早抽象" / §7.9 新增"CLI REPL 撤,入口统一 main.py"取舍说明 + 删原"CLI 双模式共存"段;RUN 顶 / 一次性初始化 / 日常命令 / 故障兜底 / 关键路径全部 `cli.py` → `main.py`,且日常命令段重写"只剩 `web / db / probe` + 所有 task 交互走 main.py web 后浏览器或 /v1`";PROGRESS 文件清单 / 状态表 / 下一步候选同步(去掉 E 路线)。**净效果**:总代码 -360 行(`cli.py` 558 行删 → `main.py` 180 行 + `core/agent_builder.py` ~320 行 = ~500;原 `main.py` 337 + `cli.py` 558 = 895;净减 -395);入口文件数 2 → 1;维护面 -1 套 task 切换语义(REPL `/new /resume /done /abandon` 全归到 `/v1/tasks*`);测试面 -1 套(原 cli build_agent 调用链 smoke 全归到 web TestClient)。 - **05-18 / 0004 schema 大瘦身:删 runs / usage_events 表,run_status / run_error 合入 tasks;路由从 run_id 维度改 task 维度**:用户复盘"为什么 cancel 接口要带 run_id?现在不是一个 task 一个 run 吗",顺手把 runs / usage_events 表也重新审视 — `usage_events` 全代码库零引用、零写入、零读取,纯死代码(为未来计费预付的架构成本);`runs` 表 `tokens_p/c` 写但从未被读(tokens 累计走 tasks 列),`started_at / finished_at / error` 也只写不读,`run_id` 唯二实用是 broker 频道键 + cancel 参数 — 但 §7.1 已选定**单活 run** 形态,同 task 同时最多 1 个活 run,客户端只需要 task_id(永远有)就够,run_id 完全冗余。按"开发期不写兼容层"心智一把切干净。**alembic 0004**:`DROP TABLE usage_events / runs`,`tasks` 加 `run_status text not null default 'idle'`(idle / running / cancelling / error)+ `run_error text null`。**ORM** `models.py` 删 `Run` / `UsageEvent` 两 class + 删 `BigInteger` import;`Task` 加两列;`storage/__init__.py` 文档示例同步;`Task.run_status` 终态语义:`ok / cancelled` 收尾都回 `idle`(用户视角"跑完 / 停了"等价不留持久标记),只有 `error` 是持久终态,起新 run 时清。**Broker**(`web/broker.py`)全面 task_id 索引:`_subs / _done / _cancel_flags` 三个 dict key 从 run_id 换 task_id;加 `start(task_id)` 入口在新 run 起来前清 `_done` 标记(避免上一轮 done 让新订阅者立刻断流)。**Sink**(`web/sinks.py`)绑 task_id 替代 run_id。**`web/app.py`**:① `_run_agent_bg(task_id, user_id, content)` 去掉 run_id 参数;装 `agent.cancel_check = lambda tid=task_id: broker.is_cancelled(tid)`;终态写 `tasks.run_status = "idle"`(原 `Run.status = "ok"/"cancelled"`)或 `"error"`(`run_error = err`);finally `broker.clear_cancel(tid) + broker.close(tid)`。② `POST /v1/tasks/{tid}/messages` 改:`SELECT Task.run_status … FOR UPDATE` 替代 `select(Run.run_id) … running/cancelling`;同事务 `UPDATE Task SET run_status='running', run_error=NULL`(error 也算可重启视为清);commit 后 `broker.start(tid)` 清 done;返 `{"events_url": "/v1/tasks/{tid}/events"}` 去掉 `run_id`。③ `POST /v1/tasks/{tid}/cancel` 取代 `POST /v1/tasks/{tid}/runs/{rid}/cancel`,只校验 task 归属 user;`run_status != 'running'` → 409。④ `GET /v1/tasks/{tid}/events` 取代 `/runs/{rid}/events`,broker.subscribe(tid)。⑤ lifespan reaper `UPDATE Task SET run_status='error' WHERE run_status IN ('running','cancelling')`,文案不变。⑥ `_task_dict` 暴露 `run_status` / `run_error` 字段给前端。**dev SPA**(`web/static/dev.html`):`state.currentRunId` 改 `state.streaming` bool;`POST /messages` 拿到 `events_url` 直接订阅,不再保存 run_id;cancel 按钮 click → `POST /v1/tasks/{tid}/cancel`(去掉 `/runs/{rid}/`)。**Migration 跑通**:本地 PG `db upgrade 0003 → 0004 (head)` 一把过(用户授权清旧数据,无 backfill)。**Smoke 18 case 全绿**(in-process TestClient + BG mock):POST /messages 返 `events_url` 无 run_id / tasks.run_status='running' / gate when running 409 / POST /cancel 202 + run_status='cancelling' + broker flag set / double cancel 409(状态非 running)/ gate during cancelling 也 409 / cancel idle 409 / cancel error 409 / error 状态可发新消息(error 不挂 gate + 清 run_error) / ghost task 404 / invalid UUID 404 / cross-user 404 / no auth 401 / GET /events 路由注册(SSE 流式跑会挂 30s 心跳,smoke 只验路径 + headers) / GET /tasks 返回 run_status / run_error 字段 / stale reaper 扫 running+cancelling 标 error / broker.start 清 _done / broker.subscribe + emit + close + late subscriber 立刻收 done / broker.request_cancel + is_cancelled + clear_cancel。**净增量**:核心代码 -200 行(删表 ORM + 两路由层简化),broker 加 21 行 start/cancel API,dev.html 几行字段重命名;DB 表 5 → 3,路由 `/runs/{rid}/{events,cancel}` → `/{events,cancel}`,前端 SPA 不再需要先拿 run_id 才能 cancel / 订阅 — 客户端只需 task_id。**文档同步**:DESIGN §7.2 路由表 messages 路由返 `events_url`(去 `run_id`)+ cancel / events 改 task-level + lead-in 注 0004 简化 + SSE schema text event 字段 `delta`(实际就是 delta,文档原 `content` 笔误);§7.4 schema 块 tasks 加两列 + 注 0004 合并;§7.9 hard cascade 行注 "原 usage_events 0004 删" + 加专项取舍说明"0004 删 runs + usage_events 表";§7.7 风险表两行同步 / 改 task-level 路由名;RUN 路由表三路由全改 + 故障兜底 cancel 409 文案改 + db upgrade head 改 0004;PROGRESS 已完成 + 状态表 + 文件清单。 - **05-18 / cancel run endpoint + AgentLoop 协作式 cancel + dev SPA stop 按钮**:用户反馈"等待回复或 LLM 操作时没有停止接口"。落地 DESIGN §7.2 原标"待"的 `POST /v1/tasks/{id}/runs/{rid}/cancel`。**Broker**(`web/broker.py`):加 `request_cancel(rid)` / `is_cancelled(rid)` / `clear_cancel(rid)` 三方法,内部 `dict[UUID, threading.Event]` per-run;`setdefault` 保证 BG 还没 register 也能 set。**Loop**(`core/loop.py`):`AgentLoop` 加 `cancel_check: Optional[Callable[[], bool]]` 字段(CLI 路径不传 = None 永不 cancel),`_is_cancelled()` helper + `_fill_cancelled_tool_results(remaining)` 给未执行的 tool_call 全部 append `[cancelled by user]` tool message —— LiteLLM 协议要求每个 assistant tool_call 必须有匹配 tool result,否则 resume 时 LLM 报错。Check 点:每轮 LLM 前 + tool_calls 之间。命中 emit `cancelled` event + return `[cancelled]`。**LLM 同步 call 本身不可中断**(litellm 同步阻塞,无原生 cancel)—— 接受最坏等当前一轮跑完(通常几十秒),注释里讲清楚。**Endpoint**(`web/app.py::cancel_run`):校验 task 归属 user + run 归属 task(else 404),`run.status` 必须是 `running`(else 409 含具体 status);标 `cancelling`(过渡态)+ `broker.request_cancel(rid)`;202。`_run_agent_bg` 装配时 `agent.cancel_check = lambda rid=run_id: broker.is_cancelled(rid)`,run 完时判 `broker.is_cancelled` 写终态 `cancelled` vs `ok`;finally `broker.clear_cancel + broker.close`。**Gate 同步扩**:`post_message` 单活 run 检查从 `status == 'running'` 改 `status in ('running', 'cancelling')`,确保 cancel 后旧 BG 还没退出时新 POST 仍 409(避免新旧 run 撞 messages.idx)。**Reaper 同步扩**:lifespan 启动也扫 `cancelling`(进程 crash 时 BG 来不及写终态 cancelled,反正没线程在跑就清掉)。**dev SPA**(`web/static/dev.html`):chat 表单加 ``(常态 hidden);state 加 `currentRunId`;sendMessage 拿到 run_id 后 show stop,fetchSse `try/finally` 收尾时一并 hide stop / 清 currentRunId / 复原 send button(确保 SSE 失败路径 UI 也 reset 不卡死)。cancel 按钮 click → `POST /runs/{rid}/cancel`;409 静默忽略(并发 done 不算错)。`handleSseEvent` 加 `cancelled` case → 在当前 assistant 卡贴一个虚线红框 "已停止(stopped by user)" badge。CSS 加 `.cancelled-badge`。**Smoke 15 case 全绿**:HTTP 层 11 case(cancel happy + 双 cancel 409 + cancelling 期间 POST messages 409 + ghost run 404 + invalid UUID 404 + cross-task 404 + cross-user 404 + cancel 已 ok 409 + cancel 已 error 409 + no auth 401 + stale reaper 扫 cancelling);Loop 层 4 case(cancel before first iter 不调 LLM / cancel between tool_calls 补 cancelled placeholder 3 个 + 保协议 + emit cancelled / 正常 done 不 emit cancelled / CLI 路径 cancel_check=None 默永不 cancel)。**没动 SSE handler 的 break list**(`("done", "error")`):cancelled 在 SSE 里走流给前端看,broker.close 之后立即跟 done 收流。**文档同步**:DESIGN §7.2 路由表 cancel 行从"待"扩成完整描述 + SSE 事件加 `cancelled{}` 行 + §7.7 风险表加"Run 跑太久 / 用户想中断"行;RUN 路由表加 cancel 行 + POST /messages 409 文案改 "running / cancelling" + 故障兜底加三行(cancel 409 / 点 stop 没立刻停 / reaper 扫 cancelling);PROGRESS 已完成 + 下一步重排(去掉 cancel,留 OIDC / C Executor / E CLI 双模式)。 diff --git a/web/static/dev.html b/web/static/dev.html index 6bfb139..a33c00b 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -2,7 +2,7 @@
-