# 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.)` 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 ``` 本機開發可以開啟 `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 `。 - 回應與錯誤訊息不會 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:` 的 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。