# Spec: 巡樓後端(Phase C · live) > Status: `approved` > Status note: 使用者 2026-07-10「spec 通過」 > Source: `docs/product/haixun-backend/requirements.md`(**approved**) > Last updated: `2026-07-10` > 修訂:平台 key/BYOK 計費;會員 API 齊全、頁面可不開;**§9 嚴格驗收場景全量** ## 1. 摘要 本規格定義 **Harbor Desk(`apps/web`)接 live** 時後端必須具備的對外行為與跨切契約。 骨架 **搬入並改造 `stand-alone-service`**,撰寫 **一律 go-zero**(`.api` → generate → handler/logic/module)。 P0:數字 uid、權限、**Threads OAuth**、Job/通知、AI·Exa·dev 設定(**平台 key+BYOK**)。 P1:創作/**Outbox 真發送**、人設、靈感研究、用量、**海巡雙路徑**、**邀請關係**。 **會員相關 API 必須完整**(含註冊/三方登入等);**前端頁面可不提供**(現階段 UI 以 admin 代建+email 登入為主)。 Worker 為 **獨立 binary**;Mongo **index init** 與 **seeder** 分包裝。 ## 2. 名詞與實體 | 名詞 | 定義 | |------|------| | **Member** | 租戶內登入主體;主鍵 **`uid` 為數字**;以 email(或三方綁定)登入;角色含至少 `member`/`admin` | | **Permission** | 能力點或角色集合;中介層依 token 內身份擋 API | | **Platform keys** | 寫在 **系統設定檔(etc)** 的 AI/Exa 等金鑰;全站預設用這組代打外部服務 | | **BYOK** | Bring Your Own Key:會員在設定填了自己的 AI 或 Exa key 時,該 meter 改打會員 key | | **Key mode** | 單次呼叫解析結果:`platform` 或 `byok`(寫入 UsageEvent) | | **ThreadsAccount** | 經 **OAuth** 綁定的 Threads 帳號 | | **ThreadPlay** | 互回方案;可指定 `schedule_start_at` | | **OutboxBundle / OutboxStep** | 發送佇列;**worker 真打 Threads** | | **Job** | 背景任務;guarded 更新 | | **Notification** | 站內通知 | | **Persona / Brand / Scout\*** | 人設、品牌、海巡 | | **Invite** | 邀請碼、`parent_uid`(數字 uid) | | **AiSettings / PlacementSettings** | 會員級覆寫:provider/model、是否已配 key、dev_mode | | **Usage / Plan** | free|starter|pro;事件含是否 BYOK | | **Gateway / Worker** | go-zero HTTP;獨立 worker | ## 3. 狀態機 ### 3.1 Member ```text created (admin 代建 | 自助註冊 API | 三方首次登入) --> email_unverified | active email_unverified --> active : 驗證成功(規則依註冊管道) active --> suspended : admin 停權 suspended --> active : admin 復權 * --> password_reset_pending : 申請重設(token 時限) ``` - **頁面**:現階段前端**不提供**公開註冊/三方登入 UI。 - **API**:會員域 **完整提供**(含註冊、三方 OAuth 綁定/登入等),供日後開頁或外部客戶端;未開 UI ≠ 不做 API。 - 停權會員:登入拒絕;既有 token 在 me/中介層視為無效。 ### 3.2 ThreadsAccount(OAuth) ```text (none) --> authorizing : 發起 OAuth authorizing --> connected : callback 成功換 token authorizing --> failed : 使用者拒絕/換票失敗 connected --> connected : refresh session 成功 connected --> expired_or_error : token 失效 expired_or_error --> connected : 重新授權或 refresh 成功 * --> disconnected : 解除綁定(刪除或標記不可用) ``` - `is_usable`:僅 `connected` 且未過期可供 lead/cast/發送。 ### 3.3 Job ```text pending --> queued --> running --> succeeded running --> failed running --> cancel_requested --> cancelled queued|pending --> cancelled ``` - 更新必須 **guarded**(樂觀條件或等價);Redis lock value = `workerID`(`AGENTS.md`)。 ### 3.4 OutboxBundle / OutboxStep ```text Bundle: scheduling --> active --> completed active --> partial_failed * --> cancelled Step: draft|scheduled --> publishing --> published publishing --> failed failed --> publishing : retry(規則允許時) * --> cancelled|blocked ``` - **真發送:** `published` 僅在 Threads API(或等價)回成功後寫入。 - **root(主貼)失敗:** 後續 reply **不得**標為 published;可標 blocked/維持 scheduled。 - 步驟 `scheduled_at` 來自 play 的 `schedule_start_at` 與間隔;**未到時間 worker 不送**。 - **單篇 compose** 亦必帶可選 `schedule_start_at`(預設 now)。 ### 3.5 Invite 綁定 ```text parent_uid = null --> parent_uid = inviter_uid : 會員 apply 邀請碼(一次) parent_uid != null --> (會員不可改) admin: parent_uid 任意合法調整(防環:不可掛到自己的子孫下) ``` ### 3.6 海巡掃描路徑 ```text dev_mode_enabled == false --> scan_via_api dev_mode_enabled == true --> scan_via_crawler (Chrome extension session) ``` 兩條路徑皆須實作;結果統一寫入 ScoutPost 語意。 ## 4. 使用者流程(行為) ### 4.1 會員進站(P0) **主路徑(現前端):** 1. Admin 代建島民 → **數字 uid**、臨時密碼、角色、邀請碼、可選 parent;密碼僅回傳一次。 2. 島民 email/密碼 login → JWT。 3. 未驗證信箱:對齊前端閘(僅驗證/登出/有限 profile)。 **完整 API(頁面可暫無):** 1. 自助註冊、寄碼、驗證、三方 OAuth 註冊/登入/綁定解綁等 **後端齊全**。 2. 行為與權限與 admin 代建一致(數字 uid、角色預設 member、產邀請碼)。 3. 前端何時開頁不阻擋 API 驗收。 ### 4.2 Threads OAuth(P0) 1. 已登入會員請求「開始綁定」→ 系統回授權 URL(含 state)。 2. 使用者在 Threads/Meta 完成授權 → callback 至後端。 3. 後端換 token、寫入 ThreadsAccount(密文 token **不進** JWT、不進前端永久存儲)。 4. 前端列表顯示 username、connection、session 過期語意。 5. 刷新 session:成功延長/更新 `session_*` 欄位;失敗標記 error。 ### 4.3 互回/單篇 → Outbox 真發送(P1) 1. 會員存 ThreadPlay(含 steps、lead/cast、`schedule_start_at`、間隔)或 compose 單篇(含 `schedule_start_at`)。 2. submit/publishSingle → 建立 OutboxBundle+Steps(`scheduled_at` 算好)。 3. **Worker** 到點取步驟 → publishing → 呼叫 Threads → published 或 failed。 4. 會員看 Outbox 詳情;失敗可 retry(合規則)。 ### 4.4 海巡(P1) 1. 會員維護 Brand/Product;prepareBrief → 可審 scan_terms。 2. runScan:讀會員 `dev_mode_enabled` 選 API 或爬蟲。 3. 命中列表可 draft outreach/skip/mark/send(發送仍受 Outbox 或直接回覆契約約束,spec 實作時與前端 mock 對齊)。 ### 4.5 邀請(P1) 1. 每位會員有唯一 `invite_code`。 2. 無邀請人:`applyInviteCode` 綁定;已綁定拒絕。 3. 會員讀自己網絡(邀請人、直邀、延伸計數)。 4. Admin:搜尋、焦點調 `parent_uid`、防環。 ### 4.6 設定 AI/Exa 與金鑰模式(P0) 1. **系統預設 key(平台模式)** - 寫在 **唯一 etc 設定檔**(或 env 覆寫同鍵):至少 - AI:各 provider 的平台 API key(或目前啟用 provider 的 key) - 搜尋:Exa 平台 API key - 會員未填自己的 key 時,所有 AI/搜尋呼叫走 **platform keys**。 2. **會員 BYOK** - 會員在設定填寫自己的 AI key 和/或 Exa key → 該類呼叫改走會員 key。 - 讀取 API 只回 `*_configured`(是否已填),**永不回明文**。 - 清除會員 key → 回到平台 key。 3. **解析順序(每次外部呼叫)** ```text if 會員該 meter 已配置自己的 key: use BYOK key → key_mode = byok else if etc 有平台 key: use platform key → key_mode = platform else: fail 明確錯誤(未設定可用金鑰) ``` 4. Provider/model:單一 provider(`xai`|`opencode-go`)+單一 model;換 provider 回模型清單並保留同名或預設第一個。 5. `dev_mode_enabled` 只影響海巡掃描路徑,與 BYOK 正交。 ### 4.7 用量怎麼算(平台 vs BYOK)— 契約(分開計、分開顯示) 目標: 1. **平台代付**才扣方案 **credits**,並計入「平台用量」。 2. **BYOK** 不扣方案 credits,但 **必須另計次數**,且 **報表/API/前端展示與平台用量完全分開**,禁止混成一條「總次數」讓人分不清誰付的。 #### 4.7.1 記帳規則 | 情況 | 扣方案 credits | 記什麼 | event 標記 | |------|----------------|--------|------------| | AI · platform | **是** | 平台點數 + 平台次數 | `key_mode=platform`,`credits>0` | | AI · byok | **否** | **僅 BYOK 次數**(credits 固定 0) | `key_mode=byok` | | 搜尋 · platform | **是** | 同上 | platform | | 搜尋 · byok | **否** | 僅 BYOK 次數 | byok | | 混合(同流程 AI+搜尋) | 各 meter 獨立 | 兩筆 event 可不同 key_mode | 例:AI byok、搜尋 platform → 平台只扣搜尋點;BYOK 只加 AI 次數 | | 雙端皆無 key | 失敗 | 不寫成功用量 | 錯誤可讀 | | 外部呼叫失敗 | 不扣成功點 | 可選 failed 事件 | 不計入成功 BYOK/平台次數 | **額度檢查(僅 platform):** 呼叫前查剩餘 credits → 不足拒絕 → 成功後原子扣點並寫 platform event。 **BYOK:** 不查方案 credits;可另設 **BYOK 專用 rate limit**(每分鐘/日,etc 可配);計入 **byok_count** 系列欄位。 #### 4.7.2 彙總欄位(必須分開,禁止混加) `UsageMonthSummary`(或等價)至少拆成兩組,**不得**只回一個模糊的 `total_calls`: | 欄位組 | 含義 | 計入條件 | |--------|------|----------| | **platform** | `credits_used`、`credits_remaining`、可選 `platform_call_count` by meter | `key_mode=platform` 成功事件 | | **byok** | `byok_call_count` 總計 + **by meter**(如 `ai_copy` / `web_search` / `ai_research` / `ai_image`) | `key_mode=byok` 成功事件 | - 平台「已用點數」**只**加總 platform。 - BYOK「使用次數」**只**加總 byok;**不得**加進 platform 的 used 或讓 UI 誤顯示為同一進度條。 - 明細 `listEvents` 每筆必有 `key_mode`;篩選參數建議支援 `key_mode=platform|byok|all`(預設 all 但列表仍標示清楚)。 #### 4.7.3 顯示契約(前端/Admin) | 畫面 | 必須 | |------|------| | 會員用量摘要 | **兩塊**:① 方案點數(平台)② BYOK 本月次數(可按 meter 拆) | | 進度條/剩餘 | **只綁平台點數**;BYOK 用數字計數,不用同一條 bar 混算 | | 事件明細 | 每列標示「平台/自己的 Key」 | | Admin 全體用量 | 平台消耗與 BYOK 次數 **分欄或分圖**,禁止單一「總呼叫」混加 | #### 4.7.4 為何 - 經濟清楚:誰付錢、扣誰的額度。 - 產品清楚:會員知道自帶 key 用了幾次,不被算進方案進度。 - 營運可稽核:日後若對 BYOK 加服務費,已有獨立計數。 ### 4.8 失敗通則 - 未實作功能:**非 2xx 成功空 data**;回明確錯誤碼與 message。 - Auth 失敗:不強制清前端所有 local 狀態的「假成功」;前端依錯誤處理(對齊既有精神)。 ## 5. 介面契約 ### 5.1 跨切 | 項目 | 契約 | |------|------| | 成功 envelope | `{ "code": 102000, "message": "...", "data": ..., "error": null }`(與 `AGENTS.md` 一致;若 stand-alone 既有成功碼不同,**搬改時統一為 102000 並在 migrate 文件註明**) | | 失敗 | `code` ≠ 成功;`message` 可讀;`error` 可帶細節;**禁止** data 裝成功空列表假裝有實作 | | 時間 | 全部 **UTC unix nanoseconds** `int64` | | 列表 | 請求 `page`(從 1)、`pageSize`;回應 `pagination` + `list` | | Auth | `Authorization: Bearer `;refresh 另端點 | | 權限 | Auth middleware 解析 uid;Admin middleware/permission 檢查 `admin` | | 身分 | 對外 JSON 會員主鍵欄位名 **`uid` 型別數字**(JSON number);禁止字串 slug 主鍵 | ### 5.2 能力分組(對齊 `apps/web` repos · 路徑前綴建議 `/api/v1`) > 下列為 **行為邊界**;精確 path 可在 go-zero `.api` 微調,但 **能力不得缺**。未列 P2 可不做。 | 域 | 主要行為 | 權限 | |----|----------|------| | **Auth(API 齊全)** | login, logout, refresh, me, updateProfile, requestPasswordReset, resetPassword, sendEmailVerification, verifyEmail;**register**;**三方 OAuth 登入/綁定/解綁**(provider 清單可配置) | 公開/己;UI 可暫不暴露 register/三方 | | **Admin Members** | listPage, get, create, setSuspended, setEmailVerified, setRoles, resetPassword | admin | | **Threads Accounts** | oauthStart, oauthCallback, list, refreshSession, disconnect | member | | **Jobs** | list, get, createDemo/依 template 建立 | member | | **Notifications** | list, unreadCount, markRead, markAllRead | member | | **Settings** | get/save Ai, get/save Placement, listModels(provider);回傳是否 platform_key_available/member_key_configured(無明文) | member | | **Personas** | CRUD, setActive, analyzeText, analyzeAccount | member | | **Plays** | list/get/save/remove, resolveExternalLink, submit, **listByPost**, **listByExternalUrl** | member | | **Outbox** | list/get, retryStep, remove;(發送由 worker,無「假成功」模擬當 live 完成條件) | member | | **OwnPosts / Mentions / Compose** | ownPosts: list/sync/lastSyncedAt/generateReply/sendReply/**analyzePost**;mentions: list/generateReply/markReplied/skip;compose: **mimic**、**analyzeViral**、publishSingle(+schedule)。**不做** generateFromFormula | member | | **Inspiration / Research / Media** | **僅現 UI:** trends(list/refresh)、elements CRUD、session(get/save/clear)、chat(chat\|generate);research.search;generateImage;**attach 真上傳**(持久 URL)。**不做**舊靈感 list/viral/bookmark/spark/searchTrends | member | | **Usage** | summary, events(含 key_mode), plan, purchase(mock pay), admin tenant summary/analytics, setMemberPrefs | member/admin | | **Scout** | brands/products CRUD, importProductFromUrl, prepareBrief, runScanFromBrief, posts, outreach*, homework*。**不做** ScoutTopic CRUD(現 UI 未用) | member | | **Invite** | getMyNetwork, applyInviteCode, admin getTree/move/listParentCandidates | member/admin | ### 5.3 前端對照(live 必須可驅動) | 前端區域 | 後端域 | |----------|--------| | Login / Verify / Reset / Profile | Auth | | AdminUsers | Admin Members + Usage prefs | | Crew 帳號 | Threads Accounts OAuth | | Crew 人設 | Personas | | Studio 方案/Wizard/單篇+排程 | Plays, Compose, Outbox | | Outbox 頁 | Outbox + Worker | | Jobs / 鈴鐺 | Jobs, Notifications | | Settings | Settings + 擴充 ZIP(靜態或 downloads API) | | Scout / Brands | Scout | | Invite | Invite | | Usage / Plans | Usage | ### 5.4 Job template(P0–P1 最低) | Template | 觸發 | 成功 | |----------|------|------| | `demo` 或健康檢查類 | 手動/設定 | 進度跑完 succeeded + 通知 | | `persona_analyze_*` | 分析人設 | persona 更新 | | `threads_sync_posts` | 同步已發 | own posts 更新 | | `outbox_publish_step` | worker 輪詢 | 步驟 published/failed | | `scout_scan` | 海巡掃描 | ScoutPost 寫入 | | `inspire`/`research` 可選 | 長任務 | 結果落庫或回寫 session | ## 6. 與現況對照 | 能力 | Retain | Replace | Remove | 備註 | |------|--------|---------|--------|------| | `stand-alone-service` go-zero 骨架 | ✅ 結構、middleware、module 模式 | 業務域改巡樓 | 電商 cart/order/product… | **直接搬** | | stand-alone **多份 etc** | — | **收斂為一份** `etc/gateway.yaml`(+ env) | gateway-dev/prod/nas 碎檔常態 | 決策 #9 | | stand-alone 數字/可產 uid 方向 | ✅ **數字 uid** | 對齊巡樓 Member | 前端 mock 字串 uid 不當契約 | 決策 #10 | | stand-alone permission/admin middleware | ✅ 概念 | 收斂 member|admin | 電商權限雜訊 | 決策 #11 | | `apps/web` mock 行為 | ✅ 產品語意 | live HTTP 實作 | localStorage 當唯一真相 | 行為來源 | | `apps/web` mock `user_*` 字串 | 僅 mock | live 用數字 uid | 當 API 契約 | | | `old/backend` 海巡/OAuth/發文 | 演算法參考 | 新 module 重寫 | 整包搬 old | | | Outbox 前端 simulate 按鈕 | mock 可留 | live 以 worker 真發為準 | 以 simulate 當 live 完成 | 決策 #3 | | 舊靈感 list/viral/spark… | — | — | **Remove**;只做現 UI chat 流 | 決策 2026-07-10 | | generateFromFormula / ScoutTopic | — | — | **Remove**(現 UI 未用) | 決策 2026-07-10 | | 附圖 attachLocal | mock data URL | **真上傳** + 可取 URL | 假上傳當完成 | 決策 #19 | | Insights 真管線 | 頁面 local 可留 | — | Phase C 後端真管線 | 決策 #20 P2 | | Member uid | JSON number | FE toString 顯示 | 字串 slug 主鍵 | 決策 #10+#21 | | 公開註冊 **頁面** | — | — | 前端暫不開 | 決策 #1 UI | | 公開註冊/三方 **API** | ✅ 完整實作 | — | 不做半套 | API 齊、頁可後開 | | AI/搜尋 key | etc 平台 key | 會員 BYOK 覆寫 | 僅會員 key、無平台 fallback 當唯一模式 | §4.6–4.7 | | Mongo index 混在啟動 | — | **獨立 init 包** | 啟動時隱式 bulk | 決策 #12 | | Seeder 混業務 | — | **獨立 seeder 包** | 與 init 合併 | 決策 #13 | ## 7. 非功能 - **安全/權限:** JWT 鑑權;admin 路由雙重檢查;provider API key 加密或至少與 JWT 分離存儲;OAuth token 僅服務端。 - **錯誤碼:** 業務錯誤用穩定數字/scope(可沿 stand-alone FullCode 精神);成功 **102000**。 - **可觀測:** Job `progress_percent`/`progress_summary`;Outbox step `error` 字串;worker 日誌不含 key。 - **效能邊界(初值):** 列表預設 pageSize≤50;Outbox worker 併發可配置;海巡單次命中上限可配置(spec 實作預設 50–100)。 - **設定:** **單一 etc 檔**;secret 優先環境變數覆寫。 ## 8. 資料保存(邏輯層) | 實體 | 關鍵欄位 | 生命週期 | |------|----------|----------| | Member | uid(number), email, password_hash, roles[], status, email_verified, invite_code, parent_uid?, profile… | admin 建→用→可停權 | | ThreadsAccount | id, owner_uid, platform ids, tokens(server), connection, session_expires_at | OAuth 建→刷新→解除 | | ThreadPlay | id, owner_uid, steps[], schedule_start_at, status | 草稿→提交 | | OutboxBundle/Step | id, play_id, steps status, scheduled_at, error | 至 completed/取消 | | Job | id, template_type, status, progress_*, error | 終態保留可查 | | Notification | id, uid, kind, ref_*, read_at | 可累積 | | Persona / Brand / Product / Scout* | owner_uid + 領域欄位 | CRUD | | Invite 邊 | parent_uid on Member | 補填或 admin 調 | | Platform secrets | etc/env:AI keys、Exa key | 部署設定 | | AiSettings / Placement | per uid:provider, model, member keys configured, dev_mode | 覆寫更新 | | UsageEvent | uid, meter, credits, **key_mode**, created_at | platform 與 byok **分列** | | UsageMonthSummary | platform 點數欄 + **byok 次數欄(可 by meter)** | **禁止**混成唯一總次數 | | Plan prefs | uid, plan_id, unlimited? | 只約束 platform 扣點 | | Indexes | 各集合查詢鍵 | **init 指令**維護 | | Seed | admin 帳、demo 選用 | **seeder 指令** | ## 9. 驗收場景(Given / When / Then)— 嚴格 PM 全量 **通過標準:** 下列每一條皆可自動化或手測重現;任一失敗=該域未過關。 **共通斷言(凡成功回應皆適用,下稱「成功 envelope」):** `code=102000`;`message` 非空;時間欄位為 **int64 unix ns UTC**;列表含 `pagination`(`page,pageSize,total,totalPages`)+ `list`;**絕不**在未實作時回 200/102000 且 `data` 空成功。 **共通斷言(失敗):** `code≠102000`;`message` 人類可讀;不得洩漏其他使用者密碼/明文 API key/OAuth token。 --- ### 9.0 基礎設施/工程約束 | ID | Given | When | Then | |----|-------|------|------| | A-01 | 倉庫交付 | 檢視後端專案 | 源自 stand-alone-service 改造;go-zero `.api`+handler/logic/module 結構存在 | | A-02 | 交付物 | 列 `etc/` | **僅一份**主設定檔(如 `gateway.yaml`);無多份 dev/prod/nas 常態碎檔 | | A-03 | 交付物 | 建置 | 存在 **gateway** 與 **獨立 worker** 兩個可執行目標 | | A-04 | 乾淨 Mongo | 只跑 **init** | 必要 index 存在;**無**強制插入業務種子會員 | | A-05 | 已 init 空業務 | 只跑 **seeder** | 種子 admin(或文件約定帳)出現;可重複執行不產生重複關鍵唯一鍵(idempotent) | | A-06 | gateway 啟動 | 不跑 seeder | 服務可起、健康檢查 OK;不依賴 seeder 才會 bind port | | A-07 | 任意 API | 成功 | 成功 envelope;失敗非空成功 | | A-08 | 文件/契約 | 查會員主鍵 | **uid 為數字**(JSON number),非 `user_xxx` 字串主鍵 | --- ### 9.1 Auth · 登入/會話/權限 | ID | Given | When | Then | |----|-------|------|------| | AU-01 | 已 seeder 的有效帳密 | login 正確 | 102000;回 access+refresh;body 含數字 uid、roles | | AU-02 | 有效帳 | login 錯誤密碼 | 失敗;不洩漏「帳號是否存在」以外必要資訊(至少不回 token) | | AU-03 | 不存在 email | login | 失敗(與 AU-02 同形可接受) | | AU-04 | 有效 access | me | 回當前會員;uid 數字;與 login 一致 | | AU-05 | 無 Authorization | me | 401/未授權類失敗 | | AU-06 | 過期 access | me | 失敗;**不**回成功空 member | | AU-07 | 有效 refresh | refresh | 新 access(及可選新 refresh);舊 access 策略文件化(作廢或並存短時) | | AU-08 | 無效 refresh | refresh | 失敗 | | AU-09 | 已登入 | logout | 成功;之後用原 access me 失敗(或等同登出語意) | | AU-10 | suspended 會員 | login | 失敗,message 表明停權 | | AU-11 | 曾登入後被停權 | me 或受保護 API | 失敗/拒絕 | | AU-12 | 非 admin | 任一 admin-only API | 權限失敗 | | AU-13 | admin | 同一 admin API | 可成功(其餘條件滿足時) | | AU-14 | access 屬於 uid=A | 讀改 uid=B 的私有資源 | 失敗(除非 admin 且 API 允許) | --- ### 9.2 Auth · 註冊 API(頁面可不存在) | ID | Given | When | Then | |----|-------|------|------| | AR-01 | 註冊 API 已部署 | 文件/OpenAPI/.api | **端點存在**,可被整合測試呼叫 | | AR-02 | 合法 email+密碼 | POST 註冊 | 102000;得數字 uid;預設角色含 member;有 invite_code | | AR-03 | email 已存在 | 再註冊 | 失敗 | | AR-04 | 密碼過短/非法 | 註冊 | 失敗,可讀原因 | | AR-05 | 註冊成功且需驗證 | 未驗證 login 進主功能 | 對齊閘:主功能拒或僅允許驗證流 | | AR-06 | 註冊後 | 用回傳或流程寄碼→verify | email_verified=true | | AR-07 | 錯誤驗證碼 | verify | 失敗;verified 仍 false | | AR-08 | 過期驗證碼 | verify | 失敗 | | AR-09 | 前端無註冊頁 | 只打 API | **仍可完整走完** AR-02~AR-06(API 驗收不依賴 UI) | --- ### 9.3 Auth · 重設密碼/改密/改資料 | ID | Given | When | Then | |----|-------|------|------| | AP-01 | 已知 email | requestPasswordReset | 一律成功形(或不洩漏是否存在);有 token 機制 | | AP-02 | 有效 reset token | resetPassword 新密碼 | 成功;舊密碼 login 失敗;新密碼 login 成功 | | AP-03 | 過期/用過 token | resetPassword | 失敗 | | AP-04 | 已登入 | updateProfile 改 display_name | 成功;me 反映 | | AP-05 | 已登入 | 改 email | 成功後 email_verified=false(或文件約定);需重驗證 | | AP-06 | 改密 | 錯 current_password | 失敗 | | AP-07 | 改密 | 正確 current + 合法 new | 成功;新密碼可登 | | AP-08 | 頭像 | 合法圖上傳/data | 成功;非法格式/過大失敗 | --- ### 9.4 Auth · 三方登入 API(頁面可不開) | ID | Given | When | Then | |----|-------|------|------| | AO-01 | 配置至少一 provider | 開始三方登入 | 回授權 URL 或等價 | | AO-02 | 首次三方成功 callback | 完成 | 新建 Member 數字 uid;可發 JWT | | AO-03 | 同三方帳再次登入 | callback | **同一 uid**,不重複建檔 | | AO-04 | 已 email 登入會員 | 綁定三方 | 綁定成功;之後可用三方登 | | AO-05 | 解綁 | 呼叫解綁 | 成功後不可再用該三方登(除非再綁) | | AO-06 | 三方失敗/拒權 | callback | 失敗可讀;不建半套會員 | --- ### 9.5 Admin 島民管理 | ID | Given | When | Then | |----|-------|------|------| | AD-01 | admin | createMember 合法 | 數字 uid;臨時密碼**僅此次**回應;DB 有密碼雜湊 | | AD-02 | admin | create 重複 email | 失敗 | | AD-03 | admin | listPage page=1 | pagination 正確;list 元素含數字 uid | | AD-04 | admin | query 名稱/email/uid | 只回匹配列 | | AD-05 | admin | setSuspended true | 該帳 login 失敗 | | AD-06 | admin | setSuspended false | 可再 login | | AD-07 | admin | 停權自己 | 失敗 | | AD-08 | 系統僅一 active admin | 停權或降級該 admin | 失敗(至少保留一位) | | AD-09 | admin | setRoles 加 admin | 成功 | | AD-10 | admin | setEmailVerified | 狀態與 verified_at 正確 | | AD-11 | admin | resetPassword 無自訂 | 回新臨時密碼;舊密失效 | | AD-12 | admin | resetPassword 指定密 | 該密可登 | | AD-13 | member | createMember | 權限失敗 | | AD-14 | admin | getUser 存在/不存在 | 有則完整;無則 404 形失敗 | --- ### 9.6 Threads OAuth 帳號 | ID | Given | When | Then | |----|-------|------|------| | TH-01 | 已登入 | oauthStart | 回授權 URL;state 可驗證 | | TH-02 | 合法 callback | 換票成功 | 帳號列表多一筆;connection=connected;is_usable=true;token **不在** JSON 明文 | | TH-03 | 使用者拒絕授權 | callback | 失敗/無 usable 帳;不寫假 connected | | TH-04 | 換票失敗 | callback | failed 語意;可讀錯誤 | | TH-05 | 已綁帳號 | list | 屬當前 uid;含 session 過期欄位語意 | | TH-06 | connected 帳 | refreshSession 成功 | 更新 session_expires/refreshed;仍 usable | | TH-07 | refresh 失敗 | refreshSession | 標記 error/不可用;不假裝成功 | | TH-08 | 已綁 | disconnect | 列表不再 usable;不可再當 lead 發送 | | TH-09 | 未 OAuth 只「登記」 | (若有旁路) | **P0 不得**靠旁路算驗收通過;驗收以 TH-02 為準 | | TH-10 | 他會員帳號 id | 我方 refresh/disconnect | 失敗 | --- ### 9.7 Job 與通知 | ID | Given | When | Then | |----|-------|------|------| | JB-01 | 已登入 | 建立 demo Job | status 從 pending/queued 可查 | | JB-02 | Job running | worker 推進 | progress_percent 單調不亂跳;summary 更新 | | JB-03 | Job 成功 | 終態 | status=succeeded;completed_at 有值 | | JB-04 | Job 失敗 | 終態 | status=failed;error 非空 | | JB-05 | Job 成功/失敗 | 通知 | 對應 uid 有 Notification;unread+1 | | JB-06 | 有未讀 | unreadCount | 數字正確 | | JB-07 | 未讀通知 | markRead | read_at 有值;unread-1 | | JB-08 | 多則未讀 | markAllRead | unread=0 | | JB-09 | 他 uid 的 job | 我 get | 失敗 | | JB-10 | 非法狀態跳轉 | 強改(若有內部 API) | guarded 拒絕 | --- ### 9.8 設定 · AI/Exa/dev · 平台 key/BYOK | ID | Given | When | Then | |----|-------|------|------| | ST-01 | 已登入 | getAi | 有 provider/model;`api_key_configured` 布林;**無**明文 key | | ST-02 | 已登入 | saveAi 換 provider | 成功;research_* 與 provider/model 一致 | | ST-03 | provider=xai | listModels | 回該 provider 清單非空 | | ST-04 | 換 provider | listModels+預設 model | 同名保留否則第一個 | | ST-05 | etc 有平台 AI key、會員未填 | 任 AI 呼叫成功 | key_mode=platform;**扣 credits**;平台次數+1;BYOK 次數不變 | | ST-06 | 會員 save AI key | getAi | configured=true;仍無明文 | | ST-07 | 會員已填 AI key | AI 呼叫成功 | key_mode=byok;**credits=0**;方案剩餘不變;**BYOK 次數+1**;平台 used 不變 | | ST-08 | 會員清除 AI key | 再 AI | 回 platform(若 etc 有) | | ST-09 | etc 無 AI 且會員無 | AI 呼叫 | 失敗可讀;不假成功 | | ST-10 | etc 有 Exa、會員未填 | 搜尋成功 | platform 扣點;平台搜尋次數+1 | | ST-11 | 會員填 Exa key | 搜尋成功 | byok;不扣點;**BYOK 搜尋次數+1** | | ST-12 | AI byok + 搜尋 platform 同流程 | 各記 event | 平台只扣搜尋;BYOK 只加 AI 次;摘要兩組分開 | | ST-13 | platform 額度不足 | AI 呼叫 | **拒絕**、不打外部(預設) | | ST-14 | getPlacement | — | Exa 語意;dev_mode 布林;無明文 key | | ST-15 | savePlacement dev_mode true/false | get | 值持久 | | ST-16 | 擴充 ZIP | GET 下載 | 可下載非空 zip(靜態或 API) | --- ### 9.9 用量與方案 | ID | Given | When | Then | |----|-------|------|------| | USG-01 | 新會員 | getSummary | plan 預設合理;**platform 剩餘**與 **byok 次數=0** 分欄 | | USG-02 | 僅 platform 消耗後 | getSummary | platform used↑ remaining↓;**byok 次數不變** | | USG-03 | 僅 BYOK 消耗後 | getSummary | platform remaining **不變**;**byok_call_count↑**(可按 meter) | | USG-04 | 混合:AI byok + 搜尋 platform | getSummary | 平台只扣搜尋點;BYOK 只加 AI 次數;**兩組數字各自正確** | | USG-05 | listEvents | — | 每筆含 meter、credits、**key_mode**、時間 ns | | USG-06 | listEvents 篩 key_mode=byok | — | **僅** byok 列;無 platform 列 | | USG-07 | listEvents 篩 platform | — | 僅 platform 列 | | USG-08 | 回傳 summary JSON | 檢查 | **禁止**單一 `total_calls` 把 platform+byok 加總當唯一欄位(若有 grand total 必須另標且文件化;預設不要) | | USG-09 | 前端用量頁契約 | 對照 API | 點數條只吃 platform;BYOK 另區顯示次數 | | USG-10 | admin tenant analytics | — | 平台消耗與 BYOK 次數 **分欄/分序列**,不可混成一條 | | USG-11 | member | purchasePlan(假付款) | plan 變更;purchase 紀錄 | | USG-12 | admin | setMemberPrefs plan | 該 uid 平台方案變 | | USG-13 | admin | unlimited | platform 不因額度拒;byok 計數仍獨立累加 | | USG-14 | member | set 他人 prefs | 失敗 | --- ### 9.10 人設 Persona | ID | Given | When | Then | |----|-------|------|------| | PE-01 | 已登入 | create/save | 得 id;list 可見 | | PE-02 | 多則 | setActiveId | getActive 為該 id | | PE-03 | 刪除 active | remove | active 重設規則明確(空或另一則) | | PE-04 | analyzeFromText | 成功 | status/ready 欄位更新;可落庫 | | PE-05 | analyzeFromAccount | 成功或 Job | 結果可查;失敗 Job failed+通知 | | PE-06 | 他 uid 人設 | 我改刪 | 失敗 | --- ### 9.11 互回方案 Play 與單篇 Compose | ID | Given | When | Then | |----|-------|------|------| | PL-01 | 合法 lead+steps | save | 持久;get 一致 | | PL-02 | reply 帳號不在 speakers | submit | 失敗 | | PL-03 | 合法 play + schedule_start_at 未來 | submit | Outbox 建立;step.scheduled_at 對齊 | | PL-04 | resolveExternalLink 合法 URL | — | 回 target 預覽欄位 | | PL-05 | 非法 URL | resolve | 失敗 | | PL-06 | compose 單篇 + schedule 未來 | publishSingle | Outbox 一 step;scheduled_at=指定時間 | | PL-07 | compose 不傳 schedule | publishSingle | scheduled_at≈now(允許小誤差) | | PL-08 | 無可用帳號 | publish/submit | 失敗 | | PL-09 | 刪 play | remove | list 不再見;已提交 outbox 策略明確(保留或級聯文件化) | | PL-10 | 已有綁 ownPost 的 play | listByPost | 只回該 post 的方案 | | PL-11 | 已 resolve 外部 URL | listByExternalUrl | 只回該 URL 正規化後的方案 | | CP-01 | 已登入+人設 | compose.mimic | 回仿寫正文;platform/byok 計費適用 | | CP-02 | 已登入 | compose.analyzeViral | 回結構分析欄位;計費適用 | --- ### 9.12 Outbox 真發送(Worker) | ID | Given | When | Then | |----|-------|------|------| | OB-01 | step 未到 scheduled_at | worker tick | **不**呼叫 Threads;狀態仍 scheduled | | OB-02 | step 已到點且 token 有效 | worker | publishing→**published**;Threads 側可見(或 sandbox 可證) | | OB-03 | Threads API 失敗 | worker | step=failed;error 非空;**非** published | | OB-04 | root failed | 查 bundle | 後續 reply **皆非** published(blocked/scheduled 等) | | OB-05 | root 成功 reply 到點 | worker | reply published | | OB-06 | failed step 可重試 | retryStep | 再進 publishing;成功則 published | | OB-07 | 不可重試狀態 | retryStep | 失敗 | | OB-08 | 全部成功 | bundle | status=completed | | OB-09 | 部分失敗 | bundle | partial_failed | | OB-10 | remove bundle | — | list 不可見;worker 不再送 | | OB-11 | 他 uid outbox | 我 get/retry | 失敗 | | OB-12 | live 環境 | 僅靠前端 simulate | **不算** OB-02 驗收通過 | --- ### 9.13 已發貼文/提及 | ID | Given | When | Then | |----|-------|------|------| | OP-01 | usable 帳 | sync | 列表更新;lastSyncedAt 更新 | | OP-02 | sync 失敗 | — | 錯誤可讀;不清空當成功 | | OP-03 | 有貼文 | generateReply | 回文字;platform 或 byok 計費規則適用 | | OP-04 | sendReply | 成功 | Threads 可見;本地狀態已回 | | OP-05 | sendReply 失敗 | — | 本地不標已送成功 | | OP-06 | mention | markReplied / skip | 狀態正確 | | OP-07 | 他帳 accountId | sync | 失敗 | | OP-08 | 有貼文 | analyzePost | 分析欄位更新;platform/byok 計費適用 | | OP-09 | 有 mention | list(可篩 account) | 契約完整;時間 ns | | OP-10 | 有 mention | generateReply | 回文字草稿;計費適用 | | OP-11 | — | generateFromFormula | **不提供**(Remove;呼叫應 404/未實作失敗,不得假成功) | --- ### 9.14 靈感/研究/媒體 > **範圍:** 僅現 UI(InspirePanel + Today 趨勢)。舊 list/viral/bookmark/spark/searchTrends **不做、不驗收**。 | ID | Given | When | Then | |----|-------|------|------| | IN-01 | listTrends | — | 有 list(可空但契約完整) | | IN-02 | refreshTrends | — | 更新或明確錯誤 | | IN-03 | save/remove element | — | CRUD 正確 | | IN-04 | chat mode=chat | — | session 寫入 messages | | IN-05 | chat mode=generate | — | draft.body 乾淨正文可取 | | IN-06 | research.search | platform/byok | 計費規則同 §9.8 | | IN-07 | generateImage | — | 回 url;計費規則適用 | | IN-08 | attach/upload(**真上傳**) | 合法檔 multipart(或等價) | 回持久 **url**(+id);非法/過大失敗;**禁止**僅回本機 data URL 當 live 完成 | | IN-09 | 未實作上傳 | 呼叫 | **非** 102000+空成功 | --- ### 9.15 海巡 Scout | ID | Given | When | Then | |----|-------|------|------| | SC-01 | 已登入 | createBrand/saveProduct | 持久 list 可見 | | SC-02 | 刪 brand | — | 所屬 product 策略明確(級聯或拒刪) | | SC-03 | prepareBrief 有 intent | — | 回 brief含 scan_terms 可改 | | SC-04 | intent 空 | prepareBrief | 失敗 | | SC-05 | dev_mode=false | runScanFromBrief | **走 API 路徑**(log/欄位 `scan_path=api` 可證) | | SC-06 | dev_mode=true 且 session 可用 | runScan | **走 crawler**(`scan_path=crawler`) | | SC-07 | dev_mode=true 但無 session | runScan | 失敗可讀或明確降級策略(文件+實作一致) | | SC-08 | 掃描成功 | listPosts | 有命中;時間 ns | | SC-09 | draftOutreach | — | 草稿文字寫入 | | SC-10 | skip / markPublished | — | 狀態對 | | SC-11 | sendOutreach 真送 | — | 對齊發送契約;失敗不假 published | | SC-12 | removePost / removeTheme | — | 列表消失 | | SC-13 | homework save/get/list/remove | — | CRUD 正確 | | SC-14 | importProductFromUrl 合法 | — | 回填草稿欄位;非法 URL 失敗 | --- ### 9.16 邀請 Invite | ID | Given | When | Then | |----|-------|------|------| | IV-01 | 新會員 | getMyNetwork | 有 invite_code;parent 空或 seeder 值 | | IV-02 | 無 parent | apply 合法碼 | parent=邀請人 uid;upline 可見 | | IV-03 | 已 parent | apply 任意碼 | 失敗 alreadyBound | | IV-04 | apply 自己的碼 | — | 失敗 | | IV-05 | apply 不存在碼 | — | 失敗 | | IV-06 | apply 空碼 | — | 失敗 | | IV-07 | 有直邀 | getMyNetwork | downlines 正確;計數一致 | | IV-08 | admin | getTree | 森林結構完整 | | IV-09 | admin | move 到合法父 | parent 更新;子孫跟著 | | IV-10 | admin | move 到自己子孫下 | 失敗防環 | | IV-11 | admin | move 到自己 | 失敗 | | IV-12 | member | moveMember | 權限失敗 | | IV-13 | admin | listParentCandidates(排除子樹) | 不含該子樹節點 | --- ### 9.17 契約與安全紅線 | ID | Given | When | Then | |----|-------|------|------| | X-01 | 任一未實作路徑 | 呼叫 | **非** 102000+空 data | | X-02 | 成功回應 | 檢查 | 無 password、無 raw api key、無 oauth access_token 欄位 | | X-03 | worker/gateway log | 打 key 的請求 | log **無**明文 key | | X-04 | 列表 page=0 或負 | — | 正規化或 400,行為一致 | | X-05 | pageSize 過大 | — | cap 到上限(如 50) | | X-06 | 時間欄位 | 抽樣 | 皆 ns 量級 int64(非秒、非 ms 混用) | --- ### 9.18 驗收執行約定(嚴格 PM) 1. **P0 過關:** §9.0 + §9.1 + §9.3 + §9.5 + §9.6 + §9.7 + §9.8(ST-01~16 中與 P0 相關)+ §9.17 全過;§9.2/§9.4 API 存在性可測。 2. **P1 過關:** P0 全過 + §9.9~§9.16 全過(含 OB 真發送、SC 雙路徑、IV)。 3. 每條 **ID 必須在測試清單有對應 case 名稱**(自動化或手測腳本表);禁止「大概可以」。 4. 外部依賴(Threads/AI)可用 **契約測試 + 受控沙箱**;但 OB-02/TH-02 不得用「只改 DB 狀態」冒充。 ## 10. 開放規格問題 1. 成功碼若 stand-alone 現況≠102000,搬遷時統一策略(建議:**統一 102000**)。 2. OAuth callback 網域與 state 儲存(Redis/DB)細節。 3. 爬蟲 session 與 gateway 的傳遞安全(僅 dev_mode)。 4. 目錄落點:**`apps/backend`**(gateway + `cmd/worker` 等同 monorepo 根;已定)。 5. BYOK 是否要加「平台服務費」(每呼叫固定點)— **預設否**;僅記 event,日後可加。 6. 三方登入支援哪些 provider 第一版(Google/Apple/…)— **plan 列最低集合**,API 介面先通用。 ## 11. 批准 - [x] Spec 已批准(2026-07-10 / 使用者「spec 通過」):