
# Agent Handoff Notes

這個資料夾是新的巡樓後端核心，請優先維持乾淨邊界，不要把舊 Next.js 或 `template-monorepo` 的業務包袱搬進來。

## 產品決策與重啟流程

- **產品真相來源：** `docs/product/<slug>/`（requirements → spec → plan → tasks）。
- **流程 skill：** 專案內 `/spec-driven`（`.grok/skills/spec-driven/`）。未批准 plan/tasks 前不要開大範圍實作。
- 現況架構導覽：`docs/architecture.md`。技術債備忘：`docs/improvement-notes.md`（不取代 product requirements）。

## 核心原則

- 全系統時間一律 **UTC+0**；寫入 Mongo / API 的時間欄位一律 **unix nanoseconds**（`int64`）。排程的 `timezone` 只用於 cron 解讀與下發 payload，不作為儲存時區。
- 複製模式，不複製舊業務。
- `logic` 做 API 編排，`model/usecase` 做可重複使用能力。
- provider adapter 不讀 setting、不碰 Mongo、不知道 HTTP。
- setting 是通用 key-value model，不依賴 AI 或其他業務。
- token / API key 第一版每次 request 帶入，不寫入 config。
- SSE contract 由本服務 normalize，前端不要讀 provider 原始 chunk。
- JSON API 必須使用 `code/message/data/error` envelope 與 `SSCCCDDD` 錯誤碼。
- 列表 API 必須使用 `page/pageSize` query，並在 `data` 回傳 `pagination/list`。
- Job 狀態轉移必須使用 guarded/conditional update；不要在 API/worker 直接裸 `Update` 覆蓋 job 狀態。
- Redis job lock 的 value 是 `workerID`；release / refresh 必須檢查 owner，長任務必須 heartbeat。
- Auth 目前是 native email/password + JWT，不包含 OAuth / OTP / MFA / Zitadel。不要為了相容 template-monorepo 把重依賴搬進來。
- AI provider token 與會員 JWT 是兩種不同 token；AI token 每次 request header 帶入，會員 JWT 由 `/api/v1/auth/*` 簽發。

## 設計文件

- `docs/job-system-plan.md`：通用 job system 規劃，包含 template、run、schedule、Redis queue/lock、取消語意與 API 草案。
- `docs/post-project-plan.md`：發文計畫（文案任務 Flow A 砍掉重做）— 主題發想、風格分析、矩陣產 N 篇草稿、定時發送。
- `docs/scan-placement-plan.md`：海巡獲客（流程 B）— 知識圖譜、Brave 擴展、雙軌爬取（7 天重點 / 30 天補充）、產品匹配、島民交接。

## 新增 API 流程

1. 修改 `generate/api/*.api`。
2. 優先使用 `make gen-api` 重新產生 handler/logic/types。
3. 若手寫 handler，仍需遵守 `response.Write` 與 validator 流程。
4. SSE endpoint 不使用 `response.Write`，直接輸出 `text/event-stream`。
5. 更新 `README.md` 的 API 與架構說明。

## Response / Error Code

錯誤碼格式是 `SSCCCDDD`：

```text
SS  = scope
CCC = category
DDD = detail
```

目前 scope：

```text
10 = Facade
32 = Setting
33 = AI
34 = Job
35 = Auth
36 = Member
37 = Permission
```

建立錯誤時使用：

```go
errs.For(code.AI).InputMissingRequired("缺少 AI provider token")
errs.For(code.Setting).ResNotFound("找不到設定")
errs.For(code.Job).ResInvalidState("job state changed; update rejected")
errs.For(code.Auth).AuthUnauthorized("missing bearer token")
```

不要直接手寫 `33104000` 這種數字，也不要回傳裸 `error` 給 handler 後讓使用者看到內部錯誤。

## Pagination

列表 API 使用：

```text
?page=1&pageSize=10
```

response：

```json
{
  "code": 102000,
  "message": "SUCCESS",
  "data": {
    "pagination": {
      "total": 100,
      "page": 1,
      "pageSize": 10,
      "totalPages": 10
    },
    "list": []
  }
}
```

`page/pageSize` 必須是 server 正規化後的值。不要使用 `offset/limit/items`。

## 新增 Model 流程

模組放在：

```text
internal/model/<module>/
  domain/entity
  domain/repository
  domain/usecase
  repository
  usecase
```

依賴方向：

