# 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 與目前會員權限查詢。
- `threads automation`：Threads 帳號、OAuth/API 診斷、發文 queue、補庫存、智慧時段、頻率護欄、成效追蹤與語調庫。

暫時不包含 template-monorepo 裡較重的 OAuth / OTP / MFA / Zitadel 整合，也不包含 notification、Playwright worker。這些之後要接時再按服務邊界新增。

## 快速開始

```bash
cd haixun-backend
go mod download
make run
```

正式前端：

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

預設服務：

```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 -> 儲存策略
```

人設 8D 分析**不需** Chrome extension session；Node worker 以匿名 Playwright 讀取對標帳號的公開個人頁。若公開頁無法讀到足夠樣本，job 會標記為 `failed` 並顯示原因，不會停在等待狀態。AI 分析使用會員設定頁的研究用 provider / API key（有選定經營帳號時沿用該帳號設定）。

## 專案結構

```text
haixun-backend/
  gateway.go                    # go-zero server 入口
  Makefile                      # gen-api / fmt / test / run
  web/                          # 正式巡樓 Console（Vite + React + TypeScript）
  dev-console/                  # 本機診斷用最小開發面板，不是產品 UI
  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
      publish_queue/            # Threads 發文 queue、guarded transition、retry/missed
      publish_inventory/        # 自動補庫存 policy
      publish_guard/            # 發文頻率護欄與 pause/resume
      publish_queue_event/      # Queue 狀態轉移與告警事件
      style_preset/             # 人設語調 preset、CTA、禁用詞
    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` 需要 admin 會員 JWT，公開登入頁不提供自行註冊；之後開放註冊時再移除這層權限。`register` / `login` 回傳：

```json
{
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 900,
  "uid": "user_uid",
  "token_type": "Bearer"
}
```

保護路由使用：

```http
Authorization: Bearer <access_token>
```

Admin 島民管理 API（封測用，皆需 admin JWT）：

```text
GET   /api/v1/members?page=1&pageSize=20
PATCH /api/v1/members/:uid/profile
PATCH /api/v1/members/:uid/roles
PATCH /api/v1/members/:uid/password
```

角色第一版只支援 `admin` / `user`，admin 不能調整自己的角色，避免把自己降權後鎖住管理入口。Admin 可協助更新島民基本資訊、狀態與商務 email/phone 驗證狀態。
封測建立帳號權限由 `Permission.SeedAdminRegister` 控制是否 seed 進 Mongo；之後要開放公開註冊時先關 config，再清掉 DB 裡的 `auth.register` permission 即可。`member.manage` 留在一般權限目錄，但 user 預設不會拿到，仍只有 admin 可管理島民。

本機開發可以開啟 `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。
