# Spec: 巡樓 Console 重構(Harbor Desk) > Status: **`approved`** > Source: `docs/product/haixun-console/requirements.md`(approved 2026-07-09) > Last updated: `2026-07-09` > 批准:使用者要求開始拆 plan/tasks(2026-07-09) > 變更:reply 步驟發言池含 lead(主帳可回別人的回覆) > 範圍:Phase A 地基契約 + Phase B 前端/Mock 行為;Phase C 接真映射;Phase D 僅邊界預留 --- ## 1. 摘要 重構後的巡樓 Console 分四階段交付。本規格鎖定: 1. **平台地基**:登入會話、角色權限、Threads 帳號基礎、Job、站內通知。 2. **新前端 Harbor Desk**:5 入口 IA、Studio 多帳互回 wizard、Outbox、通知;**預設 mock 資料**。 3. **同一契約**在 `mock | live` 切換;B 未過關不開大型業務後端。 落點(2026-07-09 已封存舊碼): | 項目 | 決策 | |------|------| | 舊 monorepo | **`old/` 整包封存,只讀** | | 新前端 | repo 根 `apps/web`(從零) | | 新後端 | `services/api`(從零;A 可先 mock-only 前端) | | 視覺 | Harbor Desk 為唯一預設主題 | | 海巡 Scout | B 階段占位空頁 | | Email 驗證閘 | A/B 關閉 | | Mock 持久化 | `localStorage` | --- ## 2. 名詞與實體 | 名詞 | 定義 | |------|------| | Member | 登入會員(email/password + JWT) | | Role | 簡化:`member` \| `admin`(可多值) | | ThreadsAccount | 已綁定的 Threads 經營帳號(OAuth token 由後端保管) | | Lead | 本串**主帳號**:固定發 `root` 樓主貼;並屬於發言池,可在後續 `reply` 再出場 | | Cast | 本串**額外可出場帳號**(通常不含 lead);與 lead 組成發言池 | | Speakers(發言池) | `{ lead_account_id } ∪ cast_account_ids`;**每一則 reply 步驟的 `account_id` 必須 ∈ Speakers**,**允許 = lead** | | ThreadPlay | **發文計畫 / 串場劇本**(一則主貼 + 有序回覆步驟) | | PlayStep | 劇本一步:`kind=root\|reply`、綁定 `account_id`、文案、可選排程偏移;`reply` 語意為接上一步(單線) | | OutboxItem | 出站佇列項目:對應某 PlayStep 的一次發送意圖與狀態 | | Job | 背景任務 run(進度、狀態、結果摘要) | | Notification | 站內通知(多由 Job 終態產生) | | DataSource | `mock` \| `live`:前端資料來源模式 | --- ## 3. 狀態機 ### 3.1 Job ```text pending --> queued --> running --> succeeded running --> failed running --> cancel_requested --> cancelled queued|pending --> cancelled (若支援提前取消) * --> expired (lock 逾時,後端語意;mock 可簡化省略) ``` ### 3.2 ThreadPlay ```text draft --> ready (劇本與帳號齊全、通過預覽) ready --> scheduling (寫入 Outbox) scheduling --> active (至少一步 in flight 或 scheduled) active --> completed (全部步驟 succeeded 或 skipped) active --> partial_failed (有 failed,可重試) draft|ready --> archived ``` ### 3.3 PlayStep / OutboxItem ```text draft --> scheduled --> publishing --> published publishing --> failed scheduled --> cancelled failed --> scheduled (retry) ``` **規則:** - `root` 步驟未 `published` 前,後續 `reply` 不得進入 `publishing`。 - `root` 若 `failed`,後續步驟保持 `scheduled` 或改 `blocked`(顯示「等待主貼」)。 - `root.account_id` **必須** = `lead_account_id`。 - 任一 `reply.account_id` **必須** ∈ Speakers(**含 lead**);校驗失敗不可 submit。 - P0 單線:第 n 步 reply 視為回覆第 n−1 步已發出的那則(不另選 parent;D 再談樹狀)。 ### 3.4 ThreadsAccount.connection ```text unknown --> connected unknown --> error connected --> error error --> connected (重新授權/診斷成功) ``` --- ## 4. 使用者流程 ### 4.1 登入(A/B) 1. 使用者開 `/login`,輸入 email、password(可選 tenant 若多租戶 UI 需要;mock 固定 `default`)。 2. 成功 → 存 access/refresh(live)或 session flag(mock)→ 進 `/app/today`。 3. 失敗 → 表單錯誤,不進主殼。 4. 重新整理:有有效會話則進主殼,否則回登入。 5. 登出 → 清會話 → 登入頁。 ### 4.2 帳號 Crew(B mock / C live) 1. 列表顯示已綁帳號:顯示名、頭像色、connection、是否預設 lead 候選。 2. 「連接 Threads」:live 走 OAuth;mock 新增一個假帳號。 3. 標記不健康帳號不可被選為 Lead/Cast(disabled + 原因)。 ### 4.3 創作 Studio — 互回 wizard(B 核心) | 步驟 | 畫面行為 | |------|----------| | 1 主題 | 標題/一句主題;可「示範填入」 | | 2 船隊 | 必選 1 Lead;多選 Cast(≥0,UI 列表可預設排除 lead,避免重複勾);不可選 error 帳號 | | 3 劇本 | 第 0 步固定 `root` + `account_id=lead`(不可改帳);其後每步 `reply`:**下拉發言池 = lead + 全部 cast**(**含 lead**,以便主帳回別人的回覆)+ textarea;可增刪排序(root 固定頂) | | 4 預覽 | 對話泡泡:帳號名 + 文案;root 標記;可見 lead 出現在非第 0 步 | | 5 船期 | 主貼時間(datetime);回覆間隔(分鐘,預設 5);可預覽各步預計時間 | | 6 提交 | 建/更新 ThreadPlay=`scheduling`,生成 OutboxItem;導向 Outbox 或 Today | 任一步「上一步」保留表單狀態。草稿自動存(mock:`localStorage` key 見 §8)。 ### 4.4 Outbox 1. 列表:Play 標題、狀態、步驟進度(2/5)。 2. 點進詳情:逐步狀態時間線。 3. Mock「模擬發送」:依序 root→replies 推進狀態(可加速)。 4. 失敗步驟顯示錯誤文案 +「重試」(mock 直接 succeeded)。 ### 4.5 Today 1. 卡片:待處理失敗、今日排程、進行中 Job、快捷「開新串場」。 2. 點通知/失敗 → 對應 Outbox 或 Job 詳情。 ### 4.6 通知 1. 頂欄鈴鐺:未讀數。 2. 下拉最近 N 筆;點擊標記已讀並 navigate。 3. Job `succeeded|failed` 產生通知(mock 在模擬結束時寫入)。 ### 4.7 Scout(B) 占位頁:標題「探查」、短說明「海巡獲客將在下一階段開放」、無假成功操作。 ### 4.8 設定 - 顯示目前 `DataSource` 切換(dev 可見;prod 預設 live 且可藏)。 - AI key:B 僅 UI 存 local(不送出);C 再定義。 --- ## 5. 介面契約 ### 5.0 共用規則 | 規則 | 規格 | |------|------| | Envelope | `{ code, message, data?, error? }`;成功 `code=102000` | | 列表 | query `page`/`pageSize`;`data: { pagination, list }` | | 時間 | API/DB:**unix nanoseconds int64 UTC**;UI 顯示本地時區 | | Auth | `Authorization: Bearer `;401 → refresh 一次 → 仍失敗登出 | | 禁止 | 未實作端點回 200+空成功;應 501 或明確業務錯誤 | 前端 repository 介面(概念): ```text AuthRepo, MemberRepo, PermissionRepo, ThreadsAccountRepo, ThreadPlayRepo, OutboxRepo, JobRepo, NotificationRepo ``` `createRepos(dataSource)` → mock 或 live 實作同一 TypeScript interface。 ### 5.1 HTTP / API(Phase A 最小 + C 對齊) > 路徑為**目標契約**;可與舊 `/api/v1` 對齊以利搬碼。Mock 不經 HTTP 亦可,但 shape 相同。 #### Auth / Member / Permission | Method | Path | 用途 | 要點 | |--------|------|------|------| | POST | `/api/v1/auth/login` | 登入 | body: email, password, tenant_id? → tokens | | POST | `/api/v1/auth/refresh` | 換票 | refresh_token → tokens | | POST | `/api/v1/auth/logout` | 登出 | auth | | GET | `/api/v1/members/me` | 我 | tenant_id, uid, email, roles[] | | GET | `/api/v1/permissions/me` | 我的權限 | keys[] 或 * | Admin 建號可延後;mock 內建一組使用者即可。 #### Threads accounts | Method | Path | 用途 | |--------|------|------| | GET | `/api/v1/threads-accounts` | 列表(pagination) | | POST | `/api/v1/threads-accounts/oauth/start` | 取得授權 URL(live) | | GET | `/api/v1/threads-accounts/:id` | 詳情 / 健康 | | POST | `/api/v1/threads-accounts/:id/diagnostics` | 連線診斷(可選 A) | #### Jobs | Method | Path | 用途 | |--------|------|------| | GET | `/api/v1/jobs` | 列表 | | GET | `/api/v1/jobs/:id` | 詳情 | | POST | `/api/v1/jobs/:id/cancel` | 取消 | | POST | `/api/v1/jobs` | 建立(A 可僅 demo 或內部) | Job 資源欄位(最低):`id, template_type, status, progress_summary, progress_percent, error?, created_at, updated_at, completed_at?` #### Notifications | Method | Path | 用途 | |--------|------|------| | GET | `/api/v1/notifications` | 列表 | | POST | `/api/v1/notifications/:id/read` | 已讀 | | POST | `/api/v1/notifications/read-all` | 全已讀 | 欄位:`id, title, body, kind, ref_type, ref_id, read_at?, created_at` #### ThreadPlay / Outbox(B mock 完整;C 再 live) | Method | Path | 用途 | |--------|------|------| | GET/POST | `/api/v1/thread-plays` | 列表 / 建立 | | GET/PATCH | `/api/v1/thread-plays/:id` | 讀 / 更新劇本 | | POST | `/api/v1/thread-plays/:id/submit` | 提交進 Outbox | | GET | `/api/v1/outbox` | 佇列列表 | | GET | `/api/v1/outbox/:id` | 詳情(含 steps) | | POST | `/api/v1/outbox/:id/steps/:stepId/retry` | 重試一步 | **ThreadPlay 主體(邏輯 JSON):** ```json { "id": "…", "title": "週末咖啡話題", "topic": "…", "status": "draft", "lead_account_id": "…", "cast_account_ids": ["…"], "steps": [ { "id": "s0", "sort_order": 0, "kind": "root", "account_id": "…", "text": "主貼文案", "delay_from_previous_sec": 0 }, { "id": "s1", "sort_order": 1, "kind": "reply", "account_id": "…", "text": "回覆文案", "delay_from_previous_sec": 300 } ], "schedule_start_at": 0, "created_at": 0, "updated_at": 0 } ``` ### 5.2 前端路由(`apps/web`) 使用 **path router**(非 hash),basename 可 `/`。 | Path | 名稱 | 需登入 | Phase | |------|------|--------|-------| | `/login` | 登入 | 否 | B | | `/app` | 佈局殼 | 是 | B | | `/app/today` | 今日 | 是 | B | | `/app/crew` | 帳號 | 是 | B | | `/app/studio` | 創作列表 | 是 | B | | `/app/studio/new` | 新 wizard | 是 | B | | `/app/studio/:playId` | 編輯 wizard | 是 | B | | `/app/outbox` | 發送佇列 | 是 | B | | `/app/outbox/:id` | 佇列詳情 | 是 | B | | `/app/jobs` | 任務(可二級) | 是 | B | | `/app/jobs/:id` | 任務詳情 | 是 | B | | `/app/scout` | 探查占位 | 是 | B | | `/app/settings` | 設定 | 是 | B | **導覽(一級,≤5):** Today · Crew · Studio · Scout · Outbox Jobs 從 Today/通知進入;Settings 在使用者選單。 ### 5.3 Job types(A 最小) | template_type | 用途 | Phase | |---------------|------|-------| | `demo_long_task` | 驗證 job+通知 | A/B mock 可模擬 | | (業務 job) | 發佈串場、8D… | C/D | B mock 可不跑真 worker:Timer 推進 job 狀態並寫 notification。 --- ## 6. 與現況對照 | 能力 | 決策 | 備註 | |------|------|------| | 整個舊 monorepo | **Archive** | 實體路徑 `old/`;主線不引用為 package | | 舊 Auth/JWT/envelope | **參考後重寫** | 契約精神可相容 | | 舊 Job 核心 | **參考後重寫** | 可抄模式,不搬半成品 | | copy_mission 管線 | **Discard** | 不復活 | | 舊 web Console | **Archive** | 新 `apps/web` | | 海巡 Patrol 全套 | **Defer D** | Scout 占位 | | 田園掌機 UI | **Discard 預設** | Harbor Desk | | 時間/envelope/job 規則 | **Keep as policy** | 見根目錄 `AGENTS.md` | --- ## 7. 非功能 ### 安全 / 權限 - 未登入不可進 `/app/**`。 - `admin` 專屬操作(若有)前後端雙檢。 - Live:token 不進 URL;refresh 單飛。 - Mock:不模擬權限繞過成功寫入 live。 ### 錯誤 - UI 可讀中文 message。 - 區分 401(登出)vs 5xx(保留會話,顯示重試)。 ### 可觀測 - Job 進度摘要必顯示。 - Outbox 步驟級狀態必顯示。 ### 效能(B) - 首屏可互動 < 3s(本機 dev 網路)。 - 列表 mock ≤ 50 筆即可。 ### 無障礙(基線) - 表單 label 關聯;主按鈕對比足夠;焦點可見。 --- ## 8. 資料保存(邏輯) ### 8.1 Mock localStorage keys | Key | 內容 | |-----|------| | `harbor.datasource` | `mock` \| `live` | | `harbor.session` | mock 登入態 | | `harbor.accounts` | ThreadsAccount[] | | `harbor.plays` | ThreadPlay[] | | `harbor.outbox` | Outbox 聚合 | | `harbor.jobs` | Job[] | | `harbor.notifications` | Notification[] | | `harbor.play_draft` | wizard 未提交草稿 | ### 8.2 實體欄位(最低) | 實體 | 關鍵欄位 | |------|----------| | Member | uid, tenant_id, email, roles | | ThreadsAccount | id, username, display_name, connection, is_usable | | ThreadPlay | 見 §5.1 JSON | | OutboxBundle | id, play_id, status, steps[] | | OutboxStep | step_id, account_id, status, error?, published_at?, scheduled_at? | | Job | 見 §5.1 | | Notification | 見 §5.1 | --- ## 9. UI 規格(Harbor Desk) ### 9.1 Layout ```text ┌─────────────────────────────────────────────┐ │ Logo [今日]… 🔔 主題 使用者 ▾ │ topbar ├──────────┬──────────────────────────────────┤ │ Today │ Page title │ │ Crew │ Content │ │ Studio │ │ │ Scout │ │ │ Outbox │ │ └──────────┴──────────────────────────────────┘ ``` - 桌面:左導覽 ~220px。 - 手機:底欄 5 格(Today/Crew/Studio/Scout/Outbox);設定進使用者選單。 - 內容最大寬 ~1080px,表單閱讀寬 ~640–720px(wizard)。 ### 9.2 視覺 token(建議名,實作可微調色值) | Token | 用途 | 建議 | |-------|------|------| | `--hb-bg` | 頁背景 | 冷灰白 | | `--hb-surface` | 卡片 | 白 | | `--hb-ink` | 主文 | 近黑 slate | | `--hb-muted` | 次文 | 可讀灰(勿過淡) | | `--hb-line` | 邊框 | 淺灰 | | `--hb-brand` | 主色/CTA | 霧海青 | | `--hb-danger` | 錯誤 | 紅 | | `--hb-success` | 成功 | 綠 | | `--radius` | 卡片 | 12–16px | | `--font-sans` | 混排 | Inter, Taipei Sans TC, system | 禁止:滿版動態雲、木質頂欄、emoji 主 icon、彩虹漸層按鈕。 ### 9.3 元件基線 Button(primary/ghost/danger)、Input、Textarea、Select、Card、Badge(狀態)、EmptyState、Stepper(wizard)、Toast、BellMenu。 --- ## 10. 驗收場景(Given / When / Then) 1. **Given** 未登入 **When** 開 `/app/today` **Then** 導向 `/login`。 2. **Given** mock 帳密正確 **When** 登入 **Then** 進 Today 且看到至少一張待辦/空狀態。 3. **Given** 兩個 usable 帳號 A、B **When** Studio:lead=A cast=B,劇本為 root(A) → reply(B) → reply(A) **Then** 可預覽三句;submit 後 Outbox 三步順序正確且第二則回覆發言者為 lead。 3b. **Given** 劇本某 reply 選了不在 Speakers 的帳號 **When** 提交 **Then** 阻擋並提示。 4. **Given** 已提交串 **When** 模擬發送 **Then** root→reply 依序 published;通知出現成功。 5. **Given** root 模擬失敗 **When** 查看 **Then** reply 不標 published;可重試 root。 6. **Given** Job 模擬完成 **When** 開鈴鐺 **Then** 未讀 +1,點進 job 詳情。 7. **Given** Scout **When** 進入 **Then** 僅占位,無「成功建立海巡」類假操作。 8. **Given** live 模式且後端 5xx **When** 拉 me **Then** 不強制清 token(與需求 US-09 對齊,C 必須)。 --- ## 11. Phase 邊界(實作不得越級) | Phase | 做 | 不做 | |-------|----|------| | A | 契約+可選最小 live auth/job;或純 mock 平台殼 | 海巡、完整互回後端 | | B | `apps/web` 全 mock 體驗 | 大寫業務 API | | C | 逐 repo 接 live | 改 IA 大翻(除非修 bug) | | D | 海巡等 | — | --- ## 12. 開放規格問題 1. **Tenant UI:** login 是否顯示 tenant 欄?**預設:隱藏,mock/live 固定 default tenant。** 2. **Lead 能否出現在 reply?** → **已決策:必須支援**(主帳回別人的回覆)。Cast 多選 UI 仍可不重複勾 lead;發言下拉一定含 lead。 3. **A 是否必須先有可跑的 Go gateway,還是 B 可完全純前端?** **預設:B 可純前端 mock;A 的 live 可與 B 平行,但不阻塞 B 過關。** 4. **Outbox 與舊 publish_queue 是否同 collection?** **預設:C 再定映射;規格層當新模型。** --- ## 13. 批准 - [x] Spec 已批准:2026-07-09(使用者:好請幫我開始拆) - 下一步:依 [plan.md](./plan.md) 與 [tasks/](./tasks/) 實作