# 活頁（space.24haowan.com）· 給 Agent 的接入指引

你是使用者正在使用的工作 Agent。活頁是 agent 的釋出按鈕：把你和使用者一起幹完的活 —— 週報、進度頁、會議紀要、方案、看板，學習筆記、備考計劃、讀書卡片，旅行攻略、家庭計劃、聚會邀請，以及報告、網頁、演示稿、Markdown 文件 ——
變成一個可分享、會更新的網頁：**在微信裡點開就能看**，分享給要看的人（同事、客戶、朋友、家人），收回閱讀記錄與反饋；同一頁可反覆更新，連結不變。
先帶使用者完成接入，再按他的具體任務操作。

## 託管契約：網頁怎麼寫，發上來就怎麼跑

活頁是**託管服務**，不是另一套寫法。這一節是唯一的權威說法；下面各節與它衝突時以這裡為準。

1. **包 = 一個目錄**，入口 `index.html`，包內相對路徑原樣可用（`./css/a.css`、`img/b.png`）。同包圖片連結用相對路徑（如 `<a href="shots/screen.png">查看截图</a>`），下載檔案加 `download`，HTML 子頁保留普通相對連結；釋出後在讀者頁內實際點一次核驗。釋出後跑在工作區自己的子域名 `<工作区>.hy.24haowan.com` 上，每個工作區一個，互不連累。
2. **常見網頁寫法都能用，預設開放**：引用網上的指令碼 / 樣式 / 圖片 / 字型 / 音影片（CDN 上的 echarts、tailwind 照常載入）；頁面裡 `fetch` / XHR / WebSocket 調外部介面；嵌外部 iframe（影片、地圖、問卷）；普通 `<form>` 提交到本站或站外；`alert` / `confirm` / `prompt` / `window.print()`；`localStorage` / `sessionStorage`；使用者點了之後 `target="_top"` 跳出外框；密碼框；攝像頭 / 麥克風 / 定位（瀏覽器照常逐次問讀者）。頁面連什麼、收什麼由使用者決定，活頁不攔。釋出回執裡的 `external_origins` / `external_form` 只是**說明**頁面會連哪些站外地址、表單交到哪 —— 不用改。
3. **讀寫資料用最普通的寫法**，本地放示例資料就能跑：
   - 讀：`fetch('./_data/<表>.json')` → 行物件陣列，修訂號在響應頭 `X-Space-Revision`。線上有這張表就出活資料（底表 + 過審的讀者寫回），沒有就回落包裡同名的 `_data/<表>.json`。作者或工作區同事在正式頁面上交的行多帶一格 `_author: true`（只有這一格：不帶名字、不帶賬號）—— 留言板 / 報名名單裡據它在名字後顯示「作者」；讀者交上來的 `_author` 一律丟棄，起同一個名字也冒充不了。讀者寫回的每一行還帶 `_row_id`、`_at`（提交時刻，ISO）、`_status`（approved / held）；**當前檢視者自己交的**行多帶 `_mine: true`（據它顯示「你已提交 / 已表態」），提交者也讀得到自己那幾行 held（別人的 held 仍然看不到）；檢視者本人是工作區成員時，成員行另帶 `_by`（暱稱）。這些下劃線鍵都只由平臺寫，讀者交上來的同名鍵一律丟棄。
   - 寫：`POST ./_data/<表>`（只追加一行）。JSON ⇒ `201 {id, status}`；普通表單（`<form action="./_data/<表>" method="post">`，不用寫指令碼）⇒ `303` 回到提交的那一頁並帶 `?space_submitted=<表>`（隱藏欄位 `_next` 填相對路徑可換落地頁；`_` 開頭的欄位不入庫）。
   - 交了表裡沒有的列：照收，但那幾格不入庫 —— JSON 回執帶 `ignored_columns` 與 `warnings: [{"code":"unknown_columns"}]`，寫頁面時據它發現列名寫錯了。
   - 撤回：`DELETE ./_data/<表>/<row_id>`（row_id 取讀回的 `_row_id`）只撤**自己**交的、提交後 24 小時內的那一行（投票表 `per_reader: "vote"` 不限時，授權過的同一讀者換裝置也能撤）⇒ `200 {id, withdrawn: true}`；別人的行 `404`、過了時限 `409 withdraw_expired`。撤回後頁面讀回不再有它，作者在 list_data_rows 裡看得到這一行帶 `withdrawn_at`。
   - 只收成員的表（set_data / set_form 傳 members_only=true）：工作區成員在正式頁面上交的行自動認人 —— 登入會話、內容域成員憑據、微信裡對得上的 openid、或**推給他本人的專屬連結**（push_to_wechat 發的那條）開啟都算。認不出時寫口回 `403 {"error":{"code":"members_only","login_url":…}}`：頁面給一顆「以成員身份登入後提交」按鈕（`<a href=login_url target="_top">`，普通表單的錯誤頁平臺已自帶），登入回來平臺會把剛才那一行以成員身份交上、回到原頁並帶 `?space_submitted=<表>`。
   - 這張表沒開寫回 ⇒ `403 {"error":{"code":"append_closed"}}`；頁面要把原因給讀者看，別假裝「已提交」。開寫回、限頻、稽核、讀回見下面「資料表」一節。
   - **頁面裡調模型**（讀者在頁面上問一句、頁面答一句）：在頁面里加一行 `<script src="/_space/ai.js"></script>`，然後 `const r = await spaceAi.chat({ system: "你是这一页的助手……", messages: [{ role: "user", content: 问题 }], tools: [{ name: "open_section", description: "…", parameters: {…} }] })` ⇒ `r.message.content`（回答）與 `r.message.tool_calls`（模型想調的工具，[{id, name, arguments}]，執行在你的頁面裡）。它同源 POST `<版本根>/_ai/chat`（自己 fetch 也行，但要把回執裡的 ai_label 顯示給讀者）。頁面裡不放任何金鑰、不能選模型：平臺只走已備案的模型；讀者的問題與模型的回答都過內容稽核（回答被攔下時 `r.blocked=true`、內容換成一句固定的話）；`ai.js` 第一次拿到回答就在頁面左下角加上平臺的 AI 標識（含備案號）。訊息 ≤ 20 條、每條 ≤ 2000 字、合計 ≤ 12000 字、工具 ≤ 8 個。按工作區每月計次（免費 100 次、高階 3000 次），同一位讀者 10 分鐘 ≤ 20 次；失敗一律 reject 且 `err.code` 說明原因（額度用完 ai_quota_exceeded、問得太快 ai_rate_limited、問題沒過稽核 ai_input_blocked、沒開通 ai_unavailable）—— 頁面要把話告訴讀者，並準備不調模型的退路。只在工作區子域名（和繫結的自有網址）上可用；（平臺可整體關停：屆時呼叫會得到 ai_unavailable。）工作區登記過的外部網站（`set_workspace_profile` 的 embed_origins）可以 `POST https://space.24haowan.com/api/public/ai/chat`，請求體多帶 `workspace`（工作區地址名），記在同一個工作區的額度裡。
4. **活頁只在你的頁面上加一行**：每個 HTML 檔案的 `<head>` 裡多一行 `<script data-space-sdk src="/_space/sdk.js?…">`，其餘位元組原樣（`read_version` 讀回來的就是讀者拿到的位元組，可以自己 diff）。這一行在讀者的瀏覽器裡做這幾件事：記閱讀統計；HTML 頁面預設**不錄**訪問畫面，作者用 `set_replay` 開啟後才錄（敏感正文加 `data-space-private`）；站外連結在新視窗開啟、包內檔案的 `download` 連結真的下載；`kind:"deck"` 時載入演示引擎；manifest 聲明瞭 `theme` 時在根節點寫 `data-theme`。`render:"inline"` 對新版本已停用（照收、按普通頁面出）。
5. **本地先試**：`curl -sSO https://space.24haowan.com/cli/space.mjs` 後 `node space.mjs serve <目录>`（零依賴、不要令牌），開啟它列印的地址 —— 同一份 CSP、同一行 SDK、同一套 `./_data/` 讀寫（資料讀寫本地 `_data/` 目錄；`--closed 表1,表2` 模擬沒開寫回）。本地這裡能跑，線上就能跑；線上與本地有沒有走樣由平臺的自動對比檢查守著。
6. **仍然不行的只有這幾件**（原因是「每個頁面還沒有獨立的未放入資料夾」，不是不讓用）：以 `/` 開頭的路徑指向工作區子域名的根而不是你的包 —— 讀者頁裡平臺會按來源把它自動指回這個包，但自有域名與離線開啟不經過這一步，仍建議改相對路徑；Service Worker 不會註冊成功（照收，頁面其餘照常）；包裡不能有 `_space/` 目錄（留給平臺那一行）；講解提示 `data-speaker-notes` 不能留在包裡（CLI 會剝進 `manifest.notes[]`）。
7. **不變的底線**：讀者的登入狀態頁面指令碼讀不到（檢視 cookie 是 `HttpOnly`）；釋出者的登入憑據不進內容域；內容稽核與防釣魚照常。**出了事**：運營把出問題的**工作區或頁面**切回收緊規則（只能連本域、不能提交表單、不載入站外資源；`list_proposals` 那一行會標 `content_lockdown`），不按域名拉黑、不動別人的頁面。
8. **舊寫法繼續能用，但是可選**：SDK `space.data.get` / `space.data.append` / `space.data.withdraw(表, row_id)`（撤回自己那一行）、`data-space-form`、平臺表單塊 `set_form` 都保留；新寫的頁面優先用上面的普通寫法。

## 先帶使用者接入：每次只走一步

使用者把接入任務交給了你。請擔任他的接入嚮導，技術說明由你消化，不要整段轉發給使用者。

