# Easel 源码分析：不写主循环，把 OpenClaw 焊成社媒内容工作台

> **分析对象**：Easel（[`ZJU-REAL/Easel`](https://github.com/ZJU-REAL/Easel)），基于 **commit `765f5a6`**（2026-09-10 10:52:51 +0800，`main`）。许可 **Apache-2.0**。版本号 `pyproject.toml` 写 **0.1.0**，`easel/__init__.py` 仍是 `0.0.1`——两处还没对齐。
> **代码规模**（sparse 检出，跳过 `assets/` 宣传视频）：**705 个文件**。真正干活的代码是 **`easel/` 9 个 Python 文件 / 768 行** + **`web/app.py` 2 509 行** + **前端 `web/frontend/src` 约 5 238 行 TS/TSX** + **112 个 OpenClaw SKILL** + **44 个 `skills/shared/scripts/*.py`**。测试 **650 行**。仓库很大（GitHub 上约 300MB+），绝大部分是展示素材。
> **社区体量**：**733 star / 90 fork / 2 open issue**（2026-09-10 GitHub API）。仓库 **2026-08-28** 创建——分析当天大约两周。浙大 REAL Lab + 北大 OpenDCAI。主页 [zju-real.github.io/Easel](https://zju-real.github.io/Easel/)。
> **一句话定位**：**面向国内社媒创作者的开源内容工作台**。它自己**不写 Agent 主循环**，把 [OpenClaw](https://github.com/) 当成执行引擎，用账号画像、112 个可执行 SKILL、真实浏览器发布，把「发现 → 策划 → 创作 → 发布 → 归因」焊成一条能落盘、能真发的流水线。
> **读者对象**：读过本系列任一份分析的读者。**请先换轴**：前面多数项目在回答「怎么让 AI 写代码 / 干活」；Open Design 回答「怎么不写循环、把别人的 CLI 当引擎做设计」；MiroFish 回答「怎么让一群 AI 把未来预演一遍」。Easel 坐在 Open Design 同一侧——**宿主**——但客体不是设计文件，是**小红书卡片、短视频、知乎回答，以及它们发到真实平台之后的数据**。

---

**目录**

**第一部分 · 它是什么**
1. [项目概览：两周 700 星，它到底在卖什么](#ch1)
2. [技术栈全解：一张薄整合层 + 一个借来的引擎 + 一座技能库](#ch2)
3. [五层工作流：产品叙事，不是运行时调度器](#ch3)

**第二部分 · 宿主怎么接住引擎**
4. [三入口单一真相源：chat / skill / web](#ch4)
5. [Prompt 四层栈：SOUL → AGENTS → CONTEXT → SKILL](#ch5)
6. [sync.sh：workspace 是剧本，项目根才是工地](#ch6)
7. [超时、代理、doctor：把环境做成可检查的产品](#ch7)

**第三部分 · 技能即产品**
8. [SKILL-SPEC：三层加载与 200 行上限](#ch8)
9. [112 个 Skill：按层看完这座库](#ch9)
10. [outputs 契约：内容是项目，不是聊天记录](#ch10)
11. [代表作：xhs-note-creator 怎么把一张笔记做出来](#ch11)

**第四部分 · 真发与工作台**
12. [两道闸门：content_guard 硬拦，persona_gate 只提醒](#ch12)
13. [登录与一键发布：Playwright + 短信墙](#ch13)
14. [SSE 对话：断线不杀、双锁、uuid5、thinking 自愈](#ch14)
15. [前端工作台：11 页 SPA 与跨标签会话](#ch15)

**第五部分 · 评价**
16. [真正新的东西](#ch16)
17. [工程质量：亮点与硬伤](#ch17)
18. [对做新产品的十二条启发](#ch18)
19. [它在本系列里的位置](#ch19)
20. [🔍 源码指路表](#ch20)

---

<h2 id="ch1">第 1 章 项目概览：两周 700 星，它到底在卖什么</h2>

### 1.1 先看一组反差数字

| 指标 | 数值 |
|---|---|
| Star | **733**（分析当日） |
| Fork | 90 |
| 仓库年龄 | **约 13 天**（2026-08-28 → 2026-09-10） |
| Python 整合包 | **768 行 / 9 个文件** |
| Web 后端 | **`web/app.py` 单文件 2 509 行** |
| 前端 | **约 5 238 行 TS/TSX** |
| Skill | **112 个**（discover 9 / plan 16 / produce 50 / publish 20 / attribute 11 / general 6） |
| 共享脚本 | **44 个** `skills/shared/scripts/*.py` |
| 许可 | Apache-2.0 |
| 主页 | https://zju-real.github.io/Easel/ |

**Python 包只有 768 行，Skill 有 112 个。** 这组数字本身就是架构声明：

> Easel 的产品不在「再写一个 Agent 循环」，而在「给一个现成的 Agent 配上社媒领域的手、规矩和工位」。

README 开篇那句最重要：

> 你的私人、持续进化的社媒运营助手。从一个想法开始，完成发现、策划、创作、发布与复盘。

六个平台写进能力表：小红书、抖音、快手、知乎、B 站、微信视频号。研究愿景写得很直白——把实验室里的社交智能，接到创作者每天真的要发的那条内容上。

### 1.2 它明确不是什么

| 不是 | 是 |
|---|---|
| 又一个编程 Agent | 社媒内容工作台 |
| 自己实现的 ReAct 循环 | OpenClaw 的 **profile = `easel`** 隔离工作区 |
| 「功能清单」式的 skill 目录 | **带可运行脚本、产物落盘、真发闸门**的技能库 |
| 全局一份 USER.md / MEMORY.md | **每个请求自包含的画像前缀** + 按画像隔离的 `memory.md` |
| 聊天记录里的半成品 | `outputs/<人类可读主题>/` 的项目目录 |

README 还写了两条使用警告，值得当成产品约束读：

1. **推荐 Web 前端**，CLI 只是入口之一。
2. **谨慎自动发布到小红书**——平台可能检测自动化，有验证、限流、风控。预览 + 人确认后再发。

### 1.3 谁在做、向谁致谢

浙大 REAL Lab + 北大 OpenDCAI。`docs/ACKNOWLEDGMENTS.md` 把 112 个 Skill 的来源摊开：卡片模板明确写了借鉴 **nexu-io/open-design**，去 AI 味借鉴「说人话」，模板复用借鉴 fabric……每个 Skill 目录另有 `EASEL-META.md` 记来源。这很诚实：**技能库是汇编 + 自研，不是从零发明社媒方法论**。工程贡献在把它们收成**同一套契约**（frontmatter、outputs、闸门、画像）。

> 🧠 **一句话**：Easel 卖的是「会记住你账号的内容搭档」，工程上它是 **OpenClaw 的垂直宿主**。本系列里，Open Design 把 25 个 CLI 当引擎做设计；Easel 把 **1 个** OpenClaw 当引擎做社媒。

---

<h2 id="ch2">第 2 章 技术栈全解：一张薄整合层 + 一个借来的引擎 + 一座技能库</h2>

### 2.1 装配图

```mermaid
flowchart TB
    subgraph UI["① 你能看到的"]
        WEB["React 工作台<br/>对话 / 技能库 / 内容库 / 账号 / 画像<br/>热点 / 日历 / 发布 / 复盘"]
        CLI["easel chat / skill / doctor"]
    end
    subgraph HOST["② 整合层（Easel 自己写的）"]
        PY["easel/ 768 行<br/>persona · timeouts · CLI"]
        API["web/app.py 2509 行<br/>SSE · 登录 · 发布 · 产物树"]
        SS["skills/shared/scripts 44 个<br/>output_paths · manifest · content_guard"]
    end
    subgraph ENGINE["③ 借来的引擎"]
        OC["openclaw --profile easel<br/>gateway :18789"]
        WS["~/.openclaw/workspace-easel/<br/>SOUL.md + AGENTS.md + skills 副本"]
    end
    subgraph DISK["④ 项目磁盘（唯一真相）"]
        SK["skills/openclaw/ 112 SKILL"]
        PF["profiles/ 六维画像"]
        OUT["outputs/ 内容项目"]
        ENV[".env 模型与媒体 Key"]
    end

    WEB --> API
    CLI --> PY
    API -->|subprocess + 画像前缀| OC
    PY -->|subprocess + 画像前缀| OC
    OC --> WS
    WS -.symlink.-> PF
    WS -.symlink.-> OUT
    OC -->|"cd 到项目根再跑脚本"| SK
    SK --> SS
    SS --> OUT
```

### 2.2 完整技术栈

| 层 | 技术 | 源码位置 |
|---|---|---|
| CLI | argparse 子命令：`chat` / `doctor` / `gateway` / `ping` / `skill` / `web` | `easel/cli.py:124-171` |
| 画像 | 六维 md 拼接 + 消息前缀 + 每轮 TURN_REMINDER | `easel/persona.py` |
| 超时 | `TIMEOUT_PRODUCE=7200` / `TIMEOUT_DIRECT=300` / `TIMEOUT_CHAT=7200` | `easel/timeouts.py:11-13` |
| Web | FastAPI + SSE（sse-starlette）+ CORS `*` | `web/app.py:190-191` |
| 前端 | React + Vite，11 个页面组件 | `web/frontend/src/App.tsx`、`Sidebar.tsx:11` |
| 引擎 | 全局 `openclaw` CLI，profile 名写死 `"easel"` | `easel/cli.py:31`、`web/app.py:46` |
| Gateway | `127.0.0.1:18789/healthz` | `easel/commands/doctor.py:75-81` |
| 发布 | Playwright Chromium + biliup + 各平台脚本 | `pyproject.toml:20-21`、`web/app.py:1996-2053` |
| 媒体 | Pillow / OpenCV / faster-whisper / edge-tts / rembg / librosa | `pyproject.toml:18-21` |
| 运行时 | Python ≥3.10、Node ≥22.19、FFmpeg | `easel/commands/doctor.py:148-158` |

`pyproject.toml:25-26` 的 CLI 入口只有一行：`easel = "easel.cli:main"`。Web 不是 Python 包里的 ASGI 应用，而是 `easel web` 再 `subprocess` 去跑 `web/app.py`（`easel/cli.py:157-166`）。

### 2.3 被否方案，从注释里读出来

整合层非常薄，但注释里全是「我们试过、翻过车、才收敛到这里」：

| 被否方案 | 为什么否 | 现在怎么做 |
|---|---|---|
| 全局 `USER.md` 写当前画像 | 并发会话互相覆盖 | 画像作为**消息内联**（`easel/persona.py:1-7`） |
| 全局 `MEMORY.md` 承载账号知识 | 跨画像污染 | sync 时把 workspace 的 `MEMORY.md` **清空**（`openclaw/sync.sh:101-103`） |
| 三入口各写各的超时 | 同一任务经 CLI 能跑完、经 Web 被掐断 | `easel/timeouts.py:1-8` |
| 三入口各写各的画像逻辑 | 行为不一致 | 收敛到 `easel/persona.py` |
| `openclaw.json5` 当生效配置 | setup 从不应用它，会和真实配置漂移 | 文件自己在头部警告（`openclaw/openclaw.json5:2-5`） |
| OpenClaw 默认 `thinking=high` | thinking 块存进历史时**丢签名**，Bedrock 回放 400 | 默认 `low` + `session_heal` 清洗（`web/app.py:51-54`） |
| 只靠 `--session-key` 续会话 | 空闲约 24h 绑定过期，隔天历史全丢 | 用 uuid5 **钉死 `--session-id`**（`web/app.py:823-833`） |

> 🧠 **一句话**：Easel 的 Python 代码少，是因为它把循环外包了；它真正花力气的地方，是**把外包之后必然出现的竞态、超时、会话丢失、密钥泄露，一个一个焊死**。

---

<h2 id="ch3">第 3 章 五层工作流：产品叙事，不是运行时调度器</h2>

### 3.1 五层写在哪

产品层（README）和 Agent 层（AGENTS.md）用的是同一套词：

| 层 | README 说法 | AGENTS.md 说法 | Skill `layer` 字段 |
|---|---|---|---|
| 发现 | 热点与机会 | 热点、爆款、二创机会 | `discover`（9） |
| 策划 | 选题、标题、脚本、排期 | 选题、脚本、分镜、封面构思 | `plan`（16） |
| 创作 / 制作 | 图文、音频、视频 | 任何要创建文件的任务，写入 `outputs/` | `produce`（50） |
| 发布 | 检查、适配、真发 | 合规、平台适配、登录、真发 | `publish`（20） |
| 归因 | 表现回流画像 | 数据、评论、Profile 回流 | `attribute`（11） |
| （横切） | 工作台基础 | — | `general`（6） |

`openclaw/workspace/AGENTS.md:41` 写得很克制：

> 清晰单层任务直接执行；跨两层以上或明显多步骤任务先给简短 Plan。一个任务可组合多个 SKILL，但不要运行无关层。

**没有**一个 Python 类叫 `WorkflowEngine` 按层调度。五层是写给模型看的**分工规则**，落地靠两样东西：

1. 每个 SKILL.md 的 YAML `layer:`（路由与文档）
2. 跨层时 `manifest.py` 在 `outputs/<主题>/.easel.json` 里记「产物路径 + 一句结论」（`docs/SKILL-SPEC.md:136-149`）

### 3.2 纵向编排的薄索引

AGENTS.md 对跨层的规定（`openclaw/workspace/AGENTS.md:43-45`）：

- 跨两层以上才建 manifest；单层不建
- 每层完成**或失败**都登记
- 下游用 `latest` / `read` 取上游，**不重新推导、不整块转发**
- 完整载荷写文件，关键决策写 `brief.md`

`docs/SKILL-SPEC.md:157-159` 把原则说得更狠：manifest **只当薄索引**，`summary` 一行给编排层路由，`outputs[]` 指路径，**不复制内容**。

这和编程 Agent 把中间结果堆在对话里，是相反的哲学：**对话是控制面，磁盘是数据面**。

```mermaid
sequenceDiagram
    participant U as 用户
    participant A as OpenClaw Agent
    participant M as manifest.py
    participant D as outputs/主题/
    U->>A: 帮我做一条小红书并排期发布
    A->>A: Plan：discover → plan → produce → publish
    A->>D: 写 brief.md / 卡片 / 文案
    A->>M: record --layer produce --outputs card_1.png,...
    A->>M: latest --layer produce
    M-->>A: 路径 + 一句结论
    A->>D: 发布脚本 --exec
    A->>M: meta --status published
```

### 3.3 制作层的特殊地位

AGENTS.md 把「制作」定义成**凡是要创建文件的任务**（`openclaw/workspace/AGENTS.md:31`），不只是「好看的视频」。超时也按这个定义给预算：chat 入口按制作层 7200 秒，因为对话中途可能生视频（`easel/timeouts.py:7-13`）。

制作流程固定五步（`openclaw/workspace/AGENTS.md:53-63`）：凝练 Profile → 明确规格 → 按 SKILL 产出 → 自检 → **仅失败时返工一次**。制作层**不读** `platforms.md`，原样遵守 preferences 红线。这是故意的：做内容时先把东西做对，平台适配留给发布层。

---

<h2 id="ch4">第 4 章 三入口单一真相源：chat / skill / web</h2>

### 4.1 三个入口，同一句话

| 入口 | 怎么进 OpenClaw | 画像怎么注入 | 超时 |
|---|---|---|---|
| `easel chat` | `openclaw --profile easel chat --session ... --message <prefix>` | 选画像后，前缀作为**初始消息** | `TIMEOUT_CHAT`（`easel/cli.py:103-117`） |
| `easel skill` | `openclaw agent --session-key skill-<ts> --message "请执行 /<skill>…"` | `persona_prefix` 拼在指令前 | 一律 `TIMEOUT_PRODUCE`（`easel/commands/skill.py:156-164`） |
| Web `/api/chat/stream` | 同上 `openclaw agent`，外加 `--session-id` 钉死 transcript | `chat_turn_message` = 前缀 + 用户原文 + TURN_REMINDER | `TIMEOUT_CHAT`（`web/app.py:1080-1084`） |
| Web `/api/skill` | `run_agent_sync` | `_persona_prefix` + `请执行 /…` | `TIMEOUT_PRODUCE`（`web/app.py:1453-1463`） |

CLI `chat` **不加** TURN_REMINDER，Web 对话才加。`easel/persona.py:72-76` 解释了为什么：SKILL 单跑不必加；对话会长，系统提示会衰减。

### 4.2 画像前缀长什么样

```text
我当前使用的画像是「科技数码达人」。本会话的账号长期记忆仅使用 profiles/科技数码达人/memory.md，不要使用工作区全局 MEMORY.md 作为账号记忆。
```

实现：`easel/persona.py:58-69`。六维文件顺序写死（`identity / style / audience / platforms / preferences / memory`，`easel/persona.py:16-20`）。`load_profile_text` 按这个顺序拼接，其它 `.md` 追加——但**注入进 OpenClaw 的并不是全文**，只是「去读哪个目录」的指针。真正凝练由 AGENTS.md 要求模型自己去读 `easel-profiles/<名>/`。

### 4.3 每轮行为提醒：用近因效应对抗指令衰减

`TURN_REMINDER`（`easel/persona.py:77-83`）拼在**用户消息末尾**，并标明「内部提醒、勿复述」。内容就三句：

1. 动手前先查技能库，别凭记忆裸做
2. 五层都由你自己把成品写到 `outputs/`
3. 问账号先查登录态；对外文案不许泄密钥、也不许自曝「AI 生成」

注释把动机写得很清楚（`easel/persona.py:72-76`）：AGENTS.md / SOUL.md 只在会话开头新鲜，长对话后面模型会「凭记忆裸做」。把最关键的反射放末尾，成本极低。

这是本系列里很少被写成代码的一件事：**系统提示会衰减，所以把承重规则再喂一遍，而且喂在用户话后面。**

### 4.4 skill 入口如何消化输入

`easel/commands/skill.py:48-66`：输入可能是文本，也可能是路径。图片改写成「请处理这个图片：绝对路径」；音视频/PDF/压缩包只传路径，避免 `read_text` 把二进制当 UTF-8 炸；普通文本文件才读进消息。过长的字符串（超过文件名上限）`Path.is_file` 会抛 `OSError`，被当成文本——`tests/test_core.py:71-74` 专门锁了这个坑。

Skill 名允许省略 `skill-` 前缀（`easel/commands/skill.py:39-45`），Web `find_skill` 同一套逻辑（`web/app.py:213-219`）。

---

<h2 id="ch5">第 5 章 Prompt 四层栈：SOUL → AGENTS → CONTEXT → SKILL</h2>

`docs/prompt-stack.md` 把组合顺序写死。对照 Open Design 的「20 层按缓存频率分带」，Easel 只分四层，而且**常驻层刻意不写具体 skill 名**。

### 5.1 Layer 1 — SOUL.md（人格，30 行）

`openclaw/workspace/SOUL.md`：你是创作者的「搭子」——懂策略，也能上手。能力总览按五层写「能做什么」，**不写 skill 名**，避免像某个平台下架那样过时（`docs/prompt-stack.md:25`）。沟通风格：中文、给 2–3 个选项、对外文案绝不暴露工具痕迹。

### 5.2 Layer 2 — AGENTS.md（业务宪法，110 行）

这是最重要的文件。核心执行规则六条（`openclaw/workspace/AGENTS.md:5-12`）：

1. **先路由 SKILL**——精确匹配 → 最接近 → 才用通用能力
2. **先到项目根**——第一个脚本前必须 `cd` 到 sync 注入的绝对路径
3. **不在 workspace 跑项目副本**——禁止从 OpenClaw workspace 的 `shared/` 跑脚本
4. **查现有信息再提问**——登录态、画像、历史产物先查
5. **付费操作先确认**——生图/生视频/音乐先给范围和费用
6. **真实产物才算完成**——计划、空壳、提示词不算

另外还有一整节「配置检查」：模型 Key 只能以项目根 `.env` 为准；`env` / `printenv` 看不到未 export 的值；workspace `ls -a` 也看不到项目根 `.env`——二者都不能用来宣称缺配置（`openclaw/workspace/AGENTS.md:16-23`）。这是被真实翻车逼出来的：Agent 在错误的 cwd 里喊「你没配 Key」，用户其实配好了。

### 5.3 Layer 3 — CONTEXT.md（半静态路径）

由 `sync.sh` 生成，含项目绝对路径。注释强调：**本 claude 版本不支持 `--cwd`**，所以必须先 `cd` 再跑脚本（`openclaw/sync.sh:128-137`）。

### 5.4 Layer 4 — SKILL（按需）

触发时加载 SKILL.md，references 按引用再读，scripts **执行时调用、代码不进 prompt**（`docs/prompt-stack.md:36-39`）。这和 Claude Code 的 skill 三层披露是同一类想法，Easel 把它写进了自己的 SKILL-SPEC。

```mermaid
flowchart LR
    S["SOUL.md<br/>人格 · 类目级能力"] --> A["AGENTS.md<br/>分工 · 编排 · 安全"]
    A --> C["CONTEXT.md<br/>项目根绝对路径"]
    C --> K["SKILL.md<br/>被触发才加载"]
    K --> R["references/<br/>领域知识按需"]
    K --> SC["scripts/<br/>不进 prompt"]
    P["画像前缀<br/>每条消息"] -.-> A
    T["TURN_REMINDER<br/>仅 Web 对话末尾"] -.-> K
```

---

<h2 id="ch6">第 6 章 sync.sh：workspace 是剧本，项目根才是工地</h2>

### 6.1 隔离 profile 的真实路径

```
workspace → ~/.openclaw/workspace-easel/
config    → ~/.openclaw-easel/openclaw.json
```

（`openclaw/sync.sh:6-8`）

`--profile easel` 把 Easel 和用户机器上可能存在的其它 OpenClaw 工作区切开。

### 6.2 同步做了什么

1. **删除源里已经没有的 SKILL**（`openclaw/sync.sh:30-38`）
2. **整目录 `cp -r` 每个 SKILL** 到 workspace/skills（`openclaw/sync.sh:46-53`）
3. **复制 `skills/shared/`** 到 workspace/shared（给 SKILL 里 `../../shared/` 相对引用）
4. **复制 AGENTS.md / SOUL.md**，再**追加「运行时项目根」绝对路径**（`openclaw/sync.sh:77-93`）
5. **删掉残留 USER.md**，**清空 MEMORY.md**（`openclaw/sync.sh:97-103`）
6. **symlink** `easel-profiles` → 项目 `profiles/`，`outputs` → 项目 `outputs/`（`openclaw/sync.sh:106-124`）
7. 写 `CONTEXT.md`

symlink 那段写了个真实坑：必须先 `rm` 再 `ln -s`，否则 `ln -sf` 会跟着旧链接走进目标目录造成循环（`openclaw/sync.sh:107`）。

### 6.3 为什么脚本必须在项目根跑

`manifest.py:58-61` 自己招认：脚本会被 sync **拍平复制**到 `workspace/shared/scripts/`，那里 `__file__` 少一层 `skills/`，上溯会算成 `~/.openclaw`，产物写错地方。所以 **`EASEL_ROOT` 环境变量不可省**。CLI 和 Web 都在 `_proxy_env` 里 `setdefault("EASEL_ROOT", 项目根)`（`easel/cli.py:36`、`web/app.py:303`）。

AGENTS.md 同步后追加的那段，等于每轮都把「去哪 `cd`」焊进系统提示。workspace 里那份 `shared/` 只给模型读文档用，**不是执行 cwd**。

### 6.4 openclaw.json5 是一份不会生效的模板

文件头写得毫不留情（`openclaw/openclaw.json5:2-5`）：

> 本文件仅作参考，实际配置由 setup.sh 通过 `openclaw config set` 直接写入 `~/.openclaw-easel/openclaw.json`，此模板从不被 setup.sh 应用。

Gateway 端口 18789、模型 `anthropic/claude-sonnet-4-6` 只是示意。**读源码的人如果把 json5 当运行时配置，会错。** 这是一种少见的诚实：把「可能漂移的模板」标成危险品，而不是假装它是单一真相源。

---

<h2 id="ch7">第 7 章 超时、代理、doctor：把环境做成可检查的产品</h2>

### 7.1 超时三常数

```
TIMEOUT_PRODUCE = 7200   # 生视频 / 多镜合成
TIMEOUT_DIRECT  = 300    # 发现 / 策划 / 发布 / 归因
TIMEOUT_CHAT    = TIMEOUT_PRODUCE
```

（`easel/timeouts.py:11-13`）

chat 按制作层给预算，是因为用户在对话里随时可能说「做成视频」。skill CLI 一律用制作层上界，简单粗暴，避免再按 layer 分支出错。

### 7.2 代理：保护内网直连

`_proxy_env` 把 `EASEL_PROXY` 填进 `http_proxy` / `https_proxy`，同时 `no_proxy` 包含 `localhost`、`127.0.0.1`、`*.xiaohongshu.com`、`*.devops.xiaohongshu.com`、`10.*`（`easel/cli.py:33-40`）。这是一张实验室内网痕迹：小红书内部域名要直连，外网走代理。`content_guard` 稍后会把这些内部域名当成 **BLOCK 级泄露**。

### 7.3 doctor 检查清单

`easel doctor`（`easel/commands/doctor.py:143-207`）按产品依赖逐项打勾：

Python ≥3.10 / venv / Node ≥22.19 / FFmpeg / `openclaw` 在 PATH / fastapi·uvicorn·sse_starlette·multipart / 前端 `dist/index.html` / Playwright Chromium / `.env` 里任一认证通道 / gateway `:18789/healthz` / `~/.openclaw/workspace-easel/skills/` 非空 / 关键文件存在。

认证通道是「任一即可」（`easel/commands/doctor.py:92-140`）：`ANTHROPIC_API_KEY`，或 `EASEL_LLM_API_KEY + BASE_URL`，或 `ANTHROPIC_AUTH_TOKEN + BASE_URL`，或 `OPENAI_API_KEY`，或火山 MaaS。占位符 `REPLACE_ME` 不算配好。

`easel ping` 再做一次真连通：healthz + 让 agent 说 PONG（`easel/commands/ping.py:48-78`）。doctor 是静态存在性，ping 才是权威。

---

<h2 id="ch8">第 8 章 SKILL-SPEC：三层加载与 200 行上限</h2>

`docs/SKILL-SPEC.md` 是技能库的接口规范 v0.3。

### 8.1 目录与三层

```
skills/
├── openclaw/     五层 SKILL，OpenClaw 直接执行
└── shared/       跨 SKILL 脚本与配置
skill-xxx/
├── SKILL.md      必须，< 200 行，只写「怎么做」
├── references/   领域知识，按需加载
├── scripts/      运行时调用，代码不进 prompt
└── tests/        test1.prompt / test1.expected
```

| 层 | 加载时机 | token |
|---|---|---|
| Metadata（name, description, layer） | 常驻，用于路由 | 极小 |
| Instructions（SKILL.md 主体） | 被触发 | 中等 |
| Resources（references + scripts） | 执行中按需 | 按需 |

frontmatter **只保留三个常规字段**：`name` / `description` / `layer`。禁止 `version`、`allowed-tools`、`tags` 等（`docs/SKILL-SPEC.md:73-78`）。`description` 必须用中文写清能力、触发说法、与相邻 SKILL 的边界。

校验两条命令：`scripts/validate_skills.py`（frontmatter、资源链接、输出与发布安全契约）和 `scripts/validate_skill_commands.py`（SKILL 里的 Python 命令 vs 脚本 argparse，防路径漂移）。

### 8.2 设计约束五条

独立可调、无 Profile 也能用、接口稳定、SKILL.md 精简、泛化不给具体 case（`docs/SKILL-SPEC.md:167-173`）。

「无 Profile 也能用」决定了产品可以先玩起来再慢慢建画像；「独立可调」决定了 112 个目录可以并行改，而不是一张大网。

### 8.3 和编程 Agent 的 skill 有何不同

Claude Code / Codex 的 skill 多半是「教模型怎么用工具」。Easel 的 skill **自己就是一条制作流水线**：SKILL.md 里写着要跑哪条 `python skills/.../scripts/foo.py`，产物路径被 `output_paths.py` 门禁，发布前被 `content_guard.py` 扫描。Skill 不是说明书，是**带执行器的 SOP**。

---

<h2 id="ch9">第 9 章 112 个 Skill：按层看完这座库</h2>

数量以 `docs/skill-function-mapping.md` 与检出目录为准，frontmatter `layer` 统计：

| 层 | 数量 | 这一层在干什么 |
|---|---:|---|
| general | 6 | 产物管理、批量处理、登录账号查询、画像构建/管理、模板库 |
| discover | 9 | 热搜、竞品、内容缺口、跨平台差异、节日、行业资讯、RSS、算法更新、UGC |
| plan | 16 | 定位、受众、人设、选题矩阵、评分、日历、钩子、大纲、分镜、直播、商单 |
| produce | **50** | 文字 / 视觉 / 音频 / 视频 / 小说 / 短剧 / 论文解读——半壁江山 |
| publish | 20 | 六平台上传、跨平台分发、排期、质量门禁、评论回复、短链、通知 |
| attribute | 11 | 数据、评论洞察、ROI、复盘、画像记忆 |

制作层 50 个不是「50 种文案」，而是把 ffmpeg、TTS、生图、生视频、字幕、绿幕、卡点、相册……收成 Agent 可调的原子。共享脚本层才是真正的多媒体 SDK。

Web 侧 `get_skills()`（`web/app.py:270-286`）扫 `skills/openclaw/*/SKILL.md`，解析 description/layer，并对少数 Skill 标 `needsApi`（生图/生视频/音乐/克隆/短剧/论文解读，`web/app.py:142-170`）。前端技能库页用这张表决定要不要感叹号「去配 Key」。

`SKILL_API_REQUIREMENTS` 和 `skills/shared/scripts/model_registry.py` 共用同一真相——`tests/test_core.py:33-39` 断言 Web 的 provider 列表等于注册表，序列化结果里不得出现密钥。

### 9.1 制作层里值得单独点名的几类

| 类型 | 代表 Skill | 特点 |
|---|---|---|
| 平台总入口 | `xhs-note-creator` | 小红书图文/视频的 SOP，强制去 AI 化、走 card-design |
| 一键出片 | `auto-short-video` | 文案→配图/AI 视频→配音→字幕→BGM→合成 |
| 长内容 | `novel-writer` / `paper-explainer` / `short-drama` | 文件化状态、跨章一致性、剧集圣经 |
| 确定性媒体 | `image-editing` / `video-editing` / `audio-editing` | 不靠模型，靠 OpenCV/ffmpeg |
| 去 AI 味 | `text-polisher` | 七轮扫描 + 中文 AI 标记表，被其它 Skill 引用为权威源 |

`xhs-note-creator` 的 SKILL.md 明确写：整套笔记用本 SKILL；仅渲染卡片用 `card-xiaohongshu`；其它平台用 `social-content`（`skills/openclaw/xhs-note-creator/SKILL.md:4-6`）。**边界写在 description 里**，这就是路由的全部机制——没有 Python 路由器。

---

<h2 id="ch10">第 10 章 outputs 契约：内容是项目，不是聊天记录</h2>

### 10.1 目录规约

```
outputs/<人类可读主题>/
├── note.md / final.mp4 / card_1.png   成品，放项目根
├── assets/                            中间件：帧、切片、草稿
└── .easel.json                        展示头 + steps[]（隐藏）
```

禁止：泛名（`xhs` / `test` / `tmp` / `主题`……，`output_paths.py:25-29`）；成品散落 `outputs/` 根；内容写入 `_` 系统目录。系统目录白名单：`_analytics _debug _inbox _login _probe _profile_build _publish _scratch _sessions`（`output_paths.py:30-33`）。

`validate_output_path`（`output_paths.py:41-75`）是所有新脚本的门禁：解析到项目根、必须在 `outputs/` 下、内容至少两级路径、泛名直接抛 `OutputPathError`。系统写入必须 `allow_system=True` 且 top 在白名单。

### 10.2 manifest 的两份工作

`skills/shared/scripts/manifest.py`：

1. **展示头**（`meta`）：title / platform / kind / status / tags / cover / deliverables——给前端内容库做卡片
2. **steps[]**（`record`）：layer / skill / at / status(done|failed) / outputs[] / upstream[] / summary——给下游编排

`kind` ∈ article / xhs-note / video / cards / poster / audio / other；项目 `status` ∈ draft / ready / published。`atomic_write` 用 mkstemp + `os.replace`（`manifest.py:107-118`）。

Web `_read_project_meta`（`web/app.py:505-520`）只取展示字段，不把 steps 泄露给内容库 UI。封面解析：声明的 cover → 首个成品媒体 → 目录里第一张图/视频。

### 10.3 附件隔离

用户上传进 `outputs/_inbox/`，按会话隔离。本轮消息附「系统附件清单」，**只允许用清单里的路径**，禁止扫描 inbox 其它文件（`openclaw/workspace/AGENTS.md:91`、`web/app.py:790-795`）。纳入项目时复制到 `outputs/<项目>/assets/`，inbox 原件保留以便重试。

这是发布类 Agent 特有的威胁模型：inbox 里可能有别的会话刚上传的素材，模型如果 `ls` 一下就串味。

---

<h2 id="ch11">第 11 章 代表作：xhs-note-creator 怎么把一张笔记做出来</h2>

小红书是 Easel 的主场。这个 SKILL 把「笔记」定义成 **3–9 张 3:4 卡片或 15–90 秒竖屏分镜**，长文只是中间产物（`skills/openclaw/xhs-note-creator/SKILL.md:14-19`）。

工作流（不可跳步）：

0. Intake：主题 / 形态 / 素材 / 风格——一次问完  
0.5 卖点公式：稀缺性 × 实用性 × 可感知  
1. 有素材则 `analyze_material.py` 出清单  
2. 观点类必须外部参考，核心数据 ≥2 源  
3. 写 2000–4000 字长文原稿，**先给用户确认**  
4. **强制去 AI 化**——规则不在本 SKILL 维护副本，统一走 `text-polisher` 的 references（`SKILL.md:73-79`）  
5. 视觉走 `card-design` 九种风格，渲染走 `card-xiaohongshu`  
6. 质检、落 `outputs/`、登记 manifest

爆款五原则里有一句很关键：**视觉不能糙**。想要「素人/手账」也要选 card-design 的手账贴纸风格，而不是真的用糙 t2i 加大 emoji（`SKILL.md:25`）。这是 Open Design 那套「反 AI 味」在社媒卡片上的落地，ACKNOWLEDGMENTS 也承认卡片模板借鉴了 open-design。

脚本目录里有 `validate_meta.py`、`analyze_material.py`、`normalize_slug.py`、`crop_watermark.py`、`text_on_image.py`、`collage_3x4.py`——模型负责判断和文案，像素级操作交给确定性脚本。

---

<h2 id="ch12">第 12 章 两道闸门：content_guard 硬拦，persona_gate 只提醒</h2>

这是 Easel 最值得抄的产品决策之一：**会真发到公开平台的 Agent，必须假设模型会把内部设置写进文案。**

### 12.1 content_guard：fail-closed，退出码 7

`skills/shared/scripts/content_guard.py` 头部把事故写出来了（`:3-7`）：过去从生成到 `--exec` 之间没有任何过滤，密钥、内部 URL、代理 IP、「由 AI 生成」一旦发出去就删不掉。

两级强制（`content_guard.py:97-105`）：

| 级别 | 类别 | 真发时 |
|---|---|---|
| **BLOCK** | api-key / env-value / internal-host / proxy-ip / internal-path / env-name | **exit 7**，改稿重发 |
| **WARN** | ai-disclosure / model-name | 只告警。论文解读里「Claude」可能是正文 |

扫描是双路：正则表 + **`.env` 里敏感键的字面值**（`content_guard.py:141-176`）。命中片段在报告里打码（`_mask` / `_snippet`），避免日志二次泄露。dry-run 全部只告警；放行硬拦必须显式 `--allow-unsafe`。AGENTS.md 要求：**除非用户明确要求，不要用这个开关绕过**（`openclaw/workspace/AGENTS.md:72`）。

正则里能看到实验室拓扑：`maas.devops.xiaohongshu.com`、`webide-gateway.devops.xiaohongshu.com`、`/mnt/tidal-alsh01`、`happyhorse` 生视频内部名（`content_guard.py:50-85`）。闸门既是通用产品，也是这份代码从内部工具开源出来时的脱敏层。

### 12.2 persona_gate：人设检查永不阻断发布

发布层 SKILL 执行前，有 Profile 就跑 `skill-persona-check`（LLM 打分），再交给 `persona_gate.py check` 把分数变成 pass/warn。阈值 80（`persona_gate.py:29`）。**退出码永远是 0**（`persona_gate.py:10-11, 54`）：`publish_allowed: true`。

AGENTS.md 把理由说死（`openclaw/workspace/AGENTS.md:74-78`）：人设检查只提醒；用户已明确要发就继续，不额外索要确认。内容安全与平台合规仍独立硬拦。

`record` 把评分写进 manifest 的 publish 步，便于事后审计。

> 🧠 **一句话**：密钥泄露是不可逆的公共事故，所以硬拦；「这条不很符合人设」是创作判断，所以只提醒。两道闸门的力度反着来，是对「自动化发布」责任边界的正确划分。

```mermaid
flowchart TD
    C[待发文案] --> G{content_guard}
    G -->|BLOCK 命中| X[exit 7 改稿]
    G -->|通过或仅 WARN| P{有画像?}
    P -->|是| S[skill-persona-check LLM 评分]
    S --> PG[persona_gate classify]
    PG -->|pass / warn 都继续| E[--exec 真发]
    P -->|否| E
```

---

<h2 id="ch13">第 13 章 登录与一键发布：Playwright + 短信墙</h2>

### 13.1 六平台登录后端

`LOGIN_RUNNERS`（`web/app.py:98-105`）：

| 前端 key | 显示名 | backend |
|---|---|---|
| xiaohongshu | 小红书 | `xhs` |
| douyin | 抖音 | `douyin` |
| kuaishou | 快手 | `web` + `wp=kuaishou` |
| weixin-channels | 微信视频号 | `web` + `wp=weixin-channels` |
| zhihu | 知乎 | `web` + `wp=zhihu` |
| bilibili | B 站 | `biliup` |

浏览器 profile 在 `~/.easel-browser-profiles`（`web/app.py:82`）。登录状态写 `outputs/_login/`。whoami 真校验要起 headless 浏览器，进程内缓存 600 秒（`web/app.py:93-96`），避免账号页和工作台重复开浏览器。

### 13.2 `/api/publish/{platform}`

二次确认在前端。后端拼出 `--exec` 命令（`web/app.py:1996-2053`）：

- 小红书：`xhs_publish.py publish` 或 `publish-video`
- 抖音：同样结构，但**异步**——可能触发短信墙，状态写 `outputs/_publish/douyin.json`，验证码写 `douyin.code`，前端轮询到 `sms_required` 弹框
- B 站：直接 `biliup upload`，默认分区 tid=36「知识」，无标签兜底「日常」
- 其它：`web_publisher.py --platform <wp>`

约束：有的平台必须带媒体；图和视频不能同时发；视频号类只能发视频。超时 600 秒。每次发布追加 `outputs/_publish.log`。

发布走 `_publish_env()`，和对话代理环境分开，避免把代理打到平台域名上把登录态搞丢。

### 13.3 为什么 README 要你小心小红书

自动化操作会被风控。Easel 选择「能真发」而不是「只给文案」——这是产品差异化，也是合规与账号安全的代价。代码把 dry-run、预览、人确认、content_guard 叠在 `--exec` 前面，但**挡不住平台侧的行为检测**。这不是 bug，是宿主选择进入真实世界之后必须写在包装上的警告。

---

<h2 id="ch14">第 14 章 SSE 对话：断线不杀、双锁、uuid5、thinking 自愈</h2>

`web/app.py` 里的对话实现，是这份薄整合层里最「系统」的一段。注释密度极高，几乎每条都对应一次线上事故。

### 14.1 问题 1：CLI 不流式

`openclaw agent` 会把模型输出缓冲到结束才打印（`web/app.py:1028-1032`）。解法：设 `OPENCLAW_RAW_STREAM=1` + 每轮独立 jsonl 路径，后端 tail 文件，把 `assistant_text_stream` 的 `text_delta` 转成 SSE `token`，thinking 转成 `thinking`。每轮独立文件，无并发串扰。

### 14.2 问题 2：代理会掐断 SSE，但制作要跑很久

supervisor（跑 openclaw）和 forward（给浏览器）拆开（`web/app.py:1037-1039, 1352-1374`）：

- 客户端断开**只结束 forward**，不取消 supervisor
- 完整结果落 `outputs/_sessions/<sk>.json`，前端用 `/api/chat/last` 取回
- 另有 `/api/chat/jobs/{turn_id}/stream` 按 jsonl 续 SSE（`web/app.py:986-1021`）
- **只有用户点停止**才 `terminate` 进程（`web/app.py:1408-1415`）；断线不杀

长任务时 WebIDE 代理掐 SSE，是注释里点名的场景（`web/app.py:946-947`）。

### 14.3 问题 3：同一会话两个 openclaw = takeover 崩溃

OpenClaw 并发写同一 session 会抛 `EmbeddedAttemptSessionTakeoverError`，表现是「答一半停在冒号」（`web/app.py:808-811`）。双层锁：

1. **进程内** `asyncio.Lock` 按 session-key（`web/app.py:812-820`）
2. **跨进程** `fcntl.flock`（Windows 走 `msvcrt.locking`）（`web/app.py:841-896`）

不同会话仍可并行。拿不到 flock 就返回「正在另一个窗口运行」。前端还有 BroadcastChannel 探测跨标签占用（第 15 章）。

### 14.4 问题 4：隔天再问就忘了

OpenClaw 靠 `--session-key` 解析 transcript，空闲超过约 24h（`threadBindings.idleHours`）绑定过期，会新起空 transcript（`web/app.py:823-827`）。解法：用固定命名空间做 uuid5，**同一 web sessionId 永远同一 `--session-id`**（`web/app.py:828-833`），绕开 key→绑定过期。

### 14.5 问题 5：thinking 块丢签名，回放 400

OpenClaw 把 Claude thinking 存进 jsonl 时丢了 signature，内网 Bedrock 回放校验失败（`scripts/session_heal.py:3-6`、`web/app.py:51-54`）。每轮 spawn 前 `_heal_openclaw_session` 删 thinking / redacted_thinking 块和空消息。默认 `EASEL_THINKING_LEVEL=low` 减少产生量。

### 14.6 问题 6：「答完了」其实没答完

收尾检测（`web/app.py:1267-1290`）不把「已经吐了字」当成成功。正常收尾的**唯一标志**是 raw 流最后一个事件为 `assistant_message_end`。否则追加一段给用户看的警告：触顶截断、进程被杀、停在 `tool_use`、流被掐、**正文停在中文冒号**（用户实测「所有莫名停止都停在冒号」）。

每次收尾还往 `outputs/_debug/chat-stream.jsonl` 打一行诊断（`web/app.py:1313-1333`）：rc、stop_reason、last_ev、token/thinking 字数、是否误收了别的 session 的 raw 事件。

```mermaid
flowchart TB
    B[浏览器] -->|POST /api/chat/stream| F[forward 生成器]
    F -->|SSE token/thinking/activity| B
    S[supervisor 后台任务] -->|client_q| F
    S --> L[asyncio 锁 + flock]
    L --> P[openclaw agent 子进程]
    P --> RAW[每轮 jsonl raw stream]
    S -->|tail| RAW
    S -->|原子写| T[outputs/_sessions/sk.json]
    B -.断线.-> F
    S -.断线后继续.-> T
    B -->|/api/chat/last| T
```

---

<h2 id="ch15">第 15 章 前端工作台：11 页 SPA 与跨标签会话</h2>

### 15.1 页面

`Sidebar.tsx:11` 的 `Page` 类型：

`dashboard | chat | trends | ideas | calendar | publish | breakdown | skills | outputs | accounts | profile`

侧栏只放六个主导航（工作台 / 对话 / 技能库 / 内容库 / 账号 / 画像），热点、选题、日历、发布、复盘收进工作台 SubNav，避免侧栏爆炸（`Sidebar.tsx:31-39`）。

`App.tsx` 把**流式状态放在根组件**，切页不卸载、不丢流（`web/frontend/src/App.tsx:164` 注释）。这是对「用户去内容库看一眼生成物、对话还在跑」的正确结构。

### 15.2 跨标签：BroadcastChannel + flock 兜底

挂载逻辑（`App.tsx:55-125`）：

1. 同标签刷新：`sessionStorage` 记着本标签会话 → 续上
2. 新标签：用 BroadcastChannel 问「谁在用上次活跃会话」；有人应答 `owned` 就开新会话，避免两个窗口撞同一 OpenClaw session
3. 不支持 BroadcastChannel：退回旧逻辑，后端 flock 仍能挡住崩溃

Onboarding：没有任何画像且没看过引导 → 推荐配置向导（`App.tsx:144-147`）。还处理了一次品牌更名：旧 localStorage key 从 `postcraft_onboarding_seen` 迁到 `easel_onboarding_seen`（用 `['post','craft'].join('')` 躲开简单的字面扫描，`App.tsx:36`）。

### 15.3 API 客户端

`web/frontend/src/lib/api.ts` 用当前 pathname 推 `BASE`，方便子路径部署。错误优先展示 FastAPI `detail`。类型里把 `.easel.json` 展示头和产物树节点写全，和后端 `_read_project_meta` / `_build_output_node` 对齐。

---

<h2 id="ch16">第 16 章 真正新的东西</h2>

在本系列坐标系里，Easel 不是「又一个更强的循环」。它新在五件事：

### 16.1 垂直宿主，而且宿主只接一个引擎

Open Design 的宿主接 25 个 CLI，适配器即数据。Easel 只接 OpenClaw，把差异化全部做在 **SKILL + 画像 + 真发闸门**。更窄，所以能把社媒 SOP 做深（112 个、带脚本、带契约）。

### 16.2 对话是控制面，项目目录是数据面

编程 Agent 的产物经常活在 diff 和聊天里。Easel 强制 `outputs/<主题>/` + 成品/中间件分离 + 薄 manifest。这是内容团队能理解的「一个选题一个文件夹」，不是工程师的 session 日志。

### 16.3 画像是请求的一部分，不是全局文件

否掉 USER.md 之后，并发会话不再互踩。记忆按 `profiles/<名>/memory.md` 隔离，全局 MEMORY.md 被同步脚本保持为空。这是多账号运营的正确内存模型。

### 16.4 真发之前的分级闸门

content_guard 的 BLOCK/WARN 分级、persona_gate 永不阻断、`.env` 字面值扫描——针对的是「Agent 会把内部世界说出去」这个发布场景特有的事故，编程 Agent 的沙箱解决不了。

### 16.5 把 OpenClaw 的坑当成产品表面来修

thinking 丢签名、24h session 绑定过期、并发 takeover、SSE 被代理掐断、指令衰减、cwd 跑错导致误报缺 Key——这些都不是「调用 CLI」就能自动好的。Easel 的 2509 行后端，一大半是在**为上游引擎做适配器式的伤口包扎**。这和 Open Design 为 25 个 CLI 写解析器，是同一类工作。

---

<h2 id="ch17">第 17 章 工程质量：亮点与硬伤</h2>

### 17.1 亮点

| 点 | 证据 |
|---|---|
| 三入口收敛单一真相源 | `persona.py`、`timeouts.py`，注释写明历史分叉 |
| 确定性脚本带 selftest | content_guard / persona_gate / manifest / output_paths |
| 原子写 | manifest `os.replace`；turn 结果 `.tmp` 再 replace |
| 测试锁契约 | `tests/test_core.py` 覆盖输入分类、路径安全、注册表脱敏、persona_gate |
| Skill 来源可追溯 | ACKNOWLEDGMENTS + 每 Skill 的 EASEL-META.md |
| 配置模板标明无效 | `openclaw.json5` 头部警告 |
| 诊断日志 | `chat-stream.jsonl` 把「莫名停下」分类 |

### 17.2 硬伤与代价

1. **`web/app.py` 单文件 2509 行。** 路由、锁、SSE、登录、发布、产物树、画像 CRUD 全挤在一起。对两周的仓库可以理解，对后续贡献者不友好。
2. **版本号分裂。** `pyproject.toml:7` 的 `0.1.0` vs `easel/__init__.py:3` 的 `0.0.1`。
3. **引擎不在仓库里。** OpenClaw 是 `npm i -g openclaw`。分析无法核对循环本身；Easel 的正确性依赖上游 CLI 的 flags（`--session-id`、`OPENCLAW_RAW_STREAM`）继续存在。
4. **json5 模板与真实配置漂移**是已知的，但新人仍会被文件名骗。
5. **Skill 质量必然不均。** 112 个目录，有的是完整流水线（xhs-note-creator），有的更接近提示词包装。`validate_skills.py` 能锁格式，锁不住「这个 SOP 在真实运营里好不好用」。
6. **测试只有 650 行**，且明确只测整合层纯函数（`tests/test_core.py:1-4`）。发布脚本、Playwright、OpenClaw 交互是测试盲区——真发路径最危险的部分反而最难测。
7. **CORS `allow_origins=["*"]`**（`web/app.py:191`）对本地工作台够用，不是能暴露到公网的形状。
8. **小红书自动发布的风控**写进了 README，但代码路径仍然提供 `--exec`。产品把选择权交给用户，也把风险交给用户。
9. **sync 是整目录复制**，不是增量。112 个 Skill 每次全量 cp，workspace 和源可能在 Agent 跑着的时候被覆盖。
10. **TURN_REMINDER 每轮追加**，长对话的 token 成本线性涨；换来的是路由命中率。这是公开的取舍（`easel/persona.py:72-76`）。

---

<h2 id="ch18">第 18 章 对做新产品的十二条启发</h2>

1. **先决定循环归谁。** 如果领域 SOP 比通用推理更值钱，就别写循环，写宿主。
2. **宿主的核心工作是包扎引擎伤口**，不是 `subprocess.run` 一行搞定。
3. **领域产物用文件系统当一等公民**，别让它们活在聊天记录里。
4. **跨步骤传递用薄索引 + 路径，不要口传全文。**
5. **会发到公网的文本，必须有确定性扫描。** 提示词里的「不要泄露」不够。
6. **闸门分级：不可逆事故硬拦，审美/人设只提醒。**
7. **多租户记忆不要放全局文件。** 画像是请求的一部分。
8. **长对话要假定系统提示会衰减**，把承重规则用近因再喂一遍。
9. **SSE 与长任务解耦：断线不杀，结果落盘，前端来取。**
10. **同一会话同一时刻只允许一个引擎进程**——锁要跨到进程外。
11. **环境检查做成产品命令**（doctor / ping），不要只写 README。
12. **把「这份文件不生效」写在文件头上。** 漂移的模板比没有模板更危险。

---

<h2 id="ch19">第 19 章 它在本系列里的位置</h2>

```mermaid
flowchart LR
    subgraph LOOP["自己写循环"]
        CC["Claude Code / Codex / dsh<br/>通用：写代码、干活"]
        MF["MiroFish<br/>垂直：仿真预演"]
    end
    subgraph HOST["不写循环 · 宿主"]
        OD["Open Design<br/>垂直：设计文件"]
        EA["Easel<br/>垂直：社媒内容 + 真发"]
    end
    CC -.skill 思想.-> EA
    OD -.卡片视觉 / 宿主形态.-> EA
```

| 项目 | 问题 | 和 Easel 的关系 |
|---|---|---|
| Open Design | 不写循环，接 25 个 CLI 做设计 | **最近亲缘**：都是宿主。Easel 只接 1 个引擎，技能库更垂直；卡片视觉还借鉴了它 |
| OpenWorker | 通用任务同事，收件箱 + 审批 | 都面向「非编程工作」；OW 强调人不在时的恢复，Easel 强调内容项目与真发 |
| MiroFish | 仿真，不干活 | 同为非编程；一个预演世界，一个生产内容 |
| Claude Code / Codex | 写循环的编程 Agent | Easel 把它们那种 skill 思想用在社媒 SOP 上，循环本身外包 |
| DeepSeek Harness | 循环也是插件 | 对照：dsh 把可替换做到极致；Easel 把可替换让给上游，自己锁死领域契约 |

**谱系补丁**：前七家编程 Agent → OpenWorker 同事 → Open Design 设计宿主 → MiroFish 仿真 → **Easel 社媒宿主**。它让「不写主循环」这条路线从设计工具走进了国内创作者每天要面对的六个平台。

配套阅读：[从零构建社媒内容工作台（Easel 系）](../从零构建Easel-开发全流程教程.html) · 交互实验台 [Easel-解析.html](../Easel-解析.html)

---

<h2 id="ch20">第 20 章 🔍 源码指路表</h2>

全部对照 commit `765f5a6`。

| 你想看 | 去这里 |
|---|---|
| CLI 入口与 chat 注入画像 | `easel/cli.py:49-121` |
| 三入口画像前缀 / TURN_REMINDER | `easel/persona.py:1-99` |
| 超时常量 | `easel/timeouts.py` |
| skill 路由与二进制输入 | `easel/commands/skill.py:31-164` |
| doctor / ping / gateway | `easel/commands/doctor.py`、`ping.py`、`gateway.py` |
| Prompt 栈说明 | `docs/prompt-stack.md` |
| SKILL 规范 | `docs/SKILL-SPEC.md` |
| 112 Skill 能力地图 | `docs/skill-function-mapping.md` |
| Agent 宪法 | `openclaw/workspace/AGENTS.md` |
| 人格 | `openclaw/workspace/SOUL.md` |
| 同步与 symlink | `openclaw/sync.sh` |
| 「此配置不生效」 | `openclaw/openclaw.json5:1-35` |
| SSE supervisor / 双锁 / uuid5 | `web/app.py:808-1401` |
| 停止 vs 断线 | `web/app.py:1408-1415` |
| Skill HTTP | `web/app.py:1453-1463` |
| 产物树与展示头 | `web/app.py:488-545` |
| 登录表 | `web/app.py:98-105` |
| 一键发布 | `web/app.py:1996-2068` |
| 前端跨标签 | `web/frontend/src/App.tsx:55-125` |
| 页面枚举 | `web/frontend/src/components/Sidebar.tsx:11-39` |
| 路径门禁 | `skills/shared/scripts/output_paths.py` |
| 层间契约 | `skills/shared/scripts/manifest.py` |
| 出站扫描 | `skills/shared/scripts/content_guard.py` |
| 人设提醒 | `skills/shared/scripts/persona_gate.py` |
| thinking 自愈 | `scripts/session_heal.py` |
| 小红书 SOP | `skills/openclaw/xhs-note-creator/SKILL.md` |
| 依赖与版本 | `pyproject.toml` |
| 整合层测试 | `tests/test_core.py` |
| Skill 致谢 | `docs/ACKNOWLEDGMENTS.md` |
