从零构建 · 社媒内容宿主 · 15 步 · 对照 765f5a6

不写主循环
先造插座和闸门

普通社媒 Agent 的病是「能聊不能发、能发会泄密、两个窗口会互踩」。这份教程用 15 步造一个 minieasel:循环外包给 OpenClaw,你写画像前缀、项目目录契约、出站扫描、SSE 与会话锁。每步先问一句「写在全局文件里会怎样」。

15
构建步骤
12
场景里的墙
15
条验收断言
12
个必踩的坑

这份教程教你什么:不是再写一个 ReAct 循环,而是造一个把别人的 Agent CLI 当引擎、专门产出真实社媒内容并可以真发的宿主——Easel(ZJU-REAL/Easel,Apache-2.0,commit 765f5a6)那种。 最反直觉的一点:这条路线里,你一行 Agent 主循环都不写。你要写的是「插座 + 剧本 + 项目目录 + 出站闸门」。 怎么教:跟着一句真实需求从头走到尾——「用我的科技数码账号,做一条小红书笔记并发出去」。每撞一堵墙补一个零件。 对照源码:每个零件给出 Easel 的 文件:行号。配套阅读《Easel 源码分析》和交互实验台 Easel-解析.html前置:知道「Agent = 模型说话 → 软件替它动手 → 结果喂回模型」就够了。你不需要会剪视频,也不需要会写 OpenClaw。


目录


Part 1

先看终点:一个社媒工作台长什么样

编程 Agent 和社媒工作台的分界,不在模型,而在这六件事:

  1. 它自己不推理——推理是机器上那个 openclaw --profile easel 在干。
  2. 产物是给人看、给人发的,不是给编译器看的。
  3. 一个选题必须是一个文件夹,后续改稿、重试、发布不能散在聊天里。
  4. 账号有人设,而且多个账号会并行开会话——全局 USER.md 会互踩。
  5. 发出去的字删不掉,所以密钥和内部域名必须确定性扫描。
  6. 制作可能跑两小时,浏览器 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,但这六件事一件都不能少,否则「能聊」和「能发」会在第一周就分家。


Part 2

第 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 登录态

后面每一步拆一堵墙。


Part 3

第 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-106easel/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)。


Part 4

第 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)。


Part 5

第 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 这种具体名字。


Part 6

第 4 步 · sync:workspace 是剧本,项目根是工地

为什么:OpenClaw 的 cwd 是 ~/.openclaw/workspace-easel/。如果你让它在那儿跑 python skills/...,缺 .env、路径少一层、产物写到家目录。

最小实现(对照 openclaw/sync.sh):

  1. 把 SKILL 和 shared 复制进 workspace
  2. 把项目根绝对路径追加进 AGENTS.md:77-93
  3. ln -s profiles、outputs(先 rm 再 ln,防循环,:107
  4. 清空 MEMORY.md,删除 USER.md
  5. 进程环境 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 为真。


Part 7

第 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 对照,防文档漂移。


Part 8

第 6 步 · outputs 门禁:内容是项目

为什么:模型喜欢写 outputs/test/out.png。下周你有 40 个 test。

最小实现:所有写盘脚本先过 validate_output_pathskills/shared/scripts/output_paths.py:41-75):

  • 必须在 outputs/
  • 内容路径至少 outputs/<主题>/<文件>
  • 主题不能是泛名集合里的词
  • _ 开头是系统目录,必须 allow_system=True 且在白名单

成品放项目根,中间件进 assets/,元数据只有 .easel.json

验收:试图写入 outputs/xhs/a.pngOutputPathError(泛名 xhsGENERIC_PROJECT_NAMES:25-29)。


Part 9

第 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.jsonsteps[].outputs 是相对路径,不内嵌文案正文。


Part 10

第 8 步 · TURN_REMINDER:近因对抗指令衰减

为什么:第 1 轮模型会去读 SKILL;第 12 轮用户说「标题再改改」,模型凭记忆直接改,绕过 card-design 和去 AI 化。

最小实现:只在对话入口把一小段内部提醒拼在用户消息末尾easel/persona.py:72-99)。单跑 skill 不加。前端只显示用户原文。

验收:抓一条 Web 发给 openclaw 的 --message,末尾能看到「本轮动手前先查技能库」,且这段不出现在聊天气泡里。


Part 11

第 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。


Part 12

第 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。


Part 13

第 11 步 · SSE:断线不杀,结果落盘

为什么:制作层超时 7200 秒(easel/timeouts.py:11)。浏览器和反向代理活不了那么久。如果 SSE 断开就 kill 子进程,视频永远做不完。

最小实现(对照 web/app.py:1024-1401):

  1. asyncio.create_task(supervisor()),客户端生成器只 forward 队列
  2. 子进程环境打开 OPENCLAW_RAW_STREAM,tail 每轮 jsonl 推 token
  3. _save_turn 原子写 outputs/_sessions/<sk>.json
  4. 断开不 terminate;只有 /api/chat/stop 杀进程
  5. 前端断线后轮询 /api/chat/last