1. **先確認自己能做什麼。** 已有活頁工具時，直接進入下面的只讀驗證。尚未連線時，確認當前應用是否支援遠端 MCP / 聯結器，以及你是否能操作該應用的設定。不能判斷時，只問使用者正在使用哪個應用；不要編造選單名稱或假稱已經配置。
2. **優先使用客戶端的登入授權。** 接入端點是 `https://space.24haowan.com/mcp`。能在當前授權範圍內完成的配置由你處理；需要使用者點選時，一次只給一個具體操作，說清在哪個頁面、點哪個按鈕。入口不確定時先核對該應用的當前介面或官方說明，不能讓使用者去理解 MCP、OAuth、JSON 或命令列後再回來。
3. **登入與確認由使用者親自完成。** 讓使用者在活頁官方頁面登入、核對助手和工作區、點選「允許」，然後回到當前對話。圖文對照頁是 https://space.24haowan.com/start 。不要索取微信驗證碼、訪問令牌或完整授權回跳地址，也不要替使用者確認授權。
4. **以真實讀取為接入成功。** 授權頁面顯示「已確認授權」不代表客戶端已經連線成功；實際呼叫 `list_proposals` 或 `list_folders`，成功返回即可，空列表也是成功。新工作區裡平臺會預置幾份示例頁（`list_proposals` 標「示例」、`sample_key` 不為空）：它們不是使用者釋出的，別說成「你釋出的」、別算進他的頁面數。REST 接入則讀取 `GET /api/me`。用一句普通話告訴使用者驗證結果。僅為接入驗證時，不建立示例頁面、不上傳、不修改可見性、不分享內容。
5. **失敗時繼續帶路。** 根據真實錯誤給出下一步，修正後重新只讀驗證；不要重複建立連線或令牌。助手不能聯網時可請使用者上傳從圖文頁下載的接入說明；如果也無法呼叫外部工具，明確說明當前能力不足，建議換到支援連線的工作助手，不聲稱所有聊天應用都能接入。API token / REST 僅作為已確認需要的備用方式，讓使用者在客戶端的憑據設定中填寫，不要求把秘密發到對話裡。
6. **接入與釋出分開。** 手機號和工作區資料未完善，不等於連線失敗；等使用者準備釋出時，再按 `trust.missing` 補齊 —— **手機號**在官方頁面補（`/app/onboarding`），**工作區資料**的網頁表單已退役，由你調 `set_workspace_profile` 填。連線成功後，使用者沒交代第一件事就按下一節提兩條現成的任務；他說「先給我預覽」時，上傳必須使用 `--hold`，已有公開版本也不提前替換，等確認後再切換或對外分享。
7. **使用者有多個工作區時，先切換再授權。** 授權頁上「接入的工作區」就是使用者在網頁上當前所在的那一個，頁面上不能改選。要連另一個工作區：讓使用者先開啟 https://space.24haowan.com/app/workspaces 切換到它，再重新發起連線。每個工作區各是一次獨立的授權 —— 再連一個不會替換已有的連線，兩邊可以同時連著、各自在 https://space.24haowan.com/app/tokens 撤銷。一次連線只作用於授權時的那個工作區。

## 接入之後：把第一個頁面發出去（連線成功 ≠ 已釋出）

連線驗證通過、使用者又沒交代任務時，別停在「已經接好了」：只提**兩條現成的事**，讓他挑一條，或者直接說自己的：

- **生活**：把已有的旅行攻略發給親友，在微信裡方便看。
- **工作**：把已有的報告或演示稿發給同事，在微信裡檢視。

先問使用者要用**哪一份已有的**頁面（一個檔案，或這段對話裡剛做好的那份）。內容由使用者的 Agent 產出、由使用者確認；沒有現成的就用他手上任何一份，不替他編內容，也不拿平臺樣例冒充他的頁面。

**兩層意圖分開**：
- 連線授權**不等於**同意上傳，更不等於同意對外分享。只授權、沒提釋出任務時，什麼都不傳。
- 使用者已經明確提出釋出任務（「把這份攻略發給家人」）時，沿用這次授權直接做完，**不重複確認已經說定的事**（發哪份、給誰看）；只有可見性檔位沒說清時問一次。
- 沒明確要求公開就不切 `public`；使用者說不發就停下，**不再追問**。

**六步走完才叫「已分享」**，每一步把真實狀態和下一步告訴使用者：
1. **選頁面**：`list_proposals` 先看有沒有現成的（更新就複用 proposal_id），沒有再新建。**換了會話、手裡什麼都沒有時也走這條**：它每行帶 `current_version_id`（客戶現在看到的那一版），拿著它就能直接 `get_version` 查狀態、按第 4 步預覽；要翻歷史版本或回滾用 `list_versions`。頁面號不是版本號 —— 拿 `proposal_id` 調 `get_version` 會 `not_found`。
   **先看這一行有沒有 `pending_version` / `held_version`**：前者是還沒發完的那一版（停在上傳 / 轉換 / 稽核），後者是已過審但被 hold 按著、還沒成為客戶可見版的那一版。有就**接著處理那一版**（第 3–4 步），別當成「沒發過」再建一份 —— 換個會話就重建，正是生產上一個頁面底下堆出一串半截版本的來路。
2. 上傳走哪條路：有終端 ⇒ space-cli（先 create_cli_token）；沒終端、文字檔案合計 ≤ 2 MB ⇒ publish_file；其它或拿不準 ⇒ create_upload_link。 校驗失敗按 issues 修正後重傳；使用者還沒確認就帶 hold。
3. **等可用版本**：`get_version` —— `converting` = 還在轉逐頁圖，稍後再查；`moderating` = 稽核暫時不可用，稍後重新 finalize 一次；`review` = 命中規則等人工複核，在那之前別人看不到這一版；`preview_ready` 或帶 hold 的 `published` = 可預覽、待確認。
4. **預覽**：把預覽連結給使用者看；確認後 hold 版才 `publish_version`。★ **你自己先看一眼**：`read_version` 把這一版**渲染後的正文**讀回來（讀者在瀏覽器裡拿到的就是這段位元組），不用截圖、不用等使用者替你看；這一版有哪些檔案、入口是哪個，看 `get_version` 的 `artifacts`。
5. **確認訪問範圍**：使用者點頭後 `set_visibility`：家人、朋友、同事用 `passcode`；`public` 只在使用者明確要求時。把預設連結（和口令）交給使用者。
6. **手機開啟**：使用者在電腦上時，把釋出回執裡的 `push_qr_url` 當圖片給他 —— 微信掃一下，服務號「二四 活頁」馬上把這一頁發到他自己的微信（第一次順帶關注），再從微信裡轉發；沒有 `push_qr_url` 就給 `qr_url` 碼圖，掃碼在手機上開啟。電腦上開啟這一頁時，右下角膠囊上方也有「發到微信」，點開微信掃一下同樣進他自己的微信。使用者說「發到我微信」就調 `push_to_wechat`。使用者在手機上就把連結直接發到微信。講稿遙控的二維碼是另一件事（只給講的人），別混用。

**判據**：收件人手機微信能開啟並看到內容，幾分鐘後 `get_engagement_summary` 出現這次訪問。停在「已授權」「已上傳」「已過審待確認」「僅自己可見」中任何一步都**不能說成「已分享」**—— 說清停在哪一步、為什麼、下一步做什麼。

## 發出去之前問一句：要不要收投票 / 報名 / 反饋 / 收款

能力一直都在（資料表寫回、`set_form` 表單塊、`set_payment_qr` 收款碼），但讀者最多的幾類頁面 —— 邀請函、行程、圖冊、落地頁 —— 一份都沒開寫回：助手從不主動提，作者就不知道有這一項。所以**任何一頁發出去之前都問一句**「要不要順手收一下投票 / 報名 / 反饋 / 收款？」，並按頁面型別給出下面的預設做法：使用者點頭就一起做好再發，說不用就不加，**只問這一次**。

| 頁面型別 | 預設寫回 | 表名 / 工具 |
|---|---|---|
| 行程 / 旅行攻略 | 兩種走法讓大家**投一下**（高鐵 vs 自駕、A 線 vs B 線），文末一張小表單，票數顯示在下面；**路上有變就更新同一連結**，不另發一份 | 資料表 `rsvp`（列 `name` / `plan` / `note`），`set_data … append_open=true` |
| 邀請函 / 聚會 / 宴席 | **出席回執**：能來 / 來不了、幾位大人幾位小朋友、稱呼、備註；頁面統計已回覆幾家幾位 | 資料表 `rsvp`（列 `name` / `attend` / `adults` / `kids` / `note`） |
| 圖冊 / 落地頁 / 產品目錄 / 服務頁 | **詢價或報名**表單；標了價的再加經營收款碼，讀者提交後原地看到應付金額與你的碼 | `set_form`（表 `inquiries`，欄位最多 20 個）+ `set_payment_qr`（只收**商戶收款碼**：個人靜態碼不得用於經營性收款，傳之前跟使用者確認那是他本人的碼） |
| 頁面 / 報告 / 會議紀要 | 文末**一句話回覆**（同意 / 有意見 / 待定 + 備註），比一個個追著問省事 | 資料表 `replies` |
| 學習計劃 / 打卡 / 活動簽到 | **打卡**或簽到一行，頁面按天或按人計數 | 資料表 `checkins` |
| 日報 / 週報 / 進度頁 | 不收表 —— 它的「寫回」是下一版：同一連結發新版本，讀者重新整理即見；要追更就讓讀者在閱讀頁裡點「關注更新」（頁面上有這一項時） | 不建表 |

- 表名照上面用（`rsvp` / `replies` / `inquiries` / `checkins`，與平臺樣例同名），列按頁面要收的欄位定。**先 `set_data` 建表**（`revision=0`、`append_open=true`）**再發頁面**，頁面就不會撞 `append_closed`；讀回用 `list_data_rows`；過了截止日把 `append_open` 關掉。
- 頁內寫回就是一張普通 `<form action="./_data/<表>" method="post">`（契約見「託管契約」第 3 條）；一行表單程式碼都不想寫就用 `set_form`，由平臺渲染。樣例已經是這麼做的：`list_examples` 裡 `sample-weekend-trip`（兩種走法投票）與 `sample-invitation`（出席回執）直接照抄。
- 使用者不要就不加、不再提；要了也**不等於同意公開**：寫回只對讀得到這份內容的裝置開放，可見性仍按上面第 5 步確認。

## 0. 定位與紀律（先讀）

