397 lines
11 KiB
Markdown
397 lines
11 KiB
Markdown
# Haixun Backend
|
||
|
||
新的巡樓後端核心。這個資料夾刻意不直接複製 `template-monorepo` 的產物碼,只沿用它的架構模式、goctl handler template 概念與必要 runtime library,讓後續可以用更乾淨的邊界重建服務。
|
||
|
||
## 目前範圍
|
||
|
||
第一版先放六個核心能力:
|
||
|
||
- `setting`:通用設定模型,支援 `scope + scope_id + key` 儲存不同類型設定。
|
||
- `ai`:可替換 AI provider interface,第一版支援 OpenCode Go 與 Grok/xAI,並提供 SSE 串流回應。
|
||
- `job`:通用背景任務系統,支援 template/run/schedule/event、Redis queue/lock、進度、retry 與 cooperative cancel。
|
||
- `auth`:native email/password 登入、JWT access/refresh token、logout revoke。
|
||
- `member`:目前登入會員的 profile 讀寫。
|
||
- `permission`:permission catalog 與目前會員權限查詢。
|
||
|
||
暫時不包含 template-monorepo 裡較重的 OAuth / OTP / MFA / Zitadel 整合,也不包含 notification、Playwright worker。這些之後要接時再按服務邊界新增。
|
||
|
||
## 快速開始
|
||
|
||
```bash
|
||
cd haixun-backend
|
||
go mod download
|
||
make run
|
||
```
|
||
|
||
預設服務:
|
||
|
||
```text
|
||
http://127.0.0.1:8890
|
||
```
|
||
|
||
健康檢查:
|
||
|
||
```bash
|
||
curl http://127.0.0.1:8890/api/v1/health
|
||
```
|
||
|
||
### 8D Node 爬蟲 worker 驗證
|
||
|
||
`style-8d` job 由 `worker_type=node` 消費。啟動 Gateway 與 Redis 後,另開一個終端:
|
||
|
||
```bash
|
||
make node-worker-style-8d
|
||
```
|
||
|
||
也可以在 repo 根目錄執行:
|
||
|
||
```bash
|
||
npm run worker:style-8d
|
||
```
|
||
|
||
常用環境變數:
|
||
|
||
```text
|
||
HAIXUN_BACKEND_URL=http://127.0.0.1:8890
|
||
HAIXUN_WORKER_SECRET=... # 若 etc/gateway.yaml 設了 InternalWorker.Secret,worker 需帶同一把
|
||
HAIXUN_NODE_WORKER_ID=local-8d # 可選,方便辨識 lock holder
|
||
HAIXUN_8D_MIN_SAMPLES=1 # 驗證期預設 1;要嚴格一點可調高
|
||
```
|
||
|
||
前端在人設詳情頁按「開始 8D 分析」後,任務會進入:
|
||
|
||
```text
|
||
確認連線 -> 抓取樣本 -> AI 8D -> 儲存策略
|
||
```
|
||
|
||
目前 Node worker 先用 Playwright 抓 Threads 公開頁樣本並產生可驗證的 8D 結構;若公開頁無法讀到足夠樣本,job 會標記為 `failed` 並顯示原因,不會停在等待狀態。
|
||
|
||
## 專案結構
|
||
|
||
```text
|
||
haixun-backend/
|
||
gateway.go # go-zero server 入口
|
||
Makefile # gen-api / fmt / test / run
|
||
etc/ # runtime config
|
||
generate/
|
||
api/ # goctl .api 定義
|
||
goctl/api/handler.tpl # 從 template-monorepo 精簡改來的 handler 模板
|
||
internal/
|
||
config/ # config struct
|
||
handler/ # HTTP handler,目前手寫;之後可由 goctl 生成
|
||
logic/ # API 編排層
|
||
model/
|
||
setting/ # 通用設定 model
|
||
ai/ # AI provider interface + adapter
|
||
job/ # Job template/run/schedule/event usecase + repository
|
||
auth/ # JWT token issue/refresh/logout + Redis revoke store
|
||
member/ # Native member profile + password hash
|
||
permission/ # Permission catalog + role permission mapping
|
||
worker/ # 常駐背景 worker / scheduler / reaper
|
||
library/ # 最小 runtime library
|
||
response/ # 統一 JSON response envelope
|
||
svc/ # ServiceContext 組裝依賴
|
||
types/ # API request/response types
|
||
```
|
||
|
||
## 分層規則
|
||
|
||
## Response 與錯誤碼標準
|
||
|
||
所有一般 JSON API 都必須回傳同一層 envelope:
|
||
|
||
```json
|
||
{
|
||
"code": 102000,
|
||
"message": "SUCCESS",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
成功固定:
|
||
|
||
```text
|
||
HTTP 200
|
||
code = 102000
|
||
message = SUCCESS
|
||
```
|
||
|
||
失敗格式:
|
||
|
||
```json
|
||
{
|
||
"code": 33101000,
|
||
"message": "缺少 AI provider token",
|
||
"error": {
|
||
"biz_code": "33101000",
|
||
"scope": 33,
|
||
"category": 104,
|
||
"detail": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
錯誤碼採 `SSCCCDDD`:
|
||
|
||
```text
|
||
SS = scope,服務或模組範圍
|
||
CCC = category,錯誤分類
|
||
DDD = detail,細分錯誤碼,未細分時為 000
|
||
```
|
||
|
||
目前 scope:
|
||
|
||
```text
|
||
10 = Facade / request parse / validation
|
||
32 = Setting
|
||
33 = AI
|
||
34 = Job
|
||
35 = Auth
|
||
36 = Member
|
||
37 = Permission
|
||
```
|
||
|
||
常用 category:
|
||
|
||
```text
|
||
101 = InputInvalidFormat
|
||
104 = InputMissingRequired
|
||
204 = DBUnavailable
|
||
301 = ResourceNotFound
|
||
303 = ResourceConflict
|
||
401 = AuthUnauthorized
|
||
505 = AuthForbidden
|
||
601 = SystemInternal
|
||
802 = ServiceThirdParty
|
||
```
|
||
|
||
實作規則:
|
||
|
||
- Handler 成功/失敗都用 `internal/response.Write`,SSE endpoint 例外。
|
||
- Request parse / validation 錯誤用 `response.WrapRequestError`,會落在 Facade scope。
|
||
- Model/usecase 內建立錯誤時使用 `errors.For(code.<Scope>)` builder,不要手刻數字。
|
||
- 不要把 provider 原始錯誤完整洩漏到前端;必要時只保留可排查的摘要。
|
||
|
||
### 分頁標準
|
||
|
||
列表型 API 的 query 使用 `page` / `pageSize`:
|
||
|
||
```text
|
||
GET /api/v1/settings/user/user_123?page=1&pageSize=10
|
||
```
|
||
|
||
回應的分頁資訊放在 `data.pagination`,資料陣列放在 `data.list`:
|
||
|
||
```json
|
||
{
|
||
"code": 102000,
|
||
"message": "SUCCESS",
|
||
"data": {
|
||
"pagination": {
|
||
"total": 42,
|
||
"page": 1,
|
||
"pageSize": 10,
|
||
"totalPages": 5
|
||
},
|
||
"list": []
|
||
}
|
||
}
|
||
```
|
||
|
||
規則:
|
||
|
||
- `page` 從 1 開始。
|
||
- `pageSize <= 0` 時由 server 套用預設值。
|
||
- `pageSize` 超過 server 上限時由 server 截斷。
|
||
- `totalPages = ceil(total / pageSize)`。
|
||
- response 內的 `page/pageSize` 必須回傳 server 正規化後的值。
|
||
|
||
### logic
|
||
|
||
`internal/logic/*` 只負責一次 API 請求的流程編排:
|
||
|
||
- 轉換 HTTP types 與 usecase DTO
|
||
- 呼叫一個或多個 model usecase
|
||
- 不直接操作 Mongo / Redis
|
||
- 不放 provider HTTP 細節
|
||
|
||
### model
|
||
|
||
`internal/model/*` 放可重複使用的業務能力:
|
||
|
||
- `domain/entity`:資料結構
|
||
- `domain/repository`:repository interface
|
||
- `domain/usecase`:usecase interface 與 DTO
|
||
- `repository`:Mongo / Redis 實作
|
||
- `usecase`:業務能力實作
|
||
|
||
### provider
|
||
|
||
`internal/model/ai/provider` 只負責外部 AI API adapter:
|
||
|
||
- 不讀 setting
|
||
- 不碰 HTTP handler
|
||
- 不存 token
|
||
- token 每次由 request 帶入
|
||
|
||
## Setting Model
|
||
|
||
設定使用 typed setting 形式:
|
||
|
||
```json
|
||
{
|
||
"scope": "user",
|
||
"scope_id": "user_123",
|
||
"key": "ai.default",
|
||
"value": {
|
||
"provider": "opencode-go",
|
||
"model": "deepseek-v4-pro",
|
||
"temperature": 0.7,
|
||
"max_tokens": 2000
|
||
},
|
||
"version": 1
|
||
}
|
||
```
|
||
|
||
API:
|
||
|
||
```text
|
||
GET /api/v1/settings/:scope/:scope_id?page=1&pageSize=10
|
||
GET /api/v1/settings/:scope/:scope_id/:key
|
||
PUT /api/v1/settings/:scope/:scope_id/:key
|
||
DELETE /api/v1/settings/:scope/:scope_id/:key
|
||
```
|
||
|
||
`setting` model 不知道 AI、Threads、crawler 等業務含義。各業務 model 自己解讀對應 key 的 value。
|
||
|
||
## Auth / Member / Permission
|
||
|
||
這版從 `template-monorepo` 精簡搬入會員、權限與 token 的核心概念,但不搬 OAuth / OTP / MFA / Zitadel 依賴。
|
||
|
||
Auth 採 native email/password:
|
||
|
||
```text
|
||
POST /api/v1/auth/register
|
||
POST /api/v1/auth/login
|
||
POST /api/v1/auth/refresh
|
||
POST /api/v1/auth/logout
|
||
```
|
||
|
||
`register` / `login` 回傳:
|
||
|
||
```json
|
||
{
|
||
"access_token": "...",
|
||
"refresh_token": "...",
|
||
"expires_in": 900,
|
||
"uid": "user_uid",
|
||
"token_type": "Bearer"
|
||
}
|
||
```
|
||
|
||
保護路由使用:
|
||
|
||
```http
|
||
Authorization: Bearer <access_token>
|
||
```
|
||
|
||
本機開發可以開啟 `Auth.DevHeaderFallback`,用 header 模擬登入:
|
||
|
||
```http
|
||
X-Tenant-ID: default
|
||
X-UID: user_uid
|
||
```
|
||
|
||
Member API:
|
||
|
||
```text
|
||
GET /api/v1/members/me
|
||
PATCH /api/v1/members/me
|
||
```
|
||
|
||
Permission API:
|
||
|
||
```text
|
||
GET /api/v1/permissions/catalog?tree=true
|
||
GET /api/v1/permissions/me?include_tree=true
|
||
```
|
||
|
||
資料模型:
|
||
|
||
- `members`:tenant-scoped profile、email、bcrypt password hash、roles。
|
||
- `permissions`:平台 permission catalog。
|
||
- `role_permissions`:tenant + role_key 對 permission catalog 的綁定。
|
||
- Redis `auth:jwt:*`:access/refresh pair 與 blacklist。Redis 未配置時仍可簽發 token,但 refresh/logout revoke 不會持久化。
|
||
|
||
## AI Provider
|
||
|
||
AI token 不存在 config,呼叫時每次帶入,且**只放 HTTP header**,不要放 JSON body(避免 log / 回應洩漏):
|
||
|
||
```http
|
||
Authorization: Bearer sk-...
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"provider": "opencode-go",
|
||
"model": "deepseek-v4-pro",
|
||
"messages": [
|
||
{ "role": "user", "content": "請幫我寫一段文案" }
|
||
]
|
||
}
|
||
```
|
||
|
||
API:
|
||
|
||
```text
|
||
GET /api/v1/ai/providers
|
||
POST /api/v1/ai/providers/:provider/models
|
||
POST /api/v1/ai/chat
|
||
POST /api/v1/ai/chat/stream
|
||
```
|
||
|
||
- `GET /providers`:只回傳 catalog(id、label、streams),不含 models、不含 token。
|
||
- `POST /providers/:provider/models`:向 provider 的 `/models` 動態拉清單,需帶 `Authorization: Bearer <token>`。
|
||
- 回應與錯誤訊息不會 echo token;provider 原始錯誤 body 也不會直接回傳給前端。
|
||
|
||
串流 endpoint 使用 SSE:
|
||
|
||
```text
|
||
event: delta
|
||
data: {"type":"delta","text":"..."}
|
||
|
||
event: done
|
||
data: {"type":"done","finish_reason":"stop"}
|
||
```
|
||
|
||
## Job System
|
||
|
||
Job 系統的詳細設計在 `docs/job-system-plan.md`。目前 runtime 原則:
|
||
|
||
- MongoDB 的 `job_runs` 是狀態真相來源;claim、cancel、complete、fail、retry 必須使用 conditional update,避免 worker 與 API 互相覆蓋狀態。
|
||
- Redis `jobs:lock:<jobId>` 的 value 是 `workerID`;release / refresh 必須檢查 owner,只能由持有 lock 的 worker 操作。
|
||
- Worker 執行長任務時要定期呼叫 `RefreshRunLock(jobId, workerID, ttlSeconds)`,避免 reaper 誤判過期。
|
||
- Runner 支援 `RegisterStepHandler(stepID, handler)` 註冊自訂 step handler;未註冊時會走 demo handler。自訂 handler 可用 `StepContext.Heartbeat` 續約 lock。
|
||
- 取消採 cooperative cancellation:API 先寫 `cancel_requested` 與 Redis cancel signal,worker checkpoint 讀取後呼叫 `AcknowledgeCancel(jobId, workerID)`。
|
||
|
||
## OpenCode Go 注意事項
|
||
|
||
第一版 OpenCode Go 先走 OpenAI-compatible `/chat/completions`:
|
||
|
||
```text
|
||
https://opencode.ai/zen/go/v1/chat/completions
|
||
```
|
||
|
||
目前已處理 Kimi 模型 `temperature = 1` 的特殊規則。部分 OpenCode Go 模型官方文件標示為 Anthropic-compatible `/messages`,後續可在 `internal/model/ai/provider` 新增 messages adapter,不需要改 logic 或前端 SSE contract。
|
||
|
||
## 下一步建議
|
||
|
||
1. 用 `goctl` 重新生成 handler / logic / types,確認 `.api` 與手寫版本對齊。
|
||
2. 補 `setting` repository 測試與 Mongo integration 測試。
|
||
3. 補 AI provider mock,讓 `logic/ai` 不需要真的打 provider 也能測。
|
||
4. 新增 credential service 或 Vault/KMS 整合,但不要把 token 放進 provider config。
|
||
5. 新增 worker/job model,讓 Go worker 與 Node Playwright worker 共用同一套 job contract。
|
||
|
||
## 設計文件
|
||
|
||
- [Job 核心系統規劃](docs/job-system-plan.md):通用 job template、run、schedule、事件、取消語意與 worker contract。
|