# LivePage — connect your Agent

LivePage (方案空间) is the publish button for agents: it turns the work you finish with your user into a web page that can be shared and keeps getting updated. Weekly reports, progress pages, meeting notes, proposals and dashboards; study notes, exam plans and reading cards; travel guides, family plans and party invitations — as well as reports, web pages, decks and Markdown documents — become a link that opens right inside WeChat. Share it with the people who should see it (colleagues, clients, friends or family) and collect reading activity and feedback. Update the same page as often as needed; the link stays the same. Organize versions and binders as the work evolves. The platform does not generate content.

When you finish something the user would show to others (an HTML page, report, PDF, deck or Markdown document), ask once whether they want it as a WeChat-ready link. Upload only after they agree, and do not ask again if they decline. Work starts private; opening it to other people needs their confirmation. Never make work public on the user's behalf: use a passcode for family, friends or a group of colleagues, and choose public only when the user explicitly asks for it.

Content boundary (from 2026-09-21): commercial and transactional content is allowed. Work may be shared with the people the user chooses or publicly (the user decides), and it may be commercial, transactional or marketing content: list prices, sell things, and collect orders, contact details and sign-ups from the user's own customers. You choose how to collect payment: use the platform's forms and payment block, or put your own payment QR code, prices or a third-party payment link on the page. Not allowed: phishing (impersonating others, collecting other people's passwords or payment details) and unlawful content.

## Hosting contract: pages run as written

LivePage is a hosting service, not a separate way of writing pages. This section is the single authoritative statement; where later sections disagree, this one wins.

1. A package is one directory with index.html as its entry; relative paths inside it work unchanged. Published pages run on the workspace's own subdomain, `<workspace>.hy.24haowan.com`, one per workspace, so one workspace's trouble never spills onto another's.
2. Ordinary web code works, open by default: scripts, styles, images, fonts and media from the web (CDN echarts or tailwind load as usual); fetch / XHR / WebSocket to external APIs; external iframes (video, maps, surveys); plain <form> submissions to this site or elsewhere; alert / confirm / prompt / window.print(); localStorage / sessionStorage; target="_top" after a user click; password fields; camera, microphone and geolocation (the browser still asks the reader each time). The user decides what the page connects to and collects; LivePage does not block it. external_origins / external_form in the publish receipt only state which outside addresses the page uses and where forms submit — nothing to fix.
3. Read and write data the ordinary way, so sample data runs locally:
   - Read: fetch('./_data/<table>.json') returns an array of row objects, with the revision in the X-Space-Revision response header. When the table exists online you get live rows (base table plus approved viewer rows); otherwise the package's own _data/<table>.json is served.
   - Write: POST ./_data/<table> appends one row. JSON returns 201 {id, status}; a plain form (<form action="./_data/<table>" method="post">, no script) returns 303 back to the submitting page with ?space_submitted=<table> (a hidden _next field with a relative path picks another landing page; fields starting with _ are not stored).
   - A table without write-back open returns 403 {"error":{"code":"append_closed"}}; show the reader why instead of pretending it was submitted.
4. LivePage adds exactly one line to your page: each HTML file gets <script data-space-sdk src="/_space/sdk.js?…"> in its <head>, and every other byte stays as uploaded (read_version returns what readers receive, so you can diff it). On the platform's own pages (the official home page and samples) that line is the same tag carrying a small inline bootstrap that loads the same sdk.js asynchronously, so the page paints first. In the reader's browser that line records reading analytics, records the visit by default for HTML work (mark sensitive text with data-space-private), opens external links in a new window and makes download links to bundled files actually download, loads the deck engine for kind "deck", and writes data-theme on the root element when the manifest declares theme. render "inline" is retired for new versions (accepted and served as a normal page).
5. Try it locally first: curl -sSO https://space.24haowan.com/cli/space.mjs, then node space.mjs serve <dir> (no dependencies, no token) and open the address it prints — the same CSP, the same SDK line and the same ./_data/ reads and writes (against the local _data/ directory; --closed t1,t2 simulates tables without write-back). If it runs there, it runs online; an automated comparison keeps the two in step.
6. What still does not work (because each page does not yet have its own root): paths starting with / point at the workspace subdomain's root, not your package, so use relative paths; Service Workers are rejected; a package may not contain a _space/ directory; data-speaker-notes may not stay in the package (the CLI moves them into manifest.notes[]).
7. Fixed guarantees: page scripts cannot read the reader's session (the viewing cookie is HttpOnly); publisher credentials never reach the content domain; moderation and anti-phishing checks still run. If something goes wrong, operations switch the offending workspace or page back to the locked-down rules (same-origin requests only, no form submissions, no outside resources; list_proposals shows content_lockdown on that row). Nothing is blocked by domain, and other pages are untouched.
8. The older helpers remain optional: the SDK's space.data.get / space.data.append, data-space-form and the set_form block keep working, but new pages should prefer the ordinary code above.

## Connect

Remote MCP URL: https://space.24haowan.com/mcp

Transport: Streamable HTTP. Authentication: OAuth authorization code with mandatory PKCE S256, dynamic client registration and rotating refresh tokens. Configure the URL in a client that supports authenticated remote MCP. Let the user sign in and review access in the official browser page. Never ask them to paste a password or login code into chat. Revoke access at https://space.24haowan.com/app/tokens.

Pick the line for the client the user has. Enter the URL with no headers; the first tool call opens the browser sign-in.

For CodeBuddy Code with a terminal:

```sh
codebuddy mcp add --scope user --transport http space https://space.24haowan.com/mcp
```

For Claude Code with a terminal:

```sh
claude mcp add --transport http space https://space.24haowan.com/mcp
```

For WorkBuddy: install the "LivePage" (方案空间) connector from the connector market; there is no URL to type. For any other client: add an MCP server of type Streamable HTTP with the URL above and no headers.

Clients that have connected in production: CodeBuddy Code, WorkBuddy, Claude Code, Codex. The bar is "authenticated remote MCP", not the name: an unlisted client works when it supports Streamable HTTP with OAuth, or can send a header. When the client cannot run OAuth or has no browser, the user creates an API token at https://space.24haowan.com/app/tokens and puts it in the Authorization: Bearer header (CodeBuddy: the headers field in ~/.codebuddy/.mcp.json with type http).

The setup route is https://space.24haowan.com/start?lang=en. Explain only the next user action. After connecting, call list_proposals to confirm access; an empty list is a successful connection. Do not upload or share anything just to test authorization.

If the user has more than one workspace, switch first, then authorize. The workspace on the consent page is the one the user is currently in on the website; it cannot be changed on that page. To connect a different one, have the user open https://space.24haowan.com/app/workspaces, switch to it, then start the connection again. Each workspace is a separate authorization: connecting another does not replace the existing connection, both stay connected, and each can be revoked on its own at https://space.24haowan.com/app/tokens. A connection only acts on the workspace it was authorized for.

## After connecting: ship the first piece of work (connected is not published)

When the read check passes and the user has not said what to do, do not stop at "connected". Offer exactly two ready-made tasks and let them pick one or name their own:

- Life: send an existing travel guide to family or friends so it opens easily in WeChat.
- Work: send an existing report or deck to colleagues so they can read it in WeChat.

Ask which existing piece of work to use (a file, or the one just finished in this conversation). The content comes from the user's Agent and the user confirms it; do not invent content and do not pass off platform samples as their work.

Keep the two layers of intent apart. Connecting does not mean consent to upload, and never means consent to share. If the user only authorized, upload nothing. If the user has already asked for a publishing task ("send this guide to my family"), carry it out under that authorization without re-confirming what they already said (which work, who sees it); ask once only when the audience is unclear. Never switch to public unless explicitly requested; if the user declines, stop and do not ask again.

The work counts as "shared" only after these six steps, and at each one tell the user the real state and the next step:
1. Choose the work: list_proposals first, reuse the proposal_id for an update, create only when nothing exists. In a new session with no context this is also the way back in: every row carries current_version_id, the version clients see now, which get_version takes directly to check status before step 3; list_versions lists earlier versions for a rollback. A proposal_id is not a version_id, and get_version on one returns not_found.
2. Which upload route: terminal ⇒ SPACE CLI (create_cli_token first); no terminal and text files up to 2 MB in total ⇒ publish_file; anything else or unsure ⇒ create_upload_link. Fix validation issues and upload again; use hold until the user approves.
3. Wait for a usable version: get_version. converting means page images are still rendering, check again later; moderating means moderation is temporarily unavailable, finalize again later; review means a rule was hit and a person is reviewing, nobody else sees this version yet; preview_ready, or a held published version, means ready to preview and awaiting approval.
4. Preview: give the user the preview URL; publish_version a held version only after they approve. Look at it yourself first: read_version returns the rendered body of that version, the same bytes a reader gets in the browser, with no screenshot and no waiting for the user to look for you; get_version artifacts lists which files the version has and which one is the entry.
5. Confirm the audience: after the user agrees, set_visibility passcode for family, friends or colleagues, public only on explicit request. Hand over the default URL and the passcode.
6. Open on the phone: on a desktop, have the user open the link and use the page's QR entry ("Scan to WeChat"), or open the returned card_url and scan it with WeChat, then forward from WeChat's top-right menu; on a phone, send the link into WeChat directly. The presenter remote QR is a different thing for the presenter only; do not mix them up.

Success means a recipient's phone opens the link in WeChat and shows the content, and get_engagement_summary lists that visit a few minutes later. Stopping at authorized, uploaded, approved-but-held or private is never "shared": say which step it stopped at, why, and what comes next.

Current account requirements: sign-in uses WeChat. Publishing eligibility normally requires a verified mainland China phone number and a workspace profile. You can complete both yourself: send_phone_code then verify_phone_code for the phone number (send the code, let the USER read it out to you, then submit it — never ask for a code on your own initiative), and set_workspace_profile for the workspace profile, which has no web form any more and is also the only source of the company name on WeChat share cards. A user who would rather do the phone step themselves can still use the official onboarding page. These requirements are not removed by selecting English. If the user cannot complete them, explain the limitation and refer them to support; do not bypass trust.missing or claim that international account onboarding is available.

## Before you send: ask once about collecting replies

The write-back tools have been there all along (data tables, the set_form block, set_payment_qr), yet the pages with the most readers (invitations, itineraries, galleries, landing pages) collect nothing, because no assistant ever offered. So before any page goes out, ask once: "Want to collect votes, sign-ups, replies or payment on this page?" Give the default for the page type, build it in when the user says yes, leave it out when they say no, and do not ask again.

| Page type | Default write-back | Table / tool |
|---|---|---|
| Itinerary / travel guide | Let the group vote between two routes (train vs. drive, plan A vs. plan B) with a small form at the end and the tally under it; when plans change on the road, update the same link instead of sending a new one | table rsvp (columns name / plan / note), set_data … append_open=true |
| Invitation / party / banquet | RSVP: coming or not, how many adults and kids, name, note; the page counts replies | table rsvp (columns name / attend / adults / kids / note) |
| Gallery / landing page / catalog / service page | An inquiry or sign-up form; with prices, add the business payment QR so a reader sees the amount due and your code right after submitting | set_form (table inquiries, up to 20 fields) + set_payment_qr (merchant codes only: a personal static code may not be used for business payments; confirm with the user that it is their own code) |
| Proposal / report / meeting notes | A one-line reply at the end (agree / objections / undecided + note) | table replies |
| Study plan / check-in / event attendance | One check-in row per day or per person, counted on the page | table checkins |
| Daily or weekly report / progress page | No table: its write-back is the next version on the same link; readers who want updates tap "Follow updates" on the reading page where that control is present | none |

- Use these table names (they match the platform samples) and define the columns from the fields the page collects. Create the table with set_data (revision 0, append_open true) before publishing so the page never hits append_closed; read the rows with list_data_rows; close append_open after the deadline.
- In-page write-back is a plain <form action="./_data/<table>" method="post"> (hosting contract item 3); use set_form when you would rather not write form markup. list_examples has both defaults ready to copy: sample-weekend-trip (the vote) and sample-invitation (the RSVP).
- If the user declines, add nothing and do not bring it up again. Saying yes to a form is not consent to publish: write-back is only open to devices that can read the page, and visibility is still confirmed in step 5 above.

## Organize and upload

Use list_proposals to find existing work and list_folders before making a binder. For one-off work, omit folder_id to use the root. Create work with create_proposal, then upload a version.

Which upload route (check in order, use the first that fits):
1. You can run terminal commands and read local files ⇒ the local SPACE CLI (`node space.mjs push`): any file, any size; get a credential with `create_cli_token`.
2. No terminal, and the work is text files (one HTML / Markdown file, or a page with a few text files such as css / js / json / svg, 2 MB in total) ⇒ `publish_file` with the text content.
3. Anything else, or you are not sure ⇒ `create_upload_link`: hand the upload page URL to the user unchanged; they pick the files on their phone (WeChat works) or computer (PDF / PPTX / images / large files), and you poll `get_version` every 15–30 seconds.
`create_upload_session` is not a fourth route but a step the CLI runs itself: without a terminal do not open a session, since you cannot PUT to its presigned URLs and the version stays stuck in uploading forever.

With publish_file or create_upload_link, validation, moderation, release rules and the result are the same as finalize_upload. Pass hold: true when the user has not approved release (the upload page holds by default); an upload page link is valid for 2 hours, succeeds once and uploads only into that one work.

For route 1, use the maintained CLI at https://space.24haowan.com/cli/space.mjs. Use create_cli_token to obtain a temporary credential valid for at most 30 minutes and no longer than its parent. Put it only in the process environment, never files, logs or a user-facing reply. The CLI handles file traversal, MIME types, SHA-256, presigned PUTs, retries, download preflight and status polling.

```sh
# SPACE_TOKEN is supplied to this process securely; never store its value here.
curl -fsS https://space.24haowan.com/cli/space.mjs -o space.mjs
node space.mjs push ./work --proposal <proposal_id> --manifest ./manifest.json --hold
```

Never PUT to create_upload_session URLs by hand instead of running the CLI. Never invent a successful publication.

All uploads must be finalized. Inspect structured issues (code, file, line, fix), correct affected files and retry. What a page can use is set by the hosting contract above. A page reads the proposal's data tables with fetch('./_data/table.json'): an array of row objects, 403 rather than an empty array when the viewer has no access (the optional SDK space.data.get('table') returns {columns, rows, revision} for the same data). Tables are edited with set_data without republishing the page. Viewers can append one row from the page with a plain <form action="./_data/table" method="post"> or a JSON POST to ./_data/table (or the optional SDK space.data.append) once the publisher opens it per table (set_data append_open=true; off by default): appended rows are rate-limited, moderated before other viewers see them, rejected with a clear error when the table is closed (code append_closed) or when moderation is unavailable, kept out of get_data rows, and read back with list_data_rows (device and time only, never identities or intent). For sign-ups, orders, bookings and lead capture without writing any form code, use the first-class form block (set_form, since #8273; a plain <form> works too): define up to 20 fields (text / textarea / select / multiselect / number / phone) and the platform renders a validated, mobile-friendly form in the reading page; submissions land in the same table (each row carries _form_rev), read them with list_data_rows; an optional amount only displays a price (unit price × quantity computed by the platform into _amount_cents), no money is collected. When the form carries an amount and the workspace has a business collection QR code (set it yourself with set_payment_qr — the user does not have to go to the web console; remove_payment_qr takes it down), the viewer also gets payment guidance right after submitting — the amount, the publisher's own QR code and an “I have paid” button; that button is the viewer saying so, not a payment result, and the publisher confirms with confirm_payment after checking their own statement (only that viewer sees the confirmation, on their own row). Neither mark changes the row's moderation status, and no money passes through LivePage. Replacing the collection code is the one setting where a mistake silently sends money elsewhere: every set_payment_qr / remove_payment_qr call is audited and notifies the workspace owner on WeChat, so confirm with the user that the image is their own merchant code before uploading. finalize returns compat_notes of two kinds: things that break or overflow on phones (paths starting with /, Blob downloads generated by script, wide tables, long code lines, heavy images) — fix them and publish again; and external_origins / external_form, which only state what the page connects to — nothing to fix. Publishing is not blocked by either. Moderation is fail-closed. PDF and PPTX may return converting; poll get_version every 15–30 seconds. Fonts may be substituted. The HTML starter is https://space.24haowan.com/cli/starter.html and the deck starter is https://space.24haowan.com/cli/deck-starter.html. Preserve customer text and filenames in their original language.

## Review before releasing

Use hold=true for a version the user has not approved. Without hold, approved updates may replace the live version automatically. Send the preview URL and wait for explicit approval before publish_version. Changing sharing permissions also requires the user to choose the audience.

Each work has one stable default URL. set_visibility accepts exactly one proposal_id or folder_id and private / passcode / public. Shared binders grant access to included live work, even if an item's own direct link is private. set_internal excludes work only from binder sharing. Legacy links remain valid independently of visibility: making the work private does not stop them. revoke_share_link stops one of them immediately; get its link_id from list_proposals, which reports them for work that still carries such links. Revoking cannot be undone. reset_default_link invalidates every copy of the old URL immediately; confirm this before resetting. Search indexing is off by default; set_indexable requires explicit approval because crawler copies can persist.

Configure delivery materials with get_downloads, preview_downloads and set_downloads. Preserve revision / basis tokens to detect concurrent edits. Choose the original source file separately from ordered attachments. Display names are required; descriptions and download filenames are optional. Original filenames are hidden by default unless showFilename is explicitly enabled. Never infer which missing file replaces another.

## Reading and feedback

The work name shown in lists and binder index pages is separate from each version's manifest title; publishing never changes it. Rename work with rename_proposal (links, versions and visibility are unchanged).

When the user pastes a LivePage link and wants their own editable copy, call copy_proposal with the link as-is: it creates an unpublished draft in their workspace, independent from the source. Read it with read_version, edit it, and publish it with publish_file(proposal_id=the copy) — moderated as usual. If copying is not allowed (403 copy_not_allowed) or the link is gone (410), relay the reason instead of working around it.

Use list_feedback for comments and their version/page/text anchors. Use add_external_feedback only for feedback the user explicitly supplies for this work. Set visibility=internal for internal notes. Do not extract unrelated conversation history. Update workflow status with set_feedback_status without rewriting original comments. Answer a reader with reply_feedback (thread_id comes from list_feedback): the reply appears on the page the reader is already looking at, exactly as a reply written in the web console does. Reading a customer's question and never answering it looks, to them, like being ignored. When the user has something to say about LivePage itself (it does not work well, they wish it could do something), write their point as a short draft and show it to them; only after they agree, call send_feedback_to_livepage with user_confirmed=true. Which agent, which page and the last failed step are attached automatically; never include page content, reader data or tokens. Use list_my_feedback later for status and replies, and pass any reply on to the user. When the user is stuck and wants a person rather than to leave a suggestion, give them the LivePage support link: https://work.weixin.qq.com/kfid/kfc9e1c4b8c05ff167b?enc_scene=ENC9haVbhfMybED9wKqpaxQfs7mUVrBV6gdTfduDRRHYvxX — on a phone or inside WeChat it opens a WeCom support chat directly; on a computer, have them open any LivePage console page and hover "Talk to a person" in the footer for a QR code to scan with WeChat. Error messages that say "contact us" mean this.

get_engagement_summary provides reading facts and a computed evidence level (none/thin/usable). Explain weak evidence before drawing conclusions. Devices do not establish people, long dwell does not establish attention, and no data does not establish lack of interest. Do not invent intent scores or conversion probabilities. list_sessions and get_session_timeline provide individual visits; get_folder_analytics provides browser journeys across a binder. Before reading the numbers out, take your own side out of them: set_visitor_internal (visitor_id comes from list_sessions) marks the user's own phone and laptop, colleagues, and the opens made while demoing the work, so they stop counting towards the outward-facing figures; history is recomputed too, and how many were excluded is reported rather than silently dropped. Pass internal=false to undo. replay_url is present only when a replay actually exists.

HTML recording is on by default, subject to workspace and per-work opt-outs. get_replay_settings / set_replay_settings manage the workspace switch; set_replay manages individual work. Inputs are masked, private regions excluded, and background recording paused. Recordings expire after 30 days, with an 8 MB per-visit cap and 1 GB separate workspace capacity. Basic event retention is separate. Sign-in, administration, publisher previews and presenter remotes are not recorded.

## Telling LivePage: when to ask, when not to

This is about what the user thinks of LivePage itself (something does not work well, something they want, somewhere they got stuck), sent to the LivePage team. Feedback on a piece of work still goes through add_external_feedback.

- Offer "Shall I pass this on to the LivePage team?" at only three moments: the user clearly says they are unhappy with LivePage or want something from it; a tool just returned an error or rejected the work (validation failed, moderation refused, a limit or rate limit was hit); or a publish result carries feedback_hint or compat_notes.
- Do not ask at any other time: not after a successful publish, not while reading engagement, revising or checking the connection. Ask at most once per workspace in 24 hours; if the user says no, drop it.
- Show the user a draft first: one or two sentences in their words (what happened, what they want). Only after the user agrees, call send_feedback_to_livepage with user_confirmed=true; send their edited version if they change it, and nothing if they decline. Pass source="hint" when they are responding to feedback_hint.
- Write only the user's point: the agent, the page and the last failed step are attached automatically. Never include page content, reader data, tokens or verification codes, and never invent feedback on the user's behalf.
- Never solicit ratings: no satisfaction questions, no scores, no "how was it?" after each publish.
- Close the loop: when the user asks, or the next time they connect, check list_my_feedback for status (new / seen / planned / shipped / wontfix) and replies, and pass any reply or status change on in one sentence.
- If the user wants a person rather than to leave a suggestion, give them the support link instead of calling send_feedback_to_livepage.

## Team and language

list_members shows current workspace roles, plus every invitation link that has not expired. Owners can use invite_teammate (editor or viewer) or add_member for a known user ID. An invitation does not send a message; give its URL to the user. Do not choose someone's permissions without the user's instruction. set_nickname changes the connected user's own display name (up to 24 characters); it has no argument for anyone else, so it cannot rename another member. Each teammate connects their own Agent and remains limited to their current role.

Owners can also change things afterwards: set_member_role switches a member between editor and viewer, remove_member takes someone out of the workspace, and revoke_invite makes one unexpired invitation link stop working (get its invite_id from list_members). The workspace creator can be neither demoted nor removed, which is what guarantees a workspace always has an owner, and you cannot change your own role because demotion is a one-way door. Removing someone kills every token they minted for this workspace on its very next request — membership is rechecked per request, so no separate token revocation is needed.

After publishing, people in the workspace (the creator plus every member, never clients) can be notified on WeChat, each with their own personal link. set_publish_notify holds three owner-only switches — automatic push of each new live version, whether it also reaches whoever triggered the publish, and a reminder to the author when work has gone seven days without a reader — and you may pass only the ones you want to change. push_to_wechat sends one piece of work's current live version immediately, ignoring the automatic switch. People who have not followed the service account do not receive these; the receipt reports them separately, which is information, not an error.

The UI supports Simplified Chinese, Traditional Chinese and English. Use lang=zh-CN, lang=zh-TW or lang=en in web URLs. MCP uses Accept-Language and the client's _meta["openai/locale"] hint (legacy webplus/i18n also supported). Tool identifiers, JSON keys and enums stay unchanged. Reply in the user's language.


## Show the published experience

After get_version reports a reviewed, converted version (published with hold or preview_ready), call preview_version and hand its interactive_url to the requesting user. It needs no login, expires within 60 minutes and shows the actual SPACE viewer at desktop 1440 × 900, mobile 390 × 844, in-WeChat 390 × 763 or small-screen 375 × 667, or desktop and mobile side by side; it scales a fixed CSS viewport without changing its layout.

Screenshots are disabled on this server: screenshot_status is disabled and captures is empty. Never describe images you did not receive or call anything a checked screenshot. Start with audience=visitor; use publisher only for private speaker notes and never share a publisher preview with a client.

This does not publish or change sharing permissions. Preview grants have no API authority and stop working after expiry, token revocation or workspace removal. Visits are not recorded; feedback and permission edits are blocked; live presenter rooms are not connected. Real phones, WeChat and cross-device control still need a physical check.