# 实施进度 > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 最后更新:2026-05-28(新增 `local.r1` / `local.qwen3` 内网模型档案,涉密任务用) --- ## 状态 | Phase | 标题 | 状态 | 备注 | |---|---|---|---| | 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 | | 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 | | 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 | | 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 | | 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill | | §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D `/v1` JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;**C Step 1-3 + 3d ✅(Executor + Docker 池 + DockerExecutor + fs 工具进容器)+ Step 5 部署前置对账 ✅ + 容器资源 yaml + 应用层磁盘配额(scan+gate)✅ + dogfood 网络放开 + 容器内 pip/npm 源持久化 ✅**;**Step 4 完整 egress proxy + Step 3b PGID kill 协议延后到外部用户开放前**;**外部用户开放仍需 egress proxy + xfs project quota OS 层硬化(§7.5 落地清单 #2 #4)**。 | --- ## 已完成关键能力 ### 2026-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 / 文件布局变化)。 - **新增 `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 是天生推理模型默认就思考(响应里带 `` 标签),不通过 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,响应带 `` 推理段);`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 档案无架构变化)。 - **修 `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 教学拼 `/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=/`(去重末尾斜杠),无值保持原 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/` / 末尾斜杠拼接不双斜杠 / 未知 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 变化)。 - **新增 `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 的 `--.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 是自主设计不在其列)。 - **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 自表)。 - **新增 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 友好。 - **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 升级触发信号表后新增该段。 ### 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 description 收紧路由**:`skills/ppt/SKILL.md:3` 原文 "做汇报 PPT、把材料/会议纪要/方案转为幻灯片、生成演示稿" 含 "方案" / "生成" 字样,Claude 路由时把 "生成一个方案" 也命中到 PPT skill。改成显式白名单(PPT/幻灯片/演示文稿/.pptx/slide/deck)+ 显式反例("生成方案 / 写报告 / 出文档 / 做纪要" 不触发 —— 那是文档任务)。`DESIGN.md` 不动;`RUN.md` 不动(纯 skill 元数据)。 - **skill 热更新:`/v1/skills` 每次现扫**:此前 `web/app.py` lifespan 启动时 `app.state.skill_registry = SkillRegistry(...)` 扫一次,`GET /v1/skills` 从这份静态快照读 → 加新 skill 目录(放到 `skills//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//.skills/` 与全局 `skills/` 多 root 叠加),跟当前热更新不耦合,等真做时再加多 root 支持。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(无 CLI / env 变化)。 - **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 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 变化)。 - **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 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 ` + 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` 加 `/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 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//` 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 ` 注入 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 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` —— `` 整数 ↔ 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 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/ -e PYTHONIOENCODING=utf-8 setsid bash -c ` / `python