```text
handler -> logic -> model/domain/usecase
model/usecase -> model/domain/repository
model/repository -> Mongo / Redis
```

不要讓 `logic` import `model/<module>/repository`。

## Auth / Permission 擴充

目前已接：

```text
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/logout          # requires member JWT
GET  /api/v1/members/me
PATCH /api/v1/members/me
GET  /api/v1/permissions/catalog
GET  /api/v1/permissions/me
```

Auth matrix（`internal/handler/routes.go`）：

| 路由 | 需要會員 JWT |
|------|----------------|
| `GET /api/v1/health` | 否 |
| `POST /api/v1/auth/register/login/refresh` | 否 |
| `POST /api/v1/auth/logout` | 是（`Authorization`） |
| `GET /api/v1/ai/providers` | 否 |
| `POST /api/v1/ai/chat/stream/models` | 是（`X-Member-Authorization`）+ provider token（`Authorization`） |
| `/api/v1/members/*`、`/api/v1/permissions/me` | 是（`Authorization`） |
| `/api/v1/permissions/catalog`、`/api/v1/settings/*`、`/api/v1/jobs*`、`/api/v1/job/*` | 是（`Authorization`） |

規則：

- 保護路由用 `internal/middleware.Auth`（`Authorization: Bearer <access_token>`）；AI 變更路由用 `middleware.MemberAuth`（`X-Member-Authorization`），因 `Authorization` 保留給 provider API key。
- logic 從 `authctx.ActorFromContext` 讀 `tenant_id` / `uid`。
- 不要在 handler 直接 parse JWT；token 驗證集中在 `model/auth/usecase`。
- 密碼只存 bcrypt hash，不回傳、不寫 log。
- `members.roles` 第一版是簡化 role key。正式 RBAC 可逐步補 roles collection，但不要破壞 `role_permissions` 的 tenant + role_key contract。
- `Auth.DevHeaderFallback` 只給本機開發，正式環境應關閉。

## AI Provider 擴充

新增 provider 時：

1. 在 `internal/model/ai/domain/enum` 新增 provider id。
2. 在 `internal/model/ai/provider` 新增 adapter。
3. 在 `internal/model/ai/usecase` registry 註冊 provider 與 models。
4. 確保 adapter 回傳統一 `StreamEvent`。
5. 不要改 `logic/ai` 的 SSE 格式。

## Job Worker 擴充

新增 job step 時優先註冊 runner handler：

```go
runner.RegisterStepHandler("analyze_8d", func(ctx context.Context, step job.StepContext) error {
    if err := step.Heartbeat(ctx); err != nil {
        return err
    }
    // do work, check cancel via job usecase if needed
    return nil
})
```

規則：

- Handler 不要直接操作 Mongo / Redis，透過 job usecase 更新進度、完成、失敗或取消。
- 長任務每個 checkpoint 呼叫 `StepContext.Heartbeat` 或 `RefreshRunLock`。
- 收到 cancel signal 後呼叫 `AcknowledgeCancel(jobId, workerID)`，不要自行把狀態改成 `cancelled`。
- release lock 時必須帶 `workerID`；不要新增無 owner 的 release helper。

## 前端設計規則（`web/`）

巡樓 Console 前端在 `haixun-backend/web/`，視覺為**沉穩田園巡檢台**（動森感：天空、雲朵、奶油卡片、青綠 brand；**不是**任天堂 UI 複製）。

**字體固定** Inter + Taipei Sans TC；圖示僅 `AcIcon` / `AuthDecor` 內原創 SVG 線條圖。**禁止** emoji、貼圖 JPG、咖啡色木質頂欄、Nook / 任天堂命名。樣式集中在 `index.css`（`--hx-*` token + `hx-*` / `ac-*` / `auth-*` class）。

不要把舊 Next.js / `template-monorepo` UI 搬進來，也不要引入重型 UI 框架。

### 視覺架構（登入前後共用）

```text
全頁背景 .hx-scene          灰藍天空 → 淡草地單一漸層（淺／深各一套，見 index.css）
裝飾層 SceneDecor           雲朵（多朵緩動）+ 淡光暈 + 小葉子；登入與 Layout 共用
奶油卡片 .auth-ticket       2px line 邊框、圓角 2rem、surface 底、可選 .ac-dialog-texture 點陣
頂部品牌列                  圖示 .auth-ticket-icon（brand-soft 底）+ ink 標題；不用獨立色塊 ribbon
主內容                      表單或 Outlet；內文頁用 PageTitle / Card（綠色 .ac-title-bar 僅內容區小標）
桌面側欄 .ac-pocket-device  掌上終端外框；PATROL PAD 狀態列；固定尺寸 + .ac-pocket-scroll 內捲
手機 .ac-dock               底部最多 4 格 +「更多」sheet
```

