thread-master/README.md

397 lines
11 KiB
Markdown
Raw Permalink 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.

# 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.Secretworker 需帶同一把
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`:只回傳 catalogid、label、streams不含 models、不含 token。
- `POST /providers/:provider/models`:向 provider 的 `/models` 動態拉清單,需帶 `Authorization: Bearer <token>`
- 回應與錯誤訊息不會 echo tokenprovider 原始錯誤 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 cancellationAPI 先寫 `cancel_requested` 與 Redis cancel signalworker 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。