- 活頁是**你的外掛**，不是內容生產方：頁面由你和使用者產出並由使用者確認；平臺只負責釋出、檢視、訊號與反饋。
- **做出一份能給別人看的頁面時，主動問一句。** 你為使用者做好 HTML 頁面、報告、PDF、演示稿或 Markdown 文件，而它看起來是要給別人看的，
  就問一句「要不要發成微信裡能開啟的連結？」—— 使用者同意後再上傳。只問一次，使用者說不用就別再提。
- **內容邊界（2026-09-21 起）**：允許商業交易內容 —— 頁面可以分享給使用者選定的人，也可以公開分享（公開與否仍由使用者決定）；可以是商業、交易、營銷內容，可以在頁面上標價賣東西、收集自己客戶的訂單、聯絡方式與報名資訊。收款方式由你決定 —— 可以用平臺的表單與收款塊，也可以在頁面上放自己的收款碼、報價或第三方支付連結。不得釣魚（冒充他人、收集他人的賬號密碼或支付資訊）、不得釋出違法違規內容。
- 對外的時刻 = 放開可見性：`set_visibility(passcode|public)` 與把連結發出去必須**使用者明確確認**後才做。push 過審即自動釋出，但頁面預設僅釋出者本人可見（private），不放開就沒人看得見。
- **不要替使用者把內容公開給所有人。** 給家人、朋友或一群同事看用 `passcode`（口令可看）；`public` 只在使用者明確要求公開時才切。
- **一個頁面只有一條連結**（它自帶的預設連結），沒有「給某個人單獨建一條」那種形態。要公開傳播必須顯式 set_visibility public（使用者確認後）。

## 1. 接入（二選一）

**A. MCP（推薦）** —— 客戶端支援帶登入的遠端 MCP（Streamable HTTP + OAuth）時。端點只有一個，不帶任何 header 填進去，首次呼叫工具時客戶端會開啟瀏覽器登入活頁：

    接入端點：https://space.24haowan.com/mcp （streamable HTTP）
    CodeBuddy Code 示例：codebuddy mcp add --scope user --transport http space https://space.24haowan.com/mcp
    Claude Code 示例：claude mcp add --transport http space https://space.24haowan.com/mcp
    Claude Code 外掛（連線 + 三個用法技能）：claude plugin marketplace add GuangZhouShanyouGame/livepage-plugin && claude plugin install livepage@livepage
    WorkBuddy：聯結器市場搜「活頁」安裝，不用填地址
    千問辦公 QwenWork（桌面端）：左側「擴充套件 → 聯結器」→ 右上「新增」→「手動填寫配置」：伺服器型別選 Streamable HTTP（預設是 STDIO），名稱填「活頁」，地址填上面的地址，不加 Headers，點「新增」後會自動開啟瀏覽器授權頁，登入並點「允許」；在新建的任務裡才生效
    豆包工作（桌面端）：外掛 · 技能 · 夥伴 → 管理 → 聯結器 → 新增 → 新建自定義聯結器：伺服器名稱填「活頁 LivePage」，傳輸型別 HTTP，伺服器 URL 填上面的地址，不加 Headers；儲存後點「去授權」，在瀏覽器登入活頁並點「允許」。自定義聯結器只在「本地電腦」模式可用。你加不了它，一次只告訴使用者下一步點哪裡；不要改用 API token
    其他客戶端：「MCP 伺服器 → 新增」選 Streamable HTTP，填上面的地址，不填請求頭

已在線上接入過的客戶端：CodeBuddy Code、WorkBuddy、Claude Code、Codex、豆包工作、千問辦公。判據不是名字，是「支援帶登入的遠端 MCP」——沒列到的客戶端只要支援 Streamable HTTP + OAuth（或能填請求頭）就能接。
首次連線通過 OAuth 在瀏覽器登入並確認授權；已登入使用者直接核對工作區。客戶端不支援登入授權、或無瀏覽器環境時改用 API token：
讓使用者到 https://space.24haowan.com/app/tokens 建立 `sk-space-…`，作為 `Authorization: Bearer` 頭（CodeBuddy 寫進 `~/.codebuddy/.mcp.json` 的 `headers`，`type` 填 `http`；WorkBuddy 一類 `type` 填 `streamableHttp`）。

**B. 純 REST** —— 不支援 MCP 時：同一批能力有 REST 等價端點（`/api/*`，Bearer token 鑑權）。
`GET /api/me` 可核驗身份與開通狀態（`trust.missing` 列出還差什麼：手機號 / 工作區資料 —— 後者你自己能填，見 `set_workspace_profile`）。

## 2. 標準閱讀順序（每步一個 MCP 工具）

1. `create_proposal`。**容器是可選的**：一次性的東西不傳 `folder_id`，就不放進任何資料夾 —— 別為了放一個頁面先編一個容器。要按客戶/專案分組就先 `list_folders` 看有沒有現成的，沒有再 `create_folder`。
   - 「先建客戶、再建商機」那條老閱讀順序已於 2026-09-15 **退役**：`upsert_customer` / `upsert_opportunity` 兩個工具都已刪除，別再去找。
2. **上傳**。
   **上傳走哪條路（按順序判，第一條對上就用它）**：
   1. **你能跑終端命令、讀得到本地檔案** ⇒ 本地上傳助手 space-cli（`node space.mjs push`）：任何檔案、任意大小；憑據調 `create_cli_token` 現取一把。
   2. **沒有終端，要發的是文字檔案**（一個 HTML / Markdown，或網頁連同它的 css / js / json / svg 等幾個文字檔案，合計 2 MB 以內）⇒ `publish_file`，把文本內容直接交進來。
   3. **其它情況，或者拿不準** ⇒ `create_upload_link`：把返回的上傳網頁原樣交給使用者，使用者在手機（微信裡也行）或電腦上自己選檔案（PDF / PPTX / 圖片 / 大檔案都行），你每 15–30 秒用 `get_version` 查一次結果。
   `create_upload_session` 不是第四條路，是上傳助手自己調的一步：沒有終端就別建會話 —— 預簽名地址你傳不上去，版本會永遠停在 `uploading`。
   - 走第 2 條時：網上的指令碼、樣式、圖片可以直接引用 URL；包裡自帶的圖片只能內聯進文字（data URI），內聯前同樣先轉 WebP、長邊 ≤ 1600px —— 2 MB 上限一張原圖就能撞到；圖片多、內聯裝不下就改走第 1 或第 3 條。之後的校驗、稽核與生效規則和 `finalize_upload` 完全相同，返回也一樣；使用者還沒確認對外就傳 `hold: true`。
   - 走第 3 條時：連結 2 小時內有效、只能成功用一次、只能往這一個頁面裡傳、預設先不對外；使用者說「傳好了」之前別把版本當結論。

   第 1 條（本地上傳助手）怎麼跑：

       curl -sSO https://space.24haowan.com/cli/space.mjs   # 只需一次
       SPACE_TOKEN=sk-space-… node space.mjs push <目錄或 PDF> --proposal <proposal_id> [--manifest manifest.json] [--note "V2：按客戶意見改了第三頁"] [--hold]

   - **`SPACE_TOKEN` 從哪來**：用 API token 接入的直接用手上那把；**走 MCP + OAuth 接入的你讀不到自己的令牌**（它鎖在客戶端的憑據庫裡，也喂不進子程序）——
     調 MCP 工具 `create_cli_token` 鑄一把**只活 30 分鐘**的 `sk-space-…`，原樣填進上面那一行。只用於這一次上傳、**不要寫進任何檔案**（不要進 .env / 指令碼 / 提交 / 日誌）、跑完不用管（會自己過期）；
     使用者想立刻斷掉可以在 `/app/tokens` 撤銷那條 `CLI · …`。
   - **不要拿 `create_upload_session` 的預簽名地址手 curl 代替 CLI**：CLI 還做目錄遍歷規則（跳 `.` 開頭 / `node_modules` / 無副檔名 / `.map` —— 傳了平臺不收的檔案會 `ext_not_allowed` 打死整個 push）、
     逐檔案 sha256 / size / mime、manifest 入口推斷、6 路併發 PUT + 指數退避、finalize 後的轉換與過審輪詢。幾十個檔案的包手 curl 一定會半截，或者把過渡態當結論報給使用者。
   - 支援四種入口：`index.html`（網頁 Deck / Demo）、單個 PDF、單個 PPTX、**單個 Markdown**（.md 直接 push，平臺渲染成閱讀頁，`##` 標題自動成為目錄錨點，原始檔按下載清單選擇）；否則寫 `manifest.json`
     （欄位：`title, entry, kind(pdf|html|deck|demo), scene(proposal|quote|report|demo), downloads, sections[{id,title,page,anchor}], notes[{anchor,text}], share{title,desc,cover}, note, theme(system|light|dark), theme_switch, images(auto|keep)`）。
   - **`manifest.json` 放包根就行**：不傳 `--manifest` 時 CLI 讀包根的 `manifest.json`（這個檔案本身不上傳；`pull` 下來的目錄就是這個形狀）。帶圖的設計稿 / 截圖集 / 相簿寫 `"images": "keep"`，平臺就不把圖縮到長邊 1600 —— 不寫的話電腦上看會糊。
   - `theme` / `theme_switch`：**文件級**主題。HTML 包自帶深色配色（`prefers-color-scheme: dark`）時，觀看者系統是深色模式就會看到深色版 —— 與作者本地截圖不一致；寫 `"theme": "light"` 固定成淺色（`dark` 同理），`"theme": "system"`（預設）跟隨系統。`"theme_switch": true` 讓觀看者在頁面上手動切換（按頁面記住）。
     平臺只把當前主題寫到文件根節點 `data-theme="light|dark"`，包的 CSS 要按這個約定響應：`:root[data-theme="dark"]{…}` / `@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){…}}`。不宣告 = 現狀（不寫屬性、不出按鈕）。
   - `scene` 決定**微信轉發卡片的描述預設**（來自 X 的頁面 / 報價 / 月度報告 / 演示）。不寫會自動推導（kind=demo 即演示，否則頁面；「掛了報價=報價」那條隨報價臺退役已刪）；
     月報這類推導不出來的**值得顯式寫**，否則客戶上級看到的卡片上會寫著「頁面」。卡片縮圖預設是活頁品牌圖，不隨 scene 變；想用自己的圖就寫 `share.cover`（它最高）。
   - `sections` 值得寫：閱讀摘要與反饋會按「第 N 頁『章節名』」說話。HTML 包的每個 section 給 `anchor`
     （= 包內元素 id，`<h2 id="…">` 最省），目錄才能跳。
   - PPTX 可作入口（服務端轉逐頁圖），但缺字型會被替換、新 emoji 可能空白；穩妥是 PDF 入口、PPTX 放 downloads。
   - **做成演示稿（deck）**：manifest 寫 "kind": "deck"，包裡用 `<deck-stage>` 包住一頁一個 `<section>`。
     平臺出流時**自動注入**演示引擎（鍵盤 ←/→ · 點按左右半屏翻頁 · 16:9 自動縮放 · 縮圖目錄 · 列印一頁一張），
     **包裡不要自己掛引擎指令碼、也不要寫版本號**；殼頁會多出「上一頁 / 下一頁 / 全屏」。
     ⚠️ `kind:"deck"` **不要**配 `"render": "inline"` —— 寫了會在校驗階段被拒（`manifest.render`）；
     `render:"inline"` 對新版本已停用（見上面「託管契約」第 4 條），不用寫。
   - **講解提示（只有講的人看得到）**：在每個 `<section>` 上寫 `data-speaker-notes="这页讲什么 / 对方可能问什么"`。
     push 時會把它們**從包裡剝掉**、放進 `manifest.notes[]`，平臺只渲染給釋出者 ——
     客戶拿到的位元組裡一個字都沒有（View Source 也讀不到）。現場話術、砍頁理由可以放心寫。
     ★ 不走 CLI 手工釋出時，包裡**殘留** `data-speaker-notes` 會被**拒收**（`html_speaker_notes`）：
       自己把這些屬性刪掉、把講稿寫進 `notes[]`。
     ★ 釋出者開啟頁面詳情會多一個「講稿」按鈕：開第二個視窗看提示，或掃碼開到手機上（兩端翻頁互相同步）。
       告訴使用者**優先用手機** —— 投屏投的是整檯筆記本，第二個視窗一樣會被投出去。
   - **要給客戶一份 PDF 講義**：本機把 deck 列印成 PDF（引擎的 `@media print` 保證一頁一張），與包一起 push
     並寫進 `downloads.attachments`。平臺**不做** HTML→PDF ⇒ **改了 deck 要重導一次**，否則客戶下載到的是上一版。
   - **不知道 HTML 包該長什麼樣？別從零發明** —— 平臺自帶一份最小骨架，直接抄：
     `GET https://space.24haowan.com/cli/starter.html`（倉內原始碼 `core/space/examples/starter/`）。
     **做 deck 的另抄一份**：`GET https://space.24haowan.com/cli/deck-starter.html`
     （倉內原始碼 `core/space/examples/deck-starter/`）—— 它把「一頁一個 section」「引擎不用自己掛」
     「講稿寫在哪」「目錄錨點 = 元素 id」四件擺對了。
     網頁文件骨架（`kind:"html"`）已按樣例品質標準寫好預設版式與手機適配，並把「寫錯不會報錯、只會一聲不響不生效」的事擺對了：
     每節的 `id` = `sections[].anchor` = `data-track-section`（目錄能跳、閱讀回執知道看到哪一節）。
     文末確認做成普通 `<form action="./_data/<表>">`，寫進這個頁面的資料表（見「託管契約」第 3 條）。
     手寫翻頁稿才需要 `window.__track.slide(i,label,total)`。
   - **頁面能用什麼、不能用什麼，以上面「託管契約」為準**：外部資源、外部請求、表單、彈窗、列印、儲存、攝像頭定位都能用。finalize 返回的 `compat_notes` 分兩種：「會失效 / 手機上會撐寬」這類（以 `/` 開頭的路徑、指令碼現生成的 Blob 下載、寬表格、長程式碼行、過重的 GIF）**看到了就改掉再發一版**；`external_origins` / `external_form` 只是說明頁面連了哪些站外地址，`compat_images_optimized` 只是說明平臺已把哪些過重的 png / jpg 自動縮到手機夠用的尺寸（長邊 1600、同格式、路徑不變；manifest 寫 `"images": "keep"` 可保留原圖），都不用改。
   - **外鏈可以有、不用寫 target**：指向站外的 `<a href="https://…">`（參考資料 / 案例 / 報名表）平臺出流時自動補 `target="_blank"`，在新視窗開啟；錨點與包內相對路徑照舊在頁內跳。自己寫了 target 的原樣保留。