| 區域 | 元件 | 關鍵 class |
|------|------|------------|
| 未登入 | `AuthShell` + `LoginPage` / `RegisterPage` | `hx-scene` `auth-scene` `auth-ticket` `auth-welcome` `auth-shell-form` |
| 已登入外殼 | `Layout` | `hx-scene` `ac-app-shell` `ac-app-header` `auth-ticket` `ac-app-main-inner` |
| 背景裝飾 | `AuthDecor.tsx` → `SceneDecor` | `hx-scene-deco` `auth-cloud--*` |
| 品牌小圖 | `AuthTicketIcon` | `auth-ticket-icon`（小屋＋樹，原創 SVG） |
| 側欄導覽 | `Layout` + `navApps` | `ac-pocket-device` `ac-app-tile` |
| 手機導覽 | `MobileBottomNav` | `ac-dock` |

**登入頁刻意不做的事**：上方不要獨立大色塊 header；表單上方**不要**再放 `ac-title-bar`「登入」大牌（品牌已在 `auth-welcome`）。註冊頁同理可省略重複大標。

**已淘汰、勿加回**：`ac-island`（改用 `hx-scene`）、`ac-wood-bar` / 咖啡色木質頂欄、`public/ac/` 貼圖、Nook Phone 文案。

### 技術棧與指令

```text
web/
  src/
    api/          # API client（envelope、JWT refresh）
    auth/         # AuthContext
    components/   # Layout、AuthShell、AuthDecor、ui、ThemeToggle、MobileBottomNav、AcIcon
    theme/        # ThemeContext（淺色 / 深色）
    pages/        # 路由頁面
    lib/          # acAssets（導覽 icon key）、jobStatus 等
  index.css       # 設計 token 與場景樣式唯一來源
```

```bash
make web-dev      # dev server :5173，proxy 到 :8890
make web-build    # tsc + vite build
```

### 字型

| 語言 | 字型 | 載入方式 |
|------|------|----------|
| 繁體中文 | **台北黑體 Taipei Sans TC** | npm `taipei-sans-tc`，在 `index.css` `@import` Regular + Bold |
| 英文 | **Inter**（與 simular.co 相同，Google Fonts 免費） | `web/index.html` link |

規則：

- `body` / 中文標題：`Inter` + `Taipei Sans TC` 混排（`--font-sans`）。
- 純英文裝飾字（導覽副標、Hero 小字）：加 class `display-en`，使用 `--font-en`。
- 中文 `line-height` 維持 **1.7+**；不要用過細字重當標題（標題用 `font-bold` / `font-black`）。
- 只載入 Taipei Sans TC **Regular + Bold**，不要載入 Light，避免小字過細。
- 不要改回 Noto Sans TC，也不要手寫 `#333` 這類裸色碼當主色。

### 對比度與字級

- 內文、表頭、表單 label、卡片說明：優先 `text-ink` / `text-ink-secondary`，**不要**拿 `text-muted` 當主要閱讀文字。
- `text-muted` 只給次要提示（筆數、hint、placeholder 用 `text-subtle`）。
- 表單輸入字級 **15px**（`text-[15px]`），輸入框底用 `bg-surface` 白底，確保與背景拉開。
- 淺色 `muted` 約 `#5a6578`、深色約 `#b8c4d6`；改色時以「小字仍可舒適閱讀」為準，不要回到 `#94a3b8` 那種淡灰。

### 主題（淺色 / 深色）

- `ThemeProvider`（`src/theme/ThemeContext.tsx`）包住 App；偏好存 `localStorage` key：`haixun.theme`（`light` | `dark`）。
- `index.html` 內嵌 script 在 React 載入前設定 `data-theme`，避免閃爍。
- 所有顏色必須走 CSS 變數 `--hx-*`，再映射到 Tailwind `@theme`（`bg-canvas`、`text-brand` 等）。
- 切換按鈕用 `ThemeToggle`（`ac-btn-secondary` 樣式）；`Layout` 頂欄與 `AuthShell` 右上角都要有。
- **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色；語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。

