zcbot/DESIGN.md

462 lines
79 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 设计文档
> 本地运行的个人任务 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 等技术 sidecar 进产物目录下 `.meta/`。文件面板默认隐藏所有 dotdir用户可在当前 task 工作目录子树内用开关查看 `.meta/`、`.build/`、`.preview/` 等 dotdir但 user_root 的 `.memory/`、`.skills/`、`.kb/`、`.zcbot_*` 永不通过该开关展示,`.env` 等点文件也始终隐藏。`.meta/` 计入用户配额,用户显式查看后可检查、下载或删除,但不自动提升为 artifact。既有可见文件和旧 sidecar 不搬迁、不删除。
| 工作目录位置 | 语义 | 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 上传/删除、后台入库与 agent `write/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_events` kind="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` 契约,避免为了跑榜把主循环绑死在某个 harnessdogfooding 与固定回归并存。
---
## 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_dirname 可选,并可预设 description/skill/model。标题是 UI 元数据辅助调用,记 `usage_events.kind="task_title"`;模型调用失败时以首条消息第一行生成本地兜底标题,不阻塞主 run也不把 pending 留给后续消息误命名。
**对话产物引用(0025)**:真实文件仍是事实源,不建 artifacts 表;`messages.artifact_refs` 只保存可重建的轻量 UI 元数据,规范路径以该 task 的**当前 working_dir 为根**,形如 `{version:1, scope:"working_dir", path:"reports/a.pdf", label?:"最终报告"}`。预览/下载走 task-scoped 文件 API服务端用 task 当前 `working_dir` 解析因此顶层工作目录改名后历史卡片仍有效。普通源码树、中间文件和配套资源只留文件面板agent 仅用 `publish_artifacts` 显式提升少量最终文件,单条消息最多 10 个,图像/视频/Office 转 PDF 等成品工具可自动提升。`NULL` 表示迁移前旧消息,前端继续使用正文路径抽取,并在 task-scoped API 上启用只读兼容链(旧 user-root 含义→原样 task-relative→去掉旧目录前缀新消息写 `[]` 或结构化列表,停止启发式抽取,避免重复卡片与误识别。文件在 working_dir 内再次移动或删除后引用可失效,这是 FS 事实源语义,不复制文件、不引不可变对象存储。
### 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 + 本地文件系统
```sql
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
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, -- 0025,task-relative UI 元数据
tokens_in/out, model_profile, kind, -- 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)覆盖清掉。
- **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 硬协议,实施按此对账)**:
1. **网络 blocklist 硬编码段**(任一缺失=未完成):`169.254/16`(metadata)、内网三段、CGNAT `100.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 成功)。
2. **egress 模型**:容器 `HTTP(S)_PROXY` 走宿主 proxy + iptables DROP 其余 outbound(防 SDK 绕 env);proxy 做域名 allowlist(pypi/github/npm + 镜像)+ IP block + per-user 计量 + 审计。
3. **进程组清理**:exec 套 `setsid`,timeout/cancel/正常三路径都 `kill -- -PGID`——防 `nohup` 派生 daemon 跨 exec 持久化成"跨对话后门"。
4. **磁盘配额硬化时点**:首版应用层统计;**外部用户开放前必须升 xfs/ext4 project quota**(扫描间隙打满共享盘会拖死同节点)。
5. **Executor 接口 + runtime 注入**:不 hard-code docker exec,走 `Executor.call_tool` 抽象 + `ZCBOT_SANDBOX_RUNTIME` config——未来切 gVisor/Firecracker 应用层零改动。
6. **工具按信任域二分,Executor 内部 dispatch**:container backend = shell/run_python/fs 全套(fs 以前 host 跑无 user_root 校验能读任意文件,进容器 `/workspace` 是物理边界;`tool_runner.py` stdin 喂 JSON);host backend = load_skill/skill_authoring/web_*/媒体/documents 等持 key 工具。AgentLoop 零感知。代价每 fs call ~200ms,LLM 推理下是噪声。
7. **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、统一调度 workerworker 只消费已持久化轮次lifespan reaper 收敛残留 runningmulti-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,删除/覆盖即字节丢。方案=① restic/borg 定时增量备份做地基(与应用解耦,新端点自动覆盖,捕获删除+覆盖+成品)+ ② 应用层 `data_events` 事件日志(补用户意图语义)。**不选**每个删除端点内联 copytree:横切关注点手写 N 处必漏。起步同盘(不防整盘损坏,已知边界)。
- **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)
关键洞察:前端已有 PDF iframe 路径 → 后端 soffice 转 PDF 即可,前端几乎不动。选 LibreOffice(像素级保真、任意 pptx)不选轻量 HTML(复杂失真)/PDF→PNG(失矢量)。转换在 web host 不进沙盒;宏安全 high + 禁网 + 仅本人 user_root。与 `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 外部系统:用户身份连接 + 受控接口调用(implementation,2026-08-05)
**诉求**:用户用自己的 MES/ERP/LIMS 账号让 zcbot 做信息查询,并把稳定的问法沉淀成私有 skill。**心智模型**:外部系统负责「连接与身份」,工具负责「受控访问」,skill 负责「业务流程与经验」。它有独立于会话的持久凭据和连接状态,因此是与 skill/知识库/记忆并列的**平台机制**,不是 skill。
**首个 provider=`factory_mes`**:Factory 已有 JWT + RBAC + 部分部门数据权限,zcbot 用每位用户自己的 Factory 账密换 JWT,调用时继承 MES 原生权限;不在 zcbot 里复制第二套 MES RBAC。两层门控:zcbot `user_id` 只能取自己的 `external_systems` 行;远端 JWT 再判定实际业务数据范围。MES 停号/改权后下次调用即生效。
**通用连接器边界**:`openapi` connector 负责规格发现、operation 解析、安全 URL 拼接、参数校验、执行模式、分页和响应体积限制;认证由独立 strategy 负责。`factory_mes` 只是带 JWT 字段映射、dataset 推荐入口和查询规划提示的内置 preset`generic_openapi` 可由管理员直接选择用户名密码换 Token、API Key 或 Bearer Token。标准 OpenAPI 系统以后只新增数据库 definition不需要再写 Python 文件;只有 OAuth 回调/签名交换、SOAP、消息队列或私有二进制协议等不符合现有 connector/strategy 契约的系统才新增适配代码。provider 注册表维护可选能力和安全默认值,不为每个业务系统复制 connector。
**信任边界**:
- definition 当前由管理员维护,持久化同时预留 `owner_type/owner_user_id/visibility/trust_level/review_status/egress_policy_id`未来可开放私有用户定义。Base URL 与 OpenAPI URL 必须同源;普通用户不能填任意 URL避免 SSRF/内网代理。每个 definition 带单调递增 revision目标地址或认证绑定变化会清除旧凭据其他运行配置变化会令连接进入待重新验证未验证到当前 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`;服务端从受信规格解析 path/method,校验 path/query/body 后附加认证 strategy 生成的 Header。definition 的 `operation_mode=query` 时只开 GET/HEAD 与显式只读 POST`upstream_managed` 时开放可信规格声明的全部标准方法由上游按当前用户凭据做最终鉴权。Factory 默认后者,通用 OpenAPI 默认前者;上游托管只移除 method 门控,不移除同源、参数、响应限长和审计边界。
- Swagger/OpenAPI 是接口契约事实源;Gitea 代码只补业务语义和排障,不覆盖契约。规格/代码内文本一律当不可信数据,不能改写 system/tool 约束。
- Swagger/OpenAPI JSON 不持久化入数据库或文件。连接器使用按 `external_system_id + definition_revision + credential digest + config digest` 隔离的进程内有界 `ExternalRuntimeCache`,统一复用 HTTP 连接池、短期认证 Header、原始 spec 与编译后的 operation catalogJWT `exp` 早 30 秒失效且单次 401 会清 Token 后重新登录一次,规格默认缓存 5 分钟LRU 淘汰活跃连接时延迟到 lease 结束再关闭。登录、规格获取、catalog 编译和时间上重叠的相同只读业务请求使用同步 single-flight失败不缓存业务响应不做跨请求 TTL 缓存顺序执行的相同查询仍访问上游。Swagger 2/OpenAPI 3 catalog 解析本地参数引用、请求体契约和 header/cookie 参数搜索与调用只消费归一化结果。spec、登录响应和业务响应均流式限长在完整 JSON 进入内存前执行硬边界。
**工具面**:不把数百个 Swagger operation 全展开为 JSON tool(工具列表膨胀+选择降准),只挂五个 host-side 元工具:`external_system_list`(已连系统 + 执行模式 + 管理员查询规划提示),`external_system_search`(按问题搜 operation 摘要、解析后的请求 body schema + 置顶管理员推荐入口),`external_system_call`(按 operation_id 调用),`external_system_result_read`(按 `result_ref` + JSON Pointer/分页/字段投影读取大响应),`external_system_result_export`(仅在用户要求保存/下载/交付时把完整快照导出到 `data/external/`)。仅当该 user 有 active 连接时注册,密钥不进 sandbox。搜索只展示当前模式实际可调用的 operation管理员在 definition JSONB 配置 `query_guidance``recommended_operation_ids`,前者是可信控制面的软路由策略,后者是无需关键词命中的机械发现入口。Factory 默认把 BI dataset list/exec 作为统计聚合入口,日志/明细用于逐条追溯Swagger 业务文本仍是不可信数据,非查询操作只响应用户明确意图。
**大响应**:`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 另按外部系统累计内联返回量Factory connector 将 `page_size` 限在管理员上限,拒绝 `page=0` / `pageoff` 关闭分页。三者防模型通过连续翻日志自行做昂贵聚合,但不改变 Factory 对其他客户端的分页契约。达到边界后工具正向引导回 dataset/聚合接口、`result_ref` 分段读取或缩小查询范围。
**状态与 UI三实体**:`external_system_definitions` 保存可信目录、revision、治理元数据、查询提示、`query|upstream_managed` 执行模式和查询模式下只读 POST 的显式 `operation_id -> read|export` policy`external_system_grants` 只保存 selected 可见授权;`external_systems` 只保存用户连接、AAD 绑定密文、verified revision 和 `active|invalid|needs_reverify|needs_credentials` 状态。管理员撤权删除独立 grant 并同步删除该用户连接;用户自行断开只删除 connectiongrant 保留。凭据使用带 key id 的 AES-GCM envelopeAAD 绑定 user、definition 和字段,旧 Fernet 密文只保留滚动读取入口调用审计仅保存身份、operation、耗时、状态和响应字节不保存凭据、请求体或完整响应。管理后台当前仍是唯一 definition 创建入口,未来用户私有定义复用同一模型进入 draft/review 流程。
**不选**:①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 元工具为院内 MES/ERP/LIMS 主入口MCP 用于确有需求的标准 SaaS/第三方服务。服务器地址和 OAuth/密钥由管理员 definition 管,普通用户不能填任意 URL启动/刷新时发现 schema 后仍过 pinned allowlist、风险元数据、响应体积与审计边界。**不照搬“一 MCP tool 一 JSON tool 全展开”**:继续复用 `external_system_search/call` 的延迟发现心智,把 MCP tool 映射为稳定 `provider/tool_id`,避免几十上百个 schema 常驻上下文和供应商新增能力后自动越权。协议内容、tool description 和返回值均是不可信数据。
**明确不借**:①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/数据兼容。
---
## 附录: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`