English lecture · source-traced

Easel source analysis: do not write the main loop—weld OpenClaw into a social-media content workbench

**Subject**: Easel ([`ZJU-REAL/Easel`](https://github.com/ZJU-REAL/Easel)), based on **commit `765f5a6`** (2026-09-10 10:52:51 +0800, `main`). License **Apache-2.0**. Version number in `pyproject.toml` is **0.1.0**; `easel/__init__.py` is still `0.0.1`—the two have not yet aligned. **Code size** (sparse checkout, skipping `assets/` promo videos): **705 files**. The code that actually works is **`easel/` 9 Python file

EN
lecture
source
file:line
no
embellishment
中文
sibling

Subject: Easel (ZJU-REAL/Easel), based on commit 765f5a6 (2026-09-10 10:52:51 +0800, main). License Apache-2.0. Version number in pyproject.toml is 0.1.0; easel/__init__.py is still 0.0.1—the two have not yet aligned. Code size (sparse checkout, skipping assets/ promo videos): 705 files. The code that actually works is easel/ 9 Python files / 768 lines + web/app.py 2,509 lines + frontend web/frontend/src about 5,238 lines of TS/TSX + 112 OpenClaw SKILLs + 44 skills/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


Part 1

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:

  1. The Web frontend is recommended; CLI is only one of the entries.
  2. 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.


Part 2

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.


Part 3

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:

  1. Each SKILL.md’s YAML layer: (routing and docs)
  2. Across layers, manifest.py records “artifact path + one-sentence conclusion” in outputs/<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 / read to 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.


Part 4

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:

  1. Before acting, look up the skill library; do not do it naked from memory
  2. All five layers, you yourself write the finished product into outputs/
  3. 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).


Part 5

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):

  1. Route SKILL first—exact match → nearest → only then use general capability
  2. Go to the project root first—before the first script you must cd to the absolute path sync injected
  3. Do not run project copies in the workspace—forbidden to run scripts from OpenClaw workspace’s shared/
  4. Look up existing information before asking—login state, persona, historical artifacts first
  5. Paid operations confirm first—image/video/music gen first give range and cost
  6. 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

Part 6

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

  1. Delete SKILLs that no longer exist in the source (openclaw/sync.sh:30-38)
  2. Whole-directory cp -r each SKILL into workspace/skills (openclaw/sync.sh:46-53)
  3. Copy skills/shared/ into workspace/shared (for SKILL ../../shared/ relative refs)
  4. Copy AGENTS.md / SOUL.md, then append the “runtime project root” absolute path (openclaw/sync.sh:77-93)
  5. Delete leftover USER.md, empty MEMORY.md (openclaw/sync.sh:97-103)
  6. symlink easel-profiles → project profiles/, outputs → project outputs/ (openclaw/sync.sh:106-124)
  7. 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 set directly 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.


Part 7

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.


Part 8

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.


Part 9

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.


Part 10

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:

  1. Display header (meta): title / platform / kind / status / tags / cover / deliverables—for the frontend content library as cards
  2. 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.


Part 11

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):

  1. Intake: topic / form / material / style—ask once 0.5 Selling-point formula: scarcity × usefulness × perceivability
  2. If there is material, analyze_material.py produces a list
  3. Opinion pieces must have external references; core data ≥2 sources
  4. Write a 2000–4000 character long-form manuscript, confirm with the user first
  5. Forced de-AI—rules are not maintained as a copy in this SKILL; uniformly go through text-polisher’s references (SKILL.md:73-79)
  6. Visuals go through card-design nine styles; render goes through card-xiaohongshu
  7. 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.


Part 12

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

Part 13

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 publish or publish-video
  • Douyin: same structure, but async—may trigger an SMS wall; state writes outputs/_publish/douyin.json; verification code writes douyin.code; frontend polls until sms_required popup
  • 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.


Part 14

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}/stream continues 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:

  1. In-process asyncio.Lock by session-key (web/app.py:812-820)
  2. Cross-process fcntl.flock (Windows uses msvcrt.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

Part 15

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):

  1. Same-tab refresh: sessionStorage remembers this tab’s session → continue
  2. 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
  3. 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.


Part 16

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.


Part 17

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

  1. web/app.py single 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.
  2. Version-number split. pyproject.toml:7’s 0.1.0 vs easel/__init__.py:3’s 0.0.1.
  3. 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.
  4. json5 template drifting from real config is known, but newcomers will still be fooled by the filename.
  5. Skill quality is necessarily uneven. 112 directories; some are complete pipelines (xhs-note-creator); some are closer to prompt wraps. validate_skills.py can lock format; it cannot lock “whether this SOP is useful in real operations.”
  6. 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.
  7. 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.
  8. 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.
  9. sync is whole-directory copy, not incremental. 112 Skills full cp every time; workspace and source may be overwritten while the Agent is running.
  10. 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).

Part 18

Chapter 18 · Twelve lessons for building a new product

  1. First decide who owns the loop. If domain SOP is worth more than general reasoning, do not write the loop; write a host.
  2. The host’s core work is dressing the engine’s wounds, not one subprocess.run line.
  3. Treat domain artifacts as first-class citizens of the filesystem; do not let them live in the chat log.
  4. Cross-step transfer uses a thin index + paths, not orally passing the full text.
  5. Text that will go to the public net must have a deterministic scan. “Do not leak” in the prompt is not enough.
  6. Grade the gates: irreversible accidents hard-block; aesthetics/persona only remind.
  7. Multi-tenant memory does not live in a global file. Persona is part of the request.
  8. A long conversation must assume the system prompt will decay; feed load-bearing rules again via recency.
  9. Decouple SSE from long tasks: disconnect does not kill; results land on disk; the frontend comes to take them.
  10. The same session at the same moment allows only one engine process—the lock must go outside the process.
  11. Make environment checks a product command (doctor / ping); do not only write a README.
  12. Write “this file does not take effect” at the file head. A drifting template is more dangerous than no template.

Part 19

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


Part 20

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
↑