# 实施进度 > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 最后更新:2026-06-01(加一键部署脚本 deploy/update.sh:pull/pip/migration/build/restart/healthz,build 必先于 restart) --- ## 状态 | Phase | 标题 | 状态 | 备注 | |---|---|---|---| | 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 | | 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 | | 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 | | 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 | | 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill | | §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D `/v1` JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;**C Step 1-3 + 3d ✅(Executor + Docker 池 + DockerExecutor + fs 工具进容器)+ Step 5 部署前置对账 ✅ + 容器资源 yaml + 应用层磁盘配额(scan+gate)✅ + dogfood 网络放开 + 容器内 pip/npm 源持久化 ✅**;**Step 4 完整 egress proxy + Step 3b PGID kill 协议延后到外部用户开放前**;**外部用户开放仍需 egress proxy + xfs project quota OS 层硬化(§7.5 落地清单 #2 #4)**。 | --- ## 已完成关键能力 ### 2026-06-01 - **修 MP host 工具的全量下载(IP 被封根因)**:`mp_search_summary` 之前不给 `summary.search` 传分页参数 → mp-api 默认 `chunk_size=1000` 且 `list(docs)` 自动翻完所有页,`limit` 只在客户端切片,等于每次搜索都整库级下载 → MP 判 abusive traffic 封 host IP/ASN(403 "blocked")。改为 `search(num_chunks=1, chunk_size=limit, ...)`,服务端单页限量。`mp_get_entries` 的 `limit` 同样是"只裁剪保存、不减网络流量"的假参数,但 `get_entries_in_chemsys` 天然全量(相图用途),改不了,只在 description 里点明"拉整个 chemsys、元素越多越重、别反复调"。测试加断言锁定 `num_chunks/chunk_size` 已传。**注:宿主 IP `49.232.14.174` 当前仍被 MP 临时封(无公开时限、不确认自动解除),需发邮件 support@materialsproject.org 人工解封后才能联网复测。** - **加一键部署脚本 `deploy/update.sh`(Ubuntu / systemd)**:把日常部署固化成一把梭 —— `git pull --ff-only` → `pip install -r` → `db upgrade head` → `docker build` sandbox 镜像 → `systemctl restart zcbot` → `curl /healthz` 验活。**两处必须钉死的顺序 / 步骤**:① migration 不能漏(`db/migrations/env.py` 直读 `os.environ['ZCBOT_DB_URL']` 不读 .env,脚本从 .env 抠出来 `env ZCBOT_DB_URL=...` 喂进去);② **build 必须在 restart 之前** —— sandbox 容器 per-user 长驻复用、`tools/` 是 build 进镜像(非 mount),restart 时 `pool.shutdown_all` 清旧容器、下次 `ensure()` 才用新 `zcbot-sandbox:latest` 重建,顺序反了新 tools/ 要等下次重启才生效。**sandbox 每次都 build 无所谓**:重活(pip ~1G / chromium / 字体 / mermaid)都在 Dockerfile `COPY tools/` 之上,layer cache 让改代码部署秒过、只有 `requirements.txt` 变了才整体重建(~5-10min)。镜像源默认阿里(`${VAR-default}` 不带冒号,显式置空可回落官方源)。前置守卫:非 root / 非 git 仓库 / 工作区脏(已跟踪文件)/ 缺 .env 中止;healthz 15s 不 ok → dump journalctl 非零退出。`ZCBOT_SKIP_SANDBOX_BUILD=1` 跳过 build(host backend 机)。一次性 bootstrap(useradd / 写 unit / enable)不进脚本,留 RUN.md。git 可执行位已置(100755)。**纯运维脚本,DESIGN 不动**;`RUN.md` §部署 SOP 重写为指向脚本 + 手动逐条 fallback。 - **sandbox 镜像加中文字体,修 matplotlib / mermaid 出图中文方块**:用户报绘图(mermaid + matplotlib)出的 PNG 里中文全是豆腐块 □。**根因 = `deploy/sandbox/Dockerfile` 从 `python:3.12-slim` 起一个 CJK 字体都没装**:matplotlib `skills/plot_pub/style.py::_find_chinese_font()` 扫候选无果退回 Arial/DejaVu;mermaid 经 mmdc→chromium 渲染,chromium 经 fontconfig 也找不到中文字形;`skills/ppt/scripts/render_icon.py` 引用的 `wqy-microhei.ttc` / `NotoSansCJK-Regular.ttc` 路径根本不存在。三处同一病根。**改法**:① Dockerfile chromium 块后加一层 `apt-get install fonts-noto-cjk fonts-wqy-microhei fontconfig && fc-cache -f`(Noto 出版级 +~330MB,wqy 兜底 +~5MB 且匹配 style.py 现有候选 / render_icon 引用路径);② `style.py` 候选清单首位加 `"Noto Sans CJK SC"` 让 matplotlib 优先用 Noto。fontconfig 刷缓存供 chromium 选字;matplotlib 走自家 font_manager 扫 `/usr/share/fonts` 运行时首用自动建缓存,无需额外处理。**否决**仅装 wqy(体积小但黑体不如 Noto 精致,出版图略糙)/ 仅装 Noto(render_icon 第一候选 wqy.ttc 落空走 fallback OK 但不够干净)。**纯镜像 + 配置改,DESIGN 不动**(无架构 / 取舍 / schema 变化);`RUN.md` 故障表加一行(中文方块 → 重 build 镜像 + 清旧容器 + `fc-list :lang=zh` 验证)。**生效**:改了 Dockerfile 必须 `docker build` 重建 + `docker rm -f` 清旧容器 + restart web,旧容器仍跑老镜像不会自动更新。 - **documents / Materials Project secret-bearing 能力改 host-side tools,key 不进 sandbox**:新增 `tools/documents.py` 三工具(`document_list_kb` / `document_search` / `document_download`)和 `tools/materials_project.py` 三工具(`mp_search_summary` / `mp_get_structure` / `mp_get_entries`),`core/agent_builder.py` 仅在宿主 env `DOCUMENT_SEARCH_API_KEY` / `MP_API_KEY` 存在时注册。`document_download` / `mp_get_structure` / `mp_get_entries` 绑定当前 task_dir 写文件,模型不能传 working_dir;`document_search` 默认截断 `md_content`,避免整篇论文进上下文。同步更新 `DESIGN.md` secret-bearing domain tools 规则、`RUN.md` env / 故障兜底、`SKILL_LIST.md`、`skills/documents/SKILL.md`、`skills/pymatgen/SKILL.md`;旧 `run_python` helper 不再是带 key API 主路径。测试 `tests/test_secret_host_tools.py` 覆盖 documents search 截断、download 固定 task_dir、MP tool 不泄露 host key。 - **删 `skills/pymatgen/materials.py::mp_rester()` + `scripts/smoke_scientific_skills.py` 改走 host tool**:`mp_rester` 是 sandbox 内读 `MP_API_KEY` 的旧入口,host tool 化后多余且违背"key 不进 sandbox",直接删(连带清 `import os` / `contextlib.contextmanager`,只留 `CEMENT_PHASES` / `lookup_phase`);smoke A6 / step D 改用 `MaterialsProjectSearchSummaryTool`。**实测闭环**:初次 step D 真连 `api.materialsproject.org` 返 **403**(工具行为正确,403 干净透传成 `[Error]` 不崩),定位为 `.env` legacy 旧版 key 在新版 `mp-api` 失效;换 next-gen materialsproject.org dashboard 长 key 后**复测通过**(查 Ca3SiO5 返 3 条 `mp-xxxx` + `energy_above_hull`,~5s),MP host 工具端到端联网通路确认可用。documents 工具未联网实测(无现成可验证调用),逻辑同 web_search 形态。 ### 2026-05-29 - **Seedream 5.0 i2i base64 通路 probe + DESIGN §8.1 后续步骤落册**:用户场景"调 seedream 出图 → 基于该图二次修改" / "上传外部参考图让 agent 据此干活"两条路径,主模型 DeepSeek V4 纯文本覆盖不了。详评 3 方案后选 **E + C 组合**(`tools/seedream.py` 加 `reference_images` 参数走 seedream 5.0 i2i + 新增 `tools/look_at_image.py` 走豆包 Seed 1.6 vision tool 调度),否决 A(换豆包当主 chat,降 code / tool calling 质量 + 改 loop/memory 工程面 5×)/ B(后台隐式 vision 路由,失 agentic 控制 + 描述质量黑盒 + token 浪费)。**写探针 `scripts/probe_seedream_i2i.py` 实测**:豆包 Seedream 5.0(`doubao-seedream-5-0-260128`)`/images/generations` endpoint **接受 `image_urls=["data:image/png;base64,..."]`**,200 返回新图 TOS URL + `usage.generated_images=1`(约束:输出 `size` ≥3686400 像素 / ~1920²,单张参考 ≤10MB,最多 14 张);base64 通路成立 → **内网部署无需对象存储中介**,排除最大工程不确定性。**E+C 实施清单 / 风险 / 升级到 A 的信号已落 DESIGN §8.1,本版仅 probe + design,tool 与 prompt 改造未启动**。 - **web 端 tool_call 标题行改显中文活动描述(`dev.html`)**:用户反馈 web 端工具调用只显示 `工具调用:run_python` / `shell` 等工具名,看不出"在干啥"。**根因有个真 bug**:`dev.html` 实时流分支(`tool_call`)读 `ev.data.arguments`,但后端 `core/loop.py::_execute_tool_call` emit 的字段叫 `args`(已解析 dict)+ `args_preview`(截 200 字),字段名对不上 → 前端拿到的永远是空串,`
` 是空的、连带 `extractArtifactRels(argsStr,...)` 也抽不到产物路径。**改法**:① 新增 `toolActivityLabel(name, args)` helper(挨着 `_workingDirName`),按 12 个工具的"关键代表参数"套中文动词 + 截断值:read/write/edit→`读取/写入/编辑文件: {path}`、glob→`查找文件: {pattern}`、grep→`搜索内容: {pattern}`、shell→`执行命令: {command}`、run_python→`运行 Python: {code}`、web_fetch→`抓取网页: {url}`、web_search→`联网搜索: {query}`、load_skill→`加载技能: {name}`、seedream/seedance→`生成图像/视频: {prompt}`,未知工具回退到 `name {JSON截断}` 或 `工具调用: name`;clip 把空白压一行 + 超长加 `…`。② 实时流分支 `ev.data.arguments` → `ev.data.args`(修字段 bug)并把 `` 文案换成 label。③ 历史消息回放分支(`p.tool_calls`,LiteLLM 格式 `tc.function.arguments` 是 JSON 字符串)同步:先 `JSON.parse` 成 `argsObj` 再生成 label,保持实时 / 历史一致。完整参数仍在折叠 `
` 里,展开可看。**选型**:用前端静态模板(零成本 / 不走模型 / 立即生效),否决"让模型每次调用前产出一句中文意图"(改 prompt + 每调用烧 token + 依赖模型配合)。**纯前端改**,`DESIGN.md` / `RUN.md` / `SKILL_LIST.md` 不动(无架构 / CLI / env / skill 变化)。**生效**:刷新 web 页面即可,无需重启后端。
- **删 `_exec_shell` / `_exec_python` argv 里的 `setsid` 修 docker exec 延迟 stdout 丢失**:上一条 `_run_subprocess` 重写后用户实测 LLM 拿 `cement energy efficiency` 跑 paper_server 检索仍返空 `[exit 0]` 8 字符,且 `print(f"共命中 {len(papers)} 条结果\n")` 这种必有输出的代码也丢。**根因 = `setsid`,不是 `_run_subprocess` 的 poll loop**(上一条修对了一个独立的 bug,但不是用户当下症状的元凶)。**实证差异**:`docker exec ... setsid python -c "import time; time.sleep(2); print('hello')"` 等满 2.06s 但输出空;同条件去掉 setsid `docker exec ... python -c "..."` 等满 2.08s + 输出 `hello`。`setsid` 调 `setsid()` syscall 把进程变 new session leader without controlling terminal **之后** execvp(python),docker exec / runc 的 stdio attach 对"调用方变 session leader"敏感(具体哪一层没深挖到 runc 源码,经验上 docker exec + setsid + 延迟输出 = stdout 数据被截断,业界踩过的坑)。短输出(`print('hello')` 瞬时完成)能在窗口内漏出去,延迟输出(`search()` 等 httpx 1-2s)就全丢 —— 完美解释为什么 LLM 简单 `print(version)` 测试时部分输出能回但真业务调用全空。**为什么之前 host 侧 `docker exec ... python -c "from skills.research.paper import search; ..."` 2.94s 拿 10 条成功**:那条**没有** setsid,docker exec 等的就是 python 本身,3s 全程都阻塞读 stdout 不会丢。`setsid` 历史上是 §7.5 Stage C **Step 3b PGID kill 协议**铺路用的(PROGRESS 上有"延后到外部用户开放前"这条 work item),**协议没实现的当下 setsid 是空头载荷 + 副作用**。**改法**:`core/executor_docker.py:141` 和 `:177` 各删 1 行的 `"setsid"`,argv 形态变 `docker exec ...  bash -c ` / `... python