thread-master/docs/product/haixun-backend/spec.md

704 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
> 修訂:平台 keyBYOK 計費;會員 API 齊全、頁面可不開;**§9 嚴格驗收場景全量**
## 1. 摘要
本規格定義 **Harbor Desk`apps/web`)接 live** 時後端必須具備的對外行為與跨切契約。
骨架 **搬入並改造 `stand-alone-service`**,撰寫 **一律 go-zero**`.api` → generate → handlerlogicmodule
P0數字 uid、權限、**Threads OAuth**、Job通知、AI·Exa·dev 設定(**平台 keyBYOK**)。
P1創作**Outbox 真發送**、人設、靈感研究、用量、**海巡雙路徑**、**邀請關係**。
**會員相關 API 必須完整**(含註冊/三方登入等);**前端頁面可不提供**(現階段 UI 以 admin 代建email 登入為主)。
Worker 為 **獨立 binary**Mongo **index init****seeder** 分包裝。
## 2. 名詞與實體
| 名詞 | 定義 |
|------|------|
| **Member** | 租戶內登入主體;主鍵 **`uid` 為數字**;以 email或三方綁定登入角色含至少 `member``admin` |
| **Permission** | 能力點或角色集合;中介層依 token 內身份擋 API |
| **Platform keys** | 寫在 **系統設定檔etc** 的 AIExa 等金鑰;全站預設用這組代打外部服務 |
| **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** | 會員級覆寫providermodel、是否已配 key、dev_mode |
| **Usage / Plan** | freestarterpro事件含是否 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 ThreadsAccountOAuth
```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` 且未過期可供 leadcast發送。
### 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 OAuthP0
1. 已登入會員請求「開始綁定」→ 系統回授權 URL含 state
2. 使用者在 ThreadsMeta 完成授權 → callback 至後端。
3. 後端換 token、寫入 ThreadsAccount密文 token **不進** JWT、不進前端永久存儲
4. 前端列表顯示 username、connection、session 過期語意。
5. 刷新 session成功延長更新 `session_*` 欄位;失敗標記 error。
### 4.3 互回/單篇 → Outbox 真發送P1
1. 會員存 ThreadPlay含 steps、leadcast、`schedule_start_at`、間隔)或 compose 單篇(含 `schedule_start_at`)。
2. submitpublishSingle → 建立 OutboxBundleSteps`scheduled_at` 算好)。
3. **Worker** 到點取步驟 → publishing → 呼叫 Threads → published 或 failed。
4. 會員看 Outbox 詳情;失敗可 retry合規則
### 4.4 海巡P1
1. 會員維護 BrandProductprepareBrief → 可審 scan_terms。
2. runScan讀會員 `dev_mode_enabled` 選 API 或爬蟲。
3. 命中列表可 draft outreachskipmarksend發送仍受 Outbox 或直接回覆契約約束spec 實作時與前端 mock 對齊)。
### 4.5 邀請P1
1. 每位會員有唯一 `invite_code`
2. 無邀請人:`applyInviteCode` 綁定;已綁定拒絕。
3. 會員讀自己網絡(邀請人、直邀、延伸計數)。
4. Admin搜尋、焦點調 `parent_uid`、防環。
### 4.6 設定 AIExa 與金鑰模式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. Providermodel單一 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 <access_token>`refresh 另端點 |
| 權限 | Auth middleware 解析 uidAdmin middlewarepermission 檢查 `admin` |
| 身分 | 對外 JSON 會員主鍵欄位名 **`uid` 型別數字**JSON number禁止字串 slug 主鍵 |
### 5.2 能力分組(對齊 `apps/web` repos · 路徑前綴建議 `/api/v1`
> 下列為 **行為邊界**;精確 path 可在 go-zero `.api` 微調,但 **能力不得缺**。未列 P2 可不做。
| 域 | 主要行為 | 權限 |
|----|----------|------|
| **AuthAPI 齊全)** | 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_availablemember_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/skipcompose: **mimic**、**analyzeViral**、publishSingle(+schedule)。**不做** generateFromFormula | member |
| **Inspiration / Research / Media** | **僅現 UI** trends(list/refresh)、elements CRUD、session(get/save/clear)、chat(chat\|generate)research.searchgenerateImage**attach 真上傳**(持久 URL。**不做**舊靈感 list/viral/bookmark/spark/searchTrends | member |
| **Usage** | summary, events含 key_mode, plan, purchase(mock pay), admin tenant summary/analytics, setMemberPrefs | memberadmin |
| **Scout** | brands/products CRUD, importProductFromUrl, prepareBrief, runScanFromBrief, posts, outreach*, homework*。**不做** ScoutTopic CRUD現 UI 未用) | member |
| **Invite** | getMyNetwork, applyInviteCode, admin getTree/move/listParentCandidates | memberadmin |
### 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 templateP0P1 最低)
| Template | 觸發 | 成功 |
|----------|------|------|
| `demo` 或健康檢查類 | 手動/設定 | 進度跑完 succeeded + 通知 |
| `persona_analyze_*` | 分析人設 | persona 更新 |
| `threads_sync_posts` | 同步已發 | own posts 更新 |
| `outbox_publish_step` | worker 輪詢 | 步驟 publishedfailed |
| `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 permissionadmin middleware | ✅ 概念 | 收斂 memberadmin | 電商權限雜訊 | 決策 #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.64.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≤50Outbox worker 併發可配置海巡單次命中上限可配置spec 實作預設 50100
- **設定:** **單一 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 | etcenvAI keys、Exa key | 部署設定 |
| AiSettings / Placement | per uidprovider, 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 keyOAuth 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+refreshbody 含數字 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-02AR-06API 驗收不依賴 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 名稱emailuid | 只回匹配列 |
| 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 | 回授權 URLstate 可驗證 |
| TH-02 | 合法 callback | 換票成功 | 帳號列表多一筆connection=connectedis_usable=truetoken **不在** JSON 明文 |
| TH-03 | 使用者拒絕授權 | callback | 失敗/無 usable 帳;不寫假 connected |
| TH-04 | 換票失敗 | callback | failed 語意;可讀錯誤 |
| TH-05 | 已綁帳號 | list | 屬當前 uid含 session 過期欄位語意 |
| TH-06 | connected 帳 | refreshSession 成功 | 更新 session_expiresrefreshed仍 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=succeededcompleted_at 有值 |
| JB-04 | Job 失敗 | 終態 | status=failederror 非空 |
| JB-05 | Job 成功/失敗 | 通知 | 對應 uid 有 Notificationunread+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 設定 · AIExadev · 平台 keyBYOK
| 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**;平台次數+1BYOK 次數不變 |
| 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 | 點數條只吃 platformBYOK 另區顯示次數 |
| 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 | 得 idlist 可見 |
| 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 一 stepscheduled_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=failederror 非空;**非** published |
| OB-04 | root failed | 查 bundle | 後續 reply **皆非** publishedblocked/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 靈感/研究/媒體
> **範圍:** 僅現 UIInspirePanel + Today 趨勢)。舊 listviralbookmarksparksearchTrends **不做、不驗收**。
| 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_codeparent 空或 seeder 值 |
| IV-02 | 無 parent | apply 合法碼 | parent=邀請人 uidupline 可見 |
| 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.8ST-0116 中與 P0 相關)+ §9.17 全過§9.2§9.4 API 存在性可測。
2. **P1 過關:** P0 全過 + §9.9§9.16 全過(含 OB 真發送、SC 雙路徑、IV
3. 每條 **ID 必須在測試清單有對應 case 名稱**(自動化或手測腳本表);禁止「大概可以」。
4. 外部依賴ThreadsAI可用 **契約測試 + 受控沙箱**;但 OB-02TH-02 不得用「只改 DB 狀態」冒充。
## 10. 開放規格問題
1. 成功碼若 stand-alone 現況≠102000搬遷時統一策略建議**統一 102000**)。
2. OAuth callback 網域與 state 儲存RedisDB細節。
3. 爬蟲 session 與 gateway 的傳遞安全(僅 dev_mode
4. 目錄落點:**`apps/backend`**gateway + `cmd/worker` 等同 monorepo 根;已定)。
5. BYOK 是否要加「平台服務費」(每呼叫固定點)— **預設否**;僅記 event日後可加。
6. 三方登入支援哪些 provider 第一版GoogleApple**plan 列最低集合**API 介面先通用。
## 11. 批准
- [x] Spec 已批准2026-07-10 / 使用者「spec 通過」):