验收:生成中关掉标签页,进程仍在;重开页面能取回完整回答。


Part 14

第 12 步 · 双锁 + uuid5:会话不串味、隔天不丢

墙 11:两个窗口同一 session → EmbeddedAttemptSessionTakeoverError,答到冒号停住(web/app.py:808-811)。

最小实现

  • 进程内 asyncio.Lock 按 session-key
  • 跨进程 fcntl.flockweb/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。


Part 15

第 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 档位 lowweb/app.py:51-71)。失败不抛,best-effort。

验收:含无签名 thinking 的 jsonl 经 sanitize 后,下一轮不再 400。


Part 16

第 14 步 · 登录与 `--exec`:进入真实平台

为什么:只给文案,创作者还要复制到创作者中心——工作台就断在最后一公里。Easel 选择 Playwright / biliup 真发,并在 README 警告小红书风控。

最小实现

  1. 按平台保存浏览器 profile(~/.easel-browser-profiles
  2. 发布 API 二次确认后拼 --execweb/app.py:1996-2053
  3. 抖音短信墙:异步 + 状态文件 + 前端输入验证码(:2040-2045
  4. 发布前必须过 content_guard;人设只提醒
  5. 图/视频互斥、部分平台必须带媒体——用代码硬约束,不靠模型记

验收:dry-run 列出将发内容且不触网;--exec 在 BLOCK 命中时 exit 7。


Part 17

第 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 两步全绿。


Part 18

🎬 完整回放:这一句话到底跑了什么

用户在 Web 选画像「科技数码达人」,输入「做一条小红书:USB4 硬盘盒踩坑,做完直接发」。

  1. 前端 streamChatturnId 写入 sessionStorage(App.tsx:197-198)。
  2. _chat_message = 画像前缀 + 用户原文 + 附件清单(无)+ TURN_REMINDER(web/app.py:798-805)。
  3. supervisor 先 _save_turn(running),再 _heal_openclaw_session,再抢 asyncio 锁 + flock。
  4. openclaw --profile easel agent --session-id <uuid5> --thinking low --timeout 7200
  5. 模型读 SOUL / AGENTS / CONTEXT;TURN_REMINDER 要求先查技能库 → 命中 xhs-note-creator(produce)和后续 skill-xhs-publisher(publish)。跨两层,先短 Plan。
  6. cd 到项目根。凝练 easel-profiles/科技数码达人/ 的 identity/style/audience/preferences/memory,不读 platforms.md(制作层规矩)。
  7. 按 SKILL 问清形态(默认图文)、写长文、去 AI 化、走 card-design 渲染卡片到 outputs/USB4硬盘盒踩坑/output_paths.py 拒绝任何泛名。
  8. manifest.py record --layer produce ...meta --kind xhs-note --status ready
  9. 发布前 skill-persona-check + persona_gate(warn 也继续);content_guard 扫描标题/正文/标签。
  10. xhs_publish.py publish --images ... --exec。成功则 manifest published,日历/publish-log 留痕。
  11. raw 流 assistant_message_end_save_turn(done) → SSE done。若代理早把 SSE 掐了,前端 /api/chat/last 仍能取回。
  12. 内容库刷新,读 .easel.json 展示头,封面是第一张卡。

中间任何一层失败:manifest 记 failed,中途文件留在 assets/不以空壳当交付openclaw/workspace/AGENTS.md:12, 62)。


Part 19

附录 A · 验收断言

做完对应步骤,应能勾选:

  • [ ] openclaw --profile easel 能跑,gateway :18789/healthz 200
  • [ ] 三入口画像都走 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

Part 20

附录 B · 十二个最容易翻的车

  1. 手写循环。 你会把时间花在工具解析,而不是 SOP 和闸门。
  2. 全局 USER.md。 多账号第一天就互踩。
  3. 在 workspace 里跑脚本。 __file__ 少一层,产物写到 ~/.openclaw
  4. 相信 openclaw.json5 它自己说从不被 setup 应用(openclaw/openclaw.json5:2-5)。
  5. 成品当聊天附件。 无法复盘、无法发布中心选媒体。
  6. manifest 复制全文。 索引变脏,下游仍去「凝练」。
  7. 只靠系统提示防泄密。 真发路径必须确定性扫描。
  8. 人设分数阻断发布。 创作者会关产品。
  9. SSE 断开就杀进程。 制作层任务全部失败。
  10. 只加 asyncio 锁。 第二个浏览器标签照样 takeover。
  11. 只传 --session-key 隔天历史蒸发。
  12. 小红书默认自动发。 账号风控。默认预览,人点确认再 --exec

Part 21

附录 C · 源码对照索引

步骤 主要文件
1 引擎插座 easel/cli.pyeasel/commands/skill.py
2 画像 easel/persona.py
3 prompt 栈 docs/prompt-stack.mdopenclaw/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-1415scripts/session_heal.py
14 发布 web/app.py:1996-2068
15 环境 easel/commands/doctor.pyping.py
超时 easel/timeouts.py
前端会话 web/frontend/src/App.tsx