336 lines
13 KiB
Markdown
336 lines
13 KiB
Markdown
|
|
# Agent Handoff Notes
|
|||
|
|
|
|||
|
|
這個資料夾是新的巡樓後端核心,請優先維持乾淨邊界,不要把舊 Next.js 或 `template-monorepo` 的業務包袱搬進來。
|
|||
|
|
|
|||
|
|
## 核心原則
|
|||
|
|
|
|||
|
|
- 全系統時間一律 **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 草案。
|
|||
|
|
|
|||
|
|
## 新增 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/`,風格參考 [simular.co](https://simular.co/):**明亮、年輕、圓角多、配色克制**。不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。
|
|||
|
|
|
|||
|
|
### 技術棧與指令
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
web/
|
|||
|
|
src/
|
|||
|
|
api/ # API client(envelope、JWT refresh)
|
|||
|
|
auth/ # AuthContext
|
|||
|
|
components/ # Layout、ui、ThemeToggle、AuthShell
|
|||
|
|
theme/ # ThemeContext(淺色 / 深色)
|
|||
|
|
pages/ # 路由頁面
|
|||
|
|
lib/ # 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` Light / 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`;Layout 頂欄與 `AuthShell` 都要有。
|
|||
|
|
- **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色;語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。
|
|||
|
|
|
|||
|
|
淺色預設明亮藍白底;深色為深藍黑底。兩套都只允許 **一個主色 brand(靛藍)** + success / warning / danger 語意色,不要再加褐色、墨綠、多種 accent 亂配。
|
|||
|
|
|
|||
|
|
### 色彩 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)。Hero 背景用 class `hero-panel`;裝飾 blob 用 `glow-blob` / `glow-blob-alt`。
|
|||
|
|
|
|||
|
|
### 共用元件(優先復用)
|
|||
|
|
|
|||
|
|
新頁面必須從 `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` = 左側欄 + 頂部 sticky bar(UID + `ThemeToggle`)+ `Outlet`。
|
|||
|
|
- 已登入(手機):頂欄品牌 + 主題切換;導覽走 `MobileBottomNav`。
|
|||
|
|
- 側欄分組:**工作區**(總覽、背景任務、排程、AI)、**管理**(模板、設定、會員、權限)。
|
|||
|
|
- Active 導覽:`bg-brand text-white shadow-soft`;hover:`bg-brand-soft text-brand`。
|
|||
|
|
- 未登入:`AuthShell` 置中卡片 + 右上主題切換;背景用柔和 blob,不要花俏插圖牆。
|
|||
|
|
- 語氣:年輕、直接、短句;Hero 可有一句主標 + brand 色強調詞,避免長篇企業八股。
|
|||
|
|
|
|||
|
|
### 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` 底下)。
|
|||
|
|
2. 頁面用 `PageTitle` + `Card` + 既有元件;色票只引用 semantic token。
|
|||
|
|
3. 若需新語意色,**先**改 `index.css` 的 `--hx-*` 與 `@theme`,再改元件;不要頁面內硬編色碼。
|
|||
|
|
4. 完成後執行 `make web-build`。
|
|||
|
|
|
|||
|
|
### 前端禁忌
|
|||
|
|
|
|||
|
|
- 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。
|
|||
|
|
- 不要為單頁新增第三套配色或漸層彩虹按鈕。
|
|||
|
|
- 不要讓 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
|
|||
|
|
```
|