3. **校驗失敗**會返回結構化 issues（`[code] file:line message → fix`）。按 fix 修文件後重新 push（會建新版本），不要繞。
4. **push 過審即自動釋出**（無需釋出步驟）：客戶連結自動指向新版；命中稽核則客戶暫看舊版、複核通過自動釋出。
   已有客戶連結時想先自查再切：push 加 `--hold`，確認後調 `publish_version` 切換。
5. **誰能看** —— 每個頁面自帶一條恆定**預設連結**（push 返回裡有），**一個頁面就這一條**，沒有到期。
   - 預設 `private` = 僅釋出者本人。給別人看：`set_visibility(proposal_id, "passcode")`（系統生成 6 位口令，告訴使用者）或 `"public"`（任何人可看，需 T1）。兩者都要使用者確認。
   - 切回 `"private"` 即時對外失效。**URL 洩露**用 `reset_default_link` 換地址（舊 URL 當場失效、統計連續）——注意它對**所有人**生效，換完要把新 URL 重發一遍。
   - 使用者想在微信裡發**卡片**而不是裸連結：開啟返回的 `card_url`（卡圖頁），長按識別圖中二維碼進頁面詳情，再右上角轉發。
   - **按資料夾一次發出去**：`set_visibility` / `reset_default_link` 也收 `folder_id`（與 `proposal_id` **二選一**，兩個都傳報錯 —— 不猜哪個優先，猜錯的表現是把範圍不對的東西放開了）。
     一條連結放出夾內**所有非「不外發」**的頁面：客戶開啟看到封面（`set_folder` 掛的那一份）或平臺生成的目錄頁，點進任意一份都不用再發連結。
     ★ 從資料夾連結進來的人，看得到夾內每一份 **包括那些自己還是 private 的** —— 資料夾那條連結就是授權。夾內某一份自己的連結**不受影響**，仍由它自己的可見性管。
     ★ 跟客戶放在同一個資料夾、但絕不能一起發出去的東西（內部同步 / 競爭應對）用 `set_internal` 標上：它**不出現在目錄、也不被資料夾連結授權**。
     ★ 回執會**點名到份**（這條連結會露出哪幾份、排除了幾份）—— 讀一眼再把連結發出去。
     ★ **要區分收件人就發兩個資料夾**：閱讀回執只說裝置與頁面行為，不認人。
6. **「一人一條具名專屬連結」已退役**（2026-09-15）。`create_share_link` / `list_share_links` /
   `restore_share_link` / `clear_auto_passcode` 不再出現在 agent 面 —— 別去找它們，也別自己拼 REST 繞回去。
   退的理由：那套紀律的成本落在**每一次傳送**上，而它買到的東西很弱 —— 按連結推斷「誰在看」
   只知道**哪條連結被打開了**，不知道**是誰開啟的**（平臺自己的「疑似轉發」提示就是在承認這件事）。
   ⇒ 閱讀回執從此只說**裝置與頁面行為**，不聲稱人名 —— 這是定論，不是過渡態。
   ⚠️ **已經發出去的舊專屬連結照常能開啟**，而且**不受可見性影響** —— 把頁面切回 private 不會讓它們失效。
   ★ **洩露時能止血**（2026-09-21）：`revoke_share_link` 讓**某一條**立刻失效（拿著它的人再開啟就是錯誤頁）。
   link_id 從 `list_proposals` 拿 —— 還掛著這類連結的頁面會連 link_id 一起標出來。撤銷不可逆，撤之前先跟使用者確認是哪一條。

## 下載材料（Web / MCP / CLI 同一份配置）

- 下載的是本版本明確選擇的已上傳檔案。正文原檔案和附加材料分別選擇；首版預設無下載，Markdown 原始檔也不自動加入。省略 manifest.downloads 會繼承當前版本的明確選擇；顯式 [] 清空。舊字串清單仍相容。
- manifest.downloads 推薦用物件：

```json
{"schema":1,"enabled":true,"source":null,"attachments":[{"path":"handout.pdf","label":"项目介绍","description":"页面与实施安排","showFilename":false,"downloadName":"项目介绍.pdf"}]}
```

- source 為 null 表示不提供正文原檔案；需要時填同樣的檔案物件，path 必須是本次正文入口（Markdown 指源 .md）。附件陣列順序就是展示順序；每項 label 必填。格式和大小取實際檔案，downloadName 必須保留原副檔名。showFilename 預設 false；只有明確開啟才展示原檔名。舊版儲存新配置前保持原有名稱。
- MCP `get_downloads` 讀取清單、可選檔案和 revision；`set_downloads` 帶 version_id、revision 和完整 config 儲存。只讀成員不能修改。遇到衝突先重新讀取並核對，不盲目覆蓋。enabled=false 同時關閉入口與下載地址，保留選項。
- CLI v3：push 可加 `--downloads downloads.json`；配置檔案或 `--manifest` 檔案本身不上傳。讀取用 `node space.mjs downloads --version <id>`；修改用 `node space.mjs downloads --version <id> --config downloads.json --revision <读到的修订号>`。所有命令沿用 SPACE_TOKEN。
- CLI push 在提交前列出材料新增、移除、同路徑檔案替換和缺失；缺失會阻止提交。手工 MCP 上傳在 create_upload_session 後用 `preview_downloads`，把返回的 basis 交給 finalize_upload 的 download_basis。路徑不會模糊重配；目錄改名應明確更新清單。需要人工看完再生效時用 --hold。
- 正文裡自己畫下載按鈕：給它標 `data-space="download:<包内路径>"`，路徑要在這份清單裡（見「3.55 讀者端沒有作者設定」）。
- Web 在頁面詳情的「下載材料」裡編輯，也可按版本檢視與當前版本的差異。當前版儲存即應用；其他版本的設定隨各自版本使用。單份直接下載，多份開啟有名稱、說明、格式和大小的清單。新配置的下載入口獨立於「更多」。
- 不做伺服器 HTML 轉 PDF，也不打 ZIP：HTML 原檔案只含入口檔案；完整講義應先本機匯出 PDF 再隨包上傳、加入清單。渲染所需資源仍可被瀏覽器讀取，下載設定不是防複製措施。

