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

464 lines
17 KiB
Markdown
Raw 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: 巡樓 Console 重構Harbor Desk
> Status: **`approved`**
> Source: `docs/product/haixun-console/requirements.md`approved 2026-07-09
> Last updated: `2026-07-09`
> 批准:使用者要求開始拆 plan/tasks2026-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 視為回覆第 n1 步已發出的那則(不另選 parentD 再談樹狀)。
### 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/refreshlive或 session flagmock→ 進 `/app/today`
3. 失敗 → 表單錯誤,不進主殼。
4. 重新整理:有有效會話則進主殼,否則回登入。
5. 登出 → 清會話 → 登入頁。
### 4.2 帳號 CrewB mock / C live
1. 列表顯示已綁帳號顯示名、頭像色、connection、是否預設 lead 候選。
2. 「連接 Threads」live 走 OAuthmock 新增一個假帳號。
3. 標記不健康帳號不可被選為 Lead/Castdisabled + 原因)。
### 4.3 創作 Studio — 互回 wizardB 核心)
| 步驟 | 畫面行為 |
|------|----------|
| 1 主題 | 標題/一句主題;可「示範填入」 |
| 2 船隊 | 必選 1 Lead多選 Cast≥0UI 列表可預設排除 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 ScoutB
占位頁:標題「探查」、短說明「海巡獲客將在下一階段開放」、無假成功操作。
### 4.8 設定
- 顯示目前 `DataSource` 切換dev 可見prod 預設 live 且可藏)。
- AI keyB 僅 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 介面(概念):
```text
AuthRepo, MemberRepo, PermissionRepo,
ThreadsAccountRepo, ThreadPlayRepo, OutboxRepo,
JobRepo, NotificationRepo
```
`createRepos(dataSource)` → mock 或 live 實作同一 TypeScript interface。
### 5.1 HTTP / APIPhase 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` | 取得授權 URLlive |
| 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 / OutboxB 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**(非 hashbasename 可 `/`
| 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 typesA 最小)
| template_type | 用途 | Phase |
|---------------|------|-------|
| `demo_long_task` | 驗證 job+通知 | A/B mock 可模擬 |
| (業務 job | 發佈串場、8D… | C/D |
B mock 可不跑真 workerTimer 推進 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` 專屬操作(若有)前後端雙檢。
- Livetoken 不進 URLrefresh 單飛。
- 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表單閱讀寬 ~640720pxwizard)。
### 9.2 視覺 token建議名實作可微調色值
| Token | 用途 | 建議 |
|-------|------|------|
| `--hb-bg` | 頁背景 | 冷灰白 |
| `--hb-surface` | 卡片 | |
| `--hb-ink` | 主文 | 近黑 slate |
| `--hb-muted` | 次文 | 可讀灰勿過淡 |
| `--hb-line` | 邊框 | 淺灰 |
| `--hb-brand` | 主色/CTA | 霧海青 |
| `--hb-danger` | 錯誤 | |
| `--hb-success` | 成功 | |
| `--radius` | 卡片 | 1216px |
| `--font-sans` | 混排 | Inter, Taipei Sans TC, system |
禁止滿版動態雲木質頂欄emoji icon彩虹漸層按鈕
### 9.3 元件基線
Buttonprimary/ghost/danger)、InputTextareaSelectCardBadge狀態)、EmptyStateStepperwizard)、ToastBellMenu
---
## 10. 驗收場景Given / When / Then
1. **Given** 未登入 **When** `/app/today` **Then** 導向 `/login`
2. **Given** mock 帳密正確 **When** 登入 **Then** Today 且看到至少一張待辦/空狀態
3. **Given** 兩個 usable 帳號 AB **When** Studiolead=A cast=B劇本為 root(A) reply(B) reply(A) **Then** 可預覽三句submit Outbox 三步順序正確且第二則回覆發言者為 lead
3b. **Given** 劇本某 reply 選了不在 Speakers 的帳號 **When** 提交 **Then** 阻擋並提示
4. **Given** 已提交串 **When** 模擬發送 **Then** rootreply 依序 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 可純前端 mockA 的 live 可與 B 平行,但不阻塞 B 過關。**
4. **Outbox 與舊 publish_queue 是否同 collection** **預設C 再定映射;規格層當新模型。**
---
## 13. 批准
- [x] Spec 已批准2026-07-09使用者好請幫我開始拆
- 下一步 [plan.md](./plan.md) [tasks/](./tasks/) 實作