Commit Graph

67 Commits

Author SHA1 Message Date
caoqianming 0c5dd3b176 files(dev SPA): /v1/files/copy + /move 跨目录批量搬动 + 多选 + 目录选择弹框
后端两路由共用 _validate_transfer 预检 helper(批量原子校验:同名 409 不
覆盖、不自嵌套、不重名、target 已存);move 加闸"顶层目录是某 task
working_dir → 409"维持 working_dir = top-level invariant,copy 无此闸
(新副本无 task 关联)。dev SPA 文件行加 checkbox + 顶栏全选三态 + 黄底
toolbar(复制到/移动到/取消),目录选择弹框复用 /v1/files 浏览。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:29:59 +08:00
caoqianming 7925dcef54 files: working_dir 视为可重生 FS 视图(DELETE task 顺手清空孤儿 + delete_file 去 task-ref 闸)
DB 是 source of truth,FS working_dir 可独立删 / 用户手删 / 跨机器迁移丢失,
下次 build_agent 自动 mkdir 重建。三处改:

- core/agent_builder.py: working_dir.mkdir(exist_ok=True) 从 if not resume:
  里挪出,resume 也兜底建目录
- web/app.py DELETE /v1/tasks/{id}: 删完后若同 user 无其他 task 引用 +
  FS 空 + ROOT 内相对路径 → best-effort rmdir 清孤儿;外部 --working-dir
  (DB 绝对串)静默跳过
- web/app.py POST /v1/files/delete: 顶层目录去掉"有 task 引用 → 409"闸,
  允许独立删空目录,task.working_dir 字段不动