淺色：低飽和灰藍天空 + 灰綠草地 + 奶油 `surface` + **brand 青綠**；深色：黃昏低對比、同一套 token 自動切換。頂欄與卡片內品牌區都用 **surface / ink / brand**，不要再用木色 `#c4a882` 當 header 底。

### 場景與卡片 class（維護時對照）

| Class | 用途 |
|-------|------|
| `.hx-scene` | 全頁天空→草地漸層（登入 + 已登入根節點） |
| `.hx-scene-deco` / `SceneDecor` | 背景雲、光暈、葉子（`pointer-events: none`） |
| `.auth-ticket` | 奶油主卡片外框（登入卡、已登入主內容區） |
| `.auth-welcome` | 卡片內品牌列：圖示 + 標題 + 一句 tagline，底部分隔線 |
| `.ac-app-header` | 已登入 sticky 頂欄：半透明 surface + blur，**非**木色 |
| `.ac-title-bar` | 內容區綠色小標題（裝置色漸層）；用於 `PageTitle` 等，**不**用於登入頁表單上方大牌 |
| `.ac-pocket-device` | 側欄掌上終端；`--pocket-width`（28rem）、`--pocket-screen-height` 固定，內容在 `.ac-pocket-scroll` 捲動 |
| `.ac-app-tile` / `.ac-dock` | App 格導覽、手機底欄 |
| `.auth-shell-form` | 登入／註冊表單放大字級（僅 auth 頁） |

側欄標示用 **PATROL PAD** 等中性英文裝飾字（`display-en`）；圖示僅 `AcIcon` SVG。

### 色彩 token（語意命名）

開發時只用這些 Tailwind class（值定義在 `web/src/index.css`）：

| Token | 用途 |
|-------|------|
| `canvas` | 全頁背景 |
| `surface` / `surface-muted` | 卡片、輸入框底 |
| `ink` / `ink-secondary` / `muted` | 主文 / 次文 / 輔助 |
| `line` | 邊框 |
| `brand` / `brand-hover` / `brand-soft` | 主 CTA、active 導覽、連結 hover |
| `glow` | 裝飾色塊（`.glow-blob-alt`） |
| `success` / `warning` / `danger`（含 `*-soft`） | 狀態、錯誤、Job badge |

主按鈕一律 `Button variant="primary"` → `bg-brand`，不要用全黑按鈕。

### 圓角與陰影

```text
--radius-sm   0.75rem   小元素、code
--radius-md   1.25rem   Input / Textarea
--radius-lg   1.75rem   Card
--radius-xl   2.25rem   Hero、QuickLink、StatCard
--radius-pill 9999px    Button、Badge、導覽 pill
```

陰影用 utility：`shadow-card`（一般卡片）、`shadow-soft`（主按鈕、Hero、`.auth-ticket`）。內容 Hero 可用 `ac-bulletin` + `ac-hero-gradient` token；全頁裝飾雲朵走 `SceneDecor`，不要另加會打架的強色 blob。

### 共用元件（優先復用）

新頁面必須從 `src/components/ui.tsx` 組裝，不要另寫一套按鈕樣式：

| 元件 | 用途 |
|------|------|
| `PageTitle` | 頁面標題 + 副標 |
| `Card` | 內容區塊 |
| `Field` + `Input` / `Textarea` | 表單 |
| `Button` | `primary` / `ghost` / `danger` / `soft` |
| `Badge` | 標籤 pill（`brand` / `sky` / `success` / `warning` / `danger` / `neutral`） |
| `StatCard` / `QuickLinkCard` | 總覽統計與快捷入口 |
| `ErrorText` / `CopyableId` | 錯誤與可複製 ID |

`Button` 必須渲染 `{children}`；文案用**中文動詞**（例：「建立背景任務」「重新載入任務列表」），不要留空白小框。

### RWD（手機）

- `< lg`：隱藏左側欄；**底部固定導覽**最多 **4 格**（總覽 / 任務 / 排程 / **更多**），不要把漢堡或 ⋯ 選單放在左上角。
- 「更多」以底部 sheet 展開：AI、模板、設定、會員、權限、主題切換、登出。
- 主內容加 `layout-main` 底部 padding，避開 tab bar + `safe-area-inset-bottom`。
- 寬表格包 `overflow-x-auto` + `min-w-*`，避免小螢幕擠爆版面。

### 版面與導覽