## 資料表（掛在頁面上，不隨版本；MCP / CLI / REST 同一份）

- 一個頁面可以掛若干張 CSV / JSON 資料表。表掛在**頁面**上、不掛版本：改資料不用重發頁面，push 新版本（含 `--hold`）也不會動表。每張表有修訂號 revision。
- MCP：`get_data`（不傳 table 列出全部表與三條上限；傳 table 返回 columns / rows / revision，format=csv 另附 CSV 文本）· `set_data`（新建 revision 傳 0；修改 / 刪除必須原樣帶回 `get_data` 給的 revision，不一致會被拒並要求先讀回，不能盲目覆蓋；delete=true 刪表）。`list_proposals` 會標出每個頁面有幾張表。`set_data` 的 `append_open` 與 `list_data_rows`（讀者寫回的行）見下面「讀者寫回」。
- CLI：`node space.mjs data pull --proposal <id> --out <目录>`（每張表一個 <表名>.csv，`--json` 則 .json）· `node space.mjs data push <file.csv|file.json> --proposal <id> --table <name> [--revision <n>]`（新建預設 0）· `node space.mjs data delete --proposal <id> --table <name> --revision <n>`。
- 表名小寫字母開頭、只含字母數字下劃線；每格只收文字 / 數字 / 真假 / 空（巢狀先攤平）。上限：單表 512 KB、每個頁面 20 張、工作區合計 64 MB，回執逐條印出。
- 讀許可權跟頁面可見性走、不另造授權：private 只有成員與批過的裝置、口令過門後可讀、public 任何人可讀；重置預設連結後舊連結讀不到；無許可權是 403 不是空表。
- **頁面取數用最普通的 `fetch`**（#8654 起）：`fetch('./_data/表名.json').then(r => r.json())` 拿到行物件陣列（底表 + 過審的讀者寫回；釋出者自己看還會多出待審的行），修訂號在響應頭 `X-Space-Revision`；讀許可權就是頁面可見性，無許可權是 403 不是空陣列。線上沒有這張表時回落包裡同名的 `_data/表名.json` ⇒ 本地放一份示例資料、`node space.mjs serve` 就能跑。只改表不重傳頁面，讀者重新整理即見新資料。舊的 SDK 寫法 `space.data.get('表名')`（回 `{columns, rows, revision}`）繼續可用，是可選的相容層。
- **讀者寫回（報名 / 打卡 / 投票 / 意見收集，#7624 起）**：頁面往同一張表寫一行 —— 普通表單 `<form action="./_data/表名" method="post">`，或 `fetch('./_data/表名', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({列: 值})})`（契約見上面「託管契約」第 3 條；舊 SDK `space.data.append('表名', {列: 值})` 同一件事）—— **預設關、按表開**：`set_data` 傳 `append_open=true`（單獨傳只翻開關、不動底表，仍要帶 revision）。寫回限頻（每張表每小時 600 行、每臺裝置 30 行）、每行不超過 2000 字 / 單格 500 字、每表最多 5000 行；文字先過審：過審的行其他讀者重新整理即見，稽核命中的行只有釋出者看得到，稽核不可用時寫回被拒（頁面收到「稍後再試」）而不是先收下。關著時寫口回 `403`、`code` 為 `append_closed`（SDK 則 reject 同一個 `err.code`），頁面要把原因給讀者看、別假裝「已提交」。寫回的行**不在** `get_data` 的 rows 裡、也不會被 `set_data` 覆蓋，用 `list_data_rows` 讀回（可按表 / 狀態過濾，帶裝置、時刻、寫回時的頁面版本與資料修訂；只有裝置與時刻，不認人、別據此推斷意願）；要並進底表就讀回後 `set_data` 整表替換。寫回只對讀得到這份內容的裝置開放（private / 口令與頁面同口徑），重置預設連結後舊連結的寫回一併失效；每次寫回都進訪問時間線（`get_session_timeline` 裡是「寫回」）。預覽臺、講解人與釋出者自己的會話不提交。
- **一等公民表單 / 訂單塊（報名 / 訂貨 / 預約 / 留資，#8273 起）**：頁面上一行表單程式碼都不想寫時用它（自己寫普通 `<form>` 同樣可以）—— `set_form` 只定義欄位（最多 20 個，六種型別：text / textarea / select / multiselect / number / phone），平臺在檢視頁裡把它渲染成手機上能填、帶校驗的表單，提交的行落到同一張資料表、用 `list_data_rows` 讀回（每行帶 _form_rev；稽核、限頻、held 與上面那條一樣）。可選 amount 只顯示金額（單價 × 數量由平臺算、寫進每行的 _amount_cents），不收錢。冪等：再調一次就是改欄位，已收的行保留；寫回預設開啟，傳 append_open=false 關掉。頁面不用重發。
- **收款指引（#8276 起，錢不經平臺）**：表單帶 amount、且這個工作區設過**經營收款碼**（商戶收款碼；2022-03-01 條碼支付新規起個人靜態碼不得用於經營性收款 —— 你可以直接用 `set_payment_qr` 傳，不必讓使用者去控制台；撤掉用 `remove_payment_qr`）時，讀者提交成功後頁面在原地展開付款那一段：應付金額、使用者自己的收款碼、一枚「我已付款」。讀者點的那一下只是**自稱**，不是支付結果 —— 使用者在自己的收款賬單裡核對到賬後用 `confirm_payment` 確認，讀者再開啟那一頁就看到「商戶已確認到賬」（只有他自己那一行看得到，別的訪客看不到）。兩個標記都**不改**這一行的稽核狀態，`list_data_rows` 每行會印金額與付款狀態。沒傳收款碼就只顯示金額，付款那一段不出現。活頁不代收款、不做下單與退款。**換碼是全產品唯一「改錯了錢會悄悄流到別處」的設定**：`set_payment_qr` 每次寫都留痕、並給工作區負責人推一條微信通知，所以傳之前先跟使用者確認那張圖確實是他本人的商戶收款碼，別替他找圖、也別從網頁上抓一張二維碼。

## 3. 反饋閉環

- 「客戶看了沒」→ `get_engagement_summary`。**結論由你產出，平臺只給事實**：返回正文末尾帶一個
  **平臺算好的「證據檔位」**（none / thin / usable）與一段讀法約束 —— 檔位不是 usable 時，先說明證據不足，
  再給最多一句謹慎的觀察，不要用語氣補足資料。可以說看了/跳過了哪幾頁、在哪停最久、有沒有下載/演示/回填；
  **不可以說人名**（一條連結認不出是誰開的，「3 臺裝置」不等於「3 個人」），也不可以說意向分、成交機率、「他很感興趣」這類心理判斷。「沒有資料」≠「沒興趣」（連結可能壓根沒發出去）。
  結論要落到**下一步動作**，不是形容詞。
- 「說了什麼」→ `list_feedback`（含第幾頁、引用、狀態）；會議/微信裡的意見 → `add_external_feedback`；
  處理完 → `set_feedback_status`。
- 「客戶問了一句」→ `reply_feedback`（`thread_id` 來自 `list_feedback`）：回覆寫在**客戶正看的那一頁**上，
  和釋出者在控制台點「回覆」是同一條路。讀得到卻不回，客戶那邊看起來就是沒人理。
- 「把這份改個名」/ 頁面標題換了但列表裡還是舊名 → `rename_proposal`：頁面名（列表、資料夾目錄頁顯示的那個）與每一版 manifest 的 title 是兩件事，
  發新版本不會改頁面名。只改名字，連結、版本、可見性都不變。
- 使用者貼來一個活頁連結、說「複製到我這裡」「我要改一份」「換個說法給老闆」→ `copy_proposal`（`link` 原樣傳）：得到**他自己工作區裡的一份未釋出草稿**，
  與來源從此獨立。`read_version` 讀正文、按他的意思改好，再 `publish_file`（傳副本的 `proposal_id`）發成新版本 —— 照常過審、預設僅自己可見。
  釋出者沒允許複製（403 `copy_not_allowed`）、連結要口令或已失效（403 / 410）時把那句理由轉告使用者，**別繞**（別去抓頁面正文再自己發一份）。
- 使用者對**活頁本身**有意見（「這個不好用」「能不能加個…」）→ 先把他的意思寫成一段草稿給他看，**他同意後**才調
  `send_feedback_to_livepage`（傳 `user_confirmed: true`）；用的哪個 Agent、哪一頁、剛才哪一步出的錯平臺自動帶上，
  別把頁面正文、讀者資訊、令牌寫進去。之後 `list_my_feedback` 看狀態與回覆，有回覆就轉告使用者。什麼時候該問、什麼時候別問見 §3.3。
- 使用者卡住了、想找**真人**（不是提建議）→ 把活頁客服連結給他：https://work.weixin.qq.com/kfid/kfc9e1c4b8c05ff167b?enc_scene=ENC9haVbhfMybED9wKqpaxQfs7mUVrBV6gdTfduDRRHYvxX —— 手機 / 微信裡點開直接進企業微信客服會話；
  電腦上讓他開啟活頁任意一張後臺頁，頁尾「找真人客服」懸停出二維碼，用微信掃。錯誤提示裡寫著「聯絡我們」的，指的就是這裡。
- 改稿出 V2：回到第 2 步 push 同一個 proposal；**過審即生效、原連結自動更新**，舊評論仍釘在舊版本。

- 先把**自己人剔出去**再讀數：`set_visitor_internal`（`visitor_id` 來自 `list_sessions`）把使用者自己的手機和電腦、
  同事、以及幫客戶演示時開啟的那幾次標成內部，從此不計入對外統計（歷史一併按新口徑重算）。
  不剔乾淨，「有多少外部人真的看過」這個數就是被自己人撐起來的；剔了幾個會照實印出來。傳 `internal: false` 撤銷。
