Subject: Easel (
ZJU-REAL/Easel), based on commit765f5a6(2026-09-10 10:52:51 +0800,main). License Apache-2.0. Version number inpyproject.tomlis 0.1.0;easel/__init__.pyis still0.0.1—the two have not yet aligned. Code size (sparse checkout, skippingassets/promo videos): 705 files. The code that actually works iseasel/9 Python files / 768 lines +web/app.py2,509 lines + frontendweb/frontend/srcabout 5,238 lines of TS/TSX + 112 OpenClaw SKILLs + 44skills/shared/scripts/*.py. Tests 650 lines. The repo is large (about 300MB+ on GitHub); the vast majority is display material. Community size: 733 star / 90 fork / 2 open issue (2026-09-10 GitHub API). Repo created 2026-08-28—about two weeks on the analysis day. Zhejiang University REAL Lab + Peking University OpenDCAI. Homepage zju-real.github.io/Easel. One-line position: an open-source content workbench for domestic social-media creators. It itself does not write an Agent main loop. It treats OpenClaw as the execution engine, and with account personas, 112 executable SKILLs, and real-browser publishing, welds “discover → plan → produce → publish → attribute” into a pipeline that can land on disk and actually post. Audience: readers who have read any analysis in this series. Please change axis first: most earlier projects answer “how to make AI write code / do work”; Open Design answers “how not to write the loop, and treat others’ CLIs as the engine for design”; MiroFish answers “how to let a crowd of AIs rehearse the future.” Easel sits on the same side as Open Design—the host—but the object is not a design file. It is Xiaohongshu cards, short videos, Zhihu answers, and the data after they are posted to a real platform.
Contents
Part I · What it is 1. Project overview: 700 stars in two weeks—what is it actually selling 2. Full stack: a thin integration layer + a borrowed engine + a skill library 3. Five-layer workflow: product narrative, not a runtime scheduler
Part II · How the host catches the engine 4. Three entries, one source of truth: chat / skill / web 5. Prompt four-layer stack: SOUL → AGENTS → CONTEXT → SKILL 6. sync.sh: the workspace is the script; the project root is the worksite 7. Timeouts, proxy, doctor: make the environment a checkable product
Part III · Skills are the product 8. SKILL-SPEC: three-layer load and a 200-line cap 9. 112 Skills: read the library by layer 10. outputs contract: content is a project, not a chat log 11. Showcase: how xhs-note-creator makes a note
Part IV · Real publish and the workbench 12. Two gates: content_guard hard-blocks; persona_gate only reminds 13. Login and one-click publish: Playwright + SMS wall 14. SSE chat: disconnect does not kill, dual lock, uuid5, thinking self-heal 15. Frontend workbench: 11-page SPA and cross-tab sessions
Part V · Evaluation 16. What is actually new 17. Engineering quality: highlights and hard wounds 18. Twelve lessons for building a new product 19. Its place in this series 20. 🔍 Source pointer table
Chapter 1 · Project overview: 700 stars in two weeks—what is it actually selling
1.1 First look at a set of contrast numbers
| Metric | Value |
|---|---|
| Star | 733 (analysis day) |
| Fork | 90 |
| Repo age | about 13 days (2026-08-28 → 2026-09-10) |
| Python integration package | 768 lines / 9 files |
| Web backend | web/app.py single file 2,509 lines |
| Frontend | about 5,238 lines TS/TSX |
| Skill | 112 (discover 9 / plan 16 / produce 50 / publish 20 / attribute 11 / general 6) |
| Shared scripts | 44 skills/shared/scripts/*.py |
| License | Apache-2.0 |
| Homepage | https://zju-real.github.io/Easel/ |
The Python package is only 768 lines; Skills are 112. That pair of numbers is itself an architectural statement:
Easel’s product is not “write another Agent loop.” It is “give an already-there Agent the hands, rules, and workbench of the social-media domain.”
The most important sentence at the top of the README:
Your private, continuously evolving social-media operations assistant. Starting from an idea, finish discover, plan, produce, publish, and review.
Six platforms are written into the capability table: Xiaohongshu, Douyin, Kuaishou, Zhihu, Bilibili, WeChat Channels. The research vision is blunt—take social intelligence from the lab and plug it into the piece of content a creator actually has to post every day.
1.2 What it explicitly is not
| Not | Is |
|---|---|
| another coding agent | a social-media content workbench |
| a homemade ReAct loop | OpenClaw’s profile = easel isolated workspace |
| a “feature-list” skill catalog | a skill library with runnable scripts, artifact landing, and a real-publish gate |
| one global USER.md / MEMORY.md | a per-request self-contained persona prefix + memory.md isolated by persona |
| a half-finished piece in a chat log | a project directory under outputs/<human-readable topic>/ |
The README also writes two usage warnings that are worth reading as product constraints:
- The Web frontend is recommended; CLI is only one of the entries.
- Be cautious about auto-publishing to Xiaohongshu—the platform may detect automation; there is verification, rate limiting, risk control. Preview + a human confirm, then post.
1.3 Who is building it, and whom they thank
Zhejiang University REAL Lab + Peking University OpenDCAI. docs/ACKNOWLEDGMENTS.md lays open the sources of the 112 Skills: card templates explicitly credit nexu-io/open-design; de-AI-taste credits “speak like a human”; template reuse credits fabric……each Skill directory also has EASEL-META.md recording provenance. This is honest: the skill library is compilation + original work, not a from-scratch invention of social-media methodology. The engineering contribution is collecting them into one contract (frontmatter, outputs, gates, personas).
🧠 One sentence: Easel sells “a content partner that remembers your account.” In engineering it is OpenClaw’s vertical host. In this series, Open Design treats 25 CLIs as the engine for design; Easel treats 1 OpenClaw as the engine for social media.
Chapter 2 · Full stack: a thin integration layer + a borrowed engine + a skill library
2.1 Assembly diagram
flowchart TB
subgraph UI["① What you can see"]
WEB["React workbench
chat / skill library / content library / accounts / personas
trends / calendar / publish / review"]
CLI["easel chat / skill / doctor"]
end
subgraph HOST["② Integration layer (what Easel wrote)"]
PY["easel/ 768 lines
persona · timeouts · CLI"]
API["web/app.py 2509 lines
SSE · login · publish · artifact tree"]
SS["skills/shared/scripts 44
output_paths · manifest · content_guard"]
end
subgraph ENGINE["③ Borrowed engine"]
OC["openclaw --profile easel
gateway :18789"]
WS["~/.openclaw/workspace-easel/
SOUL.md + AGENTS.md + skills copies"]
end
subgraph DISK["④ Project disk (the only truth)"]
SK["skills/openclaw/ 112 SKILL"]
PF["profiles/ six-dimension personas"]
OUT["outputs/ content projects"]
ENV[".env model and media keys"]
end
WEB --> API
CLI --> PY
API -->|subprocess + persona prefix| OC
PY -->|subprocess + persona prefix| OC
OC --> WS
WS -.symlink.-> PF
WS -.symlink.-> OUT
OC -->|"cd to project root then run scripts"| SK
SK --> SS
SS --> OUT
2.2 Full stack
| Layer | Tech | Source location |
|---|---|---|
| CLI | argparse subcommands: chat / doctor / gateway / ping / skill / web |
easel/cli.py:124-171 |
| Persona | six-dimension md concatenation + message prefix + per-turn TURN_REMINDER | easel/persona.py |
| Timeouts | 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 |
| Frontend | React + Vite, 11 page components | web/frontend/src/App.tsx, Sidebar.tsx:11 |
| Engine | global openclaw CLI, profile name hardcoded "easel" |
easel/cli.py:31, web/app.py:46 |
| Gateway | 127.0.0.1:18789/healthz |
easel/commands/doctor.py:75-81 |
| Publish | Playwright Chromium + biliup + per-platform scripts | pyproject.toml:20-21, web/app.py:1996-2053 |
| Media | Pillow / OpenCV / faster-whisper / edge-tts / rembg / librosa | pyproject.toml:18-21 |
| Runtime | Python ≥3.10, Node ≥22.19, FFmpeg | easel/commands/doctor.py:148-158 |
The CLI entry in pyproject.toml:25-26 is one line: easel = "easel.cli:main". Web is not an ASGI app inside the Python package; easel web then subprocesses web/app.py (easel/cli.py:157-166).
2.3 Rejected alternatives, read out of comments
The integration layer is extremely thin, but the comments are full of “we tried, we crashed, then we converged here”:
| Rejected | Why rejected | What they do now |
|---|---|---|
Global USER.md writes the current persona |
concurrent sessions overwrite each other | persona as in-message inline (easel/persona.py:1-7) |
Global MEMORY.md carries account knowledge |
cross-persona pollution | on sync, empty the workspace MEMORY.md (openclaw/sync.sh:101-103) |
| Three entries each write their own timeout | the same task can finish via CLI and get cut via Web | easel/timeouts.py:1-8 |
| Three entries each write their own persona logic | behavior inconsistent | converge on easel/persona.py |
openclaw.json5 as the live config |
setup never applies it; it would drift from real config | the file itself warns at the head (openclaw/openclaw.json5:2-5) |
OpenClaw default thinking=high |
when thinking blocks are stored into history they drop the signature; Bedrock replay 400 | default low + session_heal clean (web/app.py:51-54) |
Rely only on --session-key to continue a session |
idle ~24h binding expires; next-day history all gone | use uuid5 to pin --session-id (web/app.py:823-833) |
🧠 One sentence: Easel’s Python is short because it outsourced the loop; where it actually spent effort is welding shut, one by one, the races, timeouts, session loss, and key leaks that necessarily appear after you outsource.
Chapter 3 · Five-layer workflow: product narrative, not a runtime scheduler
3.1 Where the five layers are written
The product layer (README) and the Agent layer (AGENTS.md) use the same vocabulary:
| Layer | README wording | AGENTS.md wording | Skill layer field |
|---|---|---|---|
| Discover | trends and opportunities | trends, hits, remix opportunities | discover (9) |
| Plan | topics, titles, scripts, schedule | topics, scripts, storyboards, cover ideas | plan (16) |
| Produce / make | image-text, audio, video | any task that must create files, write into outputs/ |
produce (50) |
| Publish | check, adapt, really post | compliance, platform adapt, login, really post | publish (20) |
| Attribute | performance flows back into the persona | data, comments, Profile flow-back | attribute (11) |
| (cross-cut) | workbench basics | — | general (6) |
openclaw/workspace/AGENTS.md:41 is restrained:
Clear single-layer tasks execute directly; tasks that cross two or more layers or are obviously multi-step first give a short Plan. One task may compose multiple SKILLs, but do not run unrelated layers.
There is no Python class called WorkflowEngine that schedules by layer. The five layers are division-of-labor rules written for the model, landing on two things:
- Each SKILL.md’s YAML
layer:(routing and docs) - Across layers,
manifest.pyrecords “artifact path + one-sentence conclusion” inoutputs/<topic>/.easel.json(docs/SKILL-SPEC.md:136-149)
3.2 A thin index for vertical orchestration
AGENTS.md’s rules for crossing layers (openclaw/workspace/AGENTS.md:43-45):
- Only build a manifest when crossing two or more layers; single-layer does not
- Register each layer complete or failed
- Downstream uses
latest/readto take upstream, does not re-derive, does not forward the whole block - Full payload writes to files; key decisions write
brief.md
docs/SKILL-SPEC.md:157-159 states the principle even harder: the manifest is only a thin index; summary one line for the orchestration layer to route; outputs[] points at paths; does not copy content.
This is the opposite philosophy of a coding agent stacking intermediate results in the conversation: the conversation is the control plane; the disk is the data plane.
sequenceDiagram
participant U as User
participant A as OpenClaw Agent
participant M as manifest.py
participant D as outputs/topic/
U->>A: help me make a Xiaohongshu note and schedule publish
A->>A: Plan: discover → plan → produce → publish
A->>D: write brief.md / cards / copy
A->>M: record --layer produce --outputs card_1.png,...
A->>M: latest --layer produce
M-->>A: path + one-sentence conclusion
A->>D: publish script --exec
A->>M: meta --status published
3.3 The special status of the produce layer
AGENTS.md defines “produce” as any task that must create files (openclaw/workspace/AGENTS.md:31), not merely “a pretty video.” Timeouts also budget by this definition: the chat entry uses the produce-layer 7200 seconds, because mid-conversation it may generate video (easel/timeouts.py:7-13).
The produce flow is a fixed five steps (openclaw/workspace/AGENTS.md:53-63): condense Profile → clarify spec → produce by SKILL → self-check → rework once only on failure. The produce layer does not read platforms.md; it obeys preferences red lines as-is. This is deliberate: when making content, first make the thing right; platform adaptation is left to the publish layer.
Chapter 4 · Three entries, one source of truth: chat / skill / web
4.1 Three entries, the same sentence
| Entry | How it enters OpenClaw | How persona is injected | Timeout |
|---|---|---|---|
easel chat |
openclaw --profile easel chat --session ... --message <prefix> |
after picking a persona, the prefix is the initial message | TIMEOUT_CHAT (easel/cli.py:103-117) |
easel skill |
openclaw agent --session-key skill-<ts> --message "please execute /<skill>…" |
persona_prefix concatenated before the instruction |
always TIMEOUT_PRODUCE (easel/commands/skill.py:156-164) |
Web /api/chat/stream |
same openclaw agent, plus --session-id pinning the transcript |
chat_turn_message = prefix + user original + TURN_REMINDER |
TIMEOUT_CHAT (web/app.py:1080-1084) |
Web /api/skill |
run_agent_sync |
_persona_prefix + please execute /… |
TIMEOUT_PRODUCE (web/app.py:1453-1463) |
CLI chat does not add TURN_REMINDER; Web chat does. easel/persona.py:72-76 explains why: a SKILL run alone need not; a conversation will be long, and the system prompt will decay.
4.2 What the persona prefix looks like
我当前使用的画像是「科技数码达人」。本会话的账号长期记忆仅使用 profiles/科技数码达人/memory.md,不要使用工作区全局 MEMORY.md 作为账号记忆。
Implementation: easel/persona.py:58-69. Six-dimension file order is hardcoded (identity / style / audience / platforms / preferences / memory, easel/persona.py:16-20). load_profile_text concatenates in that order; other .md files are appended—but what is injected into OpenClaw is not the full text, only a pointer of “go read which directory.” The real condensing is required by AGENTS.md: the model itself reads easel-profiles/<name>/.
4.3 Per-turn behavior reminder: recency against instruction decay
TURN_REMINDER (easel/persona.py:77-83) is concatenated at the end of the user message, marked “internal reminder, do not restate.” Content is three sentences:
- Before acting, look up the skill library; do not do it naked from memory
- All five layers, you yourself write the finished product into
outputs/ - If asking about an account, first check login state; outbound copy must not leak keys, and must not self-disclose “AI generated”
The comment writes the motive clearly (easel/persona.py:72-76): AGENTS.md / SOUL.md are only fresh at the start of a session; later in a long conversation the model will “do it naked from memory.” Putting the most load-bearing reflex at the end costs almost nothing.
This is something rarely written as code in this series: the system prompt will decay, so feed the load-bearing rules again, and feed them after the user’s words.
4.4 How the skill entry digests input
easel/commands/skill.py:48-66: input may be text, or a path. Images are rewritten as “please process this image: absolute path”; audio/video/PDF/archives only pass the path, avoiding read_text exploding binary as UTF-8; ordinary text files are read into the message. Over-long strings (exceeding the filename cap) Path.is_file will throw OSError, treated as text—tests/test_core.py:71-74 specifically locks this pit.
Skill names may omit the skill- prefix (easel/commands/skill.py:39-45); Web find_skill is the same logic (web/app.py:213-219).
Chapter 5 · Prompt four-layer stack: SOUL → AGENTS → CONTEXT → SKILL
docs/prompt-stack.md hardcodes the composition order. Contrast Open Design’s “20 layers banded by cache frequency”: Easel only splits four layers, and the resident layer deliberately does not write concrete skill names.
5.1 Layer 1 — SOUL.md (persona, 30 lines)
openclaw/workspace/SOUL.md: you are the creator’s “buddy”—you understand strategy and can also do the work. Capability overview writes “what you can do” by five layers, does not write skill names, to avoid going stale when a platform is taken down (docs/prompt-stack.md:25). Communication style: Chinese, give 2–3 options, outbound copy never expose tool traces.
5.2 Layer 2 — AGENTS.md (business constitution, 110 lines)
This is the most important file. Six core execution rules (openclaw/workspace/AGENTS.md:5-12):
- Route SKILL first—exact match → nearest → only then use general capability
- Go to the project root first—before the first script you must
cdto the absolute path sync injected - Do not run project copies in the workspace—forbidden to run scripts from OpenClaw workspace’s
shared/ - Look up existing information before asking—login state, persona, historical artifacts first
- Paid operations confirm first—image/video/music gen first give range and cost
- Only a real artifact counts as done—plans, empty shells, prompts do not count
There is also a whole “config check” section: model keys can only be judged against the project-root .env; env / printenv cannot see values that were not exported; workspace ls -a also cannot see the project-root .env—neither can be used to claim missing config (openclaw/workspace/AGENTS.md:16-23). This was forced by a real crash: the Agent in the wrong cwd yelled “you didn’t configure a Key,” when the user actually had.
5.3 Layer 3 — CONTEXT.md (semi-static paths)
Generated by sync.sh, containing the project absolute path. Comments emphasize: this claude version does not support --cwd, so you must cd first then run scripts (openclaw/sync.sh:128-137).
5.4 Layer 4 — SKILL (on demand)
On trigger, load SKILL.md; references are read when cited; scripts are called at execution time, code does not enter the prompt (docs/prompt-stack.md:36-39). This is the same family of idea as Claude Code’s three-layer skill disclosure; Easel wrote it into its own SKILL-SPEC.
flowchart LR
S["SOUL.md
persona · category-level capability"] --> A["AGENTS.md
division of labor · orchestration · safety"]
A --> C["CONTEXT.md
project-root absolute path"]
C --> K["SKILL.md
loaded only when triggered"]
K --> R["references/
domain knowledge on demand"]
K --> SC["scripts/
do not enter the prompt"]
P["persona prefix
every message"] -.-> A
T["TURN_REMINDER
only at the end of Web chat"] -.-> K
Chapter 6 · sync.sh: the workspace is the script; the project root is the worksite
6.1 The real path of the isolated profile
workspace → ~/.openclaw/workspace-easel/
config → ~/.openclaw-easel/openclaw.json
(openclaw/sync.sh:6-8)
--profile easel cuts Easel off from any other OpenClaw workspaces that may exist on the user’s machine.
6.2 What sync does
- Delete SKILLs that no longer exist in the source (
openclaw/sync.sh:30-38) - Whole-directory
cp -reach SKILL into workspace/skills (openclaw/sync.sh:46-53) - Copy
skills/shared/into workspace/shared (for SKILL../../shared/relative refs) - Copy AGENTS.md / SOUL.md, then append the “runtime project root” absolute path (
openclaw/sync.sh:77-93) - Delete leftover USER.md, empty MEMORY.md (
openclaw/sync.sh:97-103) - symlink
easel-profiles→ projectprofiles/,outputs→ projectoutputs/(openclaw/sync.sh:106-124) - Write
CONTEXT.md
The symlink section wrote a real pit: you must rm first then ln -s, otherwise ln -sf follows the old link into the target directory and creates a cycle (openclaw/sync.sh:107).
6.3 Why scripts must run at the project root
manifest.py:58-61 itself confesses: scripts are sync-flattened and copied to workspace/shared/scripts/, where __file__ is one skills/ layer short, walking up would compute ~/.openclaw, and artifacts would write to the wrong place. So the EASEL_ROOT env var is not optional. CLI and Web both setdefault("EASEL_ROOT", project root) in _proxy_env (easel/cli.py:36, web/app.py:303).
The paragraph AGENTS.md appends after sync is equivalent to welding “where to cd” into the system prompt every turn. The shared/ inside the workspace is only for the model to read docs; it is not the execution cwd.
6.4 openclaw.json5 is a template that will never take effect
The file head is unsparing (openclaw/openclaw.json5:2-5):
This file is for reference only. Actual config is written by setup.sh via
openclaw config setdirectly into~/.openclaw-easel/openclaw.json. This template is never applied by setup.sh.
Gateway port 18789, model anthropic/claude-sonnet-4-6 are only illustrative. A source reader who treats the json5 as runtime config will be wrong. This is a rare honesty: mark a “template that may drift” as hazardous, rather than pretend it is the single source of truth.
Chapter 7 · Timeouts, proxy, doctor: make the environment a checkable product
7.1 Three timeout constants
TIMEOUT_PRODUCE = 7200 # generate video / multi-shot composite
TIMEOUT_DIRECT = 300 # discover / plan / publish / attribute
TIMEOUT_CHAT = TIMEOUT_PRODUCE
(easel/timeouts.py:11-13)
chat is budgeted at the produce layer because the user in conversation may anytime say “make it a video.” skill CLI always uses the produce-layer upper bound, simple and blunt, avoiding another layer-branch error.
7.2 Proxy: protect intranet direct connect
_proxy_env fills EASEL_PROXY into http_proxy / https_proxy, while no_proxy contains localhost, 127.0.0.1, *.xiaohongshu.com, *.devops.xiaohongshu.com, 10.* (easel/cli.py:33-40). This is a laboratory-intranet fingerprint: Xiaohongshu internal domains must connect directly; the outside goes through the proxy. content_guard later treats these internal domains as BLOCK-level leaks.
7.3 doctor checklist
easel doctor (easel/commands/doctor.py:143-207) ticks product dependencies one by one:
Python ≥3.10 / venv / Node ≥22.19 / FFmpeg / openclaw on PATH / fastapi·uvicorn·sse_starlette·multipart / frontend dist/index.html / Playwright Chromium / any auth channel in .env / gateway :18789/healthz / ~/.openclaw/workspace-easel/skills/ nonempty / key files exist.
Auth channels are “any one is enough” (easel/commands/doctor.py:92-140): ANTHROPIC_API_KEY, or EASEL_LLM_API_KEY + BASE_URL, or ANTHROPIC_AUTH_TOKEN + BASE_URL, or OPENAI_API_KEY, or Volcano MaaS. Placeholder REPLACE_ME does not count as configured.
easel ping then does a real connectivity check: healthz + make the agent say PONG (easel/commands/ping.py:48-78). doctor is static existence; ping is the authority.
Chapter 8 · SKILL-SPEC: three-layer load and a 200-line cap
docs/SKILL-SPEC.md is the skill-library interface spec v0.3.
8.1 Directory and three layers
skills/
├── openclaw/ five-layer SKILL, OpenClaw executes directly
└── shared/ cross-SKILL scripts and config
skill-xxx/
├── SKILL.md required, < 200 lines, only write “how to do it”
├── references/ domain knowledge, load on demand
├── scripts/ called at runtime, code does not enter the prompt
└── tests/ test1.prompt / test1.expected
| Layer | Load timing | tokens |
|---|---|---|
| Metadata (name, description, layer) | resident, for routing | tiny |
| Instructions (SKILL.md body) | when triggered | medium |
| Resources (references + scripts) | on demand during execution | on demand |
frontmatter keeps only three regular fields: name / description / layer. Forbidden: version, allowed-tools, tags, etc. (docs/SKILL-SPEC.md:73-78). description must in Chinese write capability, trigger wording, and the boundary with neighboring SKILLs.
Two validation commands: scripts/validate_skills.py (frontmatter, resource links, output and publish safety contracts) and scripts/validate_skill_commands.py (Python commands in SKILL vs script argparse, against path drift).
8.2 Five design constraints
Independently tunable, usable without a Profile, stable interface, SKILL.md lean, generalize without giving a concrete case (docs/SKILL-SPEC.md:167-173).
“Usable without a Profile” decides that the product can be played with first, personas built slowly; “independently tunable” decides that 112 directories can be changed in parallel, rather than one big net.
8.3 How it differs from a coding agent’s skill
Claude Code / Codex skills are mostly “teach the model how to use a tool.” Easel’s skill itself is a production pipeline: SKILL.md writes which python skills/.../scripts/foo.py to run; artifact paths are gated by output_paths.py; before publish they are scanned by content_guard.py. A Skill is not a manual; it is an SOP with an executor.
Chapter 9 · 112 Skills: read the library by layer
Counts follow docs/skill-function-mapping.md and the checked-out directories; frontmatter layer stats:
| Layer | Count | What this layer is doing |
|---|---|---|
| general | 6 | artifact management, batch processing, login-account query, persona build/manage, template library |
| discover | 9 | hot search, competitors, content gaps, cross-platform differences, holidays, industry news, RSS, algorithm updates, UGC |
| plan | 16 | positioning, audience, persona, topic matrix, scoring, calendar, hooks, outline, storyboard, livestream, brand deals |
| produce | 50 | text / visual / audio / video / novel / short drama / paper explainer—half the house |
| publish | 20 | six-platform upload, cross-platform distribution, schedule, quality gate, comment reply, short links, notifications |
| attribute | 11 | data, comment insight, ROI, review, persona memory |
The produce layer’s 50 is not “50 kinds of copy.” It collects ffmpeg, TTS, image gen, video gen, subtitles, green screen, beat-sync, albums……into atoms the Agent can call. The shared-script layer is the real multimedia SDK.
Web-side get_skills() (web/app.py:270-286) scans skills/openclaw/*/SKILL.md, parses description/layer, and marks a few Skills needsApi (image/video/music gen / clone / short drama / paper explainer, web/app.py:142-170). The frontend skill-library page uses this table to decide whether to show an exclamation “go configure a Key.”
SKILL_API_REQUIREMENTS and skills/shared/scripts/model_registry.py share the same truth—tests/test_core.py:33-39 asserts the Web’s provider list equals the registry, and serialized results must not contain keys.
9.1 A few produce-layer types worth naming separately
| Type | Representative Skill | Trait |
|---|---|---|
| Platform total entry | xhs-note-creator |
Xiaohongshu image-text/video SOP, forced de-AI, goes through card-design |
| One-click film | auto-short-video |
copy→stills/AI video→voiceover→subtitles→BGM→composite |
| Long content | novel-writer / paper-explainer / short-drama |
file-ized state, cross-chapter consistency, series bible |
| Deterministic media | image-editing / video-editing / audio-editing |
not the model; OpenCV/ffmpeg |
| De-AI taste | text-polisher |
seven-pass scan + Chinese AI-marker table, cited by other Skills as the authority |
xhs-note-creator’s SKILL.md is explicit: a whole note uses this SKILL; card render only uses card-xiaohongshu; other platforms use social-content (skills/openclaw/xhs-note-creator/SKILL.md:4-6). The boundary is written in the description—that is the entire routing mechanism. There is no Python router.
Chapter 10 · outputs contract: content is a project, not a chat log
10.1 Directory convention
outputs/<human-readable topic>/
├── note.md / final.mp4 / card_1.png finished product, at the project root
├── assets/ intermediates: frames, slices, drafts
└── .easel.json display header + steps[] (hidden)
Forbidden: generic names (xhs / test / tmp / 主题……, output_paths.py:25-29); finished products scattered at the outputs/ root; content written into _ system directories. System-directory allowlist: _analytics _debug _inbox _login _probe _profile_build _publish _scratch _sessions (output_paths.py:30-33).
validate_output_path (output_paths.py:41-75) is the gate of every new script: resolve to the project root, must be under outputs/, content at least two path levels, generic names throw OutputPathError outright. System writes must allow_system=True and top in the allowlist.
10.2 Two jobs of the manifest
skills/shared/scripts/manifest.py:
- Display header (
meta): title / platform / kind / status / tags / cover / deliverables—for the frontend content library as cards - steps[] (
record): layer / skill / at / status(done|failed) / outputs[] / upstream[] / summary—for downstream orchestration
kind ∈ article / xhs-note / video / cards / poster / audio / other; project status ∈ draft / ready / published. atomic_write uses mkstemp + os.replace (manifest.py:107-118).
Web _read_project_meta (web/app.py:505-520) only takes display fields; does not leak steps to the content-library UI. Cover resolve: declared cover → first finished media → first image/video in the directory.
10.3 Attachment isolation
User uploads go into outputs/_inbox/, isolated by session. This turn’s message attaches a “system attachment list”; only paths in the list may be used; scanning other inbox files is forbidden (openclaw/workspace/AGENTS.md:91, web/app.py:790-795). When taken into a project, copy to outputs/<project>/assets/; the inbox original is kept for retry.
This is a threat model unique to publish-class Agents: the inbox may have material another session just uploaded; if the model lss once, it contaminates.
Chapter 11 · Showcase: how xhs-note-creator makes a note
Xiaohongshu is Easel’s home field. This SKILL defines a “note” as 3–9 3:4 cards or a 15–90 second vertical storyboard; long text is only an intermediate (skills/openclaw/xhs-note-creator/SKILL.md:14-19).
Workflow (steps cannot be skipped):
- Intake: topic / form / material / style—ask once 0.5 Selling-point formula: scarcity × usefulness × perceivability
- If there is material,
analyze_material.pyproduces a list - Opinion pieces must have external references; core data ≥2 sources
- Write a 2000–4000 character long-form manuscript, confirm with the user first
- Forced de-AI—rules are not maintained as a copy in this SKILL; uniformly go through
text-polisher’s references (SKILL.md:73-79) - Visuals go through
card-designnine styles; render goes throughcard-xiaohongshu - QA, land
outputs/, register the manifest
Among the five hit principles there is a load-bearing sentence: visuals cannot be crude. Wanting “amateur/hand-account” still picks card-design’s sticker style, rather than actually using crude t2i plus large emoji (SKILL.md:25). This is Open Design’s “anti-AI-taste” landing on social-media cards; ACKNOWLEDGMENTS also admits the card templates borrowed open-design.
The scripts directory has validate_meta.py, analyze_material.py, normalize_slug.py, crop_watermark.py, text_on_image.py, collage_3x4.py—the model owns judgment and copy; pixel-level operations go to deterministic scripts.
Chapter 12 · Two gates: content_guard hard-blocks; persona_gate only reminds
This is one of Easel’s most copy-worthy product decisions: an Agent that will really post to a public platform must assume the model will write internal settings into the copy.
12.1 content_guard: fail-closed, exit code 7
The head of skills/shared/scripts/content_guard.py writes the accident (:3-7): in the past there was no filter between generation and --exec; keys, internal URLs, proxy IPs, “generated by AI,” once posted, cannot be deleted.
Two forced levels (content_guard.py:97-105):
| Level | Category | On real publish |
|---|---|---|
| BLOCK | api-key / env-value / internal-host / proxy-ip / internal-path / env-name | exit 7, rewrite and resend |
| WARN | ai-disclosure / model-name | warn only. In a paper explainer, “Claude” may be body text |
Scan is dual-path: a regex table + literal values of sensitive keys in .env (content_guard.py:141-176). Hit fragments are masked in the report (_mask / _snippet), avoiding a second leak via logs. dry-run all only warn; releasing a hard block requires explicit --allow-unsafe. AGENTS.md requires: unless the user explicitly asks, do not use this switch to bypass (openclaw/workspace/AGENTS.md:72).
In the regexes you can see lab topology: maas.devops.xiaohongshu.com, webide-gateway.devops.xiaohongshu.com, /mnt/tidal-alsh01, happyhorse internal video-gen name (content_guard.py:50-85). The gate is both a general product and a desensitization layer when this code was open-sourced from an internal tool.
12.2 persona_gate: persona check never blocks publish
Before a publish-layer SKILL executes, if there is a Profile, run skill-persona-check (LLM scores), then hand to persona_gate.py check to turn the score into pass/warn. Threshold 80 (persona_gate.py:29). Exit code is always 0 (persona_gate.py:10-11, 54): publish_allowed: true.
AGENTS.md nails the reason (openclaw/workspace/AGENTS.md:74-78): persona check only reminds; if the user already explicitly wants to post, continue, do not extra-ask for confirmation. Content safety and platform compliance still independently hard-block.
record writes the score into the manifest’s publish step, for later audit.
🧠 One sentence: a key leak is an irreversible public accident, so hard-block; “this piece does not quite fit the persona” is a creative judgment, so only remind. The two gates’ force is inverted—the correct split of responsibility for “automated publish.”
flowchart TD
C[copy to publish] --> G{content_guard}
G -->|BLOCK hit| X[exit 7 rewrite]
G -->|pass or WARN only| P{has persona?}
P -->|yes| S[skill-persona-check LLM score]
S --> PG[persona_gate classify]
PG -->|pass / warn both continue| E[--exec real publish]
P -->|no| E
Chapter 13 · Login and one-click publish: Playwright + SMS wall
13.1 Six-platform login backend
LOGIN_RUNNERS (web/app.py:98-105):
| Frontend key | Display name | backend |
|---|---|---|
| xiaohongshu | Xiaohongshu | xhs |
| douyin | Douyin | douyin |
| kuaishou | Kuaishou | web + wp=kuaishou |
| weixin-channels | WeChat Channels | web + wp=weixin-channels |
| zhihu | Zhihu | web + wp=zhihu |
| bilibili | Bilibili | biliup |
Browser profiles live in ~/.easel-browser-profiles (web/app.py:82). Login state writes outputs/_login/. whoami real verify has to start a headless browser; in-process cache 600 seconds (web/app.py:93-96), avoiding the accounts page and the workbench opening browsers twice.
13.2 /api/publish/{platform}
Second confirmation is on the frontend. The backend assembles the --exec command (web/app.py:1996-2053):
- Xiaohongshu:
xhs_publish.py publishorpublish-video - Douyin: same structure, but async—may trigger an SMS wall; state writes
outputs/_publish/douyin.json; verification code writesdouyin.code; frontend polls untilsms_requiredpopup - Bilibili: direct
biliup upload, default partition tid=36 “Knowledge,” no-tag fallback “Daily” - Others:
web_publisher.py --platform <wp>
Constraints: some platforms must carry media; image and video cannot post together; Channels-class can only post video. Timeout 600 seconds. Each publish appends outputs/_publish.log.
Publish goes through _publish_env(), separate from the chat proxy environment, avoiding hitting the platform domain with a proxy and losing login state.
13.3 Why the README tells you to be careful with Xiaohongshu
Automated operations will be risk-controlled. Easel chose “can really post” rather than “only give copy”—this is product differentiation, and also the cost of compliance and account safety. The code stacks dry-run, preview, human confirm, content_guard in front of --exec, but cannot stop platform-side behavior detection. This is not a bug. It is a warning that must be written on the packaging after the host chose to enter the real world.
Chapter 14 · SSE chat: disconnect does not kill, dual lock, uuid5, thinking self-heal
The chat implementation in web/app.py is the most “systems” stretch of this thin integration layer. Comment density is extremely high; almost every one corresponds to an online accident.
14.1 Problem 1: CLI does not stream
openclaw agent buffers model output until the end then prints (web/app.py:1028-1032). Answer: set OPENCLAW_RAW_STREAM=1 + a per-turn independent jsonl path; the backend tails the file, turns assistant_text_stream’s text_delta into SSE token, thinking into thinking. Per-turn independent files, no concurrent crosstalk.
14.2 Problem 2: a proxy will cut SSE, but produce has to run a long time
supervisor (runs openclaw) and forward (to the browser) are split (web/app.py:1037-1039, 1352-1374):
- Client disconnect only ends forward, does not cancel supervisor
- Full result lands
outputs/_sessions/<sk>.json; frontend takes it back with/api/chat/last - Also
/api/chat/jobs/{turn_id}/streamcontinues SSE from the jsonl (web/app.py:986-1021) - Only the user clicking stop
terminates the process (web/app.py:1408-1415); disconnect does not kill
A long task when a WebIDE proxy cuts SSE is the scene named in the comments (web/app.py:946-947).
14.3 Problem 3: two openclaw on the same session = takeover crash
OpenClaw concurrent write of the same session throws EmbeddedAttemptSessionTakeoverError; the symptom is “stops at a colon halfway through the answer” (web/app.py:808-811). Dual-layer lock:
- In-process
asyncio.Lockby session-key (web/app.py:812-820) - Cross-process
fcntl.flock(Windows usesmsvcrt.locking) (web/app.py:841-896)
Different sessions can still run in parallel. If flock cannot be taken, return “running in another window.” The frontend also has BroadcastChannel probing cross-tab occupancy (Chapter 15).
14.4 Problem 4: ask again the next day and it has forgotten
OpenClaw relies on --session-key to resolve the transcript; idle beyond about 24h (threadBindings.idleHours) the binding expires, and a new empty transcript is started (web/app.py:823-827). Answer: uuid5 with a fixed namespace, the same web sessionId is always the same --session-id (web/app.py:828-833), bypassing key→binding expiry.
14.5 Problem 5: thinking blocks drop the signature, replay 400
When OpenClaw stores Claude thinking into jsonl it drops the signature; intranet Bedrock replay verification fails (scripts/session_heal.py:3-6, web/app.py:51-54). Before every spawn, _heal_openclaw_session deletes thinking / redacted_thinking blocks and empty messages. Default EASEL_THINKING_LEVEL=low reduces production.
14.6 Problem 6: “it finished answering” but it did not actually finish
Finish detection (web/app.py:1267-1290) does not treat “already spat words” as success. The only mark of a normal finish is that the last event of the raw stream is assistant_message_end. Otherwise append a warning for the user to see: hit-ceiling truncation, process killed, stopped at tool_use, stream cut, body stopped at a Chinese colon (user-measured: “every mysterious stop stops at a colon”).
Every finish also writes one diagnostic line to outputs/_debug/chat-stream.jsonl (web/app.py:1313-1333): rc, stop_reason, last_ev, token/thinking character counts, whether it mistakenly received another session’s raw events.
flowchart TB
B[browser] -->|POST /api/chat/stream| F[forward generator]
F -->|SSE token/thinking/activity| B
S[supervisor background task] -->|client_q| F
S --> L[asyncio lock + flock]
L --> P[openclaw agent subprocess]
P --> RAW[per-turn jsonl raw stream]
S -->|tail| RAW
S -->|atomic write| T[outputs/_sessions/sk.json]
B -.disconnect.-> F
S -.continue after disconnect.-> T
B -->|/api/chat/last| T
Chapter 15 · Frontend workbench: 11-page SPA and cross-tab sessions
15.1 Pages
Page type in Sidebar.tsx:11:
dashboard | chat | trends | ideas | calendar | publish | breakdown | skills | outputs | accounts | profile
The sidebar only puts six primary navs (workbench / chat / skill library / content library / accounts / personas); trends, topics, calendar, publish, review fold into workbench SubNav, avoiding sidebar explosion (Sidebar.tsx:31-39).
App.tsx puts streaming state on the root component; switching pages does not unmount, does not drop the stream (web/frontend/src/App.tsx:164 comment). This is the correct structure for “the user goes to the content library to glance at a generated artifact while the conversation is still running.”
15.2 Cross-tab: BroadcastChannel + flock fallback
Mount logic (App.tsx:55-125):
- Same-tab refresh:
sessionStorageremembers this tab’s session → continue - New tab: use BroadcastChannel to ask “who is using the last active session”; if someone answers
owned, open a new session, avoiding two windows colliding on the same OpenClaw session - No BroadcastChannel support: fall back to old logic; backend flock can still block the crash
Onboarding: no persona at all and has not seen the guide → recommend the config wizard (App.tsx:144-147). Also handled a brand rename: old localStorage key migrated from postcraft_onboarding_seen to easel_onboarding_seen (uses ['post','craft'].join('') to dodge simple literal scans, App.tsx:36).
15.3 API client
web/frontend/src/lib/api.ts derives BASE from the current pathname, convenient for subpath deploy. Errors prefer FastAPI detail. Types write the .easel.json display header and artifact-tree nodes in full, aligned with backend _read_project_meta / _build_output_node.
Chapter 16 · What is actually new
In this series’ coordinate system, Easel is not “another stronger loop.” It is new in five things:
16.1 A vertical host, and the host only plugs into one engine
Open Design’s host plugs into 25 CLIs; the adapter is data. Easel only plugs into OpenClaw, and puts all differentiation into SKILL + persona + real-publish gates. Narrower, so it can take social-media SOP deep (112, with scripts, with a contract).
16.2 Conversation is the control plane; the project directory is the data plane
A coding agent’s artifacts often live in diffs and chat. Easel forces outputs/<topic>/ + finished/intermediate split + a thin manifest. This is “one topic, one folder” that a content team can understand, not an engineer’s session log.
16.3 Persona is part of the request, not a global file
After rejecting USER.md, concurrent sessions no longer step on each other. Memory is isolated by profiles/<name>/memory.md; global MEMORY.md is kept empty by the sync script. This is the correct memory model for multi-account operations.
16.4 Graded gates before real publish
content_guard’s BLOCK/WARN grading, persona_gate never blocking, .env literal-value scan—aimed at the accident unique to the publish scene: “the Agent will say the internal world out loud.” A coding agent’s sandbox does not solve this.
16.5 Treat OpenClaw’s pits as a product surface to fix
thinking dropping the signature, 24h session-binding expiry, concurrent takeover, SSE cut by a proxy, instruction decay, wrong cwd causing a false missing-Key report—none of these automatically get better by “calling the CLI.” A large half of Easel’s 2509-line backend is adapter-style wound dressing for the upstream engine. This is the same class of work as Open Design writing parsers for 25 CLIs.
Chapter 17 · Engineering quality: highlights and hard wounds
17.1 Highlights
| Point | Evidence |
|---|---|
| Three entries converge on one source of truth | persona.py, timeouts.py; comments write the historical fork |
| Deterministic scripts with selftest | content_guard / persona_gate / manifest / output_paths |
| Atomic write | manifest os.replace; turn result .tmp then replace |
| Tests lock the contract | tests/test_core.py covers input classify, path safety, registry desensitization, persona_gate |
| Skill provenance traceable | ACKNOWLEDGMENTS + each Skill’s EASEL-META.md |
| Config template marked invalid | openclaw.json5 head warning |
| Diagnostic logs | chat-stream.jsonl classifies “mysterious stop” |
17.2 Hard wounds and costs
web/app.pysingle file 2509 lines. Routes, locks, SSE, login, publish, artifact tree, persona CRUD all squeezed together. Understandable for a two-week repo; unfriendly to later contributors.- Version-number split.
pyproject.toml:7’s0.1.0vseasel/__init__.py:3’s0.0.1. - The engine is not in the repo. OpenClaw is
npm i -g openclaw. Analysis cannot check the loop itself; Easel’s correctness depends on upstream CLI flags (--session-id,OPENCLAW_RAW_STREAM) continuing to exist. - json5 template drifting from real config is known, but newcomers will still be fooled by the filename.
- Skill quality is necessarily uneven. 112 directories; some are complete pipelines (xhs-note-creator); some are closer to prompt wraps.
validate_skills.pycan lock format; it cannot lock “whether this SOP is useful in real operations.” - Tests are only 650 lines, and explicitly only test integration-layer pure functions (
tests/test_core.py:1-4). Publish scripts, Playwright, OpenClaw interaction are a test blind spot—the most dangerous part of the real-publish path is the hardest to test. - CORS
allow_origins=["*"](web/app.py:191) is enough for a local workbench; it is not a shape that can be exposed to the public net. - Xiaohongshu auto-publish risk control is written into the README, but the code path still provides
--exec. The product hands the choice to the user, and also the risk. - sync is whole-directory copy, not incremental. 112 Skills full
cpevery time; workspace and source may be overwritten while the Agent is running. - TURN_REMINDER appended every turn; long-conversation token cost grows linearly; the exchange is routing hit rate. This is an open trade-off (
easel/persona.py:72-76).
Chapter 18 · Twelve lessons for building a new product
- First decide who owns the loop. If domain SOP is worth more than general reasoning, do not write the loop; write a host.
- The host’s core work is dressing the engine’s wounds, not one
subprocess.runline. - Treat domain artifacts as first-class citizens of the filesystem; do not let them live in the chat log.
- Cross-step transfer uses a thin index + paths, not orally passing the full text.
- Text that will go to the public net must have a deterministic scan. “Do not leak” in the prompt is not enough.
- Grade the gates: irreversible accidents hard-block; aesthetics/persona only remind.
- Multi-tenant memory does not live in a global file. Persona is part of the request.
- A long conversation must assume the system prompt will decay; feed load-bearing rules again via recency.
- Decouple SSE from long tasks: disconnect does not kill; results land on disk; the frontend comes to take them.
- The same session at the same moment allows only one engine process—the lock must go outside the process.
- Make environment checks a product command (doctor / ping); do not only write a README.
- Write “this file does not take effect” at the file head. A drifting template is more dangerous than no template.
Chapter 19 · Its place in this series
flowchart LR
subgraph LOOP["write the loop themselves"]
CC["Claude Code / Codex / dsh
general: write code, do work"]
MF["MiroFish
vertical: simulation rehearsal"]
end
subgraph HOST["do not write the loop · host"]
OD["Open Design
vertical: design files"]
EA["Easel
vertical: social-media content + real publish"]
end
CC -.skill idea.-> EA
OD -.card visuals / host shape.-> EA
| Project | Question | Relation to Easel |
|---|---|---|
| Open Design | do not write the loop; plug into 25 CLIs for design | nearest kin: both hosts. Easel only plugs into 1 engine; the skill library is more vertical; card visuals also borrowed it |
| OpenWorker | general-task coworker, inbox + approval | both face “non-coding work”; OW emphasizes recovery when the human is away; Easel emphasizes content projects and real publish |
| MiroFish | simulation, does not do work | both non-coding; one rehearses a world, one produces content |
| Claude Code / Codex | coding agents that write the loop | Easel uses that kind of skill idea on social-media SOP; the loop itself is outsourced |
| DeepSeek Harness | the loop is also a plugin | contrast: dsh takes replaceability to the extreme; Easel gives replaceability to upstream and itself locks the domain contract |
Lineage patch: the first seven coding agents → OpenWorker coworker → Open Design design host → MiroFish simulation → Easel social-media host. It takes the “do not write the main loop” route from a design tool into the six platforms a domestic creator faces every day.
Companion reading: Build a social-media content workbench from scratch (Easel lineage) · interactive benches Easel-解析.html
Chapter 20 · 🔍 Source pointer table
All against commit 765f5a6.
| You want to see | Go here |
|---|---|
| CLI entry and chat injecting persona | easel/cli.py:49-121 |
| Three-entry persona prefix / TURN_REMINDER | easel/persona.py:1-99 |
| Timeout constants | easel/timeouts.py |
| skill routing and binary input | easel/commands/skill.py:31-164 |
| doctor / ping / gateway | easel/commands/doctor.py, ping.py, gateway.py |
| Prompt-stack explanation | docs/prompt-stack.md |
| SKILL spec | docs/SKILL-SPEC.md |
| 112 Skill capability map | docs/skill-function-mapping.md |
| Agent constitution | openclaw/workspace/AGENTS.md |
| Persona | openclaw/workspace/SOUL.md |
| Sync and symlink | openclaw/sync.sh |
| “this config does not take effect” | openclaw/openclaw.json5:1-35 |
| SSE supervisor / dual lock / uuid5 | web/app.py:808-1401 |
| Stop vs disconnect | web/app.py:1408-1415 |
| Skill HTTP | web/app.py:1453-1463 |
| Artifact tree and display header | web/app.py:488-545 |
| Login table | web/app.py:98-105 |
| One-click publish | web/app.py:1996-2068 |
| Frontend cross-tab | web/frontend/src/App.tsx:55-125 |
| Page enumeration | web/frontend/src/components/Sidebar.tsx:11-39 |
| Path gate | skills/shared/scripts/output_paths.py |
| Cross-layer contract | skills/shared/scripts/manifest.py |
| Outbound scan | skills/shared/scripts/content_guard.py |
| Persona reminder | skills/shared/scripts/persona_gate.py |
| thinking self-heal | scripts/session_heal.py |
| Xiaohongshu SOP | skills/openclaw/xhs-note-creator/SKILL.md |
| Dependencies and version | pyproject.toml |
| Integration-layer tests | tests/test_core.py |
| Skill acknowledgments | docs/ACKNOWLEDGMENTS.md |