# 设计文档

> 本地运行的个人任务 agent,覆盖三类工作:汇报 PPT、科研申报书、代码。
> 模型自由(LiteLLM 接 OpenAI-compatible),代码可控。本文只记**架构与取舍(为什么)**;进度/历史见 PROGRESS,怎么跑见 RUN。

---

## 1. 边界

**做**:PPT / 申报书 / 编码(读写文件 + shell + 迭代验证)。
**不做**:子 agent / 自定义 RAG / 锁定 Anthropic / Eval Suite(dogfooding 替代)。多用户 / Web 归 §7。

**关键约束**:模型自由(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
│   ├── probe.py            # 真实探测对账 yaml 声称的能力
│   ├── session.py          # 消息列表 + meta,落 PG
│   ├── skills.py           # SkillRegistry(渐进披露,多来源)
│   ├── task.py             # TaskState
│   ├── memory.py           # per-user .memory/ 双层记忆
│   ├── shortcuts.py        # 快捷指令(入口层确定性展开)
│   ├── paths.py            # task_dir db form 归一
│   ├── storage/            # SQLAlchemy 2.x ORM
│   ├── scheduler.py        # 定时任务(§8.5)
│   ├── 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 / 媒体 / 检索 / host-side 域工具
├── skills/<name>/          # SKILL.md + references / scripts / assets
├── rendering/              # 平台渲染层 md→docx/pdf(§8.6)
├── prompts/system/general_v1.md
├── config/{agent.yaml, models/*.yaml, media/*.yaml}
├── workspace/users/<user_id>/{.memory/, .skills/, <working_dir>/}
├── web/{app.py, auth.py, admin.py, broker.py, sinks.py, static/}
├── db/migrations/          # alembic
└── main.py                 # 入口:web / db / probe / user
```

**工作目录** = `workspace/users/<user_id>/<working_dir>/`,所有 skill 产物写这里,绝对路径注入 system prompt。`user_id` 走 JWT `sub`,**无 SENTINEL fallback**。`name`(显示名)必填、`working_dir` 可选(留空用 name);都是简单名(拒 `/\..`、`.` 起头);同 working_dir 多 task 共享同目录(§7.1)。SaaS 化只换外层根目录,布局不变。

**启动**:`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 声明"该工作流用这个模型最好"(如 ppt→glm.pro52)。**单一执行点**:对话中 `load_skill` 命中 → run 内热切(loop 换 `self.llm/self.caps`,下一轮生效)+ 持久化 task 模型;切失败降级原模型。取舍:**跳档位门控**(产品决策,任何档位可用);**只信内置 skill**(用户 skill 的 model 忽略,防自写 frontmatter 绕门控);**不自动切回**("skill 结束"不可判定);**不设 DB 开关、不做建 task 预切**——frontmatter 那一行本身就是热配置(删行即停,per-skill 粒度),全局开关是第二事实源、预切是第二执行点,都砍。

### 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`=显示名(独立于目录)、`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,正是要绕开的坑。

---

## 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 够用。

---

## 5. 设计哲学

**Less Scaffolding, More Trust**:把 LLM 当会持续变强的同事,告诉它目标不告诉步骤;脚手架在模型升级后会变枷锁。

七条:① prompt 用 WHY+WHAT 不用 HOW;② skill 渐进披露;③ 工具原子切分留组合空间;④ Model Profile 化不硬编码;⑤ probing 对账;⑥ 版本化 prompt(真要切再做);⑦ ~~eval~~(dogfooding 更有效)。

借鉴:CoreCoder(主循环 + edit 唯一匹配)/ Anthropic Skills(渐进披露)/ nanobot(workspace 隔离)/ smolagents(LiteLLM + CodeAct)。

---

## 6. 风险与取舍

| 风险 | 缓解 |
|---|---|
| 本地 run_python 非真隔离 | 工作目录限制 + env 过滤;SaaS 走 docker(§7.5);本地靠用户审阅 |
| V4 复杂任务不如 Claude | dogfooding 判断,fallback 手动切 |
| skill description 触发不准 | 实战观察迭代 |
| long context 退化 | probe 探测可靠 ceiling |
| 本地 PG 离线 | docker compose 起本地 PG / 连远端 |

**Hybrid 而非纯 CodeAgent**:V4 JSON tool call 已稳,sandbox 成本按需付。**不做 subagent**:状态管理爆炸,单 agent + skill 覆盖 95%。**不做 Eval Suite**:单用户 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)。

### 7.2 资源模型(/v1)

统一 `/v1` 前缀返 JSON;UI 由 platform 实现(§7.9),本地 dev SPA dogfood。要点(细节见 `web/app.py`):

```
Tasks     POST/GET/PATCH/DELETE /v1/tasks*(分页+筛选+ordering allowlist;DELETE=软删,FS 不动)
          GET /v1/folders(working_dir + task 计数)
          GET/POST /v1/tasks/{id}/messages(POST 起 run;单活 run:running/cancelling→409,
            SELECT FOR UPDATE 锁 task 行防 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)
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 / 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, 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, 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`/`.` 起头。
- **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/cancelled 收回 idle,只有 error 持久。
- **No-subtask**:同 user 下前缀互含即拒(归一 posix 后 Python 端比对);同 working_dir 允许。
- **files API 是目录树唯一 mutation 入口,DB-FS 一致性服务端内化**:顶层目录 rename/delete 走 DB-aware 分支(事务锁关联 task、running→409、DB UPDATE 先于 FS、delete 被引用→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)、loopback、内网三段、CGNAT `100.64/10`;**PG 实际 IP 单独再 block**(belt-and-suspenders)。
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 命名隔离 |
| 并发 POST 撞 messages.idx | 单活 run gate(FOR UPDATE + 409)+ lifespan reaper;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;心智=**平台对数据 append-only,"删除"是可见性状态**。物理清理留管理员工具。
- **文件留存(设计已定,实现待办)**:用户文件在 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 配置动作)。

### 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 按需)。

### 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 双 profile)+ docx_brief + pdf(chromium 不用 weasyprint——镜像已有,保真更高)+ render.py 统一 CLI。重构前后 docx 字节一致零回归;brief 不强并 manuscript(差异大,只共叶子)。与 §8.3 分工:pptx 预览在 host 面向用户,本层在沙盒面向 agent 交付物。

### 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(design):阈值结构化摘要**(补 Hermes 阶段③):中段轮压成固定模板(目标/约束/进展/待办),增量更新、保留 path/ID/数值原文(mem0 实测摘要会静默丢精确值);双层门槛 50%+85%。分段砍跨话题累积、摘要兜单段超长,正交。
- **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 抽检,而非逐页。

---

## 附录: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-24 下线 → 必须迁 `deepseek-v4-flash/pro`