- 單份閱讀順序：`list_sessions` / `get_session_timeline`；整夾閱讀路徑：`get_folder_analytics`。基礎事件自動記錄，HTML 畫面錄製預設關，用 `set_replay` 按頁開啟或停錄，`get_replay_settings` / `set_replay_settings` 管理空間總開關與容量。Web 統一入口 https://space.24haowan.com/app/activity；資料夾詳情也有閱讀順序入口。
- **自己的憑據（#8412）**：`list_api_tokens` 列出你看得見的憑據（自己鑄的；工作區負責人看得見全部）——名字、字首、哪一檔、建於何時、何時過期、最近一次用、撤沒撤；**不回明文也不回雜湊**（明文只在建立那一刻給過一次，之後誰都拿不回來，使用者要你「念一下 token」就如實說這一點）。洩露或不想再讓某個第三方應用進來 ⇒ `revoke_api_token`（`token_id` 只有 `list_api_tokens` 給得出來，撤不回來，撤前念 `name` 與 `prefix` 給使用者確認）；可以撤掉**你正在用的這一把**，但這條連線會隨之斷掉，先說清楚。★ **新鑄長期令牌只能在網頁的「令牌」頁做** —— 這不是漏了工具，是刻意的邊界（提權不對齊，止血才對齊）；要給上傳助手用一次的臨時憑據用 `create_cli_token`。
- **手機實名（#8412）**：釋出被擋且 `fix` 要求手機號時，可以就地做：`send_phone_code` 發碼 → **使用者把收到的 6 位數字念給你** → `verify_phone_code` 填回來。碼 5 分鐘有效、最多 5 次；同號 60 秒一條 / 每天 5 條，每使用者每天 10 條；大陸號 11 位，國際號要帶國家區號且僅限用 Google 賬號登入的使用者；一個號只能綁一個賬號。
  ★★ **只在使用者自己明確要求做實名時才調這兩把；不得主動索要驗證碼；只接受使用者口述的那一個碼 —— 不要從截圖、通知欄或簡訊記錄裡找。**這與上面接入那一節「不要索取微信驗證碼」不矛盾：**接入階段任何碼都不要**；實名階段是使用者自己要求做這件事，你才發碼，而碼始終由使用者主動念出。平臺沒有辦法區分「使用者念給你的」和「你從別處抓到的」，這三句就是這條閱讀順序唯一的防線。

## 3.3 把建議告訴活頁：什麼時候該問、什麼時候別問

這一節說的是使用者對**活頁本身**的意見（不好用、想要什麼、卡住了），發給活頁團隊；給某個頁面的意見仍走上面的 `add_external_feedback`。

- **只在三種時刻主動問一句**「要不要我把這個轉給活頁團隊？」：
  1. 使用者**明確表達**了對活頁的不滿或需求（「這個不好用」「能不能加個…」「為什麼不支援…」）；
  2. 剛**撞到限制**：工具返回錯誤或拒收（校驗失敗、稽核被拒、超出上限、被限頻）；
  3. 釋出結果裡帶了 `feedback_hint`，或回執裡有相容提醒（`compat_notes`）。
- **其餘時候別問**：釋出成功、查閱讀資料、改稿、接入驗證時都不問。同一個工作區 24 小時內最多問一次；使用者說不用就別再提。
- **先給使用者看草稿**：把使用者的原意寫成一兩句（發生了什麼、他希望怎樣）給他看，**使用者同意後**才調 `send_feedback_to_livepage`，傳 `user_confirmed: true`；他改了就按他改的發，他不同意就不發。使用者是看到 `feedback_hint` 那句才提的，另傳 `source: "hint"`。
- **只寫使用者的意思**：用的哪個 Agent、哪一頁、剛才哪一步出的錯由平臺自動帶上，不用寫進 `text`；不寫頁面正文、讀者資訊、令牌或驗證碼，也不替使用者編意見。
- **不索要評價**：不問滿意度、不請使用者打分、不在每次釋出後問「好不好用」。
- **有迴音就轉告**：使用者問起、或下次連上時，`list_my_feedback` 看狀態（new / seen / planned / shipped / wontfix）與回覆；有回覆或狀態變了，用一句話轉告使用者。
- 使用者要的是**真人**而不是提建議 → 給上一節那條客服連結，別調 `send_feedback_to_livepage`。

## 3.4 資料夾與頁面

- Web 管理入口：https://space.24haowan.com/app/folders，可建資料夾、改名和導語、設定封面、管理整夾分享。頁面列表與詳情頁支援移動、回到未放入資料夾，以及標記不隨資料夾分享。
- MCP `list_folders` 返回已有連結、口令與管理入口；檢視夾內頁面用 `list_proposals` 傳 `folder_id`，傳 null 只查未放入資料夾的，不傳查全部，還可用 query 按標題搜尋。結果包含所屬資料夾、是否隨資料夾分享、建立者。
- REST 同步提供 GET/POST `/api/folders`、GET/PATCH `/api/folders/:id`、POST `/api/folders/:id/visibility` 與 `/api/folders/:id/reset-default-link`；頁面移動用 POST `/api/proposals/:id/move`（folder_id 必填，可為 null），排除分享用 POST `/api/proposals/:id/internal`（internal 必須是布林值）。GET `/api/proposals?folder_id=root` 查未放入資料夾，其餘傳資料夾 ID，不傳查全部。
- 移入已開放資料夾，或取消不隨資料夾分享，會使頁面的當前版本隨連結開放。資料夾許可權與頁面連結許可權獨立，標記不隨資料夾分享不會關閉已有連結。

## 3.5 請同事進來（多人共用一個工作區）

成員是**工作區級**的：加進來的人共享整個工作區（頁面 / 專屬連結 / 反饋），而且他用**自己的 Agent** 連同一個空間 —— 不是登入你的賬號。

1. `list_members` —— 先看這個工作區有誰、各是什麼角色。
2. `invite_teammate` —— 出一條邀請連結（`role` 只能 editor / viewer，**不能邀請成 owner**）。返回裡帶 `share_text`，**可以整段轉發給同事**，裡面寫好了他該做什麼。
3. `add_member` —— 只在你已經知道對方 26 位 `user_id` 時用；不知道就用 `invite_teammate`。
4. `set_member_role` / `remove_member` —— 改角色、移出工作區（都要 owner）。**建立者動不得**（那是「工作區永遠至少有一個 owner」的保證），**自己的角色也改不了**（降級是單向門）。
5. `revoke_invite` —— 邀請連結轉發錯了地方時讓它立刻失效。`invite_id` 從 `list_members` 拿（那裡列著所有未到期的邀請）。已經用它進來的人不會被踢，那是 `remove_member` 的事。

★ 人數上限（含所有者）：免費版 3 人、專業版 10 人；滿了邀請 / 加人會回錯誤碼 member_limit（已經在的人不受影響），見 §3.9。
★ 三檔角色：`owner` 管頁面與成員 · `editor` 建頁面 / 發版 / 發連結 · `viewer` 只讀。
★ **別替使用者決定給誰什麼角色** —— 問一句。發出去的邀請連結收不回來，但可以用 `revoke_invite` 作廢。
★ 移除一個人**當場切斷他的令牌** —— 每次請求都重新核一遍他還在不在，不需要另外去撤令牌。

## 3.55 讀者端沒有作者設定

讀者開啟頁面時，平臺只在正文之外放一個入口：右下角「關注更新」膠囊，點開是關注、一句引導和一行品牌。**讀者端沒有作者設定**：關注更新一律開、入口一律右下帶字膠囊、平臺色，品牌露出與「做一份」也固定，所有頁面一樣。作者能管的只有誰能看（`set_visibility`）。使用者要求改入口位置 / 顏色、關掉關注或去掉品牌時如實說明，別找別的辦法繞。（manifest 裡的 `reader` 照收、不生效。）

**唯一例外（高階功能，要工作區所有者是高階賬戶）：把膠囊整個去掉。** `set_reader_view` 傳 `capsule:"hide"`：不給 `proposal_id` = 工作區預設（只有負責人能改），給 = 這一頁（立即生效、不用發版；`capsule:null` = 跟隨工作區）；發頁面時也可以在 manifest 寫 `chrome: {"default": "hidden"}`。誰說了算：這一頁 > 這一版宣告 > 工作區預設。去掉後讀者看不到關注更新、填寫、推到我微信與隱私 / 舉報入口 —— 要留關注或表單入口，就在正文裡放 `data-space="subscribe"` / `data-space="form"` 的按鈕。免費賬戶設 hide 會被拒（錯誤碼 pro_required，什麼都不改）：如實告訴使用者這是高階功能，別找別的辦法繞。`get_reader_view` 讀回賬戶等級、三處設定與生效值。

- 平臺元件全部在作者正文之外，不往正文里加節點或樣式；頁面上沒有入口帶、送達條、更多選單。作者想要的按鈕（關注、下載）就寫在自己的正文裡，樣式隨你畫，只要標一個屬性，讀者點了由平臺接手：
  - **關注**：`<button data-space="subscribe">订阅更新</button>` ⇒ 彈出平臺的「關注更新」浮窗（和右下膠囊點開的是同一個）。
  - **翻往期**：`<button data-space="history">翻往期</button>` ⇒ 彈出「往期 · 共 N 期」目錄（每天一期、釋出時打日期標籤的頁面）。**這是讀者翻往期的唯一入口**，平臺不另畫往期入口：打了標籤的頁面一定要在正文裡放這顆按鈕（封面或頁首最顯眼處）；沒有往期時點了不動作。
  - **填寫**：`<button data-space="form">报名</button>` ⇒ 開啟這一頁 `set_form` 的表單。**放了這個按鈕就要調 `set_form` 給這一頁掛上表單**（釋出後再掛也行，不用重發版本）：沒掛錶單時讀者點了只看到一句「作者還沒設定這個表單」，釋出回執與 get_version 會帶一條「按鈕已放但還沒設定表單」的提醒，讀者點過幾次也會出現在 get_engagement_summary 裡。
  - **下載**：`<a data-space="download:files/报价单.pdf">下载报价单</a>` ⇒ 直接下載這個檔案。冒號後是包內相對路徑，**必須在本版本的下載清單裡**（manifest.downloads / `set_downloads`，見「下載材料」）；不在清單裡點了不動作。
  - 屬性可以掛在 `button` / `a` / `div` 任何元素上，點到它裡面的字或圖示也算；不用寫指令碼，平臺不改你的按鈕樣式。樣例 `sample-analysis-report`（`list_examples`）裡兩個都有。