- 已登入（桌面）：`Layout` = `hx-scene` 背景 + `SceneDecor` + `ac-app-header`（品牌 + 角色 chip + `ThemeToggle`）+ 左 `ac-pocket-device` + 右 `auth-ticket` 主內容 `Outlet`。
- 已登入（手機）：同上頂欄；導覽走 `MobileBottomNav`（總覽／任務／排程／更多）。
- 側欄 App 來源：`src/lib/acAssets.ts` 的 `navApps`；圖示 key 對應 `AcIcon`。
- Active 導覽：`ac-app-tile--active`（brand-soft 底 + brand 字色）；hover：`bg-brand-soft text-brand`。
- 未登入：`AuthShell` 置中 `auth-ticket` + 右上 `ThemeToggle`；`auth-welcome` 內品牌，表單緊接說明文字。
- 語氣：年輕、直接、短句；可帶「島民」「巡樓」等原創文案，避免企業八股與任天堂用語。

### API 與狀態

- JSON 一律走 `api/client.ts`（`code/message/data` envelope）；需登入加 `{ auth: true }`。
- AI 路由用 `X-Member-Authorization`；provider token 用 `Authorization`（見後端 Auth matrix）。
- Job 狀態中文與 badge 色：`src/lib/jobStatus.ts`（`jobStatusLabel` / `jobStatusBadgeClass`），列表有進行中任務時可每 3 秒 refresh。
- 不要在前端 parse JWT；`uid` / `tenant_id` 從 `AuthContext` 讀。

### 新增頁面流程

1. 在 `App.tsx` 掛路由（需登入的放在 `Layout` 底下，自動享有 `hx-scene` + 頂欄 + 主內容 `auth-ticket`）。
2. 頁面內用 `PageTitle`（含 `.ac-title-bar` 小標）+ `Card` / `ac-bulletin` + `ui.tsx` 元件；色票只引用 semantic token。
3. 若需新語意色，**先**改 `index.css` 的 `--hx-*` 與 `@theme`，再改元件；不要頁面內硬編色碼。
4. 新導覽項：改 `acAssets.ts` 的 `navApps`，並在 `AcIcon` 補 SVG path。
5. 完成後執行 `make web-build`。

### 島民頁面互動（可推廣 runtime）

掛在 `Layout` 底下的新頁面**自動**支援島民操作，不需每頁手寫 executor。

模組入口：`web/src/lib/islander/index.ts`

| 層 | 職責 |
|----|------|
| `pageSnapshot` | 掃描 `.ac-app-shell` 內可互動元素，產生 `hx-*` ref |
| `islanderActions` | 解析/剝除 `islander-actions` JSON 區塊 |
| `actionExecutor` | 執行 navigate/click/fill/select/scroll 等；可 `registerIslanderActionHandler` 擴充 |
| `islanderAgent` | 串流回覆 → 執行 action → 回傳結果 → 自動 follow-up |
| `buildIslanderContext` | 組裝送給後端的頁面快照 |

**零設定（預設）**：路由掛在 `Layout` 即可；島民讀 DOM + `PageTitle` / `h1` 辨識頁面。預設**不**主動介紹這一頁；僅在使用者明確問頁面／操作時才附【可互動元素】（`userWantsPageContext`）。

**可選增強**（擇一）：

1. `useIslanderPage({ title, purpose, hints, suggestions })` — 頁面內動態註冊說明
2. `registerIslanderPage(/^\/foo/, { title, ... })` — 在 `siteGuide.ts` 或模組 init 靜態註冊
3. HTML 慣例：`data-islander-label`（元素名稱）、`data-islander-kind`（類型）、`data-islander-ignore`（排除）、`data-islander-page-title`（頁名）

Action 協定（AI 回覆末尾）：

```islander-actions
[{ "type": "navigate", "path": "/settings" }, { "type": "click", "ref": "hx-3" }]
```

### 前端禁忌

- 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。
- 不要為單頁新增第三套配色、木質頂欄、或漸層彩虹按鈕。
- 不要在登入／註冊頁加回獨立大牌 `ac-title-bar` 或咖啡色 header ribbon。
- 不要讓 SSE / AI 直接吃 provider 原始 chunk（後端已 normalize）。
- 不要用 `offset/limit` 呼叫列表 API；用 `page` / `pageSize`。

## 驗證

完成變更後至少執行：

```bash
cd haixun-backend
go mod tidy
make fmt
go test ./...
```

有動到前端時另執行：

```bash
make web-build
```
