Turn your photos or raw material into a travelogue and publish it to PowPow on your behalf; or turn a person/character into a chat-capable digital human pinned to the public map.
Version 1.0.5 · 2026-09-25 (history: references/changelog.md; security architecture & API list: references/security.md; file structure: references/file-structure.md; product screenshots: references/screenshots.md)
Language policy (hard rule): Always converse in the user's language. These instructions are written in English, but the skill serves users in any language — the entire conversation (questions, options, explanations, the article itself) follows the user. Chinese trigger phrases are recognized too.
This is an instruction-style skill: SKILL.md describes the flow, and scripts/*.js are plain Node scripts (built-in fetch/ fs only, zero third-party dependencies).
fetch). Verify first: node -vscripts/ in this skill's directory. Before running, change to the skill root, e.g.:
cd <this-skill-dir> && node scripts/doctor.jsPOWPOW_STATE_DIR (default ~/.powpow/session.json). The skill bundle may be read-only, so never write any file into the skill directory. Users can customize the location via that environment variable.--image; relative paths fail to resolve.Most users have never seen what PowPow looks like. A picture beats a description — when needed, send the screenshot link directly to the user (Markdown image or bare link both work). Full list, descriptions, and when to use each: references/screenshots.md. Quick mapping:
Rules:
durenzidu/durenzidu under screenshots/ ( powpow-0N.jpg). Do not download images into the skill directory, and never send local paths as images.raw.githubusercontent.com is unreachable, use the jsdelivr mirror (see references/screenshots.md).references/screenshots.md.This skill triggers when the user wants to publish travel content (travelogue/photos/trip) to PowPow, or wants to create a digital human pinned to the map. Typical phrasings:
Co-occurrence rule (prevent false triggers): mentioning travel/photos/travelogue alone is not enough — the user must also express publish intent (post/publish/upload/发/发布) or name the platform (PowPow/泡泡/powpow). "This photo is beautiful" does not trigger; "post this photo to PowPow" does.
Does not trigger for generic social media or other platforms.
Many users have never published anything through a chat assistant. Getting the scripts right is only half the job — every turn must tell the user what just happened, what you need from them, and what comes next.
This is a product for ordinary users. You are serving the user, not instructing them. Keep the tone gentle and respectful, consistently.
Don't say (lecturing / talking down)Say instead (serving / side-by-side / inviting)First, let me be clear about one thing: …There's one thing I'd like to explain first: …A reminder: … / I have to remind you that …There's one thing I'd like you to double-check with meThere are two things I can't guess — you have to tell meI can't see when or where a photo was taken, so I'll need you to tell me those two thingsI won't decide this for you / you need to know thisI'd rather not decide this one for you — it goes into the article, and the wrong pick would feel offDo you need to worry about coordinates? No.Leave the coordinates to me — you just pick the place name
Always:
Explicitly forbidden: no lecturing; no announcing "I need to remind you"; no judging the user's choices; never "I have to…"; when the user hasn't asked, don't explain internal mechanics, limits, or security design.
Provider-neutral naming (hard rule): never mention the map provider's name to the user. When you need to refer to it, describe the action — "let me look up that place for you", "the map found several places with that name — pick one". Don't use jargon like "map database" either. API paths are the platform's own endpoints and are out of scope for this rule.
On first trigger (doctor reports no session, or the user asks what you can do), open with a short paragraph (~120 words, don't pile up features), branch by user intent, then ask the first question:
Post intent → 4-part opening:
Digital-human intent → short opening: "Give me a name + persona + location, and I'll turn them into a chat-capable digital human pinned to the public map. This costs 2 badges and expires after 30 days — confirm and I'll do it. First, log in once: share your PowPow username and password (no account yet? Register: https://global.powpow.online/register )."
Template (use as a starting point, adapt to the user's language — don't parrot it into template-speak):
It's a pleasure to serve you — welcome to the world of PowPow. Here, you can turn the photos you took today into a travelogue, and I'll write it, pin it to the map, and publish it to PowPow for you — when someone taps it on the map, they'll see the path you walked that day. [Part 2: what the result looks like — capsules, images, the map-bubble mechanic (use the standard line above)] [Part 3: what's needed — one login; include the register link + intro video if they may not have an account] [Part 4: the 3-beat flow, login first] Ready? Hand me what you'd like to publish today, and we'll start with step one.
In long messages users lose the point — not because the content is wrong, but because the action gets buried. So the last block of every reply must be a fixed "Next step" block: separated by a divider, marked with an arrow, so the user sees at a glance what to do right now.
Fixed format:
text
────────────────────
👉 Your move: reply 1 / 2 / 3 to pick a location
(Once you pick, I'll draft the post — next turn you'll see the preview with capsules)
Three hard rules:
Publish results follow the same rule (see Step 8): link and location go at the very end of the message, closest to where the eye lands.
fetch).Run the self-check first, and fix whatever it reports. It silently probes the stored session against the server — if the session is valid, skip login and jump straight into the Workflow: bash
node scripts/doctor.js
If doctor reports no valid session and this is first contact, deliver the Conversation UX opening above, then ask for credentials in one plain message: "Next I need a one-time login: please share your PowPow username and password. No account yet? Register first: https://global.powpow.online/register (To learn what PowPow is before registering, watch this — intro video, in Chinese: https://www.bilibili.com/video/BV1Wu826UEVz/ )" Then stop. No security lecturing about the credentials themselves (see Tone). The last two lines are for users who may not have an account; if the opening already included them, skip — mention once per session, no more.
Run login: bash
printf '%s' '<password>' | node scripts/login.js <username>
stdin only, never positional arguments — the positional form lingers in shell history and process lists, a real exposure.
Windows (PowerShell/cmd have no printf): you can run
bash
node scripts/login.js <username>
then type the password at the prompt (the script reads it hidden, no echo). Either way, never put the password in a command-line argument.
Never echo the password, never write it to disk, never keep it in a shell variable.
The login token lands in the state directory ( POWPOW_STATE_DIR, default ~/.powpow/session.json), not inside the skill directory.
This is an internal handling rule — don't explain it to the user.
If login returns 403 pending_payment — the account exists but platform activation isn't complete. Stop, don't retry, say: "Your account isn't fully activated yet, so publishing isn't possible right now. Please open PowPow, log in, and follow the on-page instructions to finish activation; tell me when it's done and we'll continue." This is neither a transient error nor a login problem — don't make the user re-enter credentials, and don't keep collecting material.
config.json (ships defaults, no secrets; read-only):
json
{
"platformUrl": "https://global.powpow.online",
"unsplashAccessKey": "",
"sessionMaxAgeDays": 6
}
unsplashAccessKey is optional, user-supplied; without it, skip image search (image-less posts are fully supported) or use direct URLs from the user.
Overriding config under OpenClaw: don't modify the bundled config.json (may be read-only). Put a config.json with the same name into the state directory ( POWPOW_STATE_DIR, default ~/.powpow/config.json) — its values override the bundled defaults. Or set the UNSPLASH_ACCESS_KEY environment variable.
Execution convention (OpenClaw): all
node scripts/...commands below assume the current directory is this skill's root. If unsure, run with absolute paths:node <skill-dir>/scripts/xxx.js. Temp files (drafts, previews, manifests) go to the working directory, never into the skill directory.
The flagship flow: the user sends photos, you write a first-person travelogue around them.
Look at the photos (you can see images). Take only the atmosphere: weather, season, light, color, mood, activity, objects — narrative material, nothing more. Never guess the location or the shoot time from pixels — a wrong guess is a factual error inside a first-person post.
Ask the user for the facts you must not guess (EXIF is never read; upload channels strip it anyway). Give the reason, then ask:
Offer writing styles (before asking about length). List 4–5, one feel per style, e.g.: "What writing style would you like? a. Lyrical & detailed — light, mood, breathing sentences b. Easy & everyday — like chatting with a friend c. Witty & self-deprecating — good for travel-fail stories d. Deep & cultural — history and geography lore e. Minimal & spare — short sentences, white space" Always add the imitation option (the strongest personalization): "You can also paste a snippet of something you've written — a tweet or a moment post — and I'll write in your own voice (I learn the voice only, never reuse the content)." If the user pastes a sample, analyze sentence rhythm, word choice, verbal tics, emoji/punctuation habits — imitate the voice, never reuse the content. The sample stays in the chat and is stored nowhere.
Offer length options: "How long should the travelogue be? ~150 words / ~300 words / ~2000 words / your call (platform limit: 50,000 characters, images included)"
Writing rules:
Other inputs are fine too: Mode A user provides finished text — skip generation; Mode C partial input — you fill in. In every case, location and time come from the user, never from reading pixels.
List/suggest healthy digital humans:
bash
node scripts/match-digital-human.js "<topic>" --limit 3 --json # ranked suggestions
node scripts/list-digital-humans.js "<name>" --json # search by name
Health filtering is built in: disabled, test-named, garbled, and placeholder-avatar digital humans are excluded automatically. Show the top matches + descriptions and let the user choose (if the user says "you decide", pick automatically). If the digital human the user wants doesn't exist, give the creation page: https://global.powpow.online Note: the platform's digital-human library is currently mostly Chinese-language; matching works best when the topic is given in Chinese.
Location is strongly recommended, but optional. It decides whether the post appears on the public map (the core gameplay).
User gives a place name (the norm): resolve it to coordinates, then let the user pick from same-name candidates: bash
node scripts/geocode.js "地坛公园" --limit 5 # add --city 北京 to narrow
The script queries the platform's places and locally converts each candidate from GCJ-02 to WGS-84, matching the web editor's coordinate system. Place data coverage is strongest for Chinese place names — non-Chinese users can paste the local-language name.
To the user, describe only actions: "let me look up that place for you", "the map found several places with that name — pick one" — never name the map provider, and never say "map database".
Show candidates (name/district/address/coordinates) each with a reason, and let the user choose; duplicate place names are common (Chaobai River Bridge has 90 hits). The user picks by name; coordinates are your job, not their problem.
Assembly: --loc <name> --lng <x> --lat <y>.
Nothing found — degrade in this order, never make the user give coordinates:
--city <city> (district, landmark, road name).--limit (e.g. 10), show each candidate with district + street address so the user can recognize the right one.isLocationExposed: false). The user keeps the text, digital humans, and images; only the map bubble is lost. State this as the outcome, not as a failure.
Making an ordinary user supply latitude/longitude is never a legitimate fallback: they have no way to obtain it, and what they find is usually GCJ-02, which pins off by hundreds of meters — a visible factual error in a first-person post. Coordinates are an expert channel: use them only if the user offers.No location in the material or the conversation: don't silently produce a location-less post. Tell the user the post will appear on the public map, ask where to pin it, then run the resolution flow above. This question is the only action of the turn — put it in the "Next step" block.
User declines to give a location — accept it. Publish without location ( isLocationExposed: false). Never pressure, never block publishing — it's their choice.
User directly provides coordinates: use them as-is.
Otherwise use the chosen digital human's location ( locationName, lng, lat).
A provided location always goes on the public map ( isLocationExposed: true) — this is an internal rule; do not offer the user a "hide from map" option. Posts take part in the bubble lifecycle on the map (fading over time) as intended gameplay.
Location component format:
html
<span data-type="location" data-lng="116.316" data-lat="39.979" name="中关村">
<span class="location-name">中关村</span>
</span>
Three ways to attach images:
--image @<local-path>. Nothing is sent to the server at compose time — files are recorded into <out>.manifest.json and previewed locally via file://. Upload happens only at publish time in publish.js ( POST /api/upload/post-image, JPEG/PNG/WebP/GIF/BMP/HEIC, no SVG, ≤10MB each, server-side compression). Editing drafts never touches the server and never creates orphan files.unsplash.com/photos/..., commonly copied from a browser) — automatically resolved to images.unsplash.com direct links. File-ID links ( unsplash.com/photos/1507513319174-...) resolve locally without a key; slug/short-ID links require an Unsplash API key (the site's bot protection blocks keyless scraping). Without a key, skip the slug form and say so — ask the user for a direct link; never embed a page link as an image (it renders broken).--image search:<english keywords> (or node scripts/search-image.js "<keywords>")No fixed image-count cap. The platform's boundary is 50,000 characters (each image tag ≈100 chars); remind the user when approaching 200 images. Display: the feed grid shows the first 9 + "+N"; the full-screen viewer shows all.
Interactive components (digital human / location) are rendered by the web frontend from rich HTML inside content. Writing just <span data-type="digital-human">Marie Curie</span> renders as plain text — only a span carrying the tiptap editor's exact structure (Tailwind classes, inner avatar <img>, pin icon, .location-name) displays as a tappable capsule.
Mandatory: always use scripts/compose.js to assemble post HTML. Never hand-write component spans, never write glue code to bypass html-formatter.js.
bash
# 1. Save the post text to a file. Separate paragraphs with blank lines.
# Placeholders (all optional; any misuse is a hard error, never silently altered):
# {{dh}} or {{数字人}} -> digital-human capsule (requires --dh)
# {{loc}} or {{位置}} -> location capsule (requires --loc)
# {{img}} or {{图}} -> image paragraph; must be its own paragraph, consumes --image in order
# (each one needs a spare --image, otherwise compose fails)
# Note: the Chinese aliases above are literal tokens recognized by the script —
# use either form, but never invent new tokens.
# Unknown/extra/inline placeholders make compose.js exit 1 and write nothing.
# Fix the text file and rerun — never delete text to force it through.
# 2. Assemble in one command (fully local, no network):
node scripts/compose.js --text-file post.txt \
--dh name:岳飞 \ # or --dh <id>, or --dh auto --topic "..."
--loc dh \ # or --loc <name> --lng 116.3 --lat 39.9
--image @./photos/west-lake.jpg \ # local file (uploaded at publish); or <direct-url>, or search:<keywords>
--out post-draft.html
compose.js enforced rules:
--dh/ --loc is given but the text has no placeholder, the capsule is auto-prepended to the first paragraph / appended to the last one.powpow-local://N placeholders + post-draft.html.manifest.json; publish.js uploads them at publish time.post-draft.html.preview.html — a Chinese-language preview page the user opens in a browser. Its banner states whether the post will appear on the public map and, if so, where it's pinned — so the map consequence is visible on the page the user actually reads (the local map renders via file://). Tell the user in their own language what the banner says.publish.js re-runs validateEditorFormat() and rejects anything else.contentItems in the publish payload — the frontend prefers structured rendering when contentItems is present, and an incomplete list drops post text. publish.js handles this.<img> as the card thumbnail.First show the four-item breakdown, then end the turn with a "Next step" block whose only action is the publish decision (publish / revise more / hold off):
<out>.preview.html) — Chinese-language preview; its banner also states whether the post goes on the public map. One line noting it's an approximation, not a pixel-perfect copy.The decision must be the last block — never buried after the breakdown. (Why so strict: in testing, users read exactly this content and replied "and then?" — the information was all there, but the action didn't land where the eye stops.)
Revision loop: if the user wants changes, make them and rerun compose.js — fully local, any number of rounds with zero server contact and zero orphan uploads. No iteration cap; only an explicit 发布/publish/confirm from the user ends it. Until then, never publish.
bash
node scripts/publish.js <html-file-path>
If the draft contains local images, this is the moment they upload — one by one, before the post goes out. Progress is persisted after each success (HTML + manifest rewritten with real URLs), so a failed rerun doesn't re-upload what already succeeded.
The post is created as the logged-in user — identity comes from the JWT; there is no way to post as someone else. The server enforces content moderation and rate limits. On 429 the script honors Retry-After automatically.
bash
node scripts/verify.js <post-id>
Checks post accessibility, author, digital-human/location components, image count, word count. Report in this order — breakdown first, result last:
text
────────────────────
✅ Post published
📍 Pinned at: Yanjiao Xinggong Ruins (116.816667, 39.95)
👉 Please open the link and check: do both capsules tap, is the image order right?
(The command line can't verify that — your eyes are the final gate)
🔗 https://global.powpow.online/posts/<post-id>
If there's no location, say so in the same block: "📍 No location — this post won't appear on the public map".
Interactive rendering (capsule taps, image lightbox) can't be verified from the CLI — the user's eyes are the final gate.
Test-post cleanup:
bash
node scripts/delete-post.js <post-id>
A separate flow from posting: turn a person (historical figure, character, the user's own avatar…) into a chat-capable digital human pinned to the public map. Triggers: "create a digital human", "turn someone into a digital human on the map", "publish a digital human".
Cost — confirm before running, just like the publish decision:
Collect from the user:
Name + persona (both required): who they are, how they talk — 2~4 sentences of persona is enough; the platform's native LLM handles the chatting.
Location (required): a place meaningful to the character — reuse Step 3's resolution flow and show candidates for the user to pick. Never guess.
Avatar, one of three (show the options):
--avatar-ref). The platform generates the avatar from persona + reference (8~30 s). A local reference photo works too — upload it to /api/upload/post-image first, then feed the public URL to --avatar-ref.--avatar @<path>): use the user's own image directly.--avatar <url>): a direct image URL.bash
node scripts/create-digital-human.js \
--name 史铁生 \
--desc-file persona.txt \ # or --desc "..."
--avatar-ref https://...jpg \ # or --avatar @./photo.jpg, or --avatar <url>
--lng 116.408195 --lat 39.952372 # from the resolution result the user picked
After creation, report the digital-human ID, location, expiry date (30 days), and badge balance, and link the map: https://global.powpow.online/map . The user can immediately @ it in a post ( compose.js --dh <id>), and it becomes a related candidate for match-digital-human.js.
To look up existing ones: node scripts/list-digital-humans.js --search "<name>".
Hard rule: never throw raw script output, node commands, or error codes (e.g. DEVICE_MISMATCH) at the user; translate every error into one human sentence + the next action.
login.js, retry once. Everything done so far is kept: drafts, material, chosen digital human and location stay local — resume from where you were, never rerun earlier steps.printf '%s' '<password>' | node scripts/login.js <username>. Never the positional form ( login.js <user> <password>): it lingers in shell history and process lists. No echoing, no disk writes, no shell variables. login.js still accepts the positional form for compatibility — that is not permission to use it. Asking the user for credentials should be one plain message, without security lecturing (see Conversation UX → Tone).--text-file/ --image, or files you created in the current working directory. Never scan the machine, never guess paths — a wrong path publishes private files.