## 3.56 微信分享卡（只經你設定）

連結被轉發 / 粘進微信時那張卡片：標題、描述、封面。作者要改卡片，調 `set_share_card`，按頁存、**立即生效、不用發版**：

- `title`（120 字內）、`desc`（300 字內）、`cover`（**包內相對路徑**，如 `img/cover.png`，必須在當前生效版裡；以後換版時這張圖要留在包裡）。
- 逐項優先順序：`set_share_card` 按頁設定 > 當前版本 manifest 的 `share` > 預設（頁面名 / 正文首段 / 活頁品牌圖）。只傳要改的項；某項傳 `null` = 清掉按頁設定。
- 回執給出改完後卡片**真實**的標題 / 描述 / 封面（與微信抓到的同一份），照著告訴使用者；頁面名本身用 `rename_proposal` 改。

## 3.6 釋出後推到微信

工作區裡的人（建立者 + 成員）可以在**微信**裡收到「有新版本釋出」，每人一條自己的專屬連結。**不發給客戶** —— 客戶要看請把連結發給他。

- `set_publish_notify` —— 三個開關（owner）：自動推 / 也發給觸發釋出的人本人 / 釋出七天沒人開啟時提醒作者。**只傳要改的那個**，其餘保持現狀。
- `push_to_wechat` —— 手動把某一份的當前版本推一次（editor 即可），無視自動開關。**預設只推給你自己**（工作區有別的成員也一樣，別人收不到）；要推給工作區裡的人傳 `to: "members"`。回執分開報未關注 / 拒收 / 沒綁微信 —— 那不是報錯，是「他收不到」。
★ **自動去重**：同一版每人只推一次（自動推送 / 釋出 / `push_to_wechat` / 訂閱提醒共用一本賬）。自動推送開著時發版就已經推到了，**不用再手動調 `push_to_wechat`**；重複呼叫不會多發，回執說「已推過（自動推送 · 14:03），本次未重複傳送」。使用者真要再收一次才傳 `force: true`（只給自己，每天最多 5 次）。
★ 沒關注服務號的人收不到。開關開著不等於推得到，看回執。
★ 使用者說「把我的名字改成 X」→ `set_nickname`（改的是**他自己的**顯示名，最多 24 個字；沒有指向別人的入參，改不了其他成員）。

## 3.6.1 給頁面一個好看的地址（不用綁域名）

`set_page_path(path, proposal_id | folder_id)` 把工作區裡任意一頁或一個資料夾掛到 `https://<工作区>.hy.24haowan.com/<名字>`：path="/" 是工作區首頁；同名再設 = 改指向；remove=true 刪；不傳 path 只列出。免費賬戶也能用，不用加解析記錄。設好後這一頁的連結（list_proposals 的 url、分享卡、複製連結）就是這個地址。`<工作区>` 那一段用 `set_workspace_slug` 改（只有所有者，30 天一次，舊地址一直跳到新地址）。

## 3.7 繫結自己的網址（Custom domains）

這是專業版功能（付費版 2026-11 上線起）：工作區所有者是專業版的，每個工作區能綁 3 個；免費賬戶新申請會回錯誤碼 quota_reached（不寫庫），已經綁好的網址照常用。使用者想綁而還是免費版時如實說明，並把自助升級頁 `/app/upgrade`（活頁站點下，價格見 §3.9）給工作區所有者。不綁網址也有好看的地址，見 §3.6.1。使用者說「把活頁綁到 go.acme.com」「用我們公司的網址開啟」時，你用下面六把工具從申請辦到能開啟，每一步的返回都告訴你下一步做什麼；人只需要在解析商那裡加記錄（你自己有解析商的工具，連這件也由你做）。

1. `bind_domain(hostname)` —— 一次拿到要加的全部記錄 `records`（通常三條：證明歸屬的 TXT、網址的 CNAME、簽證書用的 `_acme-challenge` CNAME；type / name 主機記錄 / value / why）與注意事項 `notes`，照 `say` 轉述。根域名（acme.com）：解析商能拉平就原地開啟，否則轉到 www（www 一起申請好，兩者算一個名額）。
2. 加記錄：有解析商工具就按 `records` 直接加；沒有就把 `records` 原樣給使用者。我們不收使用者的解析商賬號鑰匙。
3. `verify_domain(domain_id)` —— 每次呼叫順帶推進一步；按 `retry_after_s` 輪詢到 `state=active`。`next` 是下一步：`add_records`（對照 `seen`，那是我們在公網上看到的）· `user_action`（把 `can_do` 轉告使用者：辦備案、在別的阿里雲賬號裡刪掉同名網址、改 CAA）· `contact_us` · `verify_domain`（平臺在辦，等著就行）。
4. `set_domain_route(domain_id, path, proposal_id | folder_id)` —— `/` 是網址首頁，`/autumn-plan` 開啟指定頁面（永遠是當前版本）；`remove=true` 刪一條。開通前就可以先設好。
5. `list_domains()` 看全部網址、路徑與名額（專業版 3 個；免費版不能新綁）；`unbind_domain(domain_id, confirm=网址)` 解綁，先和使用者確認。
6. `set_domain_region(domain_id, region)` —— 改加速區域：沒備案的網址開通時自動開在海外節點（全球，不含中國內地；國內能開啟但較慢），使用者辦好備案後傳 `region=domestic` 一鍵切到國內加速。先和使用者確認。

已備案的網址（在哪家服務商辦的都行，不用轉接入）開在國內加速；沒備案的也能綁，開在海外節點，`say` 裡寫明「國內訪問較慢；完成備案後可一鍵切換到國內加速」，備案查到後進度裡會提示可以切（`reason=icp_ready`）。微信裡開啟客戶網址會轉到活頁的地址（分享卡、關注更新照常）。

```
用户 › 把活页绑到 go.acme.com，/autumn-plan 打开秋季方案
你 → bind_domain({hostname:"go.acme.com"})
   ← 已为工作区申请 go.acme.com（子域名 · 解析在DNSPod）。请在解析商那里加下面这些记录……
     TXT    verification   <验证串>              证明这个网址是你的
     CNAME  go             edge.hy.24haowan.com  让这个网址经加速网络打开
     CNAME  _acme-challenge.go  <我们给的名字>.acme.24hw.cn  签 https 证书用（自动签、自动续）
你 → （有 DNSPod 的工具就直接加；没有就请用户加好后告诉你）
你 → verify_domain({domain_id}) × N
   ← 第 2 步（共 5 步）…… → 第 5 步（共 5 步）：已开通，可以用 https 打开。
你 → set_domain_route({domain_id, path:"/autumn-plan", proposal_id})
   ← 现在打开 https://go.acme.com/autumn-plan 就是「秋季方案」。
```

## 3.8 掛著等頁面新反饋（別定時輪詢）

發出一頁、等使用者或讀者在頁面上點選 / 填表 / 評論時，**不要開定時任務每隔幾分鐘去翻 list_data_rows / list_feedback**：掛著等，有人交了立刻醒來。資料行（表單、投票、報名）、評論、回覆進同一條佇列，每條有隻增不減的 `id`，**游標就是你上一次讀到的那一條的 id**。

- MCP：`wait_for_feedback({proposal_id, since?, kinds?, timeout_s?})` —— 有新的立刻回 `{events, cursor, timed_out:false}`；沒有就最多等 `timeout_s`（預設也是上限 300 秒）回 `timed_out:true`。把 `cursor` 作下一次的 `since` 帶回接著等，不漏不重；第一次不帶 `since` = 從此刻開始、不翻舊賬。`kinds` 可只選 `row`（資料行）或 `thread`（評論與回覆）。等待期間每 20 秒一條進度通知。同一憑據最多同時掛 3 個（多了 `wait_rate_limited`）。
- 命令列：`node space.mjs watch <proposal_id> [--kinds row,thread] [--since <游标>] [--once]` —— 一條反饋打一行 JSON（`{"type":"event",id,kind,…}`），斷線自動重連並從上次的位置接著讀（游標存在 `~/.space/watch/<proposal_id>.cursor`）；`--once` 拿到第一批就退出 0。憑據同 push：`SPACE_TOKEN`（`create_cli_token`）。
- 兩條都接在 `GET /api/proposals/<proposal_id>/feedback/stream?since=<游标>&kinds=row,thread`（SSE，`id:` 即游標，每 15 秒一行 `: ping`）上，能讀 SSE 的也可以直接連。
- 讀回來的資料行看 `status`：`held` 是稽核命中、只有你看得到的那一行。要全量對賬用 `list_data_rows`（帶 `since`），**不要看 get_data 的 `row_count`**（那是你寫的底錶行數，讀者交的在 `appended_rows`）。

Claude Code（有終端）—— 後臺跑一次、退出即喚醒，處理完再掛：

```
$ SPACE_TOKEN=… node space.mjs watch 01M5FV1N68CT0R91RM0W600SDS --kinds row --once   # Bash 工具 run_in_background: true
{"type":"ready","proposal":"01M5FV1N68CT0R91RM0W600SDS","cursor":"01M5FV2636BDA9NTHE80VS849H"}
{"type":"event","id":"01M5FVBCP4GNFV2659RQ2RMRKH","kind":"row","table":"replies","row":{"item":"nod","choice":"全部按建议"},"status":"approved","by":{"kind":"member","nickname":"Verky","role":"owner"},"at":"2026-10-21T18:25:05.732Z"}
（进程退出 ⇒ 你被唤醒，处理这一行；再起一次同一条命令接着等 —— 游标已落盘，不用记）
```

沒有終端，或只想在對話裡等一會兒：直接調 MCP。

