这份教程教你什么:不是再写一个 ReAct 循环,而是造一个把别人的 Agent CLI 当引擎、专门产出真实社媒内容并可以真发的宿主——Easel(
ZJU-REAL/Easel,Apache-2.0,commit765f5a6)那种。 最反直觉的一点:这条路线里,你一行 Agent 主循环都不写。你要写的是「插座 + 剧本 + 项目目录 + 出站闸门」。 怎么教:跟着一句真实需求从头走到尾——「用我的科技数码账号,做一条小红书笔记并发出去」。每撞一堵墙补一个零件。 对照源码:每个零件给出 Easel 的文件:行号。配套阅读《Easel 源码分析》和交互实验台 Easel-解析.html。 前置:知道「Agent = 模型说话 → 软件替它动手 → 结果喂回模型」就够了。你不需要会剪视频,也不需要会写 OpenClaw。
目录
- 先看终点:一个社媒工作台长什么样
- 第 0 步 · 场景登场:一句话里藏的十二堵墙
- 第 1 步 · 决定不写 Agent:把 OpenClaw 当引擎
- 第 2 步 · 三入口共用一份画像前缀
- 第 3 步 · 四层 prompt 栈,常驻层不写 skill 名
- 第 4 步 · sync:workspace 是剧本,项目根是工地
- 第 5 步 · SKILL.md 契约:200 行 SOP + 脚本不进 prompt
- 第 6 步 · outputs 门禁:内容是项目
- 第 7 步 · manifest:薄索引,不口传全文
- 第 8 步 · TURN_REMINDER:近因对抗指令衰减
- 第 9 步 · content_guard:真发前的 fail-closed
- 第 10 步 · persona_gate:人设只提醒、永不阻断
- 第 11 步 · SSE:断线不杀,结果落盘
- 第 12 步 · 双锁 + uuid5:会话不串味、隔天不丢
- 第 13 步 · session_heal:上游把 thinking 存丢了签名
- 第 14 步 · 登录与
--exec:进入真实平台 - 第 15 步 · doctor / ping:环境做成产品命令
- 🎬 完整回放:这一句话到底跑了什么
- 附录 A · 验收断言
- 附录 B · 十二个最容易翻的车
- 附录 C · 源码对照索引
先看终点:一个社媒工作台长什么样
编程 Agent 和社媒工作台的分界,不在模型,而在这六件事:
- 它自己不推理——推理是机器上那个
openclaw --profile easel在干。 - 产物是给人看、给人发的,不是给编译器看的。
- 一个选题必须是一个文件夹,后续改稿、重试、发布不能散在聊天里。
- 账号有人设,而且多个账号会并行开会话——全局 USER.md 会互踩。
- 发出去的字删不掉,所以密钥和内部域名必须确定性扫描。
- 制作可能跑两小时,浏览器 SSE 会被代理掐断——引擎不能跟着连接死。
flowchart TB
subgraph SEE["① 创作者看见的"]
CHAT["对话"]
LIB["内容库 outputs/"]
ACC["账号登录"]
PUB["发布中心"]
end
subgraph HOST["② 你要写的宿主"]
PREFIX["画像前缀 + TURN_REMINDER"]
SSE["supervisor / forward 拆开"]
LOCK["asyncio 锁 + flock"]
GUARD["content_guard + persona_gate"]
PATH["output_paths 门禁"]
end
subgraph PLAY["③ 剧本(文件,可版本化)"]
SOUL["SOUL.md"]
AG["AGENTS.md"]
SK["skills/openclaw/*"]
PF["profiles/六维 md"]
end
subgraph ENG["④ 引擎(不是你的代码)"]
OC["openclaw CLI + gateway :18789"]
end
CHAT --> SSE
SSE --> PREFIX
PREFIX --> OC
OC --> PLAY
OC -->|cd 项目根跑脚本| PATH
PATH --> LIB
GUARD --> PUB
ACC --> PUB
对照 Easel:整合包只有 768 行,web/app.py 2509 行,技能 112 个。你要造的 minieasel 不必 112 个 skill,但这六件事一件都不能少,否则「能聊」和「能发」会在第一周就分家。
第 0 步 · 场景登场:一句话里藏的十二堵墙
用户说:
用我的科技数码账号,做一条小红书笔记并发出去。主题是「USB4 硬盘盒踩坑」。
这句话看起来像一句 prompt,拆开是十二个产品问题:
| # | 墙 | 不补零件时会发生什么 |
|---|---|---|
| 1 | 谁来推理 | 你开始手写 while 循环,三个月还在调工具解析 |
| 2 | 哪个账号 | 全局 USER.md,两个标签页互踩人设 |
| 3 | 笔记长什么样 | 模型写一篇公众号,发到小红书没人看 |
| 4 | 卡片怎么渲染 | 模型用 emoji 拼图,看起来像 PPT |
| 5 | 文件放哪 | output.png 扔在仓库根,下周找不到 |
| 6 | 策划结论怎么给制作 | 模型在对话里复述三千字,token 炸、还丢 |
| 7 | 长对话后不做 skill | 「凭记忆裸做」,脚本门禁被绕过 |
| 8 | 文案里出现 API Key | 发出去,删不掉 |
| 9 | 人设不太像 | 该拦还是该问?拦了创作者会骂 |
| 10 | 生成要 40 分钟 | SSE 被掐,前端以为死了,后端也杀了 |
| 11 | 两个窗口同一会话 | OpenClaw takeover,答到冒号停住 |
| 12 | 真发小红书 | 风控、短信墙、Playwright 登录态 |
后面每一步拆一堵墙。
第 1 步 · 决定不写 Agent:把 OpenClaw 当引擎
为什么:循环、工具、会话、模型供应商,OpenClaw 已经有了。你的差异化是社媒 SOP 和真发。再写一个循环,是在和上游抢同一份工作。
最小实现:
cmd = [
"openclaw", "--profile", "easel",
"agent", "--agent", "main",
"--timeout", str(timeout),
"--message", message,
]
subprocess.run(cmd, cwd=PROJECT_ROOT, env=proxy_env)
对照:easel/commands/skill.py:91-106、easel/cli.py:103-119。profile 名写死,和用户其它 OpenClaw 工作区隔离。
验收:which openclaw 成功;easel ping 能让 agent 回 PONG(easel/commands/ping.py:62-70)。
真实 Easel:gateway 监听 18789,doctor 打 healthz(easel/commands/doctor.py:75-81)。
第 2 步 · 三入口共用一份画像前缀
为什么:CLI chat、CLI skill、Web 对话如果各写一份「当前用户是谁」,第三天就会出现「Web 有画像、CLI 没有」。更糟的是全局 USER.md:并发会话写同一文件。
被否方案:sync 时生成 USER.md。Easel 注释写明这是历史方案(easel/persona.py:1-6)。
最小实现:
def persona_prefix(name: str | None) -> str:
if name and profile_exists(name):
return (
f"我当前使用的画像是「{name}」。"
f"本会话的账号长期记忆仅使用 profiles/{name}/memory.md,"
"不要使用工作区全局 MEMORY.md 作为账号记忆。"
)
return ""
对照:easel/persona.py:58-69。六维文件顺序:identity / style / audience / platforms / preferences / memory(:16-20)。注入的是指针,不是把六份 md 全文塞进每条消息。
CLI chat 把前缀当初始消息(easel/cli.py:112-117);Web 每轮都带(web/app.py:798-805)。超长会话被压缩后 CLI 可能丢画像——注释承认这是已知取舍(easel/cli.py:112-114)。
验收:两个并发请求分别带画像 A / B,workspace 里没有 USER.md,全局 MEMORY.md 为空(openclaw/sync.sh:97-103)。
第 3 步 · 四层 prompt 栈,常驻层不写 skill 名
为什么:112 个 skill 名写进 SOUL.md,下架一个平台你就要改人格文件。常驻层只写类目:「能做发现/策划/制作/发布/归因」。具体名字留给被触发的 SKILL.md。
对照:docs/prompt-stack.md 全文;SOUL.md 自己说「先去技能库找」(openclaw/workspace/SOUL.md:18);AGENTS.md 才写路由规则(openclaw/workspace/AGENTS.md:7)。
最小实现(四个文件,不要再多):
| 层 | 文件 | 只管 |
|---|---|---|
| 1 | SOUL.md | 语气、能力类目、对外不自曝 |
| 2 | AGENTS.md | 先路由、先 cd 项目根、先查再问、真产物才算完、出站安全 |
| 3 | CONTEXT.md | 项目绝对路径(生成,不手写) |
| 4 | SKILL.md | 被触发才加载 |
验收:SOUL.md 里 grep 不到 xhs-note-creator 这种具体名字。
第 4 步 · sync:workspace 是剧本,项目根是工地
为什么:OpenClaw 的 cwd 是 ~/.openclaw/workspace-easel/。如果你让它在那儿跑 python skills/...,缺 .env、路径少一层、产物写到家目录。
最小实现(对照 openclaw/sync.sh):
- 把 SKILL 和 shared 复制进 workspace
- 把项目根绝对路径追加进 AGENTS.md(
:77-93) ln -sprofiles、outputs(先 rm 再 ln,防循环,:107)- 清空 MEMORY.md,删除 USER.md
- 进程环境
EASEL_ROOT=项目根(manifest.py:58-61解释了为什么__file__上溯会错)
AGENTS.md 写死:禁止从 workspace 的 shared 副本跑项目脚本(openclaw/workspace/AGENTS.md:8-9)。
验收:Agent 第一个脚本前的 pwd 是项目根;test -f .env && test -d skills/shared/scripts 为真。
第 5 步 · SKILL.md 契约:200 行 SOP + 脚本不进 prompt
为什么:把 ffmpeg 命令写进系统提示又贵又易过时。SOP 写「怎么做」,像素操作放 scripts/,领域知识放 references/。
最小实现:一个 skill 目录:
---
name: xhs-note-creator
description: >
小红书内容总入口:……当用户说「做小红书笔记」时使用。
仅渲染卡片用 card-xiaohongshu;其它平台用 social-content。
layer: produce
---
description 必须包含触发说法和相邻边界——这就是全部路由器(docs/SKILL-SPEC.md:73-76)。没有 Python if intent == ...。
验收:python scripts/validate_skills.py 通过;SKILL.md < 200 行。
真实 Easel 还有 validate_skill_commands.py:把 SKILL 正文里的 python 命令和脚本 argparse 对照,防文档漂移。
第 6 步 · outputs 门禁:内容是项目
为什么:模型喜欢写 outputs/test/out.png。下周你有 40 个 test。
最小实现:所有写盘脚本先过 validate_output_path(skills/shared/scripts/output_paths.py:41-75):
- 必须在
outputs/下 - 内容路径至少
outputs/<主题>/<文件> - 主题不能是泛名集合里的词
_开头是系统目录,必须allow_system=True且在白名单
成品放项目根,中间件进 assets/,元数据只有 .easel.json。
验收:试图写入 outputs/xhs/a.png 抛 OutputPathError(泛名 xhs 在 GENERIC_PROJECT_NAMES,:25-29)。
第 7 步 · manifest:薄索引,不口传全文
为什么:跨「策划 → 制作 → 发布」如果靠模型在对话里复述脚本全文,又贵又丢。
最小实现:manifest.py record --topic USB4硬盘盒踩坑 --layer produce --skill xhs-note-creator --outputs card_1.png,note.md --summary "5 张卡,钩子是接口兼容"。
下游 latest --layer produce 只拿到路径 + 一句结论,再去读文件(docs/SKILL-SPEC.md:157-159)。失败也 --status failed,才能断点续跑。
验收:.easel.json 的 steps[].outputs 是相对路径,不内嵌文案正文。
第 8 步 · TURN_REMINDER:近因对抗指令衰减
为什么:第 1 轮模型会去读 SKILL;第 12 轮用户说「标题再改改」,模型凭记忆直接改,绕过 card-design 和去 AI 化。
最小实现:只在对话入口把一小段内部提醒拼在用户消息末尾(easel/persona.py:72-99)。单跑 skill 不加。前端只显示用户原文。
验收:抓一条 Web 发给 openclaw 的 --message,末尾能看到「本轮动手前先查技能库」,且这段不出现在聊天气泡里。
第 9 步 · content_guard:真发前的 fail-closed
为什么:提示词写「不要输出 API Key」挡不住模型把报错、命令输出粘进文案。发出去是不可逆的。
最小实现:发布脚本 --exec 前调用 guard_or_die(text)。
- BLOCK(密钥、内部域名、代理 IP、内部路径、env 名、
.env真值)→ exit 7 - WARN(「由 AI 生成」、模型名)→ 打印提醒,放行
对照:content_guard.py:11-15, 97-105。双路扫描:正则 + .env 字面值(:141-176)。报告里命中片段打码。
验收:文案含 sk- 开头密钥,真发退出码为 7;文案含「Claude」的论文解读,退出码 0。
第 10 步 · persona_gate:人设只提醒、永不阻断
为什么:密钥泄露是事故;「这条不太像数码博主」是创作判断。创作者点了发布,你再用分数拦住,产品就变监工。
最小实现:LLM 打分 → persona_gate.py check --score N → 永远 publish_allowed: true、退出码 0(persona_gate.py:10-11, 42-54)。低于 80 只是 warn,展示偏离点。AGENTS.md 规定用户已明确要发就继续(openclaw/workspace/AGENTS.md:78)。
验收:score=12 仍然 exit 0;manifest 里能 record 到这次 warn。
第 11 步 · SSE:断线不杀,结果落盘
为什么:制作层超时 7200 秒(easel/timeouts.py:11)。浏览器和反向代理活不了那么久。如果 SSE 断开就 kill 子进程,视频永远做不完。
最小实现(对照 web/app.py:1024-1401):
asyncio.create_task(supervisor()),客户端生成器只forward队列- 子进程环境打开
OPENCLAW_RAW_STREAM,tail 每轮 jsonl 推 token _save_turn原子写outputs/_sessions/<sk>.json- 断开不
terminate;只有/api/chat/stop杀进程 - 前端断线后轮询
/api/chat/last
验收:生成中关掉标签页,进程仍在;重开页面能取回完整回答。
第 12 步 · 双锁 + uuid5:会话不串味、隔天不丢
墙 11:两个窗口同一 session → EmbeddedAttemptSessionTakeoverError,答到冒号停住(web/app.py:808-811)。
最小实现:
- 进程内
asyncio.Lock按 session-key - 跨进程
fcntl.flock(web/app.py:841-896) - 前端 BroadcastChannel 探测占用(
App.tsx:55-125)
墙「隔天忘了」:OpenClaw --session-key 绑定约 24h 过期(web/app.py:823-827)。
最小实现:uuid.uuid5(固定命名空间, web_session_id) 钉死 --session-id(:828-833)。
验收:两个标签撞同一会话,第二个看到排队/请换窗口,而不是 rc=1;隔 25 小时同一 sessionId 仍指向同一 jsonl。
第 13 步 · session_heal:上游把 thinking 存丢了签名
为什么:这不是你的 bug,但用户看见的是「Session history or replay state is invalid」。OpenClaw 存 thinking 块时丢了 signature,Bedrock 回放校验失败(scripts/session_heal.py:3-6)。
最小实现:每轮 spawn 前删 thinking / redacted_thinking 块和空消息;默认 thinking 档位 low(web/app.py:51-71)。失败不抛,best-effort。
验收:含无签名 thinking 的 jsonl 经 sanitize 后,下一轮不再 400。
第 14 步 · 登录与 `--exec`:进入真实平台
为什么:只给文案,创作者还要复制到创作者中心——工作台就断在最后一公里。Easel 选择 Playwright / biliup 真发,并在 README 警告小红书风控。
最小实现:
- 按平台保存浏览器 profile(
~/.easel-browser-profiles) - 发布 API 二次确认后拼
--exec(web/app.py:1996-2053) - 抖音短信墙:异步 + 状态文件 + 前端输入验证码(
:2040-2045) - 发布前必须过 content_guard;人设只提醒
- 图/视频互斥、部分平台必须带媒体——用代码硬约束,不靠模型记
验收:dry-run 列出将发内容且不触网;--exec 在 BLOCK 命中时 exit 7。
第 15 步 · doctor / ping:环境做成产品命令
为什么:依赖链是 Python + Node 22.19 + FFmpeg + Playwright + 前端 dist + gateway + skills sync + API Key。任何一环缺失,用户都只会说「不能用」。
最小实现:doctor 静态打勾(easel/commands/doctor.py:143-207),ping 真的让 agent 说话(ping.py)。Key 检查拒绝 REPLACE_ME 占位符。
验收:拔掉 ffmpeg,doctor 红;配好后 ping 两步全绿。
🎬 完整回放:这一句话到底跑了什么
用户在 Web 选画像「科技数码达人」,输入「做一条小红书:USB4 硬盘盒踩坑,做完直接发」。
- 前端
streamChat,turnId写入 sessionStorage(App.tsx:197-198)。 _chat_message= 画像前缀 + 用户原文 + 附件清单(无)+ TURN_REMINDER(web/app.py:798-805)。- supervisor 先
_save_turn(running),再_heal_openclaw_session,再抢 asyncio 锁 + flock。 openclaw --profile easel agent --session-id <uuid5> --thinking low --timeout 7200。- 模型读 SOUL / AGENTS / CONTEXT;TURN_REMINDER 要求先查技能库 → 命中
xhs-note-creator(produce)和后续skill-xhs-publisher(publish)。跨两层,先短 Plan。 cd到项目根。凝练easel-profiles/科技数码达人/的 identity/style/audience/preferences/memory,不读 platforms.md(制作层规矩)。- 按 SKILL 问清形态(默认图文)、写长文、去 AI 化、走 card-design 渲染卡片到
outputs/USB4硬盘盒踩坑/。output_paths.py拒绝任何泛名。 manifest.py record --layer produce ...;meta --kind xhs-note --status ready。- 发布前
skill-persona-check+persona_gate(warn 也继续);content_guard扫描标题/正文/标签。 xhs_publish.py publish --images ... --exec。成功则 manifestpublished,日历/publish-log 留痕。- raw 流
assistant_message_end→_save_turn(done)→ SSEdone。若代理早把 SSE 掐了,前端/api/chat/last仍能取回。 - 内容库刷新,读
.easel.json展示头,封面是第一张卡。
中间任何一层失败:manifest 记 failed,中途文件留在 assets/,不以空壳当交付(openclaw/workspace/AGENTS.md:12, 62)。
附录 A · 验收断言
做完对应步骤,应能勾选:
- [ ]
openclaw --profile easel能跑,gateway:18789/healthz200 - [ ] 三入口画像都走
persona_prefix,无 USER.md - [ ] SOUL.md 不含具体 skill 名
- [ ] 脚本 cwd 是项目根,
EASEL_ROOT已设 - [ ] SKILL frontmatter 只有 name/description/layer
- [ ]
outputs/test/a.png被门禁拒绝 - [ ] manifest
summary一行,正文在文件里 - [ ] Web 对话消息末尾有 TURN_REMINDER,气泡没有
- [ ] 含
sk-的待发文案 exit 7 - [ ] persona score=10 仍允许发布
- [ ] 关掉标签页,openclaw 进程仍在,last turn 可取回
- [ ] 两窗口同会话:第二个排队或被拒,不崩溃
- [ ] 同一 web id 隔日后
--session-id不变 - [ ] 无签名 thinking 的历史被 sanitize
- [ ] doctor 能指出缺 ffmpeg / 缺 dist / 缺 key
附录 B · 十二个最容易翻的车
- 手写循环。 你会把时间花在工具解析,而不是 SOP 和闸门。
- 全局 USER.md。 多账号第一天就互踩。
- 在 workspace 里跑脚本。
__file__少一层,产物写到~/.openclaw。 - 相信
openclaw.json5。 它自己说从不被 setup 应用(openclaw/openclaw.json5:2-5)。 - 成品当聊天附件。 无法复盘、无法发布中心选媒体。
- manifest 复制全文。 索引变脏,下游仍去「凝练」。
- 只靠系统提示防泄密。 真发路径必须确定性扫描。
- 人设分数阻断发布。 创作者会关产品。
- SSE 断开就杀进程。 制作层任务全部失败。
- 只加 asyncio 锁。 第二个浏览器标签照样 takeover。
- 只传
--session-key。 隔天历史蒸发。 - 小红书默认自动发。 账号风控。默认预览,人点确认再
--exec。
附录 C · 源码对照索引
| 步骤 | 主要文件 |
|---|---|
| 1 引擎插座 | easel/cli.py、easel/commands/skill.py |
| 2 画像 | easel/persona.py |
| 3 prompt 栈 | docs/prompt-stack.md、openclaw/workspace/*.md |
| 4 sync | openclaw/sync.sh |
| 5 SKILL | docs/SKILL-SPEC.md |
| 6 路径 | skills/shared/scripts/output_paths.py |
| 7 manifest | skills/shared/scripts/manifest.py |
| 8 提醒 | easel/persona.py:72-99 |
| 9 出站 | skills/shared/scripts/content_guard.py |
| 10 人设 | skills/shared/scripts/persona_gate.py |
| 11–13 对话 | web/app.py:808-1415、scripts/session_heal.py |
| 14 发布 | web/app.py:1996-2068 |
| 15 环境 | easel/commands/doctor.py、ping.py |
| 超时 | easel/timeouts.py |
| 前端会话 | web/frontend/src/App.tsx |