smoke case 4 改 200 + working_dir 不变;新增 case 8(空目录自动清)/
case 9(非空保留),全 9 pass。PROGRESS / RUN 跟着更。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:10:36 +08:00
caoqianming 094f4b0cd9 doc(RUN): Ubuntu systemd 部署 unit + 无感更新指引
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:43:18 +08:00
caoqianming 781a216ca6 model: 同 task 内切模型(c 模式 task 级 / A 粒度)+ usage_events v2 表(0006); GET /v1/models; 前端顶栏下拉 + 历史 model 切换点小标
- DB(0006): messages 加 model_profile 列(assistant 行有值); 重建 usage_events 表 v2 形态(event_id/user_id/task_id/message_id/kind/model_profile/units jsonb/cost_usd + 三索引), 0004 删的旧 schema 字段不够多态; tasks.tokens_prompt/completion/cost_usd 保留作粗概览
- ModelCapabilities 加 display_name; deepseek_v4.yaml flash/pro 各填名
- GET /v1/models: 扫 config/models/*.yaml 列可选项(profile/display_name/family/thinking_mode/is_default); POST /v1/tasks + PATCH 接受 model_profile(不传 → cfg["default_model"]; 校验走 ModelCapabilities.load 失败 400)
- build_agent: resume 时优先 task.model_profile 而非 cfg default; AgentLoop 加 user_id 透传, 每轮 assistant 入库后调 record_chat_usage(litellm cost map 算钱, 失败吞掉 emit warn 不阻 loop)
- core/storage/usage.py 新文件: record_chat_usage(双写 messages.tokens_in/out + model_profile + insert usage_events 一行)
- session.append() 返回 message_id(供 usage 关联)
- 前端 dev.html: chat-meta 加模型下拉(切了 PATCH + running 中提示"跑完后生效"); 新建对话框 modal 加 nt-model select; renderMessages 按 model_profile 切换点画小标 "── DeepSeek V4 Pro ──"
- CLAUDE.md: 加"开发测试期 / 不删现有数据 / DROP COLUMN 两种情况"规则
- DESIGN §7.4 schema 加 messages.model_profile + usage_events v2 段; PROGRESS 加 0006 条目 + 文件清单

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:43:13 +08:00
caoqianming 48924d0d56 ui(dev SPA): 对话顶栏按钮改名"导出对话记录" + 语义化上色(完成绿/导出蓝/废弃橙/删除红)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:48:46 +08:00
caoqianming e7f9b005bd doc: 精简 DESIGN/PROGRESS/RUN(总 162KB → 66KB,PROGRESS -80%)
PROGRESS 每条压 2-4 句去 smoke 列表 / 文件清单 / 行数明细;DESIGN 清重复 + 删反方向已废弃的取舍段;RUN 压命令注释 + 修 db current 应输出 0005(原写 0004)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:20:48 +08:00
caoqianming 2baed6894b auth(dev SPA): 邀请码撤回 邮箱+密码 (users.email/password_hash bcrypt; 0005 加 UNIQUE; user add CLI; 登录两 tab)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:58:48 +08:00
caoqianming 53f59eb78a auth(dev SPA): 邀请码登录(invites 表 0005) + SENTINEL user 彻底撤
- 新增 POST /v1/auth/login_invite {token}: dev SPA 给同事试用,token → name → uuid5(NS, name) 推导 user_id;原 /v1/auth/login 保留为 platform 机器对机器入口
- 0005 migration 新表 invites(token PK / name UNIQUE / created_at);先用 ZCBOT_INVITES env 试了一版,讨论后升级到 DB 表 — schema 极薄,不入 user_id (uuid5 推导),不入 revoked_at (DELETE 即撤销);管理直接 SQL,后期可加 main.py invite CLI
- web/auth.py: 删 _parse_invites / AuthConfig.invites / env 读取;新模块函数 resolve_invite(token) 每次 SELECT,无缓存避免 DELETE 后还能登
- SENTINEL_USER_ID 常量 + ensure_local_sentinel 函数 + agent_builder fallback 全删 (CLI 撤后无 caller);storage/utils.py 三函数 user_id 改必填;TaskState 加 user_id 字段;build_agent user_id 改 KEYWORD_ONLY 必填;session.py 删多余 ensure_local_task_row (task 行 web 入口已 INSERT)
- DB 清: SENTINEL 行 + 5 个 dev task + 307 messages + workspace/users/00000000.../ 全删
- dev.html: 登录页 2 格 (uuid+key) → 1 格邀请码,header 显示 name·uuid 前 8 位
- 文档全套同步: RUN/DESIGN/PROGRESS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:14:31 +08:00
caoqianming f61503fbdb ui(dev SPA): 任务/文件行 ⋯ 下拉菜单 + 顶栏长名截断 + 聊天上传按钮 + 工具调用刷新右侧
- 单例浮层菜单 (position: fixed) 避开 pane overflow 裁剪
- 任务行 ⋯:完成/废弃/导出 docx/删除 (4 色, 按 status/消息数 disabled)
- 文件行 ⋯:重命名/下载(仅文件)/删除, 替代原内联按钮
- pane-head .label 加 nowrap+flex-shrink:0;files-proj 长项目名 11 字截断+title 全名
- chat-upload 复用同一 upload-input, 上传到右侧当前目录
- tool_result 触发 scheduleFilesRefresh (debounce 500ms)
- 重构 setTaskStatus/deleteTask/exportTask 接 tid 参数, 中间 pane 按钮共用同组函数

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:50:45 +08:00
caoqianming fafcb14d86 skill(proposal): mermaid 文件名 hash→caption + quality_check 加图相关 4 拦截 + SKILL.md 精简; web cache fix
用户报"图没渲染到 docx",诊断后修三件事(同一根因链):
- web/app.py /v1/files/download 加 Cache-Control: no-cache
  Starlette FileResponse 只发 ETag/Last-Modified, 浏览器走启发式缓存,
  workspace 文件改了 SPA 预览看不到新版
- quality_check 新 check_figures(): 4 条规则
  1) figures/ 有 png 但 sections 0 个 ![]() 引用
  2) fenced 代码块出现 box-drawing 字符 (┌─┐│└─┘ 等)
  3) mermaid 块必须有首行 %% caption: <题>
  4) 同 task 内 mermaid caption 不能撞名
- render_diagrams.py: hash → caption 命名
  pass-1 验证 caption 完整 + 全 task 唯一, 缺/撞 退 2
  pass-2 渲染落 fig_<sanitized>.png, 总是覆盖
- render_docx.py: mermaid 块按 caption 查 fig_<caption>.png
  无 caption / 清洗空 / png 缺 → ASCII fallback
- SKILL.md ~193 → ~160 行:
  插图段 49→22 行(压 matplotlib 细节 + 删类型选择展开)
  反模式合并 ASCII/占位/手写图编号/缺 caption/撞名
  删"为什么两段式"长说理段

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:19:09 +08:00
caoqianming 3ca37f7041 doc(PROGRESS): 05-19 dev SPA 文件预览弹框
加 05-19 条目 + 文件清单更新 dev.html 行数 + 加 web/static/vendor/ 一行。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:25:30 +08:00
caoqianming 15bbadf6d6 ui(dev SPA): 文件点击弹框预览(image/pdf/text/md/docx/xlsx, 其它 fallback)
原行为 click → 走 downloadFile 直接落盘,不能在线看。
现 click → openFilePreview(rel) 打开 #file-preview-modal(90vw × 90vh),
按扩展名分派渲染器:
- image (jpg/png/gif/webp/bmp/svg/ico) → <img> blob URL
- pdf → <iframe> blob URL + application/pdf mime
- text 类 (~30 种 txt/log/json/yaml/code) → <pre> textContent (2MB cap)
- md → 复用 renderMd(marked + DOMPurify + hljs)
- docx → 懒加载 jszip + docx-preview → renderAsync 到 DOM
- xlsx/xls → 懒加载 SheetJS → 多 sheet tab + sheet_to_html
- 其它 (pptx/doc/ppt/...) → fallback "暂不支持在线预览" + 下载按钮

机制:fetch /v1/files/download 取 blob 绕 auth header 限制(后端不动);
懒加载 vendor 脚本(_scriptCache 防重入,失败 fallback);
_trackBlobUrl + _flushBlobUrls 弹框关时统一 revoke 防泄漏;
Esc / 点 backdrop / × 三种关闭路径;
auth 401 → logout;binary 50MB / text 2MB 上限兜底防 OOM。

pptx 整个社区 JS 库都不成熟(动画/复杂版式失真),先 fallback,
真有需求再上服务端 LibreOffice 转 PDF 统一处理。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:25:20 +08:00
caoqianming e3215e023a vendor(office preview): jszip 3.10.1 + docx-preview 0.3.6 + xlsx 0.18.5
dev SPA 文件预览所需的三方 JS 库,入 git 锁版本:
- jszip 3.10.1 (MIT) — docx-preview 依赖
- docx-preview 0.3.6 (Apache-2.0) — docx → DOM 渲染
- xlsx 0.18.5 (Apache-2.0, SheetJS 社区版) — xlsx → HTML table

总共 ~1MB,前端按需懒加载(仅 office 文件首次打开才拉)。
项目无 npm 工具链,直接 vendor 比 fetch 脚本简单。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:25:07 +08:00
caoqianming d6fc004367 skill(proposal): mermaid 管线 + render_docx 图片插入 + 图题自动编号
新增 render_diagrams.py 把 ```mermaid``` 块预渲染到 figures/fig_<sha1>.png
(优先本地 mmdc, 回退 mermaid.ink 公网 API, 都失败留 WARN 不阻塞);
render_docx.py 加 ![](path) 识别 + mermaid 缓存查找, 缺缓存自动 ASCII fallback,
图题"图 N <caption>"全局自增, 替换原模板里的 [图 2-2 ...] 裸占位写法。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:37:16 +08:00
caoqianming 9aa2efc335 core(/v1/files): 加 rename + delete 顶层加 task 引用闸
- POST /v1/files/rename:任意深度;path 是顶层目录则 DB-aware
  (FOR UPDATE 锁 task / 活跃 run 互锁 / check_no_subtask exclude /
  UPDATE working_dir 先于 FS rename,FS 失败回滚)
- POST /v1/files/delete:顶层目录 + 有 task 引用 → 409,杜绝悬空
- check_no_subtask 加 exclude_task_ids,rename 平移自己不误判嵌套
- dev SPA:file row 加改名按钮,顶层改名后刷任务列表 + 当前 task header
- smoke 7 case 全绿(scripts/smoke_files_rename.py)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:06:21 +08:00
caoqianming 49be5e01e4 ui(dev SPA): SSE 回复结束后右侧文件面板自动刷新
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:51:36 +08:00
caoqianming 952a377017 prompt(skill 机制): 永远 load → 可选辅助,通用任务不必硬套
skill 字段在新建任务时已可留空(本轮 /v1/skills 下拉落地),原"永远 load 一下"
对简单问答 / 改 bug / 文件操作等通用任务过激;改为"Skill 是可选辅助",明确通用
任务直接用通用工具,匹配到 skill 领域再 load。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:27:30 +08:00
caoqianming b057c3bd06 core(/v1/skills + dev SPA): GET /v1/skills + 新建任务弹窗 skill 字段改下拉
- web/app.py: lifespan 启动扫一次 SkillRegistry 挂 app.state;新增 GET /v1/skills(JWT 鉴权)
- web/static/dev.html: nt-skill input → select + loadSkillOptions 缓存到 state.skills
- PROGRESS: 记录

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:27:10 +08:00
caoqianming f9311b069c ui(dev SPA): 菜单 / 按钮 / 状态 / 弹窗文案全部中文化
login / header / 三栏 label / chat 按钮 / new task modal 静态文案 +
renderTaskList / renderChatMeta / fetchSse / 弹窗等动态文案全套本地化。
状态码 active/completed/abandoned 显示为「进行中/已完成/已废弃」,
role user/assistant/error → 我/助手/错误。

技术字段(user_id / platform_key / UUID / tok / CSS class / SSE event 名)
保持原状,不影响 UI 中文。Smoke 13 个中文标签全在 + 8 个英文按钮无残留。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:44:50 +08:00
caoqianming 0d127a7261 core(入口归位): cli.py→main.py, 原 main.py→core/agent_builder.py, 删 REPL
按 §5 Less Scaffolding + SoC 把混三角色的 main.py 拆开:入口归位到 main.py,
装配 lib 归位到 core/agent_builder.py。dev SPA 落地后 CLI REPL(chat/tasks/
export)与 web /v1 等价,维护双套 task 切换语义只是"对称美",一并撤(§7 E
CLI 双模式路线同样撤)。

- cli.py → main.py(入口,只剩 web/db/probe 三 click 命令组)
- 原 main.py → core/agent_builder.py(build_agent / system prompt /
  validate_task_name 装配 lib;顺手删死代码 _resolve_uuid_or_prefix +
  resume "last" 分支)
- 删 chat/tasks/export 三 REPL 命令 + _cleanup_if_empty / _list_task_rows
  等 CLI-only helpers ~400 行
- web/app.py 5 处 from main import → from core.agent_builder import
- DESIGN §1/§2/§3.3/§3.6/§7.0/§7.6/§7.7/§7.8/§7.9 + RUN + PROGRESS 全套同步
- Smoke 6 case 全绿(in-process TestClient + 子进程 python main.py db current)
- 净减 486 行

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:10:59 +08:00
caoqianming 2e519ab8a6 core(0004): 删 runs / usage_events 表 + cancel/SSE 改 task-level
usage_events 全代码库零引用,纯死代码;runs 表实质就是"task 当前 in-flight 状态"
影子表,tokens_p/c 写但从未被读,run_id 唯二实用是 broker 频道键 + cancel 参数 —
单活 run 形态下完全冗余,客户端只需 task_id。按"开发期不写兼容层"心智一把切干净。

- alembic 0004:DROP runs / usage_events,tasks 加 run_status (idle/running/cancelling/error) +
  run_error 两列;ok/cancelled 终态都回 idle 不留持久标记,只有 error 持久
- ORM 删 Run / UsageEvent class
- Broker 全 task_id 索引,加 start(tid) 在新 run 前清 _done
- /v1/tasks/{tid}/runs/{rid}/{events,cancel} → /v1/tasks/{tid}/{events,cancel}
- POST /messages 返 {events_url} 去掉 run_id
- dev SPA: state.currentRunId → state.streaming(bool),路径去掉 /runs/{rid}/
- Smoke 18 case 全绿

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:05:35 +08:00
caoqianming bf41631437 core(run cancel): POST /runs/{rid}/cancel + AgentLoop 协作式 cancel + dev SPA stop 按钮
落地 DESIGN §7.2 原标"待"的 cancel 路由 — 等待回复 / LLM 操作时也能中断。

- broker 加 request_cancel / is_cancelled / clear_cancel(per-run threading.Event)
- AgentLoop 加 cancel_check 回调,每轮 LLM 前 + tool_calls 之间 poll;命中给
  未执行 tool_call 补 [cancelled by user] tool result 保 LiteLLM 协议,emit
  cancelled event
- 单活 gate + 启动 reaper 扩到 running | cancelling
- BG 装 cancel_check + 终态判 cancelled/ok + finally clear flag
- dev SPA stop 按钮 + cancelled badge + fetchSse try/finally 失败路径复原 UI

LLM 同步 call 本身不可中断,最坏等当前一轮跑完(通常几十秒)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:42:45 +08:00
caoqianming 976ef45e87 core(POST /v1/tasks/{id}/messages): 同 task 单活 run 锁 + 启动 reaper
挡住"用户连点 send 两条 → 两个 BG 线程争 messages.idx UniqueConstraint
race"的旧 TODO。POST /messages 把所有权 + 活跃 Run 检查 + 新 Run INSERT
收进一个事务,首步 SELECT Task … FOR UPDATE 锁 task 行,命中 running 已
存在则 409。lifespan 加 stale-run reaper,把进程 crash 留下的孤儿 running
标 error,避免对应 task 被 409 永挂。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:27:48 +08:00
caoqianming 9a7620f704 core(files API): user-rooted /v1/files*,去掉 task_id 前置
文件操作语义上只关心"路径 + user 边界",task_id 是多余拐杖;
同时 §7.1 心智模型把 task 和 dir 定义为正交副视图,API 不该混。

- 4 路由 /v1/tasks/{id}/files* → /v1/files*(列/下载/上传/删)
- 边界从 task_dir 改 user_root (workspace/users/<uid>/)
- dotfile 一律过滤(.memory/ 等系统区不暴露)
- dev SPA:登录即拉 user_root,选 task 自动跳到其 working_dir,
  crumbs root 标"我的",新增 upload 按钮

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:59:19 +08:00
caoqianming 0c577ba0a5 core(GET /v1/tasks): 分页 + 多维筛选 + ordering 排序
- 响应壳固定 {page, page_size, count, results}
- 6 个 query 参数:page(1-based) / page_size(1-100 clamp) /
  status / skill / working_dir(末段名,后端拼前缀比对) /
  q(name + description ILIKE)
- ordering DRF 风格逗号分隔,-field 倒序;allowlist
  created_at/updated_at/name/status;非法字段静默丢弃;**默认 -created_at**
- 单次 COUNT + 单次 SELECT LIMIT/OFFSET,无 N+1
- dev SPA:task pane 三段头(status + 刷新 / q + working_dir / ordering),
  prev/next 翻页 + "from–to / count (第 P/L 页)" + 输入 debounce 300ms +
  默认 -created_at 不发到 URL(参数干净)
- DESIGN §7.2 / RUN 路由表 / PROGRESS 同步

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:26:29 +08:00
caoqianming 4a6aaaf34d core(0003): name + working_dir + skill schema 重构 + per-user .memory
- alembic 0003: TRUNCATE tasks CASCADE + task_dir→working_dir + mode→skill + 加 name TEXT NOT NULL
- name(必填,任务显示名,UI / docx 用)与 working_dir(可选,留空 fallback 用 name 作目录)解耦;
  同 working_dir 多 task 共享物理目录(§7.1)
- skill 字段对齐 skills/ 注册表语义,后续可下拉强校验
- POST /v1/tasks {name(req), working_dir?, description?, skill?};
  PATCH 支持改 name/skill;新增 GET /v1/folders(FS 列表 + n_tasks + last_used)
- DELETE /v1/tasks/{id} 硬删 DB(messages CASCADE)+ FS working_dir 保留;
  dev SPA 加 task delete 按钮 + file per-row 删按钮
- 工作目录改 eager mkdir(取代懒创建):用户给 name 即声明项目,目录立刻存在
- dev SPA modal 拆"任务名" + "工作目录"(<datalist> autocomplete 走 /v1/folders +
  输入实时提示"复用 / 新建 / fallback");renderTaskList 主行 = t.name,副行 = 📁 + skill + desc
- files 面板 UX:pane-head 显示项目名 + crumbs root 用项目名 + 修 root 处多渲 "." crumb 的 bug
- 顺手:memory 搬 workspace/users/<uid>/.memory/(per-user dotfile 隔离);
  CLI --mode → --skill,--name + --working-dir 分开
- DESIGN §3.1 / §3.6 / §7.2 / §7.4 + PROGRESS + RUN 全量同步

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:15:37 +08:00
caoqianming 02a69058df core(§7 D + D'): /v1 JSON API + PLATFORM_KEY→JWT auth + dev SPA
整合今日累积的 §7 D 阶段主体工作:

- §7 D /v1 JSON API:删 web/templates/* + web/static/style.css,
  web/app.py 重写为纯 /v1 JSON 路由(tasks CRUD + messages +
  SSE 事件 JSON 化 + files 4 路由 + export),CORS allow_origins
  起步 *,GET / 改 302 → dev SPA(详 DESIGN §7.9)。
- §7 D' 过渡 auth:web/auth.py 新增 — PLATFORM_KEY env(共享密钥)
  + JWT_SECRET env(HS256 签),POST /v1/auth/login 校验 key → 签
  JWT(默 7d TTL),所有 /v1/tasks* 走 Depends(require_user) 验签
  并按 user_id 隔离数据;豁免 /healthz、/docs、/openapi.json、
  /static/*、/v1/auth/login。env 双必填,缺则 fail-fast。
- dev SPA:web/static/dev.html ~600 行 vanilla JS 单文件,login
  overlay(user_id 默 sentinel + platform_key)+ 3 栏布局(task
  list + chat 流 + files 浏览)+ new-task modal + done/abandon/
  export。SSE 走 fetch+ReadableStream(EventSource 不支持 Bearer)。
- task_dir 改相对存储:新增 core/paths.py(to_db_path/from_db_path)
  + alembic 0002 migration 把 ROOT-内绝对路径转 posix 相对,跨 OS
  和混合分隔符历史数据天然兼容。check_no_subtask 改 Python 端归一
  比对,逻辑更清晰。
- litellm 启动 cost map 网络警告兜底:core/llm.py 在 import 前
  setdefault LITELLM_LOCAL_MODEL_COST_MAP=True,墙内冷启动 ~5s →
  <1s。
- docs:DESIGN §7.3 改写(过渡 auth + 真 OIDC 路线)+ §7.7 状态表
  + §7.9 dev SPA 取舍;PROGRESS 加多条今日条目 + 文件清单 + 下一
  步;RUN env 双 auth env + curl 示例 + 路由表 Auth 列 + 5 条故
  障兜底新条目。CLAUDE.md 加"开发期不写兼容层"心智。

Smoke 全绿:env fail-fast / 8 路径无 token 全 401 / login 3 分
支 / 带 token CRUD / 跨 user 4 case 隔离 / token 异常 4 case /
真实 HTTP uvicorn 端到端 login + bearer call + dev.html 服务。

requirements: 加 pyjwt>=2.8.0;删 jinja2 / markdown-it-py /
mdit-py-plugins / pygments(模板路线撤一并清);保留 python-
multipart(files upload 还用)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:14:25 +08:00
caoqianming 1035b12847 core(§7 Phase G G6/new): Web 端新建 task 入口
提前于 G5 落地 — 用户反馈 Web 没"开启新对话"的地方。

- GET /new 渲染 new_task.html 表单(description / mode / task_dir 三字段);
- POST /new:strip 校验 + description 与 task_dir 至少填一个否则 400 +
  check_no_subtask 同 CLI / build_agent 一致拦前缀嵌套 → 409 +
  ensure_local_task_row 写占位行 + 303 See Other 跳转 /tasks/{tid};
- task_dir 空 → 默认派生 workspace/tasks/<uuid>/(同 _default_task_dir),
  显式 → Path.expanduser().resolve() 同 cli.py --task-dir;
- 模板 new_task.html:三字段表单 + error 渲染(400/409 重渲带 form_state
  不丢用户填的值);home.html 加 + new task 主按钮;base.html 默认 nav
  也带 tasks/new 链接;
- CSS:.btn-primary 商务红主按钮 / .new-task-form 表单 + focus / .navlinks
  .active 当前页高亮 / .head-actions flex 容纳 filter + new 按钮;
- 懒创建保留语义:Web /new 入库占位,后续 build_agent 走 resume(已存在
  不冲突);CLI REPL 仍走 build_agent 懒创建路径,两路互不干扰。

Smoke 21 路径全绿:GET 表单 200 + 三字段 / POST happy(description-only
和 custom task_dir)→ 303 + Location 正确 / DB 行字段对 + default-derived
task_dir 含 uuid / 空+空 → 400 重渲表单带 error / no-subtask 父子嵌套 →
409 + 错误文案 / home 页 + new task 按钮 + nav 链接 / /new nav active 标记。

版本 0.4 → 0.5。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:51:35 +08:00
caoqianming 7356d25652 core(§7 Phase G G4): chat 发送 + SSE 流式回复
- web/broker.py RunBroker:in-process pub/sub,subscribe/emit/close/
  unsubscribe;同 run_id 多订阅者 fan-out(刷新 / 多 tab / 桌面+移动
  都能同时看流);_done 集合让晚到订阅者立刻收 done(不挂)。
- web/sinks.py WebEventSink:实现 §7 A sink 协议,把 AgentLoop._emit
  桥到 broker.emit(run_id, ev),AgentLoop 完全不知 web 存在。
- 异步策略 = asyncio.to_thread(不改 core):POST /tasks/{tid}/messages
  async handler → INSERT runs 行 + asyncio.create_task(to_thread(
  _run_agent_bg)),_run_agent_bg 工作线程跑 build_agent + agent.run,
  sink 通过 loop.call_soon_threadsafe 跨线程把 event 桥回 asyncio queue。
- GET /tasks/{tid}/runs/{rid}/events:StreamingResponse async gen,
  响应头 text/event-stream + Cache-Control: no-cache + X-Accel-
  Buffering: no(nginx 反代友好);第一帧 retry/connected 让 ES 立
  即建立,30s 无 event 发 : ping 心跳。SSE multi-line data 每行加
  data: 前缀(SSE spec),客户端 ES 自动还原 \n 拼接的 HTML。
- _render_event_fragment 渲染 text/tool_call/tool_result/error
  HTML 片段;run_start/llm_start/llm_end/done 发空 data(只让客户端
  识别 event type)。
- 新模板:_frag_text/_frag_tool_call/_frag_tool_result/_frag_error +
  _send_response(POST 响应:user msg 卡 + msg-assistant streaming
  容器带 sse-connect/sse-swap/sse-close)。
- chat.html 加 send 表单(Enter 发,Shift+Enter 换行,HTMX hx-post /
  hx-target=#chat-stream / hx-swap=beforeend / 提交后 reset);chat
  section 改 id=chat-stream;非 active task 隐藏表单。
- CSS:.streaming .run-indicator 红点脉冲 / .send-form 输入框 /
  .tool-result-inline 追加式样式 / .msg-error 错误卡。
- runs 表写状态:POST 时 status=running,正常完结 ok + tokens_p/c,
  异常 error + error 文本(DB 写失败不放大噪声,已 emit error 给前端)。
- lifespan bind_loop(asyncio.get_running_loop()) 让 broker 拿到
  loop 引用,emit 跨线程才能 call_soon_threadsafe。
- RUN 故障兜底加 3 条:SSE 经 nginx 卡住、浏览器 send 无反应、并发
  POST messages idx 冲突(已知 TODO)。

Smoke 双层全绿:
- broker 单元 8 case (subscribe/emit/get/fan-out/跨 run 隔离/close/
  late subscribe instant done/unsubscribe/未 bind silent drop)
- 端到端 24 case (POST 200 + sse-connect/run_id 抽取 + content-type/
  x-accel-buffering/cache-control 头对 + event 序列 run_start→done
  + text 片段 <strong> + tool_call <details> + tool_result preview
  + empty body 400 + 各种 404 + late done + runs 行 INSERT)

版本 0.3 → 0.4。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:19:25 +08:00
caoqianming 514d36c481 core(§7 Phase G G3): chat 只读页 + markdown + tool 折叠
- web/app.py 加 _get_md() 单例 MarkdownIt(gfm-like + linkify +
  breaks,html=False 禁内联 HTML 防 XSS),fenced code 走 pygments
  _pygments_highlight 回调(codehilite cssclass)。
- load_chat_messages(tid):PG idx asc 读 messages。
  build_chat_blocks(messages):system / tool 不入 block(tool 内嵌进
  assistant.tool_call.result),user / assistant text 走 md 渲染,
  orphan tool_call → [no result]。_args_preview 60 字截断,
  _pretty_json 解析失败 fallback 原串。
- /tasks/{id} 渲染 chat.html;删 task_placeholder.html。
- chat.html:.msg 卡片(user 浅蓝 / assistant 白底),tool_call 用
  <details> 默认折叠(无 JS,浏览器原生);summary 显示 tool 名 +
  args 前 60 字预览,展开看 args_pretty + result。
- CSS 加 .body 内 markdown 元素样式(table / blockquote / code / pre
  / strikethrough)+ .codehilite 浅色 token 配色(keyword/string/
  comment/function/number/operator,余下黑色)。
- requirements: markdown-it-py[linkify] / mdit-py-plugins / pygments。

Smoke 28 路径全绿(in-process Starlette TestClient):4 display
blocks aggregation + GFM 特性(table/fence/autolink/strikethrough/
bold)+ tool 配对(命中 + orphan [no result])+ HTML 含 <details>/
tool-badge/codehilite/<s> + 空 task 文案 + invalid UUID 404 + util
单测(args_preview/pretty_json/render_md 边界)。版本 0.2 → 0.3。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:38:31 +08:00
caoqianming 80a658eba4 core(§7 Phase G G2): task list 页 + /tasks/{id} 占位
- web/app.py 加 list_tasks(limit, status):PG tasks + messages count,
  updated_at 降序,返回模板友好 dict。Web 与 cli.py 数据形状不一致
  (CLI 用 tuple,Web 用 dict),不预付抽象,等真有 schema 同步成本
  再抽。
- / 路由换成 task 列表,支持 ?status=active|completed|abandoned
  filter(无效值静默降级 all)。/tasks/{task_id} 占位路由:UUID 解析
  失败 → 404,DB 不存在 → 404,有效则渲 task_placeholder.html(G3 来填
  消息流)。
- Linux portability:_norm_path() 显示前 replace('\','/') 把 Win
  存的 backslash 归一,Win Path.resolve()-str → "D:/..." 显示;Linux
  forward-slash 原路通过。Path.as_posix() 在 Linux 读 Win backslash
  串时不归一,所以选 replace 而非 as_posix。
- 模板 home.html 表格(id/updated/status/mode/model/msgs/tokens/desc-dir)
  + status badge 配色(active 绿 / completed 蓝 / abandoned 灰) +
  filter 表单 + 空态文案。task_placeholder.html 渲染 G3 提示。CSS
  tabular-nums 数字对齐 / hover 高亮 / accent-soft note。

Smoke 18 路径全绿(in-process Starlette TestClient):3 task seed
(active/completed/abandoned)+ Win\Linux 双路径形态 → / 渲染对、
status filter 正反向、garbage status 静默 all、UUID 占位、notauuid
404、ghost UUID 404、limit 生效、/healthz 不退化。版本 0.1 → 0.2。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:52:11 +08:00
caoqianming 91202b6172 core(§7 Phase G G1): Web UI 脚手架 + cli.py web 子命令
- web/ 新包:app.py FastAPI 工厂(/ + /healthz + /static),Jinja2
  base.html / home.html,minimal style.css。HTMX + HTMX-SSE 走 CDN
  (无 node 链路,与 §5 Less Scaffolding 一致)。
- cli.py 加 web --host --port --reload 子命令,默认 127.0.0.1:8765,
  本地形态 sentinel user 无 auth(Phase D 才上 OIDC)。
- requirements: fastapi / uvicorn[standard] / jinja2 / python-multipart
  (multipart 为 G5 文件上传留)。
- Starlette 新签名踩坑:TemplateResponse(request, name, context),
  旧式塞 context 里会让 jinja 用 dict 当 cache key 炸 unhashable,记
  RUN.md 故障兜底。
- Linux portability:模板 path 显示约定 .as_posix();SSE 头 G4 上时
  带 X-Accel-Buffering: no(nginx 反代友好)。`cli.py web` 在
  .venv/Scripts/python.exe(Win)/ .venv/bin/python(Linux)走同一路径。

Smoke 四路径(in-process via Starlette TestClient)全绿:/healthz →
"ok" / / → 1063B(title + static + version)/ /static/style.css →
1624B / /nonexistent → 404。`cli.py web --help` 子命令注册 OK。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:37:54 +08:00
caoqianming e8dbfa57a5 core(§7 B Step 6): no-subtask 前缀嵌套校验
- core/storage/utils.py 加 check_no_subtask + NoSubtaskError;PG LIKE
  双向(new LIKE existing/%  OR  existing LIKE new/%),同 task_dir
  允许(同项目多对话),空 / whitespace 跳过。
- 分隔符容差:SQL replace(task_dir, '\', '/') 把存的 Windows 反斜杠
  与新值统一到 '/' 再比;backslash 通过 bind 参数传,绕开 SQL 转义。
- main.py::build_agent 在 resolve_task_id 后、TaskState 构造前调,
  if not resume 单层闸 —— resume 跳过(改名走未来 Folder API cascade).
- cli.py 三处 build_agent 调用现有 try/except 直接接住 NoSubtaskError.
- PROGRESS / RUN 同步:Step 6 完工,故障兜底加一条 NoSubtaskError 处理.

Smoke(9 路径 + e2e 3 分支)全绿。§7 B 完工(Step 5 取消)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:59:37 +08:00
caoqianming 2b3692c8bf core(§7 B Step 4): --task-dir 双形态 + RUN.md 运行手册
- CLI `chat --task-dir <path>` 让用户显式指定项目目录(§7.1 task-primary +
  dir 副视图心智模型落地);留空走默认派生 workspace/tasks/<uuid>/。
- main.py::resolve_task_id 加 task_dir_arg;resume 时从 PG tasks.task_dir
  读,空则降级默认派生。新增 is_managed_task_dir(td, ws) 判断 task_dir
  是否在默认模板下。
- cli.py::_cleanup_if_empty 拿 workspace_dir 作保护开关 —— 用户自指定的
  task_dir 绝不 rmtree(可能含用户已有素材);DB 行该删还是删。
- core/export_docx.py::export_chat_to_docx 重构:task_id 升一等参数(从
  task_dir.name 提取改入参传入),task_dir 留空时自动从 PG 读;cli /export
  与 cli.py export 子命令均改走 _resolve_uuid_or_prefix + task_id 直传。
- 新建 RUN.md(运行手册):env / 初始化 / 日常命令 / 故障兜底 / 关键路径。
- CLAUDE.md 加 RUN.md 维护规则(三文档边界:DESIGN=为什么 / PROGRESS=做到哪
  / RUN=怎么跑),对外行为改动同步更 RUN。

Smoke 4 路径:default-derived(managed=True, cleanup rmtree)/ --task-dir
(managed=False, FS preserved)/ resume reads DB task_dir / export 自动 PG
查路径,全绿。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:38:05 +08:00
caoqianming aeecc7f0f3 core(§7 B Step 3): TaskState ORM + Web UI 设计 (Phase G)
- TaskState dataclass 改 PG-backed:save() → upsert_task (INSERT ON CONFLICT
  DO UPDATE,显式刷 updated_at);load(task_id) → SELECT。state.json 全面
  废除,task_dir 只承担 skill 产物。
- TaskState 字段去 cwd / 加 task_dir(对齐 §7 SaaS task_dir-as-identity);
  cwd 只在 session.meta 内存视图保留(展示用)。
- core/storage/utils.py 新增 upsert_task / update_task;ORM-level UPDATE
  自带 onupdate=func.now(),DO UPDATE 需显式 set。
- session.py Session.append 的 ensure 调用补传 mode/description/
  reasoning_effort,避免首次 INSERT 后 _list_task_rows 看到空 meta。
- sync_task_tokens 改成 update_task 单字段 UPDATE,避免无谓全字段 UPSERT。
- cli.py _list_task_rows 全字段从 PG 读,status 过滤走 SQL WHERE;
  _cleanup_if_empty 去 state.json 特例(任何 FS 文件/子目录都算实质痕迹)。
- core/export_docx.py meta 走 TaskState.load(tid),CWD 字段从 meta 表移除。
- DESIGN.md 追加 Phase G(Web UI 简洁版,FastAPI + Jinja2 + HTMX + SSE),
  排在 §7.7 D 后;§7.9 补 server-render 不上 SPA 的取舍 4 条。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:25:53 +08:00
caoqianming 4f87bf14ee core(§7 B Step 2): Session ORM — messages 走 PG, task_id 切 UUID
Session 重写
- messages 落 PG `messages` 表(append-only, idx 严格递增, jsonb payload)
- system prompt 不入库(每次 build_agent 重建到 messages[0],memory 演化即时生效)
- Session.load(task_id, system_prompt=...) 从 DB 读历史
- Session.task_exists / n_user_msgs 工具

Storage utils
- ensure_local_task_row: 首条消息前 INSERT ... ON CONFLICT DO NOTHING
  打底 tasks 行(Step 3 后由 TaskState.save 接管字段更新)

task_id 切 UUID
- resolve_task_id(workspace, arg, resume): UUID + 前缀匹配,'last' 从 PG
  按 updated_at 取最近
- 显示一律截前 8 位;完整 UUID 在 /id /status 保留
- 旧 workspace 老 task(时间戳格式)**不做兼容**

CLI 适配
- _cleanup_if_empty 双检查:DB messages count + FS 产物
- _list_task_rows: PG tasks ORDER BY updated_at + state.json 兜底字段
- _task_has_messages: /export 检查改 DB
- core/export_docx.py: messages 从 PG 读,state.json 留作 meta

Step 5 (migrate-from-fs) 取消。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:55:50 +08:00
caoqianming 5fbf3746be docs(CLAUDE.md): 加文档维护规则 — 每步更 PROGRESS,必要时改 DESIGN
明确 PROGRESS / DESIGN 分工:工程笔记进 PROGRESS,架构 /
心智模型 / 取舍决策才动 DESIGN。避免把实施细节沉淀成"设计"。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:43:02 +08:00
caoqianming 425ea59937 core(§7 B Step 1): Storage 基建 — SQLAlchemy ORM + alembic + db CLI
- core/storage/{engine,models}.py: 5 表 ORM(users/tasks/messages/runs/
  usage_events)+ session_scope 上下文 + 本地 sentinel user 初始化
- alembic 初版 migration 0001_initial_schema: messages.payload GIN
  索引 + tasks (user_id, task_dir) 复合索引 + pgcrypto 扩展兜底
- cli.py: db upgrade/downgrade/current 子命令组;ZCBOT_DB_URL 未设
  给 ASCII 报错 + exit 2(避开 Windows GBK 控制台编码问题)
- requirements: +sqlalchemy>=2.0 +psycopg[binary]>=3.1 +alembic>=1.13
- DB URL 来自环境变量 ZCBOT_DB_URL,不引导 docker(用户给测试库地址)

已在远端测试 PG 跑通 db upgrade head + db current。Session/TaskState
ORM 接入留 Step 2-3。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:41:44 +08:00
caoqianming 55dc8eb99d design: §7.1 改 task-primary + dir 副视图心智模型
原"Folder-centric"标题误导,实际产品形态是 task list 扁平视图为主、
dir tree 为辅,两视图正交查同一份数据(dir 不是 task 的父容器)。

- §7.1 重写心智模型,加双视图对照表 + Mac Finder 类比
- §7.1 明写 task_dir 留空 vs 指定的产品语义(一次性对话 vs 项目化)
- §7.1 空 dir(只上传素材未开 task)行为
- §7.2 API 顺序调整,tasks 前置,加分组注释
- §7.0/§7.6 task_dir 行同步统一为"留空派生/指定路径"二分

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:15:25 +08:00
caoqianming efe4a91c33 design: 精简 DESIGN/PROGRESS (-177 行)
DESIGN 520→351,PROGRESS 88→80。砍 §7 内部重复说理与 SQL 示例,
合并 §6/§7.8 风险表,压缩 §3 字段表与启动顺序;load-bearing 细节
(rename `old/%` 前缀、ModelCapabilities 字段、阶段估时)全部保留。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:52:23 +08:00
caoqianming b4df60062e design: 更新设计 2026-05-14 08:46:53 +08:00
caoqianming 4560a95fac docs: 加 CLAUDE.md (开发环境笔记)
记录 .venv 位置, 提醒用 .venv/Scripts/python.exe 跑脚本 ——
全局 python 没装 litellm 等依赖, 直接报 ModuleNotFoundError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:14:05 +08:00
caoqianming e4a48fbb53 core: Session/TaskState 原子写 + Phase 6 双层记忆
- core.session.atomic_write_text (tmp + fsync + os.replace) 接管 Session/
  TaskState 落盘, 中途异常不留 0 字节; _cleanup_if_empty 放过 *.tmp 孤儿
- core/memory.py: workspace/memory/{core.md, extended/} 双层记忆.
  core.md 注 system prompt, extended/*.md 索引(标题+绝对路径)注 prompt,
  内容靠 read 工具按需拉
- _build_system_prompt 从 build_agent 里提出来, new 和 resume 都走同一段,
  resume 时覆盖 messages[0] -> memory 演化即时生效
- PROGRESS/DESIGN 同步: §7 platform track 行 + A 阶段完成 + 双层记忆/原子写
  + 文件清单到 2429 行

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:13:56 +08:00
caoqianming 375bb2999c core: loop 事件流化 (sink 接口, §7 A 阶段)
loop 不直接 console.print —— 改成 sink.emit({type, ...}),sink 决定怎么呈现。
新增 ConsoleEventSink 接管 spinner / [in N out N] / assistant 文本 / tool>(args) / 结果预览,
CLI 行为零回归。后续接 SSE 时只换 sink 实现,loop 不动。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:59:32 +08:00
caoqianming f6c3492514 design: 加 §7 Core/Platform 切分草案 (SaaS 化方向)
讨论后定的方案:把 core 包成独立 HTTP/SSE service,平台只做 BFF+UI+Auth+Billing。
数据归属、/v1 接口、Postgres+本地文件存储、Docker 沙盒、6 步代码改造、5 阶段落地路线。
status=design,personal-tool track 不受影响。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:59:23 +08:00
caoqianming 263cdb974a proposal: 阶段二每段卡点附"下一段要点预告"
让用户在下一段动笔前就能改方向, 比读完正文再返工便宜一个量级。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:11:18 +08:00
caoqianming b86c051290 cli+core: 加 task 对话导出为 .docx
- core/export_docx.py: 渲染 messages.json 为对话稿,左排小字 + 角色配色
  (user/assistant/tool/tool_call/reasoning),meta 信息表置文档开头,tool
  结果默认前 1000 + 中间省略 + 后 500
- cli.py: 加 `export <task_id>` 子命令(支持 last / -o / --include-system /
  --no-reasoning / --tool-head / --tool-tail)与 REPL `/export [<id>]`
- 默认跳过 system prompt(信息密度低),默认带 reasoning_content(观察价值高)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:42:45 +08:00
caoqianming ae93016442 cli: REPL /resume 切 task + 懒创建 task_dir + 切走前空清理
- 加 /resume [last|<id>] REPL 命令,无参数列最近 10 个表格让用户挑;
  和 /new 对称,都在 REPL 内重建五元组。tasks 命令复用 _list_task_rows
- main.py 新建分支不再 session.save() / task_state.save() 占位 ——
  推迟到首条 user 消息触发的 Session.append → save() 才物化 task_dir。
  启动 REPL 立刻 /exit 磁盘无痕,跨进程也安全
- _cleanup_if_empty 在 /exit /quit /new /resume + Ctrl-C/EOF 守门:
  无 user 消息 + 目录在磁盘上 + 文件集 ⊆ {messages.json} 才删,
  state.json 存在(/done /abandon /desc 留下的显式痕迹)就保

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:55:56 +08:00
caoqianming 56e414e046 proposal: 阶段二两段式 + render_docx 透传 fenced 代码块
- SKILL.md 阶段二改两段式: 先列 3-6 条要点 → 用户确认 → 再起草 → 用户确认。关键章节 (立项依据/研究方案/技术路线/考核指标) 一段一卡。一次性出全文容易把错方向推到底,要点阶段拦得早
- render_docx.py 支持 ```...``` 围栏: 中文新宋体 + 西文 Consolas + 行距 1.0 + 不缩进 + xml:space=preserve。原先 ASCII 流程图被当散文段落合并,框完全错位
- PROGRESS.md backlog 加 mermaid 预渲染 (mmdc → PNG → add_picture),等 ASCII 透传不够用再做

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:58:18 +08:00
caoqianming f4432cca20 添加col.ps1到gitignore 2026-05-08 08:03:49 +08:00