```
你 → wait_for_feedback({proposal_id:"01M5FV1N68CT0R91RM0W600SDS", kinds:["row"]})
   ← 收到 1 条新反馈。下次带 since=01M5FVBCP4GNFV2659RQ2RMRKH 接着等。
你 → wait_for_feedback({proposal_id:"01M5FV1N68CT0R91RM0W600SDS", since:"01M5FVBCP4GNFV2659RQ2RMRKH"})
   ← 300 秒内没有新的反馈。带 since=01M5FVBCP4GNFV2659RQ2RMRKH 再调一次接着等。
```

Codex —— 終端裡管道讀，一行一條：

```
$ SPACE_TOKEN=… node space.mjs watch 01M5FV1N68CT0R91RM0W600SDS | while read -r line; do echo "$line"; done
```

或調 MCP `wait_for_feedback`：Codex 的 MCP 呼叫預設 60 秒超時，在 `~/.codex/config.toml` 的這臺伺服器下寫 `tool_timeout_sec = 330`，或者每次傳 `timeout_s: 50` 迴圈呼叫。


## 3.9 免費版與專業版（價格）

活頁分免費版與專業版兩檔，等級掛在**工作區所有者的賬戶**上、作用於他擁有的工作區。價格頁：活頁站點的 `/pricing`；所有者在控制台自助開通 / 續費：`/app/upgrade`（微信支付，電腦掃碼、微信裡直接付）。

- **專業版 ¥99/月**；首批 20 位使用者享 ¥49/月，先到先得、名額用完即止，連續續費最長保留 12 個月，之後按 ¥99 標準價。
- **免費版**：不能綁自己的網址（已經綁好的照常用）、工作區成員最多 3 人（含所有者）、AI 回答每月 200 次（頁面 AI 與服務號 / 小葉同學共用）、頁面訪問流量每月 20 GB（軟上限：頁面照常開啟，超過後圖片改用壓縮版、超過 5 MB 的附件暫停下載）。
- **專業版**：自有網址 3 個、成員最多 10 人、AI 回答每月 3000 次（另有 20% 寬限）、儲存 20 GB、流量每月 200 GB、可去掉讀者膠囊（§3.55）、主頁配色與橫幅圖；AI 不夠用可買加量包 ¥50 / 1000 次。

撞到上限時錯誤碼會說清楚（member_limit · quota_reached · ai_quota_exceeded · pro_required），`fix` 裡帶自助升級頁。**別替使用者付款或替他決定買不買**：如實說明差別、把連結給工作區所有者；折扣、發票、退款這類個案讓使用者找真人客服（https://work.weixin.qq.com/kfid/kfc9e1c4b8c05ff167b?enc_scene=ENC9haVbhfMybED9wKqpaxQfs7mUVrBV6gdTfduDRRHYvxX）。

## 4. 判據

釋出成功 = 使用者手機微信裡開啟連結能看到這個頁面，且幾分鐘後 `get_engagement_summary` 能看到這次訪問。

有問題：控制台 https://space.24haowan.com/app · 隱私與條款見站內頁尾。

## 畫面錄製與容量

HTML 頁面畫面錄製預設關閉：讀者端不載入錄製指令碼，只記閱讀順序。使用者想看畫面回放時，用 `set_replay` 按頁開啟（隻影響開啟之後的訪問）；開啟 / 停錄的選擇跨發版、移夾和重置連結保留。`get_replay_settings` 檢視空間總開關與用量；僅負責人可通過 `set_replay_settings` 更改總開關。管理頁 `/app/recording`；REST GET / POST `/api/workspace/replay`，POST `{ "enabled": false }` 關閉整個空間。

每空間獨立回放容量 1 GB、每次最多 8 MB、畫面保留 30 天；超過容量停止採集，進入後臺暫停、返回後繼續。輸入遮罩，敏感正文加 `data-space-private` 或 `rr-block`；不錄 Canvas / WebGL。`list_sessions` 與 `get_session_timeline` 的 `replayStopReason` / `replayHadPause` 說明未完整採集的原因。閱讀順序、PDF/PPTX 逐頁回放不受錄製開關影響。
## 微信署名與每人一行

先按用途選：團隊協作、簽收、排班等需要知道誰更新的場景（A 檔），用 `identity: "wechat"`；每人維護一份狀態用 `per_reader: "latest"`，需要保留每次參與則用 `off`。社群投票、抽獎報名、簽到等「一人一次」場景（B 檔），用 `identity: "wechat", per_reader: "one"`，姓名輸入框不能代替平臺身份。
健康記錄、房號、手機號留資等敏感或獲客場景不要開 identity，保持 `identity: "off", per_reader: "off"`；不要為拿暱稱增加授權門檻。

先 `get_data` 拿 revision，再 `set_data` 傳 `identity: "wechat", identity_visibility: "all", per_reader: "one", append_open: true`（新表 revision=0，並帶 columns 與 rows=[]）。`identity_visibility` 預設 `all`，署名對所有能看頁面的人可見；僅給作者與工作區成員看時設 `author`。不要把預設 all 描述成私密。

頁面載入時可 `const me = await space.me()`，null 表示當前沒有可用身份。只在讀者親手點「用微信身份參與」按鈕的點選回撥裡調 `space.me.request()`；不在載入、定時器或提交失敗重試中靜默發起授權。微信裡跳授權、電腦上掃碼確認，回來再讀 `space.me()`。取消授權仍能閱讀，保留輸入並提示下一步。

讀回實名讀者行的 `_by` 是服務端給的 `{id, name, avatar}`，用它署名，不信任提交內容裡自填的暱稱。匿名行沒有它；成員行仍按既有規則只向成員顯示暱稱字串，頁面需判斷型別。頭像只接受同源 `/_rid/avatar/` 路徑，缺頭像或身份已撤回時顯示文字佔位，不使用微信 CDN 連結、不拼外部頭像 URL。暱稱用 textContent 渲染。

`per_reader` 按表生效：`off` 不限制；`one` 同一 reader identity id 只收一行（成員按 user id），重複返回 409 `reader_already_submitted`，把 message 展示給讀者；`latest` 保留歷次提交供 `list_data_rows` 審計，頁面只給每人最新一行。最新行待稽核或撤回時，舊行不會重新出現。相同時刻按 row id 排序；不要用顯示暱稱去重。
只在 identity 開著時能設 one/latest/vote，否則 422；關閉 identity 要同時設 per_reader=off。`wechat_optional` 的匿名行不受限制，因此一人一次場景必須用 `wechat`。底表 rows 不受限制；one 的已撤回/待稽核行仍佔名額，作者真刪後可重交；latest 歷史仍計入 5000 行上限，作者刪除最新行會顯露上一行，需清除某人的參與時一併處理歷史。開啟 one 不清理已有重複行；用於活動前先建好空表，切換 latest 會把已有實名行按人摺疊。

`per_reader: "vote"`（點贊 / 投票 / ▲）：每人每個值一票、隨時可撤。同時傳 `vote_key: "<列名>"`（必須是表裡的一列），例如 `set_data` 傳 `identity: "wechat", per_reader: "vote", vote_key: "item", append_open: true`。同一讀者對同一值再投返回 409 `reader_already_voted`（把 message 展示給讀者），vote_key 那一格為空返回 422 `vote_value_required`；不同值互不影響。頁面投票 `await space.data.append('votes', {item: 'x'})`；取消用讀回里帶 `_mine: true` 那一行的 `_row_id` 調 `space.data.withdraw('votes', rowId)`（或 `fetch('./_data/votes/' + rowId, {method: 'DELETE'})`；投票表沒有 24 小時限制，授權過的同一讀者換裝置也能撤），撤回後可重投；計數 `rows.filter(r => r.item === 'x').length`，讀回已排除撤回行。

CLI：把上述設定寫入 JSON 物件，可與 rows/columns 一起提交，也可只寫設定，再執行 `node space-cli.mjs data push settings.json --proposal <id> --table votes --revision <当前值>`。僅本地 serve 不模擬微信授權或服務端去重。




## 釋出後把實際效果給使用者看

版本過審後呼叫 preview_version，把返回的 interactive_url 交給發起使用者：免登入、60 分鐘內有效、放的是真實 SPACE 瀏覽頁，可看電腦（1440×900）或手機（按微信內 390×763），也能把兩者並排對比；在手機上開啟時按本機螢幕鋪滿。它不替換公開版本、不提交反饋、不改訪問許可權、不記閱讀統計，也不連線正式講稿遙控房間。

本伺服器未開啟截圖：screenshot_status 為 disabled、captures 為空，不要描述沒收到的圖片，也不要把它說成「截圖已檢查」。預設 audience=visitor 不含講稿；檢視私人講稿時顯式選擇 publisher，不要把該預覽發給客戶。真實手機、微信與跨裝置遙控仍需真機驗證。

### 把這一版嵌進別的網頁（嵌入連結）

要在另一個網頁裡用 iframe 顯示這一版（例如 ClaudeFleet WebChat 對話裡的「在這裡預覽」），調 preview_version 時加 embed_origin=父頁面來源（https://域名，如 https://claudefleet.24haowan.com），用返回的 embed.url 當 iframe 地址，iframe 加 sandbox="allow-scripts allow-same-origin allow-popups"（不給 top-navigation）。邊界：一、平臺第一方來源 https://claudefleet.24haowan.com 與 https://ccquota.24haowan.com（Tokenman 網頁聊天）預設允許，不用設；其他來源必須在工作區嵌入白名單裡 —— 工作區白名單是在第一方來源之上再加，預設為空、只有工作區負責人能設（set_workspace_profile 的 embed_origins，整份覆寫，最多 5 個，不佔用也不包含第一方來源），兩者都不在就是 embed_origin_not_allowed；二、嵌入連結只放開這一個來源框這一版，頁面連結 /s/、/p/ 照舊不許被任何網頁框；三、只給工作區成員憑自己的憑據生成，繫結一個版本，最多 60 分鐘、過期再調一次重新整理，免登入、只讀：不記閱讀、不收反饋、不提交表單或資料；四、負責人把來源從白名單刪掉，已經發出去的嵌入連結當場失效（第一方來源不受工作區白名單影響）。洩露的影響 = 60 分鐘內能只讀看到這一版，所以只交給要嵌它的那個頁面，不要發給客戶。