17 KiB
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 分四階段交付。本規格鎖定:
- 平台地基:登入會話、角色權限、Threads 帳號基礎、Job、站內通知。
- 新前端 Harbor Desk:5 入口 IA、Studio 多帳互回 wizard、Outbox、通知;預設 mock 資料。
- 同一契約在
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
pending --> queued --> running --> succeeded
running --> failed
running --> cancel_requested --> cancelled
queued|pending --> cancelled (若支援提前取消)
* --> expired (lock 逾時,後端語意;mock 可簡化省略)
3.2 ThreadPlay
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
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
unknown --> connected
unknown --> error
connected --> error
error --> connected (重新授權/診斷成功)
4. 使用者流程
4.1 登入(A/B)
- 使用者開
/login,輸入 email、password(可選 tenant 若多租戶 UI 需要;mock 固定default)。 - 成功 → 存 access/refresh(live)或 session flag(mock)→ 進
/app/today。 - 失敗 → 表單錯誤,不進主殼。
- 重新整理:有有效會話則進主殼,否則回登入。
- 登出 → 清會話 → 登入頁。
4.2 帳號 Crew(B mock / C live)
- 列表顯示已綁帳號:顯示名、頭像色、connection、是否預設 lead 候選。
- 「連接 Threads」:live 走 OAuth;mock 新增一個假帳號。
- 標記不健康帳號不可被選為 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
- 列表:Play 標題、狀態、步驟進度(2/5)。
- 點進詳情:逐步狀態時間線。
- Mock「模擬發送」:依序 root→replies 推進狀態(可加速)。
- 失敗步驟顯示錯誤文案 +「重試」(mock 直接 succeeded)。
4.5 Today
- 卡片:待處理失敗、今日排程、進行中 Job、快捷「開新串場」。
- 點通知/失敗 → 對應 Outbox 或 Job 詳情。
4.6 通知
- 頂欄鈴鐺:未讀數。
- 下拉最近 N 筆;點擊標記已讀並 navigate。
- 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 <access>;401 → refresh 一次 → 仍失敗登出 |
| 禁止 | 未實作端點回 200+空成功;應 501 或明確業務錯誤 |
前端 repository 介面(概念):
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):
{
"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
┌─────────────────────────────────────────────┐
│ 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)
- Given 未登入 When 開
/app/todayThen 導向/login。 - Given mock 帳密正確 When 登入 Then 進 Today 且看到至少一張待辦/空狀態。
- 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 阻擋並提示。 - Given 已提交串 When 模擬發送 Then root→reply 依序 published;通知出現成功。
- Given root 模擬失敗 When 查看 Then reply 不標 published;可重試 root。
- Given Job 模擬完成 When 開鈴鐺 Then 未讀 +1,點進 job 詳情。
- Given Scout When 進入 Then 僅占位,無「成功建立海巡」類假操作。
- 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. 開放規格問題
- Tenant UI: login 是否顯示 tenant 欄?預設:隱藏,mock/live 固定 default tenant。
- Lead 能否出現在 reply? → 已決策:必須支援(主帳回別人的回覆)。Cast 多選 UI 仍可不重複勾 lead;發言下拉一定含 lead。
- A 是否必須先有可跑的 Go gateway,還是 B 可完全純前端? 預設:B 可純前端 mock;A 的 live 可與 B 平行,但不阻塞 B 過關。
- Outbox 與舊 publish_queue 是否同 collection? 預設:C 再定映射;規格層當新模型。