# 方案空間（space.24haowan.com）· 給 Agent 的接入指引

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

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

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

1. **包 = 一個目錄**，入口 `index.html`，包內相對路徑原樣可用（`./css/a.css`、`img/b.png`）。釋出後跑在工作區自己的子域名 `<工作区>.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`。
   - 寫：`POST ./_data/<表>`（只追加一行）。JSON ⇒ `201 {id, status}`；普通表單（`<form action="./_data/<表>" method="post">`，不用寫指令碼）⇒ `303` 回到提交的那一頁並帶 `?space_submitted=<表>`（隱藏欄位 `_next` 填相對路徑可換落地頁；`_` 開頭的欄位不入庫）。
   - 這張表沒開寫回 ⇒ `403 {"error":{"code":"append_closed"}}`；頁面要把原因給讀者看，別假裝「已提交」。開寫回、限頻、稽核、讀回見下面「資料表」一節。
4. **方案空間只在你的頁面上加一行**：每個 HTML 檔案的 `<head>` 裡多一行 `<script data-space-sdk src="/_space/sdk.js?…">`，其餘位元組原樣（`read_version` 讀回來的就是讀者拿到的位元組，可以自己 diff）。這一行在讀者的瀏覽器裡做這幾件事：記閱讀統計；HTML 頁面預設錄製訪問畫面（敏感正文加 `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. **仍然不行的只有這幾件**（原因是「每個頁面還沒有獨立的未放入資料夾」，不是不讓用）：以 `/` 開頭的路徑指向工作區子域名的根而不是你的包 ⇒ 404，改相對路徑；Service Worker 會被拒收；包裡不能有 `_space/` 目錄（留給平臺那一行）；講解提示 `data-speaker-notes` 不能留在包裡（CLI 會剝進 `manifest.notes[]`）。
7. **不變的底線**：讀者的登入狀態頁面指令碼讀不到（檢視 cookie 是 `HttpOnly`）；釋出者的登入憑據不進內容域；內容稽核與防釣魚照常。**出了事**：運營把出問題的**工作區或頁面**切回收緊規則（只能連本域、不能提交表單、不載入站外資源；`list_proposals` 那一行會標 `content_lockdown`），不按域名拉黑、不動別人的頁面。
8. **舊寫法繼續能用，但是可選**：SDK `space.data.get` / `space.data.append`、`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`，成功返回即可，空列表也是成功。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. **手機開啟**：使用者在電腦上時，讓他開啟這條連結、點頁面外層的二維碼入口（「掃碼到微信」），或直接開啟返回的 `card_url` 用微信掃碼；開啟後再點右上角轉發。使用者在手機上就把連結直接發到微信。講稿遙控的二維碼是另一件事（只給講的人），別混用。

**判據**：收件人手機微信能開啟並看到內容，幾分鐘後 `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
    WorkBuddy：聯結器市場搜「方案空間」安裝，不用填地址
    其他客戶端：「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)`）。
   - `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。
- 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` 的事。

★ 三檔角色：`owner` 管頁面與成員 · `editor` 建頁面 / 發版 / 發連結 · `viewer` 只讀。
★ **別替使用者決定給誰什麼角色** —— 問一句。發出去的邀請連結收不回來，但可以用 `revoke_invite` 作廢。
★ 移除一個人**當場切斷他的令牌** —— 每次請求都重新核一遍他還在不在，不需要另外去撤令牌。

## 3.6 釋出後推到微信

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

- `set_publish_notify` —— 三個開關（owner）：自動推 / 也發給觸發釋出的人本人 / 釋出七天沒人開啟時提醒作者。**只傳要改的那個**，其餘保持現狀。
- `push_to_wechat` —— 手動把某一份的當前版本推一次（editor 即可），無視自動開關。回執分開報未關注 / 拒收 / 沒綁微信 —— 那不是報錯，是「他收不到」。
★ 沒關注服務號的人收不到。開關開著不等於推得到，看回執。
★ 使用者說「把我的名字改成 X」→ `set_nickname`（改的是**他自己的**顯示名，最多 24 個字；沒有指向別人的入參，改不了其他成員）。

## 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 逐頁回放不受錄製開關影響。


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

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

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