89 KiB
设计文档
本地运行的个人任务 agent,覆盖三类工作:汇报 PPT、科研申报书、代码。 模型自由(LiteLLM 接 OpenAI-compatible),代码可控。本文只记架构与取舍(为什么);进度/历史见 PROGRESS,怎么跑见 RUN。
1. 边界
做:PPT / 申报书 / 编码(读写文件 + shell + 迭代验证)。
不做:子 agent(编排型;上下文隔离的最小子循环另见 §8.11)/ 自定义 RAG / 锁定 Anthropic。多用户 / Web 归 §7。Eval 不进 core,走独立 evaluation/ 黑盒旁路(§3.9)。
关键约束:模型自由(LiteLLM,默认 DeepSeek V4);任务持久化(任意时刻关机可恢复);演化性(模型升级不大改架构);形态兼容——本地与 SaaS 共享同一份 core / PG / web /v1 API,无 CLI REPL 分叉(§7.9)。
2. 架构
zcbot/
├── core/
│ ├── capabilities.py # ModelCapabilities,从 yaml 加载
│ ├── llm.py # LiteLLM 封装,按 capabilities 自动启 features
│ ├── loop.py # ReAct 主循环 + 协作式 cancel(控制流)
│ ├── llm_transport.py # wire 层健壮性:畸形/吐空检测+留痕+非流式降级重试(自 loop 析出)
│ ├── tool_registry.py # 声明式工具注册表((组名,gate,factory);§7.5 #7 代码强制)
│ ├── probe.py # 真实探测对账 yaml 声称的能力
│ ├── session.py # 消息列表 + meta,落 PG
│ ├── skills.py # SkillRegistry(渐进披露,多来源)
│ ├── task.py # TaskState
│ ├── memory.py # per-user .memory/ 双层记忆
│ ├── file_store.py # 原子文件替换 + 跨进程 advisory file lock
│ ├── kb_lock.py # .kb/<库> mutation 单写者锁(Web/入库/fs tool 共用)
│ ├── shortcuts.py # 快捷指令(入口层确定性展开)
│ ├── paths.py # task_dir db form 归一
│ ├── storage/ # SQLAlchemy 2.x ORM;usage(计费写)/telemetry(失败埋点)/usage_report(聚合读)三分
│ ├── scheduler.py # 定时任务服务层(§8.5;执行引擎在 web/scheduler_runner)
│ ├── external_systems/ # 用户连接控制面 + provider connectors(§8.14)
│ ├── wechat/ # 渠道:ilink / wecom / service / inbound(§8.7)
│ ├── sandbox/ + executor*.py # Executor ABC + Docker per-user 容器池(§7.5)
│ └── agent_builder.py # 装配 lib:build_agent / system prompt
├── tools/ # fs / shell / run_python / skill / 媒体(共享原语 media_common)/ 检索 / host-side 域工具
├── skills/<name>/ # SKILL.md + references / scripts / assets
├── rendering/ # 平台渲染层 md→docx/pdf(§8.6;块收集器/inline 切分单一事实源在 common)
├── prompts/system/general_v1.md
├── config/{agent.yaml, models/*.yaml, media/*.yaml}
├── workspace/users/<user_id>/{.memory/, .skills/, <working_dir>/}
├── web/ # app.py=工厂+lifespan 编排;routers/*(11 路由模块,register 范式);
│ # background/scheduler_runner/wechat_runner(后台协程);
│ # run_lifecycle(统一抢占/落消息/调度);runs(BG worker)
│ # + auth/admin/broker/sinks/common/schemas/model_gate/userfiles/static/
├── db/migrations/ # alembic
├── evaluation/ # 黑盒评测旁路:任务集/API adapter/确定性评分/报告
└── main.py # 入口:web / db / probe / user
工作目录 = workspace/users/<user_id>/<working_dir>/,所有 skill 产物写这里,绝对路径注入 system prompt。user_id 走 JWT sub,无 SENTINEL fallback。DB 内 name(显示名)与 working_dir 都有值;创建 API 显式给 name 时 working_dir 可留空并沿用 name,省略/留空 name 时必须显式给 working_dir、name 先以「新对话」占位并自动生成。二者落库前都是简单名(拒 /\..、. 起头);同 working_dir 多 task 共享同目录(§7.1)。SaaS 化只换外层根目录,布局不变。
输出生命周期(2026-08-06):真实文件仍是事实源,不引入统一产物框架,也不迁移各工具已经稳定的目录。仅对已出现的局部噪声建立规则:可过期的外部大响应进用户根目录隐藏缓存 .zcbot_cache/<task_id>/external_results/,用户明确要求留存时再 export 到 <working_dir>/data/external/;图片/视频的 prompt/model/cost,以及专业软件的 plot spec、provenance 等技术 sidecar 进对应产物目录下 .meta/。文件面板默认隐藏所有 dotdir;用户可在当前 task 工作目录子树内用开关查看 .meta/、.build/、.preview/ 等 dotdir,但 user_root 的 .memory/、.skills/、.kb/、.zcbot_* 永不通过该开关展示,.env 等点文件也始终隐藏。.meta/ 计入用户配额,用户显式查看后可检查、下载或删除,但不自动提升为 artifact。既有普通工具可见文件和旧 sidecar 不做全局搬迁;已知的软件 Job 错位目录通过显式幂等修复脚本校正。
| 工作目录位置 | 语义 | Artifact 规则 |
|---|---|---|
| 根目录 | 最终报告、PPT、PDF、主分析文档 | 成品工具自动发布,或用 publish_artifacts 显式发布 |
sections/ |
报告的可编辑章节源文件 | 默认不发布 |
figures/ / videos/ |
插图、数据图表、AI 图片和视频 | 独立成品可自动发布,报告配套资源不重复发布 |
materials/ |
CIF、Materials Project 查询数据和计算输入输出 | 长期工作数据;用户要求交付时再发布 |
data/external/ |
用户明确保存的外部系统完整快照 | export 时发布 |
各产物目录下 .meta/ |
prompt、模型、参数、费用和溯源信息 | 默认隐藏,可在 task 目录开启查看;不自动发布 |
启动:main.py web → FastAPI + lifespan(reaper / scheduler / 渠道入站)→ 登录换 JWT → POST /v1/tasks/{id}/messages 起 BG 线程 → build_agent(capabilities → LLM → system prompt → 工具)→ AgentLoop.run。
3. 核心组件
3.1 主循环(core/loop.py)
ReAct:LLM → tool_calls 执行 → 结果塞回 → 再调;无 tool_call 即返回。工具结果对模型截 16K、用户预览 400 字符;事件走 sink.emit(SSE 桥),content delta 即时 emit。
- LLM 走 streaming(
chat_stream+stream_chunk_builder拼回);cancel_check在每轮 LLM 前 + chunk 间 + tool 间 poll → cancel 延迟 ~100ms;中途 cancel 已收 chunk 丢弃不入库,未执行 tool_call 补[cancelled by user]保协议。 - 停机判据 = 解耦"跑了几步"与"是否在推进"(2026-06-10):
max_iterations降为纯安全 backstop(step-count 是"不收敛"的粗糙代理,正经 80 步任务和死循环 5 步不该一刀切);主防护是进展信号——①_RepeatGuard逐指纹累计"同名同参+无产出"(SOFT2 提示 / HARD4 拦截);② run 级_stall连续 8 步全 tool 无净产出主动停。停下都 emit"回复『继续』可续跑",不静默。
3.2 Model Profile(core/capabilities.py + config/models/*.yaml)
每模型一份 yaml(context/输出/parallel_tools/thinking/计费/max_iterations 等),新模型 5 分钟接入不改代码;LLM.chat 按档案自动启 features。
3.3 Capability Probing(core/probe.py)
yaml 是手填的,probe 用真实调用对账(basic_chat/parallel_tools/thinking/long_context)。显式触发,不进启动路径。
3.4 工具系统(Hybrid 范式)
JSON tool call 管离散操作;run_python(tmp .py + subprocess + 敏感 env 过滤)管批处理/生成文档。edit 唯一匹配(old_str 重复即报错);工具按原子操作切分,不做 make_pptx() 式高级封装。持 key 的能力一律 host-side tool、仅对应 env 存在才注册(§7.5 #7)。
3.5 Skill 系统(Anthropic 渐进披露)
三层加载:Discovery(name+description,几百 token)→ Activation(load_skill 完整 SKILL.md)→ Execution(references 按需拉)。写 WHY+WHAT 不写 Step 1/2/3;description 决定触发。
用户私有 skill(2026-06-11):registry 收有序来源列表——内置 ROOT/skills(只读)+ 用户 user_root/.skills(可写)。取舍:① user wins 同名覆盖(核心用例是"copy 内置再改",覆盖只作用于本人会话,blast radius 锁死),覆盖显式标注不静默;② 创作走 host-side save_skill/fork_skill——fs 工具的 base_dir 跨 backend 够不到 user_root/.skills,host 工具一个落点两模式通吃;③ 用户 skill 加载失败收进 load_errors 注入 prompt 提示修,不崩整次扫描。
Skill 定向模型(frontmatter model:,2026-07-06):内置 skill 可声明"该工作流用这个模型最好";当前未配置定向模型的 skill。单一执行点:对话中 load_skill 命中 → run 内热切(loop 换 self.llm/self.caps,下一轮生效)+ 持久化 task 模型;切失败降级原模型。取舍:跳档位门控(产品决策,任何档位可用);只信内置 skill(用户 skill 的 model 忽略,防自写 frontmatter 绕门控);不自动切回("skill 结束"不可判定);不设 DB 开关、不做建 task 预切——frontmatter 那一行本身就是热配置(删行即停,per-skill 粒度),全局开关是第二事实源、预切是第二执行点,都砍;已选国际旗舰模型(unifyllm 网关族:Claude/GPT/Gemini)豁免切换(2026-07-15)——定向 pin 本意是给较弱的国产默认模型托底产物质量,用户已主动选了旗舰模型则尊重其选择、不降级(判据:current profile 的 family==unifyllm)。
3.6 Session 与 Task
Session = 消息列表,ORM 直写 PG messages(append-only,jsonb 存 LiteLLM 原样 payload);Task = 上层元数据,写 tasks。working_dir FS 只存 skill 产物,无 state.json。本地 + SaaS 同一份 schema/ORM,差别只在 ZCBOT_DB_URL。字段:name=显示名(独立于目录)、title_source=标题来源(auto / manual / fixed)、working_dir=相对 ROOT posix 串(多 task 共享)、skill=类型标签。快速新对话的自动标题描述当前会话主题,清空后重置为“新对话”并由下一条消息重新生成;人工标题与渠道/调度固定标题不随清空变化。working_dir 在创建入口 eager mkdir;DELETE 走软删(§7.9),FS 一律不动。原子性:PG INSERT 天然;产物走 atomic_write_text。
3.7 双层记忆(core/memory.py)
跨 task 事实放 user_root/.memory/:Core(core.md,每次 build_agent 进 prompt)+ Extended(extended/*.md,索引进 prompt、内容按需 read;索引优先 frontmatter description,legacy 退首行)。system prompt 每次 build_agent 重建,memory 演化即时生效。
- 写入 = agent 自管(prompt 契约,非后台蒸馏):
memory_block注可写路径锚点 + 维护契约(常驻,即使记忆为空——解冷启动);agent 用已有 fs 工具维护、写前查重。不引专用remember工具、不做后台蒸馏(不烧额外 token,人可审核手编)。 - memory 永远在 FS 不入 DB:用户笔记语义,编辑器手编是产品一部分;跨 task 共享靠同一目录自动达成。dotfile 命名防项目名撞车,
validate_task_name拒.起头双向防呆。 - 前端记忆面板只读,"改"全走对话:看全貌是读、直读 FS 才是地面真相;改走 agent 自管 = 单一写入口、不写坏 frontmatter。故意零写/删 API;将来若"删一条"摩擦大再单加 delete(唯一廉价确定性 mutation)。路径穿越校验收口在
core/memory.py。 - 快捷指令 ≠ memory(
core/shortcuts.py):触发词→完整指令,存.memory/shortcuts.md但内容永不注上下文——入口层(渠道核心 + web post_message 共用)整条精确匹配确定性替换,0 额外 token、渠道无关;maintenance 蹭 memory 心智(对话让模型写)。若反过来塞 core.md 靠模型概率召回:既不确定又每轮烧 token,正是要绕开的坑。
3.8 个人知识库(core/kb.py + core/kb_ingest.py,✅ 2026-07-22)
用户自建资料(规范/报告/标准/内部文档)的长期查阅层,与 §3.7 记忆同范式:纯文件 + prompt 注入契约,无向量无 DB(判据同"真实文件为准":个人库几十~百余文件,agentic search 足够;索引若引入只能是可重建派生缓存)。两层格局:zcbot 内建 .kb/ 私有小库(本节)+ 院检索服务共享大库(document_search,zcbot 只当客户端)——分工标准 = 文件数 × 查询频次,路由靠各自工具/契约描述自然分流,不在 kb 契约里点名 document_search(2026-07-22 收窄:契约只管自己怎么用,少一层耦合)。
- 做成机制而非 skill(判据:有独立于会话的持久状态需用户管理 → 机制):落盘
user_root/.kb/<库名>/(INDEX.md + docs/ 转换后 md + sources/ 原件)。"已入库"判据 = INDEX.md 有条目,sources 有而 INDEX 无 = 待入库 → 入库幂等、崩溃可恢复、零 migration。dotfile 命名同.memory双向防呆,GET /v1/files 天然隐藏。 - 入库管线(
core/kb_ingest.py,上传即触发 + 手动兜底):markitdown Python API 转 md → 扫描件 PDF(文本近零)走方舟文档理解 OCR 兜底(§8.13 同通道)→ deepseek flash 单次 chat 写 标题/摘要/关键词(失败降级文件名+正文开头,不阻塞)→ 追加 INDEX 行。编排照定时执行器:create_task + to_thread;写并发收口为共享 FS advisory lock(.kb/.locks/<hash>.lock):Web 上传/删除、后台入库与 agentwrite/edit对同一库共用一把跨进程锁,蓝绿实例间只允许一个写者,锁占用返 409/工具可重试;进程退出由 OS 自动释锁,不靠清理锁文件。文档正文、原件与 INDEX 全走同目录临时文件 + fsync +os.replace原子发布,读者只会看到完整旧版或完整新版。进度详情仍以内存保存细节,但会探测跨进程锁补出running;崩了靠 FS 判据续跑。 - agent 侧零新工具:
kb_block(照 memory_block)把 INDEX 全文 + 契约(主动查阅无需点名 / INDEX 行格式 / 答题标来源)注 prompt;零库时注极简冷启动契约(建库步骤 + INDEX 行格式 + "成篇资料进 KB / 短事实进记忆"分工,~百 token)——原"有库才注入"省 token,但零注入让模型不知道 KB 机制存在,用户说"放进知识库"被就近写进.memory/(2026-07-24 真实事故),与 memory 空契约常驻是同一课:教会第一次,建库落盘后下轮 build_agent 自然切全量。fs 工具在 user_root 内可读写、docker 沙箱整 user_root bind →.kb天然可达。INDEX 行格式是对话内手动入库与后台产出的同一契约。 - API 薄壳(
/v1/kb*8 端点):列/建/删库、详情(带入库进度)、上传即入库、手动 ingest、看/删单篇。不设 HTTP 检索端点——检索是 agent 的事。前端两栏 modal(kb.js)管上传/删除,查询全走对话。 - 记账:
usage_eventskind="kb_ingest"(OCR 那笔走 kind="vision"),无 task 上下文 → 0022 放宽 task_id 可 NULL,溯源靠 units JSONB{"kb", "source"}。
3.9 黑盒评测旁路(evaluation/,✅ 基线 2026-07-31)
Eval 与生产 core 解耦,通过现有 /v1 API 创建专用任务、监听 SSE、读取回复和下载产物;不直连 DB、不把评测依赖塞进生产 requirements.txt。任务集采用人可读 JSON,确定性断言是主评分源,逐次保留 task_id / 模型 / 耗时 / 成本 / 失败证据;非确定性任务默认重复 3 次并同时报告 pass@1 与 pass^k。五维百分制缺任一维时总分必须为 N/A,只报已覆盖维度暂定分;安全用例失败触发总分封顶。公共 benchmark(Inspect AI / ScienceAgentBench / PPT benchmark / Promptfoo)通过 adapter 渐进接入,不反向塑造主循环。
取舍:dogfooding 继续提供真实需求信号,固定 eval 提供模型升级、prompt/skill 变更前后的可复现对照;二者回答的问题不同,不再互相替代。评测默认只连 loopback,远程实例和真实费用都需 CLI 显式确认;生产地址只允许专用评测用户跑低风险冒烟集,攻击性/并发/跨用户测试必须去测试环境。Inspect AI 只通过 JSONL/报告契约在隔离环境运行——其 Click 约束与当前 Hugging Face 依赖冲突,不允许为评测降级生产 .venv。
4. 模型路由
默认 deepseek_v4.flash;复杂 bug / 终稿升 pro + reasoning_effort=max;fallback 手动切 Claude。成本量级:修 bug flash ~$0.01 / 完整申报书 flash ~$0.30(pro-max ~$1.5,Opus ~$10+)。99% 任务 flash 够用。
模型思考参数由 profile 统一表达:thinking_enabled 只表示开关,thinking_transport 只表示已验证的传输协议,reasoning_effort 只表示开启后的推理强度,reasoning_replay 只表示历史 reasoning 的 provider 回传策略(none / tool_calls / all);core/llm_params.py 是请求参数构造唯一入口,core/context.py 是历史消息清洗唯一入口。原始 assistant 响应仍完整落库供展示与导出,发模型前才按 profile 裁剪,且上下文统计、压缩与折叠都使用裁剪后的请求视图。DeepSeek、GLM、方舟当前共享 extra_body 协议,DeepSeek V4 仅为带工具调用的 assistant 消息保留 reasoning,未验证网关明确用 none、不猜参数协议,主循环不再按 family 分支。/v1/models 只返回语义明确的 thinking_enabled。
5. 设计哲学
Less Scaffolding, More Trust:把 LLM 当会持续变强的同事,告诉它目标不告诉步骤;脚手架在模型升级后会变枷锁。
七条:① prompt 用 WHY+WHAT 不用 HOW;② skill 渐进披露;③ 工具原子切分留组合空间;④ Model Profile 化不硬编码;⑤ probing 对账;⑥ 版本化 prompt(真要切再做);⑦ dogfooding 找需求、黑盒 eval 做可复现回归。
借鉴:CoreCoder(主循环 + edit 唯一匹配)/ Anthropic Skills(渐进披露)/ nanobot(workspace 隔离)/ smolagents(LiteLLM + CodeAct)。
6. 风险与取舍
| 风险 | 缓解 |
|---|---|
| 本地 run_python 非真隔离 | 工作目录限制 + env 过滤;SaaS 走 docker(§7.5);本地靠用户审阅 |
| 模型/提示升级造成隐性回退 | dogfooding 找真实案例 + evaluation/ 固定任务集对照;失败可按 task_id 回放 |
| skill description 触发不准 | 实战观察迭代 |
| long context 退化 | probe 探测可靠 ceiling |
| 本地 PG 离线 | docker compose 起本地 PG / 连远端 |
Hybrid 而非纯 CodeAgent:V4 JSON tool call 已稳,sandbox 成本按需付。不做 subagent(编排型):状态管理爆炸,单 agent + skill 覆盖 95%;上下文隔离的最小子循环是另一个问题,见 §8.11(有触发条件,无信号不实施)。Eval 做旁路而非 core 子系统:评测只消费稳定 /v1 契约,避免为了跑榜把主循环绑死在某个 harness;dogfooding 与固定回归并存。
7. SaaS 化
§1-§6 是本地 dogfood 形态;本节把同一份 core 包成多用户在线服务。不引 platform/core 切分——core 就是后端,直接对用户 auth。
7.0 与本地形态的兼容性
同一份 web /v1 服务换部署位置:Storage 都是 PG(换 ZCBOT_DB_URL);working_dir/memory 换外层根目录;Sandbox 本地 subprocess → SaaS per-user 容器;Auth 邮箱密码 + platform_key→JWT → 未来 OIDC(邮箱密码长期并存)。workspace/ 仅存产物,state/messages 全在 PG。
无感部署(蓝绿,0.41):生产双 systemd 实例(ZCBOT_INSTANCE=blue/green)+ nginx upstream 切流(对外仍 8765),取代单实例 restart 的 503 窗口。双实例并存的互踩由三件事消解:tasks.run_owner(0020)让 reaper 只收自己色、sandbox 容器名/label 带实例色各管各的、微信长轮询 PG advisory lock 选主;定时任务 claim 本就 SKIP LOCKED 天然安全。部署编排在 deploy/update_bluegreen.sh(流程/兜底见 RUN)。
broker 外置(Redis pub/sub,✅ 0.42):0.41 落地时 event/cancel broker 留在进程内,代价是切换窗口内"刷新看不到旧实例 run 直播 / 停止送达不到"两个边缘。2026-07-06 重评(用户量已非个位数)决定实施 —— ①部署时总有 in-flight run,窗口边缘从偶发变常态;②单进程逼近天花板后,最近的扩容手段是稳态双实例同时接流量,broker 外置是它的硬前提(否则 POST 与 SSE 落不同实例直播全瞎)。选 Redis 不选 PG LISTEN/NOTIFY(token 级 delta 全过主库太吵:NOTIFY 全局队列 + 8KB payload 限制,把实时路径耦到 PG)、不选 nginx sticky hash(upstream 变更时 hash 重排,恰在部署窗口失效,治标)。实现(web/broker.py,LocalRunBroker/RedisRunBroker 同接口鸭子类型):ZCBOT_REDIS_URL env 开关,不设即进程内(dev 零影响);event 走 PUBLISH zcbot:ev:<tid> + 单条 async pubsub reader 路由到本地订阅 queue(Redis 只管跨进程一跳,fan-out 最后一公里仍在进程内);done = SETEX key(60s)+ publish 双通道,订阅先挂 channel 再查 key 不漏;cancel = SETEX/GET/DEL key,loop 在 chunk 间 poll(localhost ~0.1ms)。容错纪律:启动 ping 不通 fail-fast(同 sandbox init),运行中失败 10s 节流 log + 降级丢帧,reader 1s 退避重连。Redis 不解决的:run 本体仍绑进程(进程死 run 死,drain/reaper 不变)、线程池上限(调 ZCBOT_RUN_MAX_WORKERS 的事)。
7.1 心智模型:Task 一等公民 + Dir 文件副视图
两个并列入口,正交不嵌套:Task list(主,"我的对话历史")+ Dir tree(辅,"我的文件资产")。类比 Finder + 最近使用。dir 不是 task 的父容器、无 DB 实体、path 即标识。同 working_dir 多 task 共享 = "同一项目多对话",无需"项目"实体;前缀嵌套拒(no-subtask)。skill 产物全落 working_dir,不引 artifacts 表。空 dir 正常展示(上传本身是有效行为)。多 task 并发写由软警告兜底(§7.9)。
新对话入口(0.60):登录未选 task 与左栏「+ 新对话」共用同一前端草稿页,先选择已有 working_dir 或输入新目录名,再直接写消息;草稿不落 DB,首发时才 POST /v1/tasks,避免空 task 堆积。创建请求省略/留空 name 时必须显式给 working_dir,后端据此判定自动命名,以「新对话」占位并置一次性 auto_title_pending;显式 name 的旧调用继续视为人工标题,working_dir 仍可省略并 fallback 到 name,旧 auto_title 字段只作兼容保留。首条消息并行触发短标题调用,结果只改 tasks.name、绝不改 working_dir;人工 PATCH name 同时清 pending,条件 UPDATE 保证在途标题也不能覆盖用户命名。原完整创建表单保留为「自定义」入口,UI 同样要求明确选择 working_dir,name 可选,并可预设 description/skill/model。标题是 UI 元数据辅助调用,记 usage_events.kind="task_title";模型调用失败时以首条消息第一行生成本地兜底标题,不阻塞主 run,也不把 pending 留给后续消息误命名。
对话产物与生命周期(0025/0028/0033):真实文件仍是内容事实源;artifacts 表记录已发布产物的稳定身份和生命周期,包含 user-root 相对当前路径、来源 task、复制来源、可空的 software_job_id、哈希/大小及 active/deleted、回收路径。software_job_id 不设外键,非空即表示该正式产物由对应专业软件 Job 生成;复制品保留该来源,Job 或 task 生命周期结束也不抹除来源标识。新 messages.artifact_refs 使用 {version:2, artifact_id, scope:"working_dir", path:"reports/a.pdf", label?:"最终报告"};path 是兼容快照,预览/下载优先按 artifact_id 找当前路径,因此移动或重命名后历史卡片仍有效。version 1 和 NULL 旧消息继续走原 task-scoped 兼容链。普通源码树、中间文件、.meta/ 技术信息和配套资源不登记;agent 仅用 publish_artifacts 显式提升少量最终文件。移动保持身份,复制为每个副本创建新身份并记录直接来源;删除将文件移入 .zcbot_artifact_trash/ 并软删记录,普通文件仍物理删除。
用户消息附件(0031):messages.attachment_refs 与助手产物分开表达输入附件,元素为 {version:1, scope:"working_dir", path, label, kind, media_type, size_bytes};文件仍是事实源,不登记为已发布 artifact,也不承诺独立生命周期。payload.content 只保存用户自然语言,模型上下文在内存中按附件类型补兼容提示,避免 UI 协议污染正文。NULL 表示旧客户端/旧历史,前端继续解析正文标记;[] 表示新消息明确无附件。独立 attachment 表暂缓,只有出现跨消息复用、稳定身份、版本快照或附件级审计需求时再抽象 file_assets + message_attachments。
7.2 资源模型(/v1)
统一 /v1 前缀返 JSON;UI 由 platform 实现(§7.9),本地 dev SPA dogfood。要点(细节见 web/app.py):
Tasks POST/GET/PATCH/DELETE /v1/tasks*(POST 可选 auto_title;分页+筛选+ordering allowlist;
DELETE=软删,FS 不动)
GET /v1/folders(working_dir + task 计数)
GET/POST /v1/tasks/{id}/messages(POST 起 run;单活 run:running/cancelling→409,
先校验请求;Web/渠道/定时共用 run_lifecycle,以 SELECT FOR UPDATE
将 user 消息与 running 同事务提交并统一登记 broker/inflight;
BG worker 消费已持久化轮次,不重复追加 user,防调度前崩溃丢输入/idx race)
GET /v1/tasks/{id}/events(SSE) POST /v1/tasks/{id}/cancel(协作式,202)
Auth POST /v1/auth/login(platform_key)/ login_password / change_password;GET /v1/me
Files GET /v1/files?path= / upload / download / delete / rename
(user-rooted;dotfile 隐藏;越界 400;顶层目录 DB-aware,见 §7.4)
GET /v1/tasks/{id}/files/download|preview_pdf?path=
(working_dir-rooted;结构化产物入口,保留旧 user-rooted API)
Admin GET /v1/admin/*(require_admin;overview + usage/models|users + storage/users)
Export GET /v1/tasks/{id}/export(docx)
SSE 事件:run_start / llm_start / text{delta} / reasoning{delta}(thinking 模型推理流,前端灰色折叠卡)/ tool_call / tool_result(预览,完整走 DB)/ llm_end / model_switch / warn{msg}(熔断·重复拦截·折叠失败等运行时提醒)/ context_fold{phase,...}(§8.8 Phase 2 折叠 start/done)/ cancelled / error / done。fan-out:每订阅独立 queue;迟到订阅立收 done。事件不持久化(messages 走 PG)。
版本化:/v1 minor 半年兼容,major 6 个月 deprecation。CORS:本地 *,部署收紧。
7.3 认证
两条 login 签同款 JWT(HS256,7d):login(platform 机器对机器,持 PLATFORM_KEY 可为任意 user_id 签;body 可带 name/user_name,upsert COALESCE 落 users——与未来 OIDC claim 注入同构)+ login_password(邮箱 bcrypt;错误统一 403 防探测)。require_user 提取 user_id,所有查询带 Task.user_id== 隔离;require_admin 再查 users.role=='admin'(role 走 DB 不进 JWT,改完即时生效)。信任模型:platform 是单点可信中间层,泄漏风险与 platform 自身同级,可接受。未来 OIDC 只换 login 内部校验,路由层不动;无 tenant 层(企业版再加 org_id)。
7.4 存储:Postgres + 本地文件系统
users(user_id pk, email unique null, password_hash, oidc_subject, plan, -- plan=模型档位(0.31 启用)
name, user_name, -- 0016 平台注入档案
role default 'user', -- 0009 admin 门控
created_at)
tasks(task_id pk, user_id fk, name NOT NULL, auto_title_pending default false,
working_dir NOT NULL, skill, description, status,
model_profile, tokens_*, cost_usd,
channel default 'web', -- 0013 渠道来源,仅 INSERT 写定
run_status default 'idle', run_error, -- 0004 合并 runs 表
run_owner, -- 0020 蓝绿实例归属,reaper 只收自己色;单实例 NULL
next_message_idx, -- 0029 task 行锁下原子分配 messages.idx
scheduled_job_id, -- 0017 定时任务归属,普通列表过滤
context_base_idx, -- 0019 §8.8 软重置窗口起点
deleted_at, -- 0010 软删
created_at, updated_at)
messages(pk, task_id fk, idx, payload jsonb, artifact_refs jsonb null, attachment_refs jsonb null,
tokens_in/out, model_profile, kind, -- 0025/0031 UI 元数据;kind=push 等
unique(task_id, idx); gin(payload))
usage_events(pk, user_id, task_id, message_id, kind, -- chat/image/video/vision/... 自由文本
model_profile, units jsonb, cost numeric, created_at) -- 多态用量,加媒体不动 schema
scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB)
- working_dir 存相对 ROOT posix 串,读写统一过
core/paths.py;入口validate_task_name拒空//\NUL/.起头。 auto_title_pending(0023)只是一轮 UI 命名闸,不是 task 状态机;旧创建入口/存量行恒 false,快速入口首发后消费,人工改名优先清闸。- 0004 简化:runs 表只写不读、run_id 单活 run 下全冗余 → 合并
run_status/run_error入 tasks。0006:tasks.model_profile为 source-of-truth(PATCH 切、下条 send 生效);usage_events 重建 v2 多态形态,统计 source-of-truth;tasks 三列保留作粗概览。run_status 终态:ok 收回 idle,error(出错)与 cancelled(用户停止)是持久终态 —— 前端renderPersistedRunTerminal据此在每次重渲后补持久卡(扛过收尾 loadMessages 整屏重建),刷新/切任务仍在;下次起新 run(post_message 写 running)覆盖清掉。 - 0029 消息序号:
tasks.next_message_idx在 task 行锁下统一分配messages.idx,Web、agent 与渠道追加不再各自维护序号或依赖冲突重试;分配时仍与max(idx)校准,允许蓝绿发布窗口内旧实例继续写入。清空消息与计数器在同一事务归零。 - No-subtask:同 user 下前缀互含即拒(归一 posix 后 Python 端比对);同 working_dir 允许。
- 文件面板先备料:user_root 与普通目录都可显式新建直接子目录;创建成功后前端进入该目录,用户可先上传/选入资料,再把顶层空目录选作新对话 working_dir。目录 leaf 复用
validate_task_name,不允许借 UI 创建点目录或路径式名称。 - DB-aware service 是顶层 working_dir mutation 的唯一原语,DB-FS 一致性服务端内化:文件面板 rename 与对话
rename_working_dir共用同一服务(事务锁关联 task、running→409、DB UPDATE 先于 FS);对话工具只登记本轮内存动作,等 agent 正常回复完、当前 task 退出 running 后执行,避免 executor/system prompt 仍握旧 cwd。服务在收尾前退出时动作丢失但目录不变,不引持久队列或 migration。delete 仍仅走 files API,被 task 引用时 409。 - 单一 PG ORM(本地 + SaaS 共用):一份 schema 一份查询,无 adapter,alembic 管 migration。
7.5 沙盒:Per-user 容器 + Per-tool exec
选型:每 user 长驻容器(文件模型本就以 user root 为安全边界,per-task 会切碎共享工作区)+ 每 tool 一次 docker exec(exec 级 timeout/cwd/统计)+ 空闲 5 分钟回收 + bind mount user root→/workspace。
边界划分:Control plane 留宿主(auth/DB/files 校验/SSE/LLM/受控 web 工具/配额审计),Execution plane 进容器(shell/run_python/任意生成代码)。目标不是"所有操作进容器",是"所有不可信执行不能在宿主"——否则凭据反被带进执行面。
硬限制:cgroup CPU/mem、pids-limit、exec timeout、并发数、read-only rootfs、tmpfs /tmp、no-new-privileges、drop ALL caps、非 root、--shm-size。软配额:按 user 计 DB(磁盘/LLM cost/wall time/流量/并发),超额 429。网络:默认 deny outbound,搜索抓取走宿主受控工具。
落地清单(Stage C 硬协议,实施按此对账):
- 网络 blocklist 硬编码段(任一缺失=未完成):
169.254/16(metadata)、内网三段、CGNAT100.64/10;PG 实际 IP 单独再 block(belt-and-suspenders)。容器自身 loopback(-o lo)显式放行——netns 隔离下容器内 127.0.0.1 到不了宿主,DROP 它无安全收益且误伤容器内 IPC(2026-07 实锤:puppeteer↔chromium DevTools 走 127.0.0.1,被 DROP 导致 mermaid 渲染 90 天 0 成功)。 - egress 模型:容器
HTTP(S)_PROXY走宿主 proxy + iptables DROP 其余 outbound(防 SDK 绕 env);proxy 做域名 allowlist(pypi/github/npm + 镜像)+ IP block + per-user 计量 + 审计。 - 进程组清理:exec 套
setsid,timeout/cancel/正常三路径都kill -- -PGID——防nohup派生 daemon 跨 exec 持久化成"跨对话后门"。 - 磁盘配额硬化时点:首版应用层统计;外部用户开放前必须升 xfs/ext4 project quota(扫描间隙打满共享盘会拖死同节点)。
- Executor 接口 + runtime 注入:不 hard-code docker exec,走
Executor.call_tool抽象 +ZCBOT_SANDBOX_RUNTIMEconfig——未来切 gVisor/Firecracker 应用层零改动。 - 工具按信任域二分,Executor 内部 dispatch:container backend = shell/run_python/fs 全套(fs 以前 host 跑无 user_root 校验能读任意文件,进容器
/workspace是物理边界;tool_runner.pystdin 喂 JSON);host backend = load_skill/skill_authoring/web_*/媒体/documents 等持 key 工具。AgentLoop 零感知。代价每 fs call ~200ms,LLM 推理下是噪声。 - Secret-bearing 域工具不进 sandbox、不做 key 下发:容器内任意代码可
print(os.environ),短期 token 只缩窗口不改根因;正确形态 = host-side JSON tool(LLM 传业务参数 → host 持 key 调远端 → 裁剪计量审计 → 返结果/落盘路径)。仅 env 存在才注册。
升级触发信号(无信号不升级):
| 方向 | 触发信号 | 不升级理由 |
|---|---|---|
| Docker → gVisor | 陌生注册开放 / 逃逸 CVE 窗口 / 可疑 syscall | 完整 hardening 已挡主流逃逸;gVisor syscall -30~50% 真代价 |
| gVisor → Firecracker/e2b | 合规客户 / 单机 100+ user / 兼容墙 | 每 VM 100MB+ 不划算;e2b 与 storage_root 自持冲突 |
| docker exec → 容器内 tool-runner RPC | exec 开销 >30% 持续两周 / 长驻服务 / 单轮 >20 次调用 | 自管清理+观测损失 >> 200ms×N;美学统一 ≠ 理由 |
Image 体积 / 多 user 资源 / 加包(2026-05-28):① image 大 ≠ 运行时吃资源(layer 共享、不 exec 只是磁盘字节);② 瓶颈在并发 exec 不在 idle 容器,杠杆全在运行时限制;③ 新增依赖 = base 收敛 + per-user venv(<user_root>/.venv/,bind mount 回收不丢;不放共享 volume——install 脚本是任意代码,破坏隔离)+ 使用频次沉淀进 base。
7.6 / 7.7 改造项与阶段
进度见 PROGRESS ## 状态。依赖顺序:事件流化 → PG(一次性切换无双轨)→ working_dir 语义 → Files API → no-subtask → Executor+sandbox → /v1 → CLI 双模式(撤)→ Web UI(撤,API-only)。阶段:A/B/D/D' 完;CORS 收紧应尽快;真 OIDC 选做;C 是外部用户开放 hard prereq;F(限流/监控/HA)持续。
7.8 已知风险
| 风险 | 缓解 |
|---|---|
/v1 冻死演化慢 |
minor 半年兼容 + deprecation 窗口 |
| running task 被 rename/delete | 后端校验 + UI 禁按钮 |
| DB-then-FS 中断孤儿 | rename DB 先行可回滚;delete 后台 GC 扫"FS 有 DB 无" |
| 同 wd 多 task 并发写同名 | known limitation,频率近 0;软警告 banner;宪法文件已按 short_id 命名隔离 |
| 各入口并发撞 messages.idx / 接收后进程退出丢输入 | Web/渠道/定时共用 run_lifecycle:单活 gate(FOR UPDATE)下原子提交 user 消息与 running、统一调度 worker;worker 只消费已持久化轮次;lifespan reaper 收敛残留 running,multi-worker 再换 lease |
| shell/run_python 无沙箱开放外部 = 主机沦陷 | Stage C 是 hard prereq;BLOCKED_PATTERNS 是 trivial-bypass 装饰品,不再加规则(黑名单 fundamentally broken),防线在 OS 层 |
| sandbox 出站越权 / 资源滥用 | default-deny + 受控 proxy;硬限制 + 软配额 + idle 回收 |
7.9 取舍说明
- path-as-identity 而非 folder_id:folder 真实存在于 FS,folder_id 是第二份 source of truth;rename 走 DB-aware 同事务 cascade。
- files API 单一 mutation 入口(2026-05-18):"顶层目录分支"从数据状态派生而非客户端意图,放服务端才有强制力;双命名空间(/folders vs /files)把分支搬给 client,失强制力且端点翻倍。
- task 软删除(2026-06-17 推翻 hard cascade):公测后对话轨迹是训练/研究语料,
deleted_at置位 + restore,避免用户误删立即永久丢失。当前实现仍无限期保留软删数据,物理清理仅有管理员手段;后续生命周期已定为“软删除后保留 30 天再物理清理”(待容量信号实施,见 §8.5),届时恢复能力明确限于宽限期内。 - 文件留存:普通用户文件仍是 FS 直接删除;已写入结构化
messages.artifact_refs的已发布产物,在统一 files delete 入口删除时原子移动到用户根目录的平台隐藏区.zcbot_artifact_trash/,原路径与历史卡片立即表现为已删除。递归目录只回收其中 artifact,其他文件照常删除;回收内容仍计入用户配额。上传入口默认以原子独占创建保留同名两份,只有用户从文件菜单明确选择并确认「替换文件」时才允许原子覆盖;替换保持原路径和 artifact 身份、内容更新。该机制仍不防 agent/shell 绕过 files API 或整盘损坏。完整地基仍采用 restic/borg 定时增量备份(与应用解耦,捕获删除+覆盖+所有写入口),后续容量需要时再补data_events用户意图事件和回收区清理/恢复管理。 - 0004 删 runs/usage_events 旧表:只写不读的死代码;代价是失历史 run 元数据,真要细粒度审计再补(届时是新需求非技术债)。
- 本地也用 PG 不用 SQLite:dogfood ≡ 真实路径;Docker 已是必然依赖;双 adapter 维护税 > 一次性配置。
- API-only,UI 由 platform 实现(2026-05-15):本仓库再维护一套 UI 是双套浪费;SSE payload 从 HTML 切 JSON;沉淀的 sink/broker/路径安全全保留。dev SPA 留一份作 dogfood 主路径(SSE 调试 curl/Swagger 都覆盖不了)。
- CLI REPL 撤(2026-05-18):dev SPA 落地后 REPL 与 web 完全等价,双套 task 语义只是"对称美",每个 bug 修两次;看内部状态临时写 ad-hoc script 即可。
- Memory 不入 DB:见 §3.7。Tasks/messages 在 PG、产物在 FS:查询/统计是 DB 强项;产物终用户要文件管理器看到、Office 打开——FS 是产物天然存储,DB 是元数据天然存储;bind mount = user root 让容器视角 ≡ 用户视角,无翻译层。
- 同 wd 并发只做软警告(2026-05-21):dogfood 中同 wd 多 task 是"项目对话历史",并发近 0;硬 gate 破坏切换流畅、short_id 全隔离破坏共享语义、clone task 工程过重——都不选,真高频再升级。
- shell 黑名单不加强(2026-05-21):命令注入图灵完备,枚举不完、越复杂越虚假安全;正确防线 §7.5 OS 层。本地 dogfood blast radius 限自身可接受,外部开放信任模型不同必须 Stage C。
- task 级「宪法」文件靠文件名隔离(2026-05-20):
<date>-<short_id>-<name>.<base>.md,short_id 主锚 + glob 字典序最大=current;不 cascade rename(in-flight 丢文件)、不 DB 化(工作量 5-10× 且失直接编辑)、不开物理子目录(破坏扁平共享)。升级 DB 化信号:结构化编辑视图 / 跨 task 查 spec 字段。
8. 未来步骤 / 已落地设计
实施细节进 PROGRESS + git;此处只留缺口、选型与取舍。
8.1 图像理解 + Seedream i2i(✅ 2026-06-16)
缺口:主模型纯文本;t2i 无法"改已生成图"或"读上传图"。选 E+C 组合:seedream 加 reference_images 走 i2i + 新增 look_at_image(Doubao Seed 2.0 Lite,一次读图 <¥0.01)让 DeepSeek 自决何时"借眼睛"。不选 A(主模型换多模态:V4 code/tool-calling 是核心,换=降能力+改 loop 引 multimodal,工程 5×);不选 B(每条消息隐式 vision 路由:烧 token+失 agentic 控制权)。关键实测:ARK 接受 base64 data URL → 内网无需对象存储。升级到 A 的信号:用户要"贴图直接对话读图"成高频——当前假设"图是工具调用对象"而非"对话内容"。
8.2 Token 优化与上下文治理(✅ 2026-06-04 起)
根因:全量历史每轮重发。质量边界(设计约束,后续改动都守):不改模型输入的优化(caching/计费)零风险;改可见上下文的必须保留可追溯原文(长结果落文件留路径,确认过的需求/规格/结论不删);禁止"只保留最近 N 条"当主策略。落地:core/context.py 发送前压缩旧 tool/load_skill(保协议字段,不改持久化历史)+ 压力门槛(未逼近上限完全跳过,护 DeepSeek 前缀缓存,实测命中 92-94%)。教训(2026-06-12):不压 assistant tool_call 参数——压缩 marker 会被模型仿写成真实参数(投毒),范本必须永远真实可执行。task summary 并入 §8.8 Phase 2。
8.3 PPTX 前端在线预览(✅ Stage 1)
关键洞察:后端 soffice 把 PPT/PPTX 统一转成 PDF,前端只维护一条 PDF 展示路径。选 LibreOffice(像素级保真、任意 pptx)不选轻量 HTML(复杂失真)/PDF→PNG(失矢量)。转换在 web host 不进沙盒;宏安全 high + 禁网 + 仅本人 user_root。2026-08 App WebView 暴露浏览器内置 PDF 插件不可用,展示层由 blob iframe 改为本地 PDF.js canvas;页面保持纵向连续滚动,只在视口前后懒渲染并释放远处 Canvas,兼顾阅读习惯与长文档内存。Web 端与 App 共用,不新增免鉴权文件 URL。任意 HTML 仍在 opaque-origin sandbox iframe 内运行,但内容入口改为同源静态宿主页 + postMessage,避开 WebView 的 srcdoc 空白和原生 URL 白名单误拦 blob:。与 pptx_preview.py(agent 生成期自检)分工。Stage 2 未做:常驻 listener / eager 预转。
8.4 运维监控 / 无感更新(监控 ✅ / 换版 design)
优雅 drain 已是单实例上限(SIGTERM 拒新 run + 等收尾);先撞的瓶颈是线程池(每活跃 run 占 1 线程)。落地排序:① 轻量监控(显式 executor + 60s [stats] 周期日志——要历史峰值不是快照);② 按数据决策扩容;③ --reload 缩 503 窗。不做监控界面:运维健康是少数标量,日志够;业务分析走 DB SQL。界面阶梯:日志 → /v1/stats → Grafana → 只读 dashboard,现停第一级。无感换版已由蓝绿落地(0.41)、broker 外置 Redis 已实施(0.42,均见 §7.0);扩容路径:调大线程池 → 稳态双实例分流(broker 已外置,纯 nginx 配置动作)。
工具失败聚集的信号分层(2026-07-29):7 天窗口继续保留取证,但管理端默认判断对象是“近 24h 仍活跃且跨 ≥2 task”的系统性故障;单 task 反复试错、按设计非零退出的质量门、近 24h 已归零的历史尾巴分别展示,避免正常迭代挤占故障榜首。API 保留低阈值全量 clusters,只加 category=failure|quality_gate,分区属于前端读侧语义,不删除既有字段。空输出 shell 非零退出从相邻 assistant tool_call 提取稳定命令类别(如 search/no match、dependency probe),只改善签名,不持久化第二份命令事实。RepeatGuard 只把 run_python 的 traceback + 非零退出纳入同错 streak;不泛化到 shell,避免 grep 未命中和质量门复检被误拦。provider wire 健康另走只读派生端点,按 model_profile+tool 对已有 tool_salvaged/tool_malformed 事件计算 24h/窗口抢救率;它是失败数的分母与趋势解释,不混进 cluster,不新增表/索引,也不反向改变并行调用、salvage 或重试策略。
8.5 定时任务(✅ 2026-06-18)
核心洞察:job 本体 = cron+tz + 一句 prompt + 会话模式;守护循环只负责到点把带标记的 prompt 喂进现成 agent 主管线,不造第二套执行路径。"发邮件"不是字段是 agent 动作——加任何投递能力不改 schema。业界四源(OpenClaw/Autobot/Claude Code/geta)模式收敛佐证。
- 三层投递:baseline(结果必进 task 线程)→ opt-in 推送(prompt 里说,agent 调工具)→ 可靠兜底(job.notify 结构化,run 完确定性补发)。
- 会话模式:isolated 默认(每次新 task,省 token)/ persistent(绑定 task 续上下文,token 逐日涨——仅用户明确要连续性)。mode 只管对话延续;文件夹两模式都按 job 复用。定时 task 标
scheduled_job_id不混普通列表。 - 可靠性:退避重试(transient/permanent 区分,v1 简化为下个 cron 点)、per-job 超时(默 1800s,复用协作 cancel;超时按 error 记不吞)、无补跑、定时 run 内禁 schedule_create(防自我繁殖)、连续失败 N 次自停。
- 选型:croniter 只当 next_run 计算器(vixie dom/dow OR 语义 + 时区,手搓必踩坑);不引 APScheduler/Celery(单机低并发,过度工程);不用 JSON 文件持久化(已有 PG)。persistent 绑定 task 正忙 → 跳过本次不排队。
- 前端取舍:对话端完整 CRUD(schedule_* 工具),前端只读看板 + 停用/删除——cron 构建器 UX 难题直接消失(用户对 bot 说"每天早九点");工具与 REST 共用
core.scheduler服务层不漂移。v1 纯工具不配 skill(schema 够;skill 值钱处是教写好 job.prompt,v2 按需)。 - 执行历史保留(方案已定,待容量信号实施):继续用
isolated每次创建独立 task,不引rolling;“执行隔离”与“数据生命周期”保持正交。未来由scheduler.isolated_history_limit(拟默认 100)把同一 job 超出最近 N 次的终态 task 自动置deleted_at,再由通用 Task GC 按retention.deleted_task_grace_days(拟默认 30)分批物理清理所有到期软删 task。GC 永不碰running/cancelling;messages随 task 删除,usage_events与 task 脱钩后保留,避免历史费用随内容删除而下降;task 与文件生命周期分离,共享scheduled-<jobid>工作目录及产物不随之删除。两项配置分别回答“何时进入回收站”和“回收站保留多久”,不得合并成 scheduler 专属硬删逻辑。 - 暂缓理由 / 实施信号:当前用户量与定时任务量低,无限保留尚未造成容量、查询、备份或 vacuum 压力;现在引入不可逆删除、外键 migration、后台 GC 与并发保护的复杂度没有收益。出现任一信号再实施:定时 task/messages 成为 DB 主要增量、历史接口或维护明显变慢、用户感知历史冗余,或扩展高频定时任务/更多用户前需要容量边界。实施时同步把
usage_events.task_id删除语义改为ON DELETE SET NULL,为deleted_at及(scheduled_job_id, created_at)补清理索引,并更新删除/恢复的 30 天对外契约。
8.6 平台渲染层 rendering/(✅ 2026-06-23)
心智:文档渲染是平台能力不是 skill 内容。起因:化学式白名单在三份 render_docx 逐字重复 + brief 缺 PDF 路径致线上手搓 weasyprint。不放 skills/_shared/:skill 走自包含/可 fork 标准,跨 skill import 破坏 fork。抽顶层 rendering/ bind-mount /sandbox/rendering:ro:common(叶子原语单一事实源)+ docx_manuscript(paper/proposal/report 三 profile)+ docx_brief + pdf(chromium 不用 weasyprint——镜像已有,保真更高)+ render.py 统一 CLI。重构前后 docx 字节一致零回归;brief 不强并 manuscript(差异大,只共叶子)。与 §8.3 分工:pptx 预览在 host 面向用户,本层在沙盒面向 agent 交付物。
report 通用 profile + 基座引导(2026-07-17):起因是工具失败面板最大头——建材院验收/技术/评审报告(无对应 skill)由模型裸手撸 python-docx 内联中文正文,引号/全角标点崩成 SyntaxError 反复返工烧 token(定层见 core/pysyntax.py)。根因不是缺渲染器(paper/proposal 早有全套 md→docx),而是这类自由报告没有指路。补 report profile(复用 manuscript 全套,差异:目录默认无 / 章节不强制分页 / 通用页边距)+ 基座 prompt 加软引导「出 Word 报告→正文写成 md sections 调 render.py --profile report,别手撸 python-docx」。取舍(红线):渲染器是"首选加法"不是"替代"——绝不硬拦 run_python 生成文档。确需精细定制版式 / 改写已有 docx / 处理 xlsx 等渲染器覆盖不了的,run_python 照样自由写代码;软引导让位于更具体的 skill render 指引(proposal/paper)。三层防御叠加:渲染器(根治,正文进 md 不进代码)> run_python 语法预检(core/pysyntax.py,手撸时的安全网,只诊断语法不限制工具选择)> RepeatGuard err-streak(撞墙兜底)。
8.7 微信接入(双渠道)(✅ 均落地)
诉求:简报/结果推进个人微信 + 能在微信里对话。三条路选官方 ClawBot(腾讯 2026-03 官方个人号 Bot API,零封号,后端可接任意):wechaty/hook 违规高封号排除;企业微信官方但只触达成员、要管理员——作渠道 B 并列(其无条件主动推正补 ClawBot"24h 活跃才可推"短板,定时简报必达首选)。
协议真机实测结论(细节见 core/wechat/ilink.py + scripts/probe_clawbot*.py):双向对话 + 主动推送成立。关键:client_id 每条必唯一(漏则静默丢)、context_token ~24h 可复用且每条入站刷新(→ 主动推的前提是用户开口过且 24h 内活跃,冷推不可能,超期退邮件兜底)、文件走 AES-128-ECB+CDN 可原生直推 docx/pdf、多条分块 state=1/2。取码零门槛(无预置凭据),bot_token 是 per-user 长期凭据。现实卡点:个微 8.0.70+ 灰度;腾讯保留限频/终止权力(政策风险)。
架构决策:
- 入站出站一体:主动推送依赖 context_token 而 token 只能从入站拿,"只出站"不成立;getupdates 长轮询既收对话又刷新 token(每 binding 一条,公测 N 小可接受)。
- 入站 → 每用户每渠道一条 persistent 会话 task(连续性;token 增长靠 §8.8 治理);两渠道共用
_run_channel_conversation(channel)核心,各一张 task 互不串扰。 - web 端只读镜像:web→微信不同步(回微信需 context_token,24h 窗口会把"双向"拖成不可预测的"有时同步"+ 双入口并发歧义)→ 交互权威单一锚定微信,web 只读;主动推走
wechat_push/定时(出站语义非对话)。 - 渠道抽象:
send_to_user(user_id, text, file?, channel=None)统一发送,None=广播、点名单投不回退;scheduler/tool 不感知具体渠道;active_channels()是渠道清单唯一真相源。推送即对话记录:投递成功写一条 assistant 消息(摘要+链接,messages.kind="push",取回复时跳过)进渠道 task——agent 记得自己推过什么,可基于推送追问;不塞正文防膨胀。 - 数据:统一表
channel_bindings(user_id, channel, status, config JSONB)——渠道绑定="用户在某渠道的一份配置",判别列+JSONB 与 usage_events 同范式,加渠道零 migration;分表不扛增长、宽表最差。敏感凭据(bot_token/context_token)加密入 JSONB(ZCBOT_WECHAT_SECRET_KEY),绝不进沙箱/日志/API;企微 secret 走 env 不入库。 - 渠道 B 企业微信:出站推送先行("和邮箱似的"),公测需求明确后补入站——与 ClawBot 本质不同:回调 webhook 而非长轮询(无后台 task,只加 HTTP 端点 +
wecom_crypto.py验签解密);agent >5s 超被动窗口 → 回复走 message/send 主动推回。绑定两路:手填 userid(推送是出站直连不需要域名)+ OAuth 扫码(需 HTTPS 可信域名,用 wwlogin 扫码端点非网页授权端点)。 - 不选:wechaty(违规);富排版卡片(个微能力存疑,统一纯文本+文件直推);bot_token 不落库(它是长期凭据必须持久化,安全靠加密列)。
8.8 channel 长会话上下文治理(Phase 1-2 ✅ / 3 design)
根因:IM 常驻 task 只增不减(web 任务"做完即止"有天然边界,IM 没有),全量历史越用越贵终撞 context window;§8.2 压缩只摘 tool 正文挡不住跨时段累积。业界(OpenClaw/Hermes/Claude Code)都是"阈值摘要+头尾保护",但都是单次 session,不解"IM 用三个月"——IM 独有的会话分段最高杠杆且零信息损失,自补。
心智:边界而非删除——一条消息都不删,只移动喂给模型的窗口起点;全历史留 DB,web 照旧翻完整记录。
- Phase 1(✅):
context_base_idx软重置。Session.load只装idx>=base;自动 gap(默 6h,base=最后一条 user 消息——不是失忆墙,留上一轮做续聊锚点)+ 手动「新话题」硬重置(base=总数)。关键不变量:append 续号取 DB 真实总条数而非加载条数,否则撞 unique 约束。不选"每次 gap 开新 task"(堆文件夹+task 卡片)、"boundary 标记消息"(混进消息流要处理 tool 配对);列是纯元数据零侵入。 - Phase 2(✅ 2026-07-09):阈值结构化摘要(补 Hermes 阶段③,
core/context_fold.py):run 起点窗口体量达reliable_context×85%→ 中段折叠成固定模板摘要(目标/约束决定/进展/待办 + path/ID/数值原文保留,mem0 实测自由摘要会静默丢精确值),存tasks.context_summary(0021)+ 推进context_base_idx,Session.load注入仅内存的「前情摘要」user 消息。双层门槛 50%(压缩)+85%(折叠)正交:分段砍跨话题累积、摘要兜单段超长。关键取舍:① run 起点触发而非轮间(轮间要处理 tool 配对切割 + 已加载窗口一致性,回合制下 run 起点是自然缝隙,代价是命中那一回合首 token 慢几秒、每分段一两次);② 摘要存 tasks 列不入消息流(boundary 消息会混进 tool 配对处理,Phase 1 已拒过;列是纯元数据零侵入);③ 前缀缓存友好:摘要调用复用会话 prepare 后的消息前缀 + 末尾追加指令 → 与上一轮 chat 缓存字节一致近全程 hit;增量更新 = 旧摘要本在被折前缀里,指令要求合并,不重读全史;折叠后窗口字节稳定至下次折叠,且体量回落 50% 门槛以下、压缩关闭,前缀比折叠前更稳;④ 失败零阻塞(warn + 跳过,85% 距硬上限有垫);⑤ 切点必落 user 消息(不劈 tool 配对,窗口恒以 user 开头,与 Phase 1 锚点同语义);⑥ 「新话题」硬重置/清空对话清摘要,gap 软重置保留;对全部 task 生效(机制通用,web 短任务不达阈值零影响);⑦(0.58.52)触发判定从 chars×2.5 静态估算改为 token 实测口径——DB 实测静态折算对中文密集窗口低估近一倍(名义 85% 线实际 ~155% reliable 才触发)、代码密集反向虚高一倍,estimate_window_tokens用 provider 实报 usage(messages.tokens_in/out,窗口内最后一条 assistant)覆盖窗口主体、仅实测点后尾巴按 2.5 估,loop 的 50% 压缩门槛与前端占用环同源用校准比值(夹 [1.0,4.0] 带宽,逐轮以 sent_chars/prompt_tokens 刷新);校准是信号不是正确性数据,拿不到实测回退 2.5 旧口径、绝不阻塞 run。已知残余:折叠后 run 在首次 chat 完成前崩掉会多折一次(摘要偏保守,原文全在 DB),不为此加持久化状态。 - Phase 3(design):持久检索(sqlite-vec/FTS5)解"问很久以前的精确内容";工程最重,待确认真实需求(数据没删随时能补)。
8.9 产物机检门 + 提示层禁令纪律(✅ 2026-07-06)
根因(同类事故两次):①文档层禁令拦不住绕开官方管线的产物——管线内的门只在模型用了官方脚本时生效;②否定式禁令若自带完整违规配方(精确命令/文件名/分步做法),压力下反成模型的执行菜谱(粉红大象:否定词丢了,菜谱留下);③官方脚本报错文案里的 pip install 对 agent 就是一条会被照办的指令。三层治理:平台层 core/pptx_guard.py(loop 在 shell/run_python 后对本步新 .pptx 机检"整页贴图"特征,tool 结果注入 ERROR 当场逼返工;判定刻意保守——≥2 页、≥80% 页整版位图、全 deck 零文本/表格/图表才判废,解析失败一律放行)——检产物不检命令,换什么工具造都绕不开;skill 撰写纪律——禁令只点名不给配方,正面写"唯一入口 + 报错就修/如实上报";脚本文案纪律——打印给 agent 的报错不得含安装/替代管线指令,缺依赖=环境问题,说明并上报。本条推翻 0.35.1"平台层自动检测暂缓"的拍板(复发即证据)。
8.10 ppt 验收:vision 渲图过目 → 纯代码几何质检(2026-07-07)
推翻 0.36.0 的渲图验收闭环(svg_preview 渲 PNG → look_at_image 逐页过目 → accept_pages 标 pass → 导出 gate 校验 sha1)。三条根因:①成本——逐页 vision 26-42s/页、每 deck 8-25 次调用,烧 token 大头;②环境脆弱——沙箱 chromium "找到了但渲染崩"没有回退路径,硬门+死路逼模型即兴发挥(pip install cairosvg、手写渲染循环,正是 8.9 要防的行为);③验收环节的"看"本身无法机检——gate 只能强制"渲过",看没看/看得准不准全凭模型自觉,0.36.0 就写明了这个边界。替代:质检器 check 13/14 的精确几何检测(越界/压字/错位/网格漂移/图表退化)已覆盖"正确性"面,且在导出边界自动复跑;主动放弃美学验收(配色观感/页面空挤,单调门除外)——底线正确性机检可保,观感交给用户反馈迭代。svg_preview 保留为手动工具(chromium 失败自动回退 cairosvg)但 SKILL 提示面零提及——不给渲染入口模型就不会主动渲(同 8.9"禁令不带配方"逻辑,正面只写"导出唯一入口 svg_to_pptx");accept_pages/acceptance.json/--allow-unreviewed 整套删除。回退信号:若"几何全对但观感翻车"成为用户改稿主因,再考虑便宜的整本拼图单次 vision 抽检,而非逐页。
8.11 最小子循环 delegate:上下文隔离而非多 agent 编排(design,2026-07-08,按诊断数据触发)
根因:检索/扫文类工作(文献 brief、document_search、批量读文件)的形态是"中间数据量大、最终只要结论"——在主循环里跑,中间数据必然流经主上下文,污染 + 膨胀是架构性的。已踩实例:38 篇 abstract 反复 dump 烧 2.5M token、document_search 同参调 122 次不收敛。现有缓解(_RepeatGuard 熔断、§8.2 context 压缩、brief skill 的 context 纪律)全是行为约束——劝模型别乱来,不改变"中间数据必须过主上下文"这个结构;同 8.9 的教训,提示层纪律挡不住结构性问题。
决策:§6"不做 subagent"针对的是编排型多 agent(并行、状态共享、agent 间通信、任务分解),该结论不变。本条预留的是一个工具:delegate(instruction) -> str 摘要——复用现成 AgentLoop 起一个全新空上下文的子循环,只注只读工具(检索/读文件类白名单),硬轮数上限(~20),跑完只把文字摘要返回主循环。主循环视角就是一次普通 tool call,零状态共享、零并行、零 agent 间协议;实现量约一个文件。
触发条件(无信号不实施):RepeatGuard + brief 纪律上线后,scripts/diag_*.py 数据仍显示检索型 task 是烧 token 大户 / 检索中间数据占上下文大头。数据收敛则本条永久搁置。
⚠️ 实现后撤回(0.58.30 落地 → 0.58.31 revert,记录教训):2026-07-15 曾完整实现(FilteredExecutor + 内存态子 Session + loop 拦截 + 降 flash),又整体撤回。撤回原因不是机制错(设计是对的、对标 Claude Code subagent 也成立),而是触发信号没坐实:自评时回看 diag_search_args.py 数据,motivating 案子 document_search 122 次呈"一批批不同材料体系并行搜"形态,更像批量扇出而非结果驱动的探索——若属实,它的正解和 mp_search 一样是批量工具 document_search_batch(便宜、可预测、可诊断),delegate 对它是过度设计。加上子循环 transcript 不落盘(诊断驱动的功能反而不可诊断)、20 轮上限对 122 次负载可能偏低、强制 flash 对难检索可能降质——一堆未验证的坑。重建的前置条件收紧为:先用 diag 确认某检索型 task 的 query 序列是真探索(query 依赖前序结果、无法一次性列全),而非可批量枚举;是批量就走批量工具,只有真探索才值得 delegate。教训:§5"无信号不实施"要落到"信号已被数据证伪 batchable 的可能性"这一步,不能凭"看起来像探索"就上 agentic 子循环——同 mp_search 的判断(批量扇出≠检索发散)。批量工具那条(0.58.28)验证过、保留。
不选:
- 完整多 agent 编排:状态管理爆炸(§6),且 zcbot 无真实并行需求——用户没有"同时审 3 篇"诉求,职责隔离已由 skill 体系覆盖。
- 继续加码 skill 纪律/熔断规则:行为约束对抗不了架构性上下文污染,规则越堆越像 §7.8 的黑名单(fundamentally broken)。
实施时对账清单:①子循环禁注 delegate 自身(防自我繁殖,同 8.5 定时 run 禁 schedule_create);②工具集只读白名单,不给 shell/fs 写/run_python;③计费入 usage_events 归属主 task(kind 区分);④主 run 的 cancel_check 传导进子循环;⑤子循环事件不直播 SSE(或只 emit 一条聚合摘要事件),防前端刷屏;⑥子循环产出若超长仍走"落文件留路径"纪律(§8.2 质量边界)。
8.12 后台进程 bg proc:detach + 文件系统状态,不引队列组件(2026-07-10)
根因:模型写的长脚本(批量数据处理/模拟计算)在工具内同步跑,三个结构性问题:①工具超时(shell 60s / run_python 120s)把真实长任务掐死,长程复杂任务做不了;②就算放大 timeout,run 被占死几十分钟(单活 run 锁,用户 409);③进程是 zcbot 实例的子进程,蓝绿切换旧实例退出时必然陪葬——超时调多大都躲不过部署窗口。
决策:把长进程从 zcbot 进程树上摘下来,OS 就是"任务组件",不引 Celery/RQ/队列。shell/run_python 加 background=true(模型判断:预计 >~1min 走后台;前台超时报错里提示改后台——判断错了纠错路径只有一步;用户显式指令永远优先)。状态协议纯文件:<user_root>/.zcbot_procs/<task_id>/<proc_id>/{proc.json, output.log, exit_code}——dotfile 用户不可见(同 .zcbot_tmp 惯例),exit_code 文件出现是唯一终态信号,状态判定全靠文件 + 现场探测(pid / 容器 running),无常驻登记,天然扛重启。查询/终止走配套 check_process 工具(host in-process,两种 backend 通吃)。
- host backend:detach 独立 wrapper(
core/proc_wrapper.py,stdlib-only,sys.executable 直跑不依赖 PYTHONPATH):限时(默认 7200s / cap 86400s)、超时杀进程树记 124、日志截尾 10MB、最后写 exit_code。 - docker backend:专用容器
zcbot-proc-<id>(pool.run_proc_container,同款硬化 + iptables init),product=proc+ 无 instance label —— 与 sandbox 容器的 idle reaper / shutdown_all 生命周期解耦,dockerd 托管,蓝绿切换/实例重启不中断。不用docker exec -d进 sandbox 容器:idle 5min reaper + 启动 shutdown_all 会把长进程随容器带走。 - 回收:check_process 见终态顺手 rm 容器;web lifespan 每小时
procs.sweep(终态目录 7d TTL / exited 孤儿容器),幂等,蓝绿双实例同时跑无害。 - 通知/可视:不做服务端推送 —— 前端轮询
GET /v1/procs(用户级,纯文件读取,仅有 running proc 时 5s 一拉):[Background]工具结果卡本身活化(spinner+跳秒+停止按钮,与前台工具卡同体验,历史重渲同样恢复;POST .../procs/<id>/kill)、running→终态弹 toast(跨 task 也提醒,点击跳转)。proc 完成时刻往往没有活跃 run,SSE 通道根本不在,轮询是诚实的选型。
8.13 扫描件 PDF 直读:方舟文档理解,不接外部 OCR(✅ 2026-07-21)
缺口:markitdown 只抽 PDF 文本层,扫描件(老标准/检测报告/红头指南,建材院高频)转出为空=死路。选复用 seed-2.0-lite 的方舟文档理解(chat file 内容块,PDF 整本 base64 内联)新增 read_document:零新供应商(敏感文档不出已有豆包面)、零新基础设施、记账复用 vision 通道;实测 ~1300 输入 token/页(约 1 厘/页)、100 页全覆盖、17MB 内联可用。不选专用解析 API(MinerU/Textin:版面还原最好,但申报书/专利底稿要上传新第三方 + 免费额度政策不稳);不选本地 OCR(PaddleOCR 类:镜像塞推理依赖,需求未量化前过度投资);不选 file_url/file_id 传址(前者要给用户文件开免认证公网直链=新安全面、开发机 NAT 后还跑不通;后者要接 TOS 多落一份存储;base64 是零新增面的唯一形态,行业惯例 chat 端点也不收 multipart)。防上下文爆:多页 OCR 强制 save_md 落盘只返预览。升级信号:>100 页/>30MB 巨件成高频 → 接 TOS 走 file_id;要高保真版面/公式还原 → 再评 MinerU。probe/smoke 留仓(scripts/probe_ark_doc.py / smoke_read_document.py)。
- 对话锁(前端):bg proc 运行期间该 task 的 composer 锁定(发送→停止,Enter 拦截),观感与前台执行完全一致 —— 后台化的收益定位为「进程扛超时/服务重启」,不改变"一个任务同时只做一件事"的对话心智;完成的那次轮询解锁 + toast「可继续对话」。锁只在前端,服务端不 409:「停止」入口必须可达,且多设备/渠道绕过前端锁属可接受边缘(等的是同一个进程,发了消息也不冲突)。
- 防失控:每用户并发 running 上限(
ZCBOT_MAX_BG_PROCS默 3);前台默认超时不放大(它是逼模型做前台/后台选择的杠杆)。
边界(防滑坡):只覆盖「单个本地长进程」。①外部异步作业(seedance 等 submit/poll 形态)不进这里——工具内轮询 + resume_task_id 续查已够;②job 链/依赖/自动重试不做——那是 workflow 引擎,编排的唯一归属是 agent loop(模型 check 后自己决定下一步),同 §6 拒绝编排的理由;③完成后自动续跑 run不做——zcbot 的长任务产物多为终点交付物(与 Claude Code"build 是中间步骤"不同),自动续跑=无人在场烧 token,通知给人、下一步由人/下次对话决定。
不选:Celery/RQ(多机分发/任务序列化/框架重试——单机 + 模型现写脚本的场景一个都用不上,还多两个常驻组件的部署/蓝绿适配);工具层 async 化 run 内等待(run 不结束,409 照旧,重启照丢);DB 表 + 守护(文件已是事实源,detach 进程写 PG 还得给它凭证)。升级触发:要跨机器跑计算集群时,①②的工具接口不变,只换执行后端。
8.14 外部系统:用户身份连接 + OpenAPI/MCP 受控调用(implementation,2026-08-10)
诉求:用户用自己的 MES/ERP/LIMS 账号让 zcbot 做信息查询,并把稳定的问法沉淀成私有 skill。心智模型:外部系统负责「连接与身份」,工具负责「受控访问」,skill 负责「业务流程与经验」。它有独立于会话的持久凭据和连接状态,因此是与 skill/知识库/记忆并列的平台机制,不是 skill。
Provider 边界:运行态只保留 generic_openapi 与 generic_mcp;具体 ERP、MES、LIMS 和 SaaS 都是数据库中的 definition,不再为单个业务系统维护 Python preset。用户名密码换 Token、API Key 和 Bearer Token 由通用认证 strategy 组合,业务查询提示、推荐 operation 和只读 POST policy 全部随 definition 保存。zcbot user_id 只能取自己的 connection,远端凭据再判定实际业务权限,不在 zcbot 复制上游 RBAC。
通用连接器边界:openapi connector 负责规格发现、operation 解析、安全 URL 拼接、参数 schema 校验、执行模式和响应体积限制;参数的 minimum/maximum/enum 等契约直接以 Swagger/OpenAPI 为事实源,不按 page/page_size/pageoff 等名字维护第二套分页语义。mcp connector 使用官方 MCP v2 SDK 连接管理员托管的 Streamable HTTP Server,通过 tools/list 动态发现、搜索并调用全部远端工具。两者共用认证 strategy、definition/grant/connection、revision、凭据加密、响应额度、大结果缓存与审计。标准 OpenAPI/MCP 系统只新增数据库 definition,不需要新增 Python provider;只有 OAuth 回调/签名交换、SOAP、消息队列或私有二进制协议等不符合现有 connector/strategy 契约的系统才新增适配代码。
信任边界:
- definition 当前由管理员维护,持久化同时预留
owner_type/owner_user_id/visibility/trust_level/review_status/egress_policy_id,未来可开放私有用户定义。Base URL 必须与 OpenAPI URL 或 MCP URL 同源;普通用户不能填任意 URL,避免 SSRF/内网代理。每个 definition 带单调递增 revision:目标地址、期望 MCP Server 身份或认证绑定变化保留密文但要求重新验证,未验证到当前 revision 的连接不挂调用工具。 - 凭据用独立的
ZCBOT_CREDENTIAL_MASTER_KEY在 host control plane 加密入 PG,不与JWT_SECRET复用,以隔离泄漏半径和轮换生命周期;缺 key 则拒绝新建/调用,不像早期微信绑定那样降级明文。API 只返回脱敏账号和credential_configured,不返密码/Token;凭据绝不进 prompt/messages/memory/skill/用户 FS/日志/沙箱。 - 调用工具不接受完整 URL。OpenAPI 只接受规格中的
operation_id;MCP 只接受当前 Servertools/list返回的工具名并归一化为mcp/<tool_name>,工具参数全部放入arguments。连接成功即授权发现和调用该 Server 当前暴露的全部工具,不在 zcbot 复制一份正向 allowlist;目录 TTL 到期或每次实际调用时重新发现,远端删除的工具立即拒绝。外部副作用以后统一交给 ActionPolicy,而不是把 MCP 工具清单变成第二套权限系统。 - Swagger/OpenAPI 或 MCP
tools/list是各自接口契约事实源;规格、tool description、schema 与返回文本一律当不可信数据,不能改写 system/tool 约束。MCP Server 是能力授权单元,zcbot 只保留短期目录缓存和搜索索引,不持久化工具副本。 - Swagger/OpenAPI JSON 不持久化入数据库或文件。连接器使用按
external_system_id + definition_revision + credential digest + config digest隔离的进程内有界ExternalRuntimeCache,统一复用 HTTP 连接池、短期认证 Header、原始 spec 与编译后的 operation catalog;JWTexp早 30 秒失效且单次 401 会清 Token 后重新登录一次,规格默认缓存 5 分钟,LRU 淘汰活跃连接时延迟到 lease 结束再关闭。登录、规格获取、catalog 编译和时间上重叠的相同只读业务请求使用同步 single-flight,失败不缓存;业务响应不做跨请求 TTL 缓存,顺序执行的相同查询仍访问上游。Swagger 2/OpenAPI 3 catalog 解析本地参数引用、请求体契约和 header/cookie 参数,搜索与调用只消费归一化结果。spec、登录响应和业务响应均流式限长,在完整 JSON 进入内存前执行硬边界。
工具面:不把数百个 OpenAPI operation 或 MCP tool 全展开为模型 JSON tool(工具列表膨胀+选择降准),只挂五个 host-side 元工具:external_system_list、external_system_search、external_system_call、external_system_result_read、external_system_result_export。search 对 OpenAPI 编译 catalog,对 MCP 动态消费 tools/list;call 再按 connector 执行。MCP structuredContent 优先归一化为 JSON,其他 content block 放入结构化 envelope,resource link 不自动抓取,isError 做限长脱敏后返回。其余连接状态、推荐入口、查询规划和大结果行为在两种 connector 间保持一致。
大响应:max_result_bytes 是进入模型上下文的单次内联额度,不再用于切断原始 JSON;超额响应完整写入 .zcbot_cache/<task_id>/external_results/,工具只返回合法结构化预览、result_ref、原始字节数和可继续读取的位置。reader 每次读取都重新校验当前 user 对原 external system 的 active 授权,并与 call 共享本轮 max_total_result_bytes 内联额度;export 同样重验授权,并把查询 operation/参数/时间等 provenance 与完整响应一起持久化,导出文件不受缓存 TTL 影响。缓存固定 24h TTL、单响应 10 MiB、单 task 50 MiB、单 user 200 MiB,过期或超额时优先清理最旧缓存;0.62.1 的 .zcbot_external_results/ 在读取和容量核算上保留兼容窗口。超过响应安全上限的远端结果直接拒绝并要求缩小范围,不产生半截 JSON。这里把“上游响应安全边界”“完整结果保存”“模型上下文额度”“用户明确留存”拆成四层,既不丢数据,也不靠无限提高上下文额度解决大结果问题。
明细扫描边界:单次响应保留安全下载上限与模型内联额度,每次 agent run 另按外部系统累计内联返回量;请求参数严格执行 OpenAPI schema 声明的数值、长度、数组和枚举约束。规格没有声明的分页哨兵语义不由 zcbot 猜测,应优先修正上游规格;平台通过响应与累计额度阻止模型连续拉取大量明细。达到边界后工具正向引导回聚合接口、result_ref 分段读取或缩小查询范围。
状态与 UI(三实体):external_system_definitions 保存可信目录、connector 配置、revision、治理元数据和查询提示;OpenAPI definition 另有执行模式及只读 POST policy,MCP definition 保存 URL、期望 Server 名称与传输响应上限,不保存工具清单。external_system_grants 只保存 selected 可见授权;external_systems 只保存用户连接、AAD 绑定密文、verified revision 和 active|invalid|needs_reverify|needs_credentials 状态。定义更新先对新旧配置做默认值补全后的语义比较:查询提示、推荐入口和响应限额等运行配置变化让 active 连接原子跟随新 revision;目标、登录、认证绑定、期望 Server 名称或 TLS 变化保留密文但置 needs_reverify。其余撤权、断开、密文与审计语义不变。
不选:①zcbot 直连 Factory DB(绕过现有 RBAC/审计,只读仍可越权/拖垮主库);②固定几个查询模板(把 agent 降成菜单,无法利用 Factory 已有广泛 API);③直接复用 Factory ichat 自由 SQL 原型(字符串安全判断不构成边界,且使用默认 DB 凭据);④自动把相似问题生成并上线新代码工具(候选配方可自动生成,可执行能力仍需工具门控/人审)。
8.15 OpenWorker 对照:动作治理与可恢复的人类介入(design,2026-08-06)
调研结论:OpenWorker 与 zcbot 同样采用单 agent loop、模型自由、文件产物、skill 渐进披露、持久会话、定时任务和外部连接器;这些不是 zcbot 的新增方向。真正值得借鉴的是它把「agent 能做什么」与「人此刻是否在线」拆成两条正交轴:统一权限引擎先按动作风险决定 allow/deny/ask,审批、问题、通知再进入同一个可持久化 attention inbox;人在 Web 或消息渠道任一端回应后,原调用幂等恢复。对 zcbot 的增量因此不是再造 agent/工作流框架,而是在现有 loop、scheduler、channel、external systems 之上补一层动作治理控制面。参考实现以 andrewyng/openworker 2026-08-06 的 permissions.py、risk.py、inbox.py、audit.py、selfwake.py、automation/models.py、mcp/tools.py 为样本;项目仍在 beta,只借心智与不变量,不复制其本地单机 JSON/SQLite 实现。
P0 决策候选——统一 ActionPolicy:所有有副作用的工具在执行前经过一个机械决策点,风险至少分四类:read(读文件/知识库/外部查询,默认允许)、workspace_mutation(用户工作目录内写产物,默认允许,继续由沙箱/路径边界/产物机检兜底)、external_effect(发邮件/微信、MES/LIMS 写入、创建远端对象,默认 ask)、destructive_or_privileged(删除远端对象、扩大授权、修改持久许可,始终 ask)。不照搬 OpenWorker 的“shell/本地写默认逐次问”:zcbot 的 shell 在 per-user sandbox 内且文档生产会高频写工作区,逐次审批只会制造确认疲劳;本层按 blast radius 而非工具名字分类。风险元数据由平台可信注册表声明,不能信 skill、OpenAPI/MCP 描述或模型自报;未知外部工具 fail closed。可再提供 read-only discuss/plan 模式,但它必须由同一策略层机械拒绝副作用,不能只靠 prompt。
P0 决策候选——统一 Attention Inbox:ask_user 继续服务「2-4 个方向选择、结束本轮等下一条用户消息」的轻交互;新增 attention item 服务「某个在途动作暂停后从原 tool_call 恢复」。PG 是唯一事实源,最小状态机 pending -> resolved|expired|cancelled,以 (task_id, tool_call_id) 唯一保证重连/重启不重复提问,resolve 用条件更新实现 first-responder-wins。item 至少记录 kind(approval/question/notification)、脱敏请求摘要、resolution、来源渠道和时间;Web SSE、企业微信/个人微信只是同一 item 的展示/响应 transport,不各存一份状态。删除 task、取消 run、授权被管理员撤回时确定性关闭关联 pending item;恢复前重跑权限判断,防等待期间策略或用户权限已经变化。边界:不能把待审批工具参数作为普通 user 文本让模型重新解释,批准的是被冻结且可校验的具体动作;凭据和完整敏感正文不入 item。
P0 决策候选——统一外部动作审计:新增独立 action_audit_events(不挤进回答费用口径的 usage_events,也不拿 toolfail 代替),记录 user/task/run、provider/tool/operation、风险级别、决策依据、approval item/rule、执行状态、目标资源标识、脱敏 args/result preview 与时间。审计回答「谁在什么任务中、凭哪条授权、对哪个对象做了什么、结果如何」;token/password/secret、邮件/消息正文、浏览器输入、外部响应全文机械脱敏或不落库。§8.14 的上游托管 OpenAPI 已用 external_system_audits 全量记录 operation、结果、耗时和响应大小,但不保存请求载荷;未来把写能力扩到消息、邮件、浏览器等多 provider 或加入精确目标审批时,再抽象为本表,避免现在为了单一 connector 过早统一。
P1——定时任务的精确目标长期授权:无人值守任务不能靠“整个工具永远允许”。借鉴 OpenWorker 的 task-scoped standing rule,授权归具体 scheduled job,形态为 provider + operation_id/tool + normalized_target;删除/停用 job 或管理员撤权即失效。normalized_target 的组成字段由可信 provider definition 声明(如 recipient/channel_id/plant_id/dataset_id),模型不能自行挑字段,不支持通配符;创建任务时 consent card 同时展示将读取的数据与将写入的精确目标。shell、任意文件删除及无法提取稳定目标的动作不授长期许可,每次仍 ask。现有定时查询与确定性 notify 不受影响;只有未来开放外部写时才启用该契约。
P1——显式 self-wake,修订 §8.13 的绝对边界但不改默认:保留「后台进程完成后只通知人、不自动续跑」为默认;仅当用户明确要求连续科研闭环,或 agent 显式调用类似 wake_on_process(proc_id) 时,允许进程终态触发一次新 run,注入机械完成事件、exit code 和输出路径后继续分析。设每任务自动恢复次数、token/费用预算、截止时间与取消开关;日志仍由 agent 按需读,不把全文注入上下文;后续外部副作用照常进 Attention Inbox。目标场景是「启动模拟/拟合 -> 等完成 -> 检查收敛 -> 出图和结论」,不是通用 workflow/job graph。没有真实中间计算需求信号前不实施。
P2——MCP 作为受控连接协议补充(已落地):保留 §8.14 OpenAPI 元工具,同时以 generic_mcp 接入管理员托管的 Streamable HTTP Server。MCP Server 是能力授权单元:连接成功后动态使用 tools/list 的全部工具,不维护重复正向 allowlist;zcbot 继续复用 external_system_search/call 延迟发现,将远端名称映射为 mcp/<tool_name>,并保留 URL、同源、Server 身份、传输限长、凭据、revision 和审计边界。协议内容、tool description、schema 和返回值均是不可信数据。
明确不借:①Tauri 桌面壳、本地 secret store 和 JSON/SQLite 状态——OpenWorker 是个人单机,zcbot 是多用户 Web + PG + 蓝绿;②为展示广度铺 25+ 通用 SaaS connector——优先院内 MES/LIMS/设备/知识与企微的真实需求;③多 persona/多 agent 编排——职责隔离继续由 skill 承担,§6/§8.11 的证据门槛不变;④逐次审批沙箱 shell/工作区写入——确认疲劳且不增加外部 blast-radius 安全;⑤直接复制 beta 项目代码——并发、身份、持久化和恢复不变量不同。
落地顺序/触发:先 ActionPolicy -> Attention Inbox(Web) -> 渠道响应 -> action audit,四者作为一个完整外部写安全闭环;再按真实无人值守写需求增加 exact-target standing rule,按真实长计算中间步骤增加 self-wake;有明确第三方 MCP 接入对象后再做 adapter。任何阶段都不得先开放外部写、再用 prompt 要求模型“记得询问”补安全边界。实现时需回写 §3.1 loop、§7.5 sandbox/tool registry、§8.5 scheduler、§8.7 channel、§8.13 bg proc 与 §8.14 external systems 的最终契约,并以 migration 保持现有 API/数据兼容。
8.16 Windows Node 内网 MVP(implementation,2026-08-12)
第一阶段以 docs/windows-node-mvp-intranet.md 为实现契约:Windows Node 只作为受控执行节点,通过出站 HTTP/WS 主动连接 zcbot;首批能力固定为 Origin 绘图。长期方案中的 mTLS、Service/DesktopRunner 双进程、完整租约与多节点调度暂不进入 MVP,但 URL path、Node ID、Bearer Header 和任务协议保留原位升级空间。
云端控制面使用独立的 software_node_enrollments 与 software_nodes,不复用用户外部系统连接。管理员创建的一次性注册码具有 128 bit 随机熵,数据库只保存 SHA-256 摘要;节点注册在行锁事务中校验有效期、预期名称和允许能力,成功后原子消费。每个节点获得独立高熵 Token,数据库只保存 bcrypt 强哈希,明文仅在注册响应出现一次。
专业软件采用“共享能力契约 + Node adapter”边界。仓库根目录 software-contracts/*.json 是语言无关的声明式事实源,描述 capability、请求 JSON Schema、输入额度、输出 manifest、feature 与最低 adapter 版本;Core 启动时自动发现契约,只负责身份、账本、调度、传输、摘要与最终发布,不包含 Origin 或其他软件的操作分支。Windows Node 是可信宿主,负责持久化 job 目录、下载/上传、恢复、取消和 adapter 注册;adapter 才负责探测具体软件、二次语义校验并执行。adapter 可以是 Node 内置 .NET 实现,也可以由固定 runner 启动任意语言的受信进程;进程只接收 job 目录并通过 state.json、terminal.json 与固定输出目录交接,不把 Python、COM 或某个 SDK 写入通用协议。当前 Origin adapter 的执行体恰好是固定 Python Worker,这是实现选择而非平台契约。
扩展现有 capability 的 feature 时修改共享契约、对应 adapter/Worker 和测试,不修改 Core 调度与 Job 生命周期;增加新专业软件时新增契约,并在目标 Node 安装包的单一 adapter registry 注册实现。Cloud 会自动获得校验、工具 schema、输出发布和能力发现;Node 的注册能力、配置校验与界面展示也从 registry/契约派生。当前 Node 仍按整机单执行槽保守串行,未来只有真实并行软件需求出现时,才把 slot 账本升级为 per-capability 租约,而不改变 Job 协议。
Node 通过 Authorization: Bearer 与 X-Node-Id 建立 /v1/software-nodes/connect WebSocket。进程内 Connection Manager 保证同一节点单活,新连接关闭旧连接;hello/heartbeat 更新版本、容量、软件健康与最后在线时间。管理员禁用节点时先持久化禁用态,再关闭现有连接;断线收尾不得覆盖禁用态。当前单活只覆盖单 Web 进程,生产启用多实例前必须增加 Redis/PG fencing 或将 Node API 固定路由到单一控制面实例。
第二阶段已增加 software_jobs(专业软件任务)账本与 origin.plot@v2 的 offer/accept 骨架。用户只能在本人 task 下以幂等键提交固定 schema;云端规范化请求并记录 SHA-256,按当前进程真实在线、能力匹配、健康且有空闲 slot 的 Node 创建短期 offer。Node 再次校验 schema、图形类型和输出格式,使用 write-through、flush 与原子 rename 先落本机任务目录,再回 job_accept;重复 job 只有 digest 一致才接受。过期或发送失败的 offer 回到队列,lease、Node 和 digest 不匹配的响应被拒绝。Node 接收后云端进入 dispatched 而非 running,并将 slot 降为 0;只有固定 Worker 真正启动后才进入软件无关的 software_running,具体软件和操作由 capability/request 表达。
第三阶段补齐多输入下载与恢复状态协议:请求使用通用 inputs[] 绑定 1–16 个 artifact,并由 operation.plot.series[] 以输入 key 引用各自的 X/Y 列;单文件不超过 100 MiB、总量不超过 512 MiB。已有 artifact 可直接提交;普通 task 文件先逐个调用 register_artifact(path) 登记稳定身份。Node 以自身 Bearer 身份访问每个任务绑定的只读下载端点,流式写入本 job 的 input/<key>/<filename>,同时校验大小与 SHA-256;不暴露工作区路径。Node 会原子读取/补报 terminal.json,断线后云端把活动任务标记 disconnected 并保留 Node/lease,重连按 job、lease、digest 恢复下载或幂等补报终态,不自动重派。Node UI 的“本机任务”只读取已经 accept 到本机的任务目录,不查询云端未派发 Job;每个任务以原子 state.json 持久化 accepted/downloading_inputs/ready_to_run/software_running/uploading_outputs/succeeded/failed/cancelled 通用阶段,窗口再与 request、terminal、upload-complete 合并成可恢复视图。
第四阶段落地固定 Origin Worker:Node 仅从管理员安装的固定 Python 运行时启动随程序发布的 worker.py,参数只有本机 job 目录;请求不能指定脚本、解释器或文件路径。Worker 使用 originpro 生成 OPJU、PNG、SVG、PDF、plot spec 和 provenance,校验产物签名并原子写入终态。origin.plot@v2 保持单一外层契约,series[] 以 x/y/z/y_error 统一表达数据角色,再按 plot.type 判别必需角色;当前覆盖折线、散点、线点、柱/条形、分组柱形、Y 误差棒、等高线、三维曲面、三元图和规则网格热图,并以可选的 canvas、轴排版、图例、标题和 series style 统一表达出版级尺寸与样式,旧 XY 请求原样兼容。进程内 pipeline 按 job 去重,并脱离单次 WebSocket 的取消令牌运行;连接中断只延迟状态/终态上报。Node 进程若在 Worker 启动后重启,则保守失败而不重复驱动 Origin,避免无法证明的双执行。
第五阶段完成输出上传与发布:Node 只按固定 manifest ID 逐项流式 PUT,并携带 Node、lease、request digest 与内容摘要;云端重新绑定任务身份,不信 Node 提供的路径或媒体类型。文件先进入用户根下隐藏暂存区,固定文件名、单文件/总大小和 SHA-256 全部验证后,把 plot spec、provenance 整理进 .meta/,再将完整目录原子移动到 <working_dir>/origin/<job_id>/。PNG/SVG/PDF/OPJU 等正式输出登记平台 artifact UUID 和 software_job_id,.meta/ 只落真实文件;成功状态返回 task-relative output_dir,Agent 以该目录为起点按需搜索。重复 PUT、complete 和重连均按摘要幂等;部分上传不可见,只有完整集合才能发布。Origin 执行槽与上传确认是两个正交状态:本地已有终态且固定 Worker 已退出时即释放软件执行槽,成功但尚无 upload-complete.json 的任务继续后台补传;若云端已经是 succeeded,重复 PUT/complete 必须按数据库持久化 manifest 校验并直接确认,不得按新版本目录规则重新发布旧 Job。
第六阶段增加用户级 Job 中心与 Agent typed tools。software_capability_list 只暴露固定能力及当前在线空闲节点数,software_job_submit/status/cancel 在构造时绑定当前 user/task,模型不能跨用户或跨对话指定归属。software_job_submit 的唯一入口为通用 inputs[] + operation + outputs[];输入和输出使用任务内稳定 key,具体 selector、type、format 和 options 由 capability 校验。register_artifact 是普通文件获得输入身份的唯一入口,并明确返回 UUID。右下角 Job 中心按用户聚合各对话任务,活动期短轮询、空闲期降频;终态变化通知用户,成功任务可回到原对话发起分析。取消采用协作协议:未派发任务直接终止,已派发任务先进入 cancelling,云端通过 WebSocket 发送并在心跳时重放 job_cancel,Node 杀死固定 Worker 进程树后回报 cancelled;终态写入仍由云端账本裁决。
后续仍需实现 Token 轮换;不得以任意命令或脚本接口临时代替。当前 Job 中心采用轮询而非用户事件推送,单活与 offer 选择仍只覆盖单 Web 进程;生产启用多实例前必须增加 Redis/PG fencing 或固定路由到单一控制面实例。
附录:DeepSeek V4 关键事实(2026-04-24)
- V4-Pro:1.6T/49B 激活,1M context,SWE-Bench 80.6;V4-Flash:284B/13B 激活,1M context
- 推理:non-thinking / thinking / thinking-max;价格 in ~$0.145/M、out ~$1.74/M(约 Opus 1/6)
- 旧
deepseek-chat/reasoner已于 2026-07 下线,全库仅存deepseek-v4-flash/pro