first commit
This commit is contained in:
commit
232111712d
|
|
@ -0,0 +1,9 @@
|
||||||
|
.run
|
||||||
|
.git
|
||||||
|
web/node_modules
|
||||||
|
# web/dist 保留給 deploy/Dockerfile.web.static(本機 make web-build 後 COPY)
|
||||||
|
worker/node_modules
|
||||||
|
**/*_test.go
|
||||||
|
**/.DS_Store
|
||||||
|
*.md
|
||||||
|
!deploy/**
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
72708
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
> haixun-web@0.1.0 dev
|
||||||
|
> vite
|
||||||
|
|
||||||
|
|
||||||
|
VITE v6.4.3 ready in 187 ms
|
||||||
|
|
||||||
|
➜ Local: http://localhost:5173/
|
||||||
|
➜ Network: use --host to expose
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1 @@
|
||||||
|
72713
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
72714
|
||||||
|
|
@ -0,0 +1,412 @@
|
||||||
|
|
||||||
|
# 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 草案。
|
||||||
|
- `docs/scan-placement-plan.md`:海巡獲客(流程 B)— 知識圖譜、Brave 擴展、雙軌爬取(7 天重點 / 30 天補充)、產品匹配、島民交接。
|
||||||
|
|
||||||
|
## 新增 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/`,視覺為**沉穩田園巡檢台**(動森感:天空、雲朵、奶油卡片、青綠 brand;**不是**任天堂 UI 複製)。
|
||||||
|
|
||||||
|
**字體固定** Inter + Taipei Sans TC;圖示僅 `AcIcon` / `AuthDecor` 內原創 SVG 線條圖。**禁止** emoji、貼圖 JPG、咖啡色木質頂欄、Nook / 任天堂命名。樣式集中在 `index.css`(`--hx-*` token + `hx-*` / `ac-*` / `auth-*` class)。
|
||||||
|
|
||||||
|
不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。
|
||||||
|
|
||||||
|
### 視覺架構(登入前後共用)
|
||||||
|
|
||||||
|
```text
|
||||||
|
全頁背景 .hx-scene 灰藍天空 → 淡草地單一漸層(淺/深各一套,見 index.css)
|
||||||
|
裝飾層 SceneDecor 雲朵(多朵緩動)+ 淡光暈 + 小葉子;登入與 Layout 共用
|
||||||
|
奶油卡片 .auth-ticket 2px line 邊框、圓角 2rem、surface 底、可選 .ac-dialog-texture 點陣
|
||||||
|
頂部品牌列 圖示 .auth-ticket-icon(brand-soft 底)+ ink 標題;不用獨立色塊 ribbon
|
||||||
|
主內容 表單或 Outlet;內文頁用 PageTitle / Card(綠色 .ac-title-bar 僅內容區小標)
|
||||||
|
桌面側欄 .ac-pocket-device 掌上終端外框;PATROL PAD 狀態列;固定尺寸 + .ac-pocket-scroll 內捲
|
||||||
|
手機 .ac-dock 底部最多 4 格 +「更多」sheet
|
||||||
|
```
|
||||||
|
|
||||||
|
| 區域 | 元件 | 關鍵 class |
|
||||||
|
|------|------|------------|
|
||||||
|
| 未登入 | `AuthShell` + `LoginPage` / `RegisterPage` | `hx-scene` `auth-scene` `auth-ticket` `auth-welcome` `auth-shell-form` |
|
||||||
|
| 已登入外殼 | `Layout` | `hx-scene` `ac-app-shell` `ac-app-header` `auth-ticket` `ac-app-main-inner` |
|
||||||
|
| 背景裝飾 | `AuthDecor.tsx` → `SceneDecor` | `hx-scene-deco` `auth-cloud--*` |
|
||||||
|
| 品牌小圖 | `AuthTicketIcon` | `auth-ticket-icon`(小屋+樹,原創 SVG) |
|
||||||
|
| 側欄導覽 | `Layout` + `navApps` | `ac-pocket-device` `ac-app-tile` |
|
||||||
|
| 手機導覽 | `MobileBottomNav` | `ac-dock` |
|
||||||
|
|
||||||
|
**登入頁刻意不做的事**:上方不要獨立大色塊 header;表單上方**不要**再放 `ac-title-bar`「登入」大牌(品牌已在 `auth-welcome`)。註冊頁同理可省略重複大標。
|
||||||
|
|
||||||
|
**已淘汰、勿加回**:`ac-island`(改用 `hx-scene`)、`ac-wood-bar` / 咖啡色木質頂欄、`public/ac/` 貼圖、Nook Phone 文案。
|
||||||
|
|
||||||
|
### 技術棧與指令
|
||||||
|
|
||||||
|
```text
|
||||||
|
web/
|
||||||
|
src/
|
||||||
|
api/ # API client(envelope、JWT refresh)
|
||||||
|
auth/ # AuthContext
|
||||||
|
components/ # Layout、AuthShell、AuthDecor、ui、ThemeToggle、MobileBottomNav、AcIcon
|
||||||
|
theme/ # ThemeContext(淺色 / 深色)
|
||||||
|
pages/ # 路由頁面
|
||||||
|
lib/ # acAssets(導覽 icon key)、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` 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`(`ac-btn-secondary` 樣式);`Layout` 頂欄與 `AuthShell` 右上角都要有。
|
||||||
|
- **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色;語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。
|
||||||
|
|
||||||
|
淺色:低飽和灰藍天空 + 灰綠草地 + 奶油 `surface` + **brand 青綠**;深色:黃昏低對比、同一套 token 自動切換。頂欄與卡片內品牌區都用 **surface / ink / brand**,不要再用木色 `#c4a882` 當 header 底。
|
||||||
|
|
||||||
|
### 場景與卡片 class(維護時對照)
|
||||||
|
|
||||||
|
| Class | 用途 |
|
||||||
|
|-------|------|
|
||||||
|
| `.hx-scene` | 全頁天空→草地漸層(登入 + 已登入根節點) |
|
||||||
|
| `.hx-scene-deco` / `SceneDecor` | 背景雲、光暈、葉子(`pointer-events: none`) |
|
||||||
|
| `.auth-ticket` | 奶油主卡片外框(登入卡、已登入主內容區) |
|
||||||
|
| `.auth-welcome` | 卡片內品牌列:圖示 + 標題 + 一句 tagline,底部分隔線 |
|
||||||
|
| `.ac-app-header` | 已登入 sticky 頂欄:半透明 surface + blur,**非**木色 |
|
||||||
|
| `.ac-title-bar` | 內容區綠色小標題(裝置色漸層);用於 `PageTitle` 等,**不**用於登入頁表單上方大牌 |
|
||||||
|
| `.ac-pocket-device` | 側欄掌上終端;`--pocket-width`(28rem)、`--pocket-screen-height` 固定,內容在 `.ac-pocket-scroll` 捲動 |
|
||||||
|
| `.ac-app-tile` / `.ac-dock` | App 格導覽、手機底欄 |
|
||||||
|
| `.auth-shell-form` | 登入/註冊表單放大字級(僅 auth 頁) |
|
||||||
|
|
||||||
|
側欄標示用 **PATROL PAD** 等中性英文裝飾字(`display-en`);圖示僅 `AcIcon` SVG。
|
||||||
|
|
||||||
|
### 色彩 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、`.auth-ticket`)。內容 Hero 可用 `ac-bulletin` + `ac-hero-gradient` token;全頁裝飾雲朵走 `SceneDecor`,不要另加會打架的強色 blob。
|
||||||
|
|
||||||
|
### 共用元件(優先復用)
|
||||||
|
|
||||||
|
新頁面必須從 `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` = `hx-scene` 背景 + `SceneDecor` + `ac-app-header`(品牌 + 角色 chip + `ThemeToggle`)+ 左 `ac-pocket-device` + 右 `auth-ticket` 主內容 `Outlet`。
|
||||||
|
- 已登入(手機):同上頂欄;導覽走 `MobileBottomNav`(總覽/任務/排程/更多)。
|
||||||
|
- 側欄 App 來源:`src/lib/acAssets.ts` 的 `navApps`;圖示 key 對應 `AcIcon`。
|
||||||
|
- Active 導覽:`ac-app-tile--active`(brand-soft 底 + brand 字色);hover:`bg-brand-soft text-brand`。
|
||||||
|
- 未登入:`AuthShell` 置中 `auth-ticket` + 右上 `ThemeToggle`;`auth-welcome` 內品牌,表單緊接說明文字。
|
||||||
|
- 語氣:年輕、直接、短句;可帶「島民」「巡樓」等原創文案,避免企業八股與任天堂用語。
|
||||||
|
|
||||||
|
### 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` 底下,自動享有 `hx-scene` + 頂欄 + 主內容 `auth-ticket`)。
|
||||||
|
2. 頁面內用 `PageTitle`(含 `.ac-title-bar` 小標)+ `Card` / `ac-bulletin` + `ui.tsx` 元件;色票只引用 semantic token。
|
||||||
|
3. 若需新語意色,**先**改 `index.css` 的 `--hx-*` 與 `@theme`,再改元件;不要頁面內硬編色碼。
|
||||||
|
4. 新導覽項:改 `acAssets.ts` 的 `navApps`,並在 `AcIcon` 補 SVG path。
|
||||||
|
5. 完成後執行 `make web-build`。
|
||||||
|
|
||||||
|
### 島民頁面互動(可推廣 runtime)
|
||||||
|
|
||||||
|
掛在 `Layout` 底下的新頁面**自動**支援島民操作,不需每頁手寫 executor。
|
||||||
|
|
||||||
|
模組入口:`web/src/lib/islander/index.ts`
|
||||||
|
|
||||||
|
| 層 | 職責 |
|
||||||
|
|----|------|
|
||||||
|
| `pageSnapshot` | 掃描 `.ac-app-shell` 內可互動元素,產生 `hx-*` ref |
|
||||||
|
| `islanderActions` | 解析/剝除 `islander-actions` JSON 區塊 |
|
||||||
|
| `actionExecutor` | 執行 navigate/click/fill/select/scroll 等;可 `registerIslanderActionHandler` 擴充 |
|
||||||
|
| `islanderAgent` | 串流回覆 → 執行 action → 回傳結果 → 自動 follow-up |
|
||||||
|
| `buildIslanderContext` | 組裝送給後端的頁面快照 |
|
||||||
|
|
||||||
|
**零設定(預設)**:路由掛在 `Layout` 即可;島民讀 DOM + `PageTitle` / `h1` 辨識頁面。預設**不**主動介紹這一頁;僅在使用者明確問頁面/操作時才附【可互動元素】(`userWantsPageContext`)。
|
||||||
|
|
||||||
|
**可選增強**(擇一):
|
||||||
|
|
||||||
|
1. `useIslanderPage({ title, purpose, hints, suggestions })` — 頁面內動態註冊說明
|
||||||
|
2. `registerIslanderPage(/^\/foo/, { title, ... })` — 在 `siteGuide.ts` 或模組 init 靜態註冊
|
||||||
|
3. HTML 慣例:`data-islander-label`(元素名稱)、`data-islander-kind`(類型)、`data-islander-ignore`(排除)、`data-islander-page-title`(頁名)
|
||||||
|
|
||||||
|
Action 協定(AI 回覆末尾):
|
||||||
|
|
||||||
|
```islander-actions
|
||||||
|
[{ "type": "navigate", "path": "/settings" }, { "type": "click", "ref": "hx-3" }]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端禁忌
|
||||||
|
|
||||||
|
- 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。
|
||||||
|
- 不要為單頁新增第三套配色、木質頂欄、或漸層彩虹按鈕。
|
||||||
|
- 不要在登入/註冊頁加回獨立大牌 `ac-title-bar` 或咖啡色 header ribbon。
|
||||||
|
- 不要讓 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
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
GO ?= go
|
||||||
|
GOFMT ?= gofmt
|
||||||
|
GOCTL ?= goctl
|
||||||
|
GO_ZERO_STYLE := go_zero
|
||||||
|
API_ENTRY := ./generate/api/gateway.api
|
||||||
|
GOFILES := $(shell find . -name '*.go')
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
help: ## 顯示可用指令
|
||||||
|
@echo "Haixun Backend"
|
||||||
|
@echo ""
|
||||||
|
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-12s %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
tools: ## 安裝 goctl / goimports
|
||||||
|
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install github.com/zeromicro/go-zero/tools/goctl@latest)
|
||||||
|
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
|
||||||
|
|
||||||
|
gen-api: tools ## 由 .api 生成 handler / logic / types
|
||||||
|
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
|
||||||
|
|
||||||
|
fmt: ## gofmt + goimports
|
||||||
|
$(GOFMT) -s -w $(GOFILES)
|
||||||
|
@command -v goimports >/dev/null 2>&1 && goimports -w . || true
|
||||||
|
|
||||||
|
test: ## 執行測試
|
||||||
|
$(GO) test ./...
|
||||||
|
|
||||||
|
run: ## 啟動 API(前景)
|
||||||
|
$(GO) run ./gateway.go -f etc/gateway.yaml
|
||||||
|
|
||||||
|
dev-all: ## 一鍵啟動 Mongo/Redis + API + 前端 + 8D worker(背景)
|
||||||
|
bash scripts/start-all.sh
|
||||||
|
|
||||||
|
stop-all: ## 一鍵停止全部開發服務
|
||||||
|
bash scripts/stop-all.sh
|
||||||
|
|
||||||
|
restart-all: ## 一鍵重啟全部開發服務
|
||||||
|
bash scripts/restart-all.sh
|
||||||
|
|
||||||
|
status-all: ## 查看全部開發服務狀態
|
||||||
|
bash scripts/status-all.sh
|
||||||
|
|
||||||
|
stop: stop-all ## 同 stop-all
|
||||||
|
|
||||||
|
restart: restart-all ## 同 restart-all
|
||||||
|
|
||||||
|
dev-8d: ## 一鍵啟動 API + Node 8D worker(前景,Ctrl+C 結束)
|
||||||
|
bash scripts/dev-with-style-8d.sh
|
||||||
|
|
||||||
|
CONFIG ?= etc/gateway.yaml
|
||||||
|
INIT_TENANT ?= default
|
||||||
|
INIT_EMAIL ?= admin@30cm.net
|
||||||
|
INIT_PASSWORD ?= Fafafa54088
|
||||||
|
|
||||||
|
tool-init: ## 初始化 Mongo indexes、預設權限與 admin 帳號
|
||||||
|
$(GO) run ./cmd/tool init -f $(CONFIG) -tenant $(INIT_TENANT) -email $(INIT_EMAIL) -password '$(INIT_PASSWORD)'
|
||||||
|
|
||||||
|
tool: ## 執行 cmd/tool(例:make tool ARGS="init -f etc/gateway.yaml")
|
||||||
|
$(GO) run ./cmd/tool $(ARGS)
|
||||||
|
|
||||||
|
web-install: ## 安裝前端依賴
|
||||||
|
cd web && npm install
|
||||||
|
|
||||||
|
web-dev: web-install ## 啟動前端 dev server(proxy 到 :8890)
|
||||||
|
cd web && npm run dev
|
||||||
|
|
||||||
|
extension-pack: ## 打包 Chrome 擴充為 web/public/downloads/*.zip
|
||||||
|
bash scripts/package-extension.sh
|
||||||
|
|
||||||
|
web-build: web-install extension-pack ## 建置前端靜態檔
|
||||||
|
cd web && npm run build
|
||||||
|
|
||||||
|
node-worker-style-8d: ## 啟動 Node 8D 爬蟲 worker
|
||||||
|
cd .. && npm run worker:style-8d
|
||||||
|
|
||||||
|
check: fmt test ## 格式化並測試
|
||||||
|
|
||||||
|
prod: ## 一鍵啟動 production Docker(API + Web + workers,分身數見 deploy/.env)
|
||||||
|
bash scripts/prod-up.sh
|
||||||
|
|
||||||
|
prod-update: ## 只重建/重啟 API+Web+Workers;mongo/redis 不重啟,資料留在 volume
|
||||||
|
bash scripts/prod-update.sh
|
||||||
|
|
||||||
|
prod-deps: ## 只啟動 mongo+redis(named volume 持久化)
|
||||||
|
bash scripts/prod-deps.sh
|
||||||
|
|
||||||
|
prod-down: ## 停止 stack(不刪 volume;Mongo/Redis 資料保留)
|
||||||
|
bash scripts/prod-down.sh
|
||||||
|
|
||||||
|
prod-wipe-data: ## 停止並刪除 mongo/redis volume(危險,需輸入 yes)
|
||||||
|
bash scripts/prod-wipe-data.sh
|
||||||
|
|
||||||
|
prod-logs: ## 追蹤 production logs(可傳 service 名,例:make prod-logs ARGS=api)
|
||||||
|
bash scripts/prod-logs.sh $(ARGS)
|
||||||
|
|
||||||
|
prod-build: web-build ## 建置靜態前端 + production images(不啟動)
|
||||||
|
cd deploy && docker compose -f docker-compose.prod.yml build
|
||||||
|
|
@ -0,0 +1,396 @@
|
||||||
|
# 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。
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"haixun-backend/internal/bootstrap"
|
||||||
|
"haixun-backend/internal/config"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "init":
|
||||||
|
if err := runInit(os.Args[2:]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] unknown command: %s\n", os.Args[1])
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInit(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
||||||
|
configFile := fs.String("f", "etc/gateway.yaml", "config file")
|
||||||
|
tenantID := fs.String("tenant", envOr("INIT_TENANT_ID", "default"), "tenant id for admin and role permissions")
|
||||||
|
email := fs.String("email", envOr("INIT_ADMIN_EMAIL", "admin@haixun.local"), "bootstrap admin email")
|
||||||
|
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Admin-Pass-1!"), "bootstrap admin password")
|
||||||
|
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg config.Config
|
||||||
|
conf.MustLoad(*configFile, &cfg)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
report, err := bootstrap.Init(ctx, cfg, bootstrap.InitOptions{
|
||||||
|
TenantID: strings.TrimSpace(*tenantID),
|
||||||
|
AdminEmail: strings.TrimSpace(*email),
|
||||||
|
AdminPass: *password,
|
||||||
|
DisplayName: strings.TrimSpace(*displayName),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] indexes ensured\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] permissions catalog seeded\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] role_permissions seeded (admin=all, user=default)\n")
|
||||||
|
if report.AdminCreated {
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] admin created uid=%s email=%s tenant=%s\n", report.AdminUID, *email, *tenantID)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "[tool] admin exists uid=%s email=%s tenant=%s (roles ensured admin)\n", report.AdminUID, *email, *tenantID)
|
||||||
|
}
|
||||||
|
fmt.Printf("export INIT_TENANT_ID=%s\n", *tenantID)
|
||||||
|
fmt.Printf("export INIT_ADMIN_EMAIL=%s\n", *email)
|
||||||
|
fmt.Printf("export INIT_ADMIN_PASSWORD=%s\n", *password)
|
||||||
|
fmt.Printf("export INIT_ADMIN_UID=%s\n", report.AdminUID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, fallback string) string {
|
||||||
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " tool init [-f etc/gateway.yaml] [-tenant default] [-email admin@haixun.local] [-password ...]\n")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"haixun-backend/internal/config"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configFile = flag.String("f", "etc/gateway.worker.yaml", "config file")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var c config.Config
|
||||||
|
conf.MustLoad(*configFile, &c)
|
||||||
|
if !c.JobWorker.Enabled {
|
||||||
|
fmt.Fprintln(os.Stderr, "[worker] JobWorker.Enabled must be true")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := svc.NewServiceContext(c)
|
||||||
|
defer sc.Close(context.Background())
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
"[worker] started type=%s (scheduler=%v reaper=%v)\n",
|
||||||
|
c.JobWorker.WorkerType,
|
||||||
|
c.JobScheduler.Enabled,
|
||||||
|
c.JobReaper.Enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-ch
|
||||||
|
fmt.Println("[worker] shutting down")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 複製為 deploy/.env 後再啟動:cp deploy/.env.example deploy/.env
|
||||||
|
|
||||||
|
# ── 對外埠 ──
|
||||||
|
HAIXUN_WEB_PORT=8080
|
||||||
|
|
||||||
|
# ── 前端打包模式 ──
|
||||||
|
# static = 本機 make web-build 後 nginx 只 COPY dist(預設,最快)
|
||||||
|
# docker = 在 Docker 內跑 npm build(需改 compose 用 Dockerfile.web)
|
||||||
|
# HAIXUN_WEB_BUILD_MODE=static
|
||||||
|
|
||||||
|
# ── Worker 分身數(make prod 會帶入 docker compose --scale)──
|
||||||
|
GO_WORKER_REPLICAS=5
|
||||||
|
NODE_STYLE8D_WORKER_REPLICAS=5
|
||||||
|
|
||||||
|
# ── Mongo / Redis(容器內預設,通常不用改)──
|
||||||
|
# 資料存在 Docker named volume:haixun-prod_mongo_data、haixun-prod_redis_data
|
||||||
|
# prod-down 不會刪 volume;重啟 container 資料仍在。
|
||||||
|
# 只改版程式:make prod-update(不碰 mongo/redis)
|
||||||
|
HAIXUN_MONGO_URI=mongodb://mongo:27017
|
||||||
|
HAIXUN_MONGO_DATABASE=haixun
|
||||||
|
HAIXUN_REDIS_ADDR=redis:6379
|
||||||
|
|
||||||
|
# ── 安全金鑰(正式環境務必更換)──
|
||||||
|
HAIXUN_AUTH_ACCESS_SECRET=change-me-access-secret
|
||||||
|
HAIXUN_AUTH_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
HAIXUN_WORKER_SECRET=change-me-worker-secret
|
||||||
|
|
||||||
|
# ── 首次初始化管理員(make prod 會自動跑 init;已存在則跳過建立)──
|
||||||
|
INIT_TENANT_ID=default
|
||||||
|
INIT_ADMIN_EMAIL=admin@30cm.net
|
||||||
|
INIT_ADMIN_PASSWORD=Fafafa54088
|
||||||
|
|
||||||
|
# ── Node 8D worker 選項 ──
|
||||||
|
# HAIXUN_NODE_WORKER_ID=custom-node-worker-1
|
||||||
|
# HAIXUN_WORKER_POLL_MS=3000
|
||||||
|
|
||||||
|
# ── 略過自動 init ──
|
||||||
|
# 預設:若 Mongo 已有 members 會自動跳過 init。
|
||||||
|
# 強制重跑 init:PROD_FORCE_INIT=1 make prod
|
||||||
|
# HAIXUN_SKIP_INIT=1
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 複製為 deploy/.env 後再啟動:cp deploy/.env.example deploy/.env
|
||||||
|
|
||||||
|
# ── 對外埠 ──
|
||||||
|
HAIXUN_WEB_PORT=8080
|
||||||
|
|
||||||
|
# ── 前端打包模式 ──
|
||||||
|
# static = 本機 make web-build 後 nginx 只 COPY dist(預設,最快)
|
||||||
|
# docker = 在 Docker 內跑 npm build(需改 compose 用 Dockerfile.web)
|
||||||
|
# HAIXUN_WEB_BUILD_MODE=static
|
||||||
|
|
||||||
|
# ── Worker 分身數(make prod 會帶入 docker compose --scale)──
|
||||||
|
GO_WORKER_REPLICAS=1
|
||||||
|
NODE_STYLE8D_WORKER_REPLICAS=1
|
||||||
|
|
||||||
|
# ── Mongo / Redis(容器內預設,通常不用改)──
|
||||||
|
# 資料存在 Docker named volume:haixun-prod_mongo_data、haixun-prod_redis_data
|
||||||
|
# prod-down 不會刪 volume;重啟 container 資料仍在。
|
||||||
|
# 只改版程式:make prod-update(不碰 mongo/redis)
|
||||||
|
HAIXUN_MONGO_URI=mongodb://mongo:27017
|
||||||
|
HAIXUN_MONGO_DATABASE=haixun
|
||||||
|
HAIXUN_REDIS_ADDR=redis:6379
|
||||||
|
|
||||||
|
# ── 安全金鑰(正式環境務必更換)──
|
||||||
|
HAIXUN_AUTH_ACCESS_SECRET=change-me-access-secret
|
||||||
|
HAIXUN_AUTH_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
HAIXUN_WORKER_SECRET=change-me-worker-secret
|
||||||
|
|
||||||
|
# ── 首次初始化管理員(make prod 會自動跑 init;已存在則跳過建立)──
|
||||||
|
INIT_TENANT_ID=default
|
||||||
|
INIT_ADMIN_EMAIL=admin@haixun.local
|
||||||
|
INIT_ADMIN_PASSWORD=Admin-Pass-1!
|
||||||
|
|
||||||
|
# ── Node 8D worker 選項 ──
|
||||||
|
# HAIXUN_NODE_WORKER_ID=custom-node-worker-1
|
||||||
|
# HAIXUN_WORKER_POLL_MS=3000
|
||||||
|
|
||||||
|
# ── 略過自動 init ──
|
||||||
|
# 預設:若 Mongo 已有 members 會自動跳過 init。
|
||||||
|
# 強制重跑 init:PROD_FORCE_INIT=1 make prod
|
||||||
|
# HAIXUN_SKIP_INIT=1
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM golang:1.22-bookworm AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-api .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-worker ./cmd/worker
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-tool ./cmd/tool
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates gettext-base curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /out/haixun-api /out/haixun-worker /out/haixun-tool /app/
|
||||||
|
COPY deploy/config/gateway.runtime.yaml.tpl deploy/config/gateway.worker.runtime.yaml.tpl /app/deploy/config/
|
||||||
|
COPY deploy/docker/entrypoint-api.sh deploy/docker/entrypoint-worker.sh deploy/docker/entrypoint-init.sh /app/deploy/docker/
|
||||||
|
RUN chmod +x /app/deploy/docker/entrypoint-api.sh /app/deploy/docker/entrypoint-worker.sh /app/deploy/docker/entrypoint-init.sh
|
||||||
|
EXPOSE 8890
|
||||||
|
ENTRYPOINT ["/app/deploy/docker/entrypoint-api.sh"]
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/playwright:v1.49.1-noble AS base
|
||||||
|
WORKDIR /app
|
||||||
|
COPY worker/package.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY worker/ ./
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["npx", "tsx", "style-8d-worker.ts"]
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# 備用:無本機 Node 時在 Docker 內編譯。預設請用 Dockerfile.web.static + make web-build。
|
||||||
|
|
||||||
|
FROM node:22-bookworm AS web-builder
|
||||||
|
WORKDIR /src/web
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY web/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=web-builder /src/web/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# 本機先執行 make web-build,再打包純靜態檔 + nginx(無 Node 編譯,建置最快)
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY web/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# 本機依賴(Docker Compose)
|
||||||
|
|
||||||
|
Gateway 啟用 **Notification** / **Member OTP** 需要:
|
||||||
|
|
||||||
|
| 服務 | 用途 | 預設埠 |
|
||||||
|
|------|------|--------|
|
||||||
|
| **MongoDB** | `notifications`、`notification_dlq` collections | 27017 |
|
||||||
|
| **Redis** | 冪等、配額、異步重試佇列、member OTP challenge | 6379 |
|
||||||
|
| MailHog(選用) | 本機 SMTP 測試 | 1025 / 8025 |
|
||||||
|
| OpenLDAP(`make ldap-up` / `make k6-up`) | ZITADEL LDAP IdP 本機目錄 | 389 |
|
||||||
|
| ZITADEL(`make k6-up`) | OIDC / Social / LDAP 登入 | 8080 |
|
||||||
|
|
||||||
|
Mongo **不需要**事先手動建 collection;應用程式寫入時會自動建立。索引由 init script 或 `make mongo-index` 建立。
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 啟動 Mongo + Redis
|
||||||
|
make deps-up
|
||||||
|
|
||||||
|
# 2.(選用)含 MailHog
|
||||||
|
make deps-up-smtp
|
||||||
|
|
||||||
|
# 3. 確認索引(首次 docker volume 通常已由 init 建立;可再跑一次保險)
|
||||||
|
make mongo-index
|
||||||
|
|
||||||
|
# 4. 啟動 Gateway(使用 etc/gateway.dev.yaml)
|
||||||
|
make run-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mongo collections
|
||||||
|
|
||||||
|
| Collection | 模組 | 說明 |
|
||||||
|
|------------|------|------|
|
||||||
|
| `notifications` | notification | 發送紀錄、冪等 |
|
||||||
|
| `notification_dlq` | notification | 超過 MaxRetry 的死信 |
|
||||||
|
|
||||||
|
索引定義見 [`deploy/mongo/init/01-gateway-indexes.js`](mongo/init/01-gateway-indexes.js),與 Go 的 `Index20260520001UP` 一致。
|
||||||
|
|
||||||
|
## 常用指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make deps-up # docker compose up -d mongo redis
|
||||||
|
make deps-up-smtp # 再加上 mailhog(profile smtp)
|
||||||
|
make ldap-up # 只起 OpenLDAP(profile ldap)
|
||||||
|
make k6-up # 全棧含 OpenLDAP + ZITADEL(見 deploy/zitadel、deploy/openldap README)
|
||||||
|
make ldap-test # 確認 LDAP 測試帳號 alice/bob
|
||||||
|
make deps-down # 停止並移除容器(保留 volume)
|
||||||
|
make deps-down-v # 停止並刪除 volume(會清掉 Mongo 資料)
|
||||||
|
make deps-logs # 查看 log
|
||||||
|
make mongo-index # 手動建立/補齊索引
|
||||||
|
```
|
||||||
|
|
||||||
|
LDAP 本機測試:[deploy/openldap/README.md](openldap/README.md)
|
||||||
|
|
||||||
|
## 連線設定
|
||||||
|
|
||||||
|
設定說明:[`etc/README.md`](../etc/README.md)
|
||||||
|
|
||||||
|
| 檔案 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| [`etc/gateway.yaml`](../etc/gateway.yaml) | 預設,無需 Docker |
|
||||||
|
| [`etc/gateway.dev.example.yaml`](../etc/gateway.dev.example.yaml) | 範例(可提交) |
|
||||||
|
| `etc/gateway.dev.yaml` | 本機專用(**勿提交**,見 `.gitignore`) |
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
|
Database: ${HAIXUN_MONGO_DATABASE}
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
||||||
|
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: false
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-worker
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8891
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
|
Database: ${HAIXUN_MONGO_DATABASE}
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
||||||
|
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: true
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
name: haixun-prod
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: haixun
|
||||||
|
# named volume:重啟/改版不會清資料(只有 prod-wipe-data 或 docker volume rm 才會)
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
# AOF + named volume:重啟後 queue/lock 狀態可從磁碟恢復
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.api
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8890/api/v1/health >/dev/null || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
go-worker:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.api
|
||||||
|
restart: unless-stopped
|
||||||
|
entrypoint: ["/app/deploy/docker/entrypoint-worker.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
node-worker-style-8d:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.node-worker
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
HAIXUN_BACKEND_URL: http://api:8890
|
||||||
|
HAIXUN_WORKER_SECRET: ${HAIXUN_WORKER_SECRET}
|
||||||
|
HAIXUN_NODE_WORKER_ID: ${HAIXUN_NODE_WORKER_ID:-}
|
||||||
|
HAIXUN_WORKER_POLL_MS: ${HAIXUN_WORKER_POLL_MS:-3000}
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.web.static
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${HAIXUN_WEB_PORT:-8080}:80"
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
init:
|
||||||
|
profiles: ["init"]
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.api
|
||||||
|
entrypoint: ["/app/deploy/docker/entrypoint-init.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
redis_data:
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
container_name: gateway-mongo
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: gateway
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
- ./mongo/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: gateway-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
redis_data:
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
||||||
|
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
||||||
|
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
||||||
|
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
||||||
|
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
||||||
|
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
||||||
|
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
||||||
|
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
|
||||||
|
envsubst < /app/deploy/config/gateway.runtime.yaml.tpl > /tmp/gateway.runtime.yaml
|
||||||
|
exec /app/haixun-api -f /tmp/gateway.runtime.yaml
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
||||||
|
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
||||||
|
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
||||||
|
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
||||||
|
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
||||||
|
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
||||||
|
: "${INIT_TENANT_ID:=default}"
|
||||||
|
: "${INIT_ADMIN_EMAIL:=admin@haixun.local}"
|
||||||
|
: "${INIT_ADMIN_PASSWORD:?INIT_ADMIN_PASSWORD is required}"
|
||||||
|
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
||||||
|
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
|
||||||
|
envsubst < /app/deploy/config/gateway.runtime.yaml.tpl > /tmp/gateway.runtime.yaml
|
||||||
|
exec /app/haixun-tool init \
|
||||||
|
-f /tmp/gateway.runtime.yaml \
|
||||||
|
-tenant "$INIT_TENANT_ID" \
|
||||||
|
-email "$INIT_ADMIN_EMAIL" \
|
||||||
|
-password "$INIT_ADMIN_PASSWORD"
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
||||||
|
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
||||||
|
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
||||||
|
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
||||||
|
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
||||||
|
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
||||||
|
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
||||||
|
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
|
||||||
|
envsubst < /app/deploy/config/gateway.worker.runtime.yaml.tpl > /tmp/gateway.worker.runtime.yaml
|
||||||
|
exec /app/haixun-worker -f /tmp/gateway.worker.runtime.yaml
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Gateway MongoDB 初始化(僅在 data volume 首次建立時執行)
|
||||||
|
// 與 internal/model/notification/repository/* Index20260520001UP 對齊
|
||||||
|
// 既有 volume 請執行:make mongo-index
|
||||||
|
|
||||||
|
db = db.getSiblingDB('gateway');
|
||||||
|
|
||||||
|
print('Creating indexes on notifications...');
|
||||||
|
|
||||||
|
db.notifications.createIndex(
|
||||||
|
{ tenant_id: 1, kind: 1, idempotency_key: 1 },
|
||||||
|
{ unique: true, name: 'idx_notifications_tenant_kind_idempotency' }
|
||||||
|
);
|
||||||
|
|
||||||
|
db.notifications.createIndex(
|
||||||
|
{ tenant_id: 1, uid: 1, occurred_at: -1 },
|
||||||
|
{ name: 'idx_notifications_tenant_uid_occurred' }
|
||||||
|
);
|
||||||
|
|
||||||
|
db.notifications.createIndex(
|
||||||
|
{ status: 1, attempts: 1, occurred_at: 1 },
|
||||||
|
{ name: 'idx_notifications_status_attempts_occurred' }
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Creating indexes on notification_dlq...');
|
||||||
|
|
||||||
|
db.notification_dlq.createIndex(
|
||||||
|
{ tenant_id: 1, occurred_at: -1 },
|
||||||
|
{ name: 'idx_notification_dlq_tenant_occurred' }
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Gateway Mongo init done.');
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Vite 產物:檔名含 hash,可長期快取
|
||||||
|
location /assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /downloads/ {
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /illustrations/ {
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA 入口與路由:不快取,避免部署後仍載入舊版 shell
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8890;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,381 @@
|
||||||
|
# Job 核心系統規劃
|
||||||
|
|
||||||
|
## 目標
|
||||||
|
|
||||||
|
建立一套通用 job system,讓任何長任務、流程任務、定時任務未來都能共用。Job 不只是背景任務,而是「有模板、有設定、有狀態、有進度、有取消能力、有重跑策略、有排程能力」的工作單元。
|
||||||
|
|
||||||
|
## 核心設計
|
||||||
|
|
||||||
|
採用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mongo = job/template/run/history 的真相來源
|
||||||
|
Redis = queue、distributed lock、schedule tick、短期 lease
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Api[GoAPI] --> Template[JobTemplate]
|
||||||
|
Api --> JobRun[JobRunMongo]
|
||||||
|
JobRun --> RedisQueue[RedisQueue]
|
||||||
|
Scheduler[SchedulerTick] --> RedisQueue
|
||||||
|
Worker[Worker] --> RedisQueue
|
||||||
|
Worker --> JobRun
|
||||||
|
Worker --> Step[JobStep]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
### JobTemplate
|
||||||
|
|
||||||
|
Template 定義「這種 job 要怎麼做」。例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
demo_long_task
|
||||||
|
external_worker_task
|
||||||
|
scheduled_report
|
||||||
|
multi_step_pipeline
|
||||||
|
```
|
||||||
|
|
||||||
|
Template 要回答:
|
||||||
|
|
||||||
|
- 這個 job 的輸入 payload schema 是什麼
|
||||||
|
- 有哪些 steps
|
||||||
|
- 最終狀態是什麼
|
||||||
|
- 可不可以重複執行
|
||||||
|
- 是否允許同 account / 同 target 同時跑
|
||||||
|
- retry policy 是什麼
|
||||||
|
- timeout 是多少
|
||||||
|
- 是否可被排程
|
||||||
|
- 是否支援取消,以及取消時 worker 要如何收斂
|
||||||
|
|
||||||
|
### JobRun
|
||||||
|
|
||||||
|
JobRun 是每一次執行實例。它引用 template,保存當次 payload、狀態、進度、結果、錯誤與執行歷史。
|
||||||
|
|
||||||
|
### JobSchedule
|
||||||
|
|
||||||
|
JobSchedule 是「何時建立 JobRun」。支援:
|
||||||
|
|
||||||
|
- cron
|
||||||
|
- enabled / disabled
|
||||||
|
- timezone
|
||||||
|
- payload template
|
||||||
|
- target scope,例如 user/account/system
|
||||||
|
- nextRunAt / lastRunAt
|
||||||
|
|
||||||
|
### JobFlow
|
||||||
|
|
||||||
|
Flow 是多步驟流程。第一版不用做完整 DAG,先支援線性 steps:
|
||||||
|
|
||||||
|
```text
|
||||||
|
multi_step_pipeline:
|
||||||
|
1. prepare
|
||||||
|
2. execute
|
||||||
|
3. finalize
|
||||||
|
```
|
||||||
|
|
||||||
|
之後再擴成 DAG 或 conditional branch。
|
||||||
|
|
||||||
|
## Mongo Collections
|
||||||
|
|
||||||
|
### `job_templates`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "...",
|
||||||
|
"type": "demo_long_task",
|
||||||
|
"version": 1,
|
||||||
|
"name": "示範長任務",
|
||||||
|
"description": "展示 job template、進度、取消、重跑與排程能力",
|
||||||
|
"enabled": true,
|
||||||
|
"repeatable": true,
|
||||||
|
"concurrencyPolicy": "reject_same_scope",
|
||||||
|
"dedupeKeys": ["scope_id", "target"],
|
||||||
|
"timeoutSeconds": 600,
|
||||||
|
"cancelPolicy": {
|
||||||
|
"supported": true,
|
||||||
|
"mode": "cooperative",
|
||||||
|
"graceSeconds": 30
|
||||||
|
},
|
||||||
|
"retryPolicy": {
|
||||||
|
"maxAttempts": 2,
|
||||||
|
"backoffSeconds": [30, 120]
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{ "id": "prepare", "name": "準備資料", "workerType": "go", "timeoutSeconds": 60, "cancelable": true },
|
||||||
|
{ "id": "execute", "name": "執行任務", "workerType": "go", "timeoutSeconds": 300, "cancelable": true },
|
||||||
|
{ "id": "finalize", "name": "整理結果", "workerType": "go", "timeoutSeconds": 30, "cancelable": false }
|
||||||
|
],
|
||||||
|
"createAt": 0,
|
||||||
|
"updateAt": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `job_runs`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "...",
|
||||||
|
"templateType": "demo_long_task",
|
||||||
|
"templateVersion": 1,
|
||||||
|
"scope": "user",
|
||||||
|
"scopeId": "user_123",
|
||||||
|
"status": "pending",
|
||||||
|
"phase": "prepare",
|
||||||
|
"payload": {},
|
||||||
|
"progress": {
|
||||||
|
"summary": "等待 worker 執行",
|
||||||
|
"percentage": 20,
|
||||||
|
"steps": []
|
||||||
|
},
|
||||||
|
"result": null,
|
||||||
|
"error": null,
|
||||||
|
"attempt": 0,
|
||||||
|
"maxAttempts": 2,
|
||||||
|
"lockedBy": null,
|
||||||
|
"lockedUntil": null,
|
||||||
|
"cancelRequestedAt": null,
|
||||||
|
"cancelReason": null,
|
||||||
|
"scheduledAt": null,
|
||||||
|
"startedAt": null,
|
||||||
|
"completedAt": null,
|
||||||
|
"createAt": 0,
|
||||||
|
"updateAt": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `job_schedules`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "...",
|
||||||
|
"templateType": "demo_long_task",
|
||||||
|
"scope": "user",
|
||||||
|
"scopeId": "user_123",
|
||||||
|
"enabled": true,
|
||||||
|
"cron": "0 9 * * *",
|
||||||
|
"timezone": "Asia/Taipei",
|
||||||
|
"payloadTemplate": {},
|
||||||
|
"lastRunAt": null,
|
||||||
|
"nextRunAt": 0,
|
||||||
|
"createAt": 0,
|
||||||
|
"updateAt": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `job_events`
|
||||||
|
|
||||||
|
用來觀察與 audit:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "...",
|
||||||
|
"jobId": "...",
|
||||||
|
"type": "status_changed",
|
||||||
|
"from": "pending",
|
||||||
|
"to": "running",
|
||||||
|
"message": "worker claimed job",
|
||||||
|
"metadata": {},
|
||||||
|
"createAt": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Model
|
||||||
|
|
||||||
|
```text
|
||||||
|
pending = 已建立,等待 queue
|
||||||
|
queued = 已推進 Redis queue
|
||||||
|
running = worker 執行中
|
||||||
|
waiting_worker = 等外部 worker 回寫
|
||||||
|
cancel_requested = 使用者已要求取消,等待 worker cooperative stop
|
||||||
|
succeeded = 成功完成
|
||||||
|
failed = 最終失敗
|
||||||
|
cancelled = 使用者取消
|
||||||
|
expired = lock/timeout 過期後無法恢復
|
||||||
|
```
|
||||||
|
|
||||||
|
Step status:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pending | running | succeeded | failed | skipped | cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
## 取消語意
|
||||||
|
|
||||||
|
取消是第一版必做能力,採 cooperative cancellation:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
User[User] --> ApiCancel[CancelAPI]
|
||||||
|
ApiCancel --> Run[JobRun cancel_requested]
|
||||||
|
Run --> RedisCancel[RedisCancelSignal]
|
||||||
|
Worker[Worker] -->|"poll cancel flag"| Run
|
||||||
|
Worker --> Stop[StopCurrentStep]
|
||||||
|
Stop --> Final[JobRun cancelled]
|
||||||
|
```
|
||||||
|
|
||||||
|
規則:
|
||||||
|
|
||||||
|
- `pending` / `queued`:取消後直接變 `cancelled`,並盡量從 Redis queue 移除;若無法移除,worker claim 時必須檢查狀態並跳過。
|
||||||
|
- `running`:狀態改為 `cancel_requested`,寫入 `cancelRequestedAt` / `cancelReason`,worker 必須在 step 間或長任務 checkpoint 檢查取消旗標。
|
||||||
|
- `waiting_worker`:狀態改為 `cancel_requested`,同時寫 Redis cancel signal;外部 worker 回寫前要檢查 job 狀態。
|
||||||
|
- `succeeded` / `failed` / `cancelled` / `expired`:不可取消,回傳 ResourceInvalidState。
|
||||||
|
- worker 收到取消後呼叫 `AcknowledgeCancel(jobId, workerId)`,釋放 lock,寫入 `job_events`,狀態變 `cancelled`。
|
||||||
|
- 若 `cancel_requested` 超過 template 的 `cancelPolicy.graceSeconds`,scheduler/reaper 可標記為 `cancelled` 或 `expired`,第一版建議標記 `cancelled` 並記錄 timeout event。
|
||||||
|
|
||||||
|
## 狀態與 Lock 安全規則
|
||||||
|
|
||||||
|
第一版已把最容易出問題的 race condition 收斂在 repository / usecase:
|
||||||
|
|
||||||
|
- `ClaimNext` 只能從 `pending` / `queued` conditional update 成 `running`。如果 API 同時取消,Mongo update 會被拒絕。
|
||||||
|
- `RequestCancel` 只能從 cancellable 狀態 conditional update;`pending` / `queued` 直接變 `cancelled`,`running` / `waiting_worker` 變 `cancel_requested`。
|
||||||
|
- `CompleteRun` / `FailRun` / `UpdateProgress` 必須帶 `workerID`,並且只能更新 `lockedBy == workerID` 的 job。
|
||||||
|
- Redis `jobs:lock:<jobId>` 的 value 是 `workerID`;`ReleaseLock` / `RefreshLock` 使用 owner check,避免舊 worker 誤刪新 worker 的 lock。
|
||||||
|
- Worker 長任務要定期 heartbeat,呼叫 `RefreshRunLock(jobId, workerID, ttlSeconds)`。自訂 step handler 可用 `StepContext.Heartbeat`。
|
||||||
|
|
||||||
|
之後新增狀態轉移時,不要直接使用裸 `Update`;若是生命週期狀態,應新增明確的 guarded repository 方法或使用現有 conditional update。
|
||||||
|
|
||||||
|
## Redis Keys
|
||||||
|
|
||||||
|
```text
|
||||||
|
jobs:queue:<workerType> # list 或 stream,worker 消費
|
||||||
|
jobs:lock:<jobId> # lease lock
|
||||||
|
jobs:scheduler:lock # scheduler singleton lock
|
||||||
|
jobs:dedupe:<template>:<hash> # 防止同 scope 重複跑
|
||||||
|
jobs:cancel:<jobId> # cancel signal,worker checkpoint 讀取
|
||||||
|
```
|
||||||
|
|
||||||
|
第一版建議用 Redis List 即可,之後需要 ack/replay 再升級 Redis Streams。
|
||||||
|
|
||||||
|
## API 規劃
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /api/v1/job/templates
|
||||||
|
GET /api/v1/job/templates/:type
|
||||||
|
PUT /api/v1/job/templates/:type
|
||||||
|
|
||||||
|
POST /api/v1/jobs
|
||||||
|
GET /api/v1/jobs/:id
|
||||||
|
GET /api/v1/jobs?page=1&pageSize=20
|
||||||
|
POST /api/v1/jobs/:id/cancel
|
||||||
|
POST /api/v1/jobs/:id/retry
|
||||||
|
|
||||||
|
GET /api/v1/job/schedules?page=1&pageSize=20
|
||||||
|
POST /api/v1/job/schedules
|
||||||
|
PUT /api/v1/job/schedules/:id
|
||||||
|
POST /api/v1/job/schedules/:id/enable
|
||||||
|
POST /api/v1/job/schedules/:id/disable
|
||||||
|
```
|
||||||
|
|
||||||
|
所有列表回應使用目前標準:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 102000,
|
||||||
|
"message": "SUCCESS",
|
||||||
|
"data": {
|
||||||
|
"pagination": {
|
||||||
|
"total": 42,
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 10,
|
||||||
|
"totalPages": 5
|
||||||
|
},
|
||||||
|
"list": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 分層規劃
|
||||||
|
|
||||||
|
```text
|
||||||
|
internal/model/job/
|
||||||
|
domain/entity/template.go
|
||||||
|
domain/entity/run.go
|
||||||
|
domain/entity/schedule.go
|
||||||
|
domain/entity/event.go
|
||||||
|
domain/enum/status.go
|
||||||
|
domain/repository/*.go
|
||||||
|
domain/usecase/*.go
|
||||||
|
repository/mongo_*.go
|
||||||
|
repository/redis_queue.go
|
||||||
|
usecase/template_usecase.go
|
||||||
|
usecase/run_usecase.go
|
||||||
|
usecase/schedule_usecase.go
|
||||||
|
usecase/progress_usecase.go
|
||||||
|
usecase/worker_usecase.go
|
||||||
|
|
||||||
|
internal/logic/job/
|
||||||
|
create_job_logic.go
|
||||||
|
get_job_logic.go
|
||||||
|
list_jobs_logic.go
|
||||||
|
cancel_job_logic.go
|
||||||
|
retry_job_logic.go
|
||||||
|
template_logic.go
|
||||||
|
schedule_logic.go
|
||||||
|
|
||||||
|
internal/worker/job/
|
||||||
|
runner.go
|
||||||
|
scheduler.go
|
||||||
|
dispatcher.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template 執行規則
|
||||||
|
|
||||||
|
### 可不可以重複做
|
||||||
|
|
||||||
|
由 `repeatable` 與 `concurrencyPolicy` 控制:
|
||||||
|
|
||||||
|
```text
|
||||||
|
repeatable=false
|
||||||
|
同 dedupe key 完成過就不再建立
|
||||||
|
|
||||||
|
repeatable=true + reject_same_scope
|
||||||
|
已有 running/pending job 時拒絕建立
|
||||||
|
|
||||||
|
repeatable=true + allow_parallel
|
||||||
|
允許平行跑
|
||||||
|
|
||||||
|
repeatable=true + replace_existing
|
||||||
|
取消舊 job,建立新 job
|
||||||
|
```
|
||||||
|
|
||||||
|
### 最終狀態
|
||||||
|
|
||||||
|
Template 可定義成功條件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
successWhen = all_steps_succeeded
|
||||||
|
```
|
||||||
|
|
||||||
|
第一版只支援 `all_steps_succeeded`。之後再加 `any_step_succeeded` 或 conditional flow。
|
||||||
|
|
||||||
|
### 取消能力
|
||||||
|
|
||||||
|
Template 用 `cancelPolicy` 控制取消:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supported=false
|
||||||
|
API 不允許取消此 job
|
||||||
|
|
||||||
|
mode=cooperative
|
||||||
|
worker checkpoint 檢查取消旗標後收斂
|
||||||
|
|
||||||
|
graceSeconds=30
|
||||||
|
cancel_requested 超過 grace 後由 reaper 收斂
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 用 `cancelable` 控制目前步驟是否可立即停止。若目前 step 不可取消,worker 需要在 step 結束後停止後續 steps,最後狀態仍為 `cancelled`。
|
||||||
|
|
||||||
|
## 第一版建議實作順序
|
||||||
|
|
||||||
|
1. 建 `model/job` 的 enum/entity/repository interface。
|
||||||
|
2. 實作 Mongo repositories:template/run/schedule/event。
|
||||||
|
3. 實作 Redis queue/lock repository。
|
||||||
|
4. 實作 `CreateRun`:讀 template、檢查 repeat/concurrency、建立 run、push queue。
|
||||||
|
5. 實作 `ClaimNext`:worker 從 Redis 取 job,Mongo 設 lock/status。
|
||||||
|
6. 實作 `RequestCancel`:狀態轉 `cancel_requested` 或直接 `cancelled`,寫 Redis cancel signal 與 event。
|
||||||
|
7. 實作 `AcknowledgeCancel`:worker 收斂後釋放 lock,狀態轉 `cancelled`。
|
||||||
|
8. 實作 `UpdateProgress`、`Complete`、`Fail`、`Retry`。
|
||||||
|
9. 實作 schedule tick:掃 `job_schedules.nextRunAt <= now`,建立 JobRun。
|
||||||
|
10. 實作 API 與文件。
|
||||||
|
|
@ -0,0 +1,512 @@
|
||||||
|
# 海巡獲客計畫:知識圖譜 + 雙軌爬取 + 島民交接
|
||||||
|
|
||||||
|
> 在既有「背景 Job + 島民交接」上,新增 Topic Knowledge Graph(Brave 驅動)與**雙維度 Tag**(相關 + 近期)+ **雙軌海巡**,強化流程 B 的痛點發現、關鍵字精準度與**產品-痛點匹配**驗證。
|
||||||
|
|
||||||
|
## 北極星
|
||||||
|
|
||||||
|
海巡找到的貼文/留言,**你的產品是否真的解得了那個問題**(可置入、可回覆、可追蹤)。
|
||||||
|
|
||||||
|
流程 B 主線:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Brave 知識圖譜擴散(周邊延伸)
|
||||||
|
→ 衍生雙維度搜尋 tag(相關詞 + 近期求助詞)
|
||||||
|
→ 使用者手動勾選
|
||||||
|
→ 每個 tag 雙軌爬 Threads(相關軌 + 近期軌,7d 優先 / 30d 補充)
|
||||||
|
→ productFitScore 篩選
|
||||||
|
→ 島民協助撰寫獲客留言
|
||||||
|
```
|
||||||
|
|
||||||
|
## 已拍板決策
|
||||||
|
|
||||||
|
| 項目 | 決策 |
|
||||||
|
|------|------|
|
||||||
|
| 圖譜深度 | **3 層、範圍廣**:核心 → 成因/症狀 → 相鄰情境 |
|
||||||
|
| 進海巡的 tag | **使用者手動勾選**(圖譜 UI 多選;島民可 toggle,不預設全選) |
|
||||||
|
| 近期窗口 | **7 天內為重點**;不足時補充至 **30 天**;超過 30 天排除 |
|
||||||
|
| Brave 預算 | 中等,每輪知識擴展 **10–15 次查詢**(不足可 supplemental 1 輪) |
|
||||||
|
| 痛點 tag 候選 | 圖譜衍生 **≥12 候選**,其中痛點/求助類 **≥8** |
|
||||||
|
| 流程 A | 保留 `style-8d` 捷徑;matrix + 留言收集疊加於 Phase 2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 根因診斷(舊系統痛點少、關鍵字不準)
|
||||||
|
|
||||||
|
對照舊 Next.js([`lib/ai/prompts/research-map-placement.ts`](../../lib/ai/prompts/research-map-placement.ts)、[`lib/services/scan-tasks.ts`](../../lib/services/scan-tasks.ts)、[`lib/ai/analyze-topic.ts`](../../lib/ai/analyze-topic.ts)):
|
||||||
|
|
||||||
|
| 現象 | 根因 | 新系統對策 |
|
||||||
|
|------|------|------------|
|
||||||
|
| 痛點只抓到 1–2 個 | Placement 壓 `suggestedTags` 至 **2~4**;`PLACEMENT_QUERY_MAX = 8` | 圖譜衍生 ≥8 痛點 tag + supplemental 補充迴圈 |
|
||||||
|
| 關鍵字不夠精準 | AI 憑種子詞推測,無外部知識 | Brave `knowledge_expand` 建 TKG,節點附 `evidence[]` |
|
||||||
|
| 只有「最相關」 | Recency 只是加分,無獨立近期軌 | **Tag 層**分 `relevance` / `recency`;**Crawl 層**雙軌必跑 |
|
||||||
|
| 沒有周邊延伸 | Brave 只做 `site:threads.net` | `knowledge_expand` 做領域知識(成因、懷孕、換季…)再衍生 tag |
|
||||||
|
|
||||||
|
新後端原則見 [`AGENTS.md`](../AGENTS.md):**複製模式,不複製舊業務**——移植 Playwright/過濾規則,用 Mongo + Job 重建。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架構總覽
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph input [輸入]
|
||||||
|
Seed["種子詞"]
|
||||||
|
ProductBrief["product_brief"]
|
||||||
|
Persona["人設"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph tkg [知識圖譜]
|
||||||
|
ExpandJob["expand-graph job"]
|
||||||
|
BraveK["Brave knowledge_expand"]
|
||||||
|
TKG["topic_knowledge_graphs"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph derive [Tag衍生]
|
||||||
|
DeriveFn["deriveSearchTagsFromGraph"]
|
||||||
|
RelQ["relevanceQueries"]
|
||||||
|
RecQ["recencyQueries"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph select [使用者選擇]
|
||||||
|
GraphUI["圖譜 UI 勾選節點/tag"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph scan [海巡]
|
||||||
|
ScanJob["scan job 每tag雙軌"]
|
||||||
|
Posts["scan_posts"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph outcome [驗收]
|
||||||
|
Fit["productFitScore"]
|
||||||
|
Outreach["outreach + 島民留言"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Seed --> ExpandJob
|
||||||
|
ProductBrief --> ExpandJob
|
||||||
|
Persona --> ExpandJob
|
||||||
|
ExpandJob --> BraveK --> TKG
|
||||||
|
TKG --> DeriveFn
|
||||||
|
DeriveFn --> RelQ
|
||||||
|
DeriveFn --> RecQ
|
||||||
|
RelQ --> GraphUI
|
||||||
|
RecQ --> GraphUI
|
||||||
|
GraphUI --> ScanJob --> Posts --> Fit --> Outreach
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tag 產生完整流水線
|
||||||
|
|
||||||
|
Tag **不是** AI 一次吐 2~4 個,而是五段流水線產出:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
S["1 種子詞+brief"] --> A["2 AI核心地圖"]
|
||||||
|
A --> B["3 Brave knowledge_expand"]
|
||||||
|
B --> G["4 合成TKG三層"]
|
||||||
|
G --> D["5 deriveSearchTagsFromGraph"]
|
||||||
|
D --> R["relevanceQueries"]
|
||||||
|
D --> C["recencyQueries"]
|
||||||
|
```
|
||||||
|
|
||||||
|
| 步驟 | 做什麼 | 產出 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 1 | 讀 `seed_query`、`product_brief`、`target_audience` | 輸入包 |
|
||||||
|
| 2 | AI 產核心 questions/pillars/exclusions | 研究地圖骨架 |
|
||||||
|
| 3 | Brave 10–15 次**一般網搜**(非 threadsOnly) | snippets → 候選節點 |
|
||||||
|
| 4 | AI 合成 TKG(L0/L1/L2)+ `productFitScore` + `evidence[]` | `topic_knowledge_graphs` |
|
||||||
|
| 5 | 每節點壓成 2~8 字真人搜尋詞,分兩套 | `derivedTags` |
|
||||||
|
|
||||||
|
### 雙維度 Tag(相關 + 近期都要)
|
||||||
|
|
||||||
|
每個圖譜節點衍生:
|
||||||
|
|
||||||
|
| 維度 | 用途 | 寫法範例 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **`relevanceQueries`** | 相關軌:短詞、高命中 | `敏感肌`、`屏障受損` |
|
||||||
|
| **`recencyQueries`** | 近期軌:求助語境 + 時間窗 | `敏感肌 請問`、`換季泛紅 推薦` |
|
||||||
|
|
||||||
|
- `recencyQueries` 在 Brave `threads_discover` 時加 `after:{7天前日期}`(參考舊 [`scan-web-discover.ts`](../../lib/services/scan-web-discover.ts) `buildPlacementKeywordQueries`)
|
||||||
|
- 候選總量:**≥12 tag**(痛點/求助類 **≥8**);**使用者勾選後才 crawl**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 痛點 Tag 保底機制
|
||||||
|
|
||||||
|
解決「只抓到一兩個痛點」:
|
||||||
|
|
||||||
|
```text
|
||||||
|
expand-graph 完成 → deriveSearchTagsFromGraph
|
||||||
|
IF 痛點/求助類 tag 數 < 8:
|
||||||
|
→ supplemental_round(最多 1 次,Brave +5 查詢)
|
||||||
|
→ 追加查詢例:{seed} 困擾、{seed} 求助、{L2節點} 請問、{seed} 推薦
|
||||||
|
→ AI 補節點 + 補 derivedTags
|
||||||
|
IF 仍 < 8:
|
||||||
|
→ job 標 warning;UI + 島民提示「可重跑 expand 或手動加種子詞」
|
||||||
|
```
|
||||||
|
|
||||||
|
| 指標 | 舊系統 | 新系統 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| Placement suggestedTags | 2~4 | 不沿用此上限 |
|
||||||
|
| 搜尋任務上限 | 8 | 候選 ≥12,實 crawl = 勾選數 |
|
||||||
|
| 痛點類最低 | 無保證 | **≥8**(含 supplemental) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Topic Knowledge Graph(TKG)
|
||||||
|
|
||||||
|
### Mongo collection:`topic_knowledge_graphs`
|
||||||
|
|
||||||
|
綁 `persona_id` + `seed_query`。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"seed": "敏感肌",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "n1",
|
||||||
|
"label": "敏感肌",
|
||||||
|
"nodeKind": "pain",
|
||||||
|
"type": "core",
|
||||||
|
"layer": 0,
|
||||||
|
"placementValue": "high",
|
||||||
|
"productFitScore": 95,
|
||||||
|
"selectedForScan": false,
|
||||||
|
"evidence": [],
|
||||||
|
"derivedTags": {
|
||||||
|
"relevance": ["敏感肌"],
|
||||||
|
"recency": ["敏感肌 請問", "敏感肌 推薦"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n2",
|
||||||
|
"label": "懷孕嗅覺敏感",
|
||||||
|
"nodeKind": "cause",
|
||||||
|
"type": "cause",
|
||||||
|
"layer": 2,
|
||||||
|
"relation": "可能成因",
|
||||||
|
"placementValue": "medium",
|
||||||
|
"productFitScore": 40,
|
||||||
|
"selectedForScan": false,
|
||||||
|
"evidence": [{ "url": "...", "snippet": "..." }],
|
||||||
|
"derivedTags": {
|
||||||
|
"relevance": ["懷孕皮膚癢", "嗅覺敏感"],
|
||||||
|
"recency": ["懷孕 皮膚 癢 請益"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "n3",
|
||||||
|
"label": "屏障修復原理",
|
||||||
|
"nodeKind": "knowledge",
|
||||||
|
"type": "mechanism",
|
||||||
|
"layer": 1,
|
||||||
|
"productFitScore": 70,
|
||||||
|
"selectedForScan": false,
|
||||||
|
"derivedTags": {
|
||||||
|
"relevance": ["屏障受損"],
|
||||||
|
"recency": ["屏障受損 怎麼辦"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{ "from": "n1", "to": "n2", "relation": "可能因" },
|
||||||
|
{ "from": "n1", "to": "n3", "relation": "機制" }
|
||||||
|
],
|
||||||
|
"braveSources": [{ "query": "敏感肌 懷孕 原因", "snippet": "...", "url": "..." }],
|
||||||
|
"painTagCount": 9,
|
||||||
|
"generatedAt": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三層擴散
|
||||||
|
|
||||||
|
```text
|
||||||
|
L0 核心:敏感肌
|
||||||
|
L1 直接相關:屏障受損、換季泛紅、刺癢
|
||||||
|
L2 周邊情境:懷孕荷爾蒙、嗅覺敏感、壓力熬夜、換洗臉產品過敏 …
|
||||||
|
```
|
||||||
|
|
||||||
|
### 節點語意
|
||||||
|
|
||||||
|
| 欄位 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| `nodeKind` | `pain`(痛點/求助)、`knowledge`(科普延伸)、`cause`、`symptom` |
|
||||||
|
| `placementValue` | 建議優先級,**不決定是否海巡** |
|
||||||
|
| `selectedForScan` | 使用者勾選後 `true`,才進 `scan` payload |
|
||||||
|
| `productFitScore` | 依 `product_brief`:產品解不解得了 |
|
||||||
|
| `derivedTags` | `relevance` + `recency` 兩套查詢詞 |
|
||||||
|
| `evidence[]` | L1/L2 必填(Brave snippet 可追溯) |
|
||||||
|
|
||||||
|
- `knowledge` 節點:延伸話題/科普靈感,**預設不勾選**;若 snippet 含求助語境可升級為 `pain`
|
||||||
|
- `knowledge` 不強制進 placement crawl,除非使用者勾選且 `productFitScore` 達標
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brave 雙模式
|
||||||
|
|
||||||
|
| 模式 | `threadsOnly` | 用途 |
|
||||||
|
|------|---------------|------|
|
||||||
|
| `knowledge_expand` | `false` | 建 TKG,找成因/周邊/知識 |
|
||||||
|
| `threads_discover` | `true` | 海巡時找 Threads 貼文 |
|
||||||
|
|
||||||
|
### L0/L1 查詢模板(plan_queries,上限 15/輪)
|
||||||
|
|
||||||
|
```text
|
||||||
|
{seed} 常見原因
|
||||||
|
{seed} 什麼情況會
|
||||||
|
{seed} 初期 症狀
|
||||||
|
{seed} 怎麼改善 困擾
|
||||||
|
{seed} 求助 推薦
|
||||||
|
```
|
||||||
|
|
||||||
|
### L2 周邊擴散查詢池(從 brief/受眾推導)
|
||||||
|
|
||||||
|
```text
|
||||||
|
{seed} 懷孕 相關
|
||||||
|
{seed} 壓力 熬夜
|
||||||
|
{seed} 換產品 過敏
|
||||||
|
{seed} 與 {受眾場景} 的關係
|
||||||
|
{L1節點} 原因
|
||||||
|
{L1節點} 困擾
|
||||||
|
```
|
||||||
|
|
||||||
|
Brave 回傳 title/snippet/url → AI 萃取節點與邊 → 寫入 TKG。實作:`internal/library/knowledge/` + Brave adapter(`BRAVE_SEARCH_API_KEY`;參考舊 [`lib/services/web-search.ts`](../../lib/services/web-search.ts))。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整範例:敏感肌 Walkthrough
|
||||||
|
|
||||||
|
**輸入**
|
||||||
|
|
||||||
|
- 種子詞:`敏感肌`
|
||||||
|
- product_brief:溫和修護、無香料、適合敏感/屏障受損肌
|
||||||
|
|
||||||
|
**Brave knowledge_expand(節錄)**
|
||||||
|
|
||||||
|
| 查詢 | snippet 線索 | 圖譜節點 |
|
||||||
|
|------|--------------|----------|
|
||||||
|
| `敏感肌 常見原因` | 屏障受損、過度清潔 | L1 symptom `屏障受損` |
|
||||||
|
| `敏感肌 懷孕` | 荷爾蒙、嗅覺/皮膚變敏感 | L2 cause `懷孕嗅覺敏感` |
|
||||||
|
| `換季 皮膚 泛紅` | 季節性刺激 | L1 symptom `換季泛紅` |
|
||||||
|
|
||||||
|
**衍生 tag(候選,勾選前不 crawl)**
|
||||||
|
|
||||||
|
| 節點 | relevanceQuery | recencyQuery | productFit |
|
||||||
|
|------|----------------|--------------|------------|
|
||||||
|
| 敏感肌 | `敏感肌` | `敏感肌 請問` | 95 |
|
||||||
|
| 屏障受損 | `屏障受損` | `屏障受損 推薦` | 90 |
|
||||||
|
| 換季泛紅 | `換季泛紅` | `換季泛紅 請問` | 88 |
|
||||||
|
| 懷孕皮膚癢 | `懷孕皮膚癢` | `懷孕 皮膚 癢 請益` | 視產品而定 |
|
||||||
|
|
||||||
|
**使用者**:勾選 productFit 高的 4 個節點(可不勾懷孕若產品不適用)
|
||||||
|
|
||||||
|
**startScan**:每個勾選節點的 relevance + recency 詞都跑雙軌
|
||||||
|
|
||||||
|
| 軌道 | 行為 | 本例預期 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 相關軌 | sort=relevance, limit≈12 | 高互動痛點貼文 |
|
||||||
|
| 近期軌 | 7d 優先,不足補 30d | 一週內求助帖 |
|
||||||
|
|
||||||
|
**合併** → gold / recent / relevant → `productFitScore` → 獲客台 → 島民 `generateOutreachReply` + fill
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 近期窗口
|
||||||
|
|
||||||
|
| 窗口 | 天數 | 行為 |
|
||||||
|
|------|------|------|
|
||||||
|
| **重點** | 7 天內 | 優先爬取、優先顯示、排序最高 |
|
||||||
|
| **補充** | 8~30 天 | 7 天內不足時才補,排序較低 |
|
||||||
|
| **排除** | >30 天 | 不進海巡與獲客清單 |
|
||||||
|
|
||||||
|
策略:
|
||||||
|
|
||||||
|
1. 每個勾選 tag 的**近期軌**先抓滿 7 天名額
|
||||||
|
2. 全輪痛點貼文不足目標時,自動放寬至 30 天
|
||||||
|
3. 獲客台預設篩「7 天內」,可切「含 30 天內補充」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 雙軌海巡(Tag + Crawl + UI 三層對齊)
|
||||||
|
|
||||||
|
**近期軌不是相關軌的副產品**——每個勾選 tag 的 relevance 與 recency 查詢都**必跑**。
|
||||||
|
|
||||||
|
| 層級 | 相關 | 近期 |
|
||||||
|
|------|------|------|
|
||||||
|
| **Tag** | `derivedTags.relevance` 短詞高命中 | `derivedTags.recency` 求助語境 + after 日期 |
|
||||||
|
| **Crawl** | 相關軌 sort=relevance, limit≈12 | 近期軌 7d 滿額 → 30d 補 |
|
||||||
|
| **UI** | 可篩 `priority=relevant` | 預設 7d + `priority=gold` 置頂 |
|
||||||
|
|
||||||
|
合併優先級:
|
||||||
|
|
||||||
|
1. 兩軌皆有 → `gold`
|
||||||
|
2. 僅近期軌 → `recent`
|
||||||
|
3. 僅相關軌 → `relevant`
|
||||||
|
|
||||||
|
過濾:移植 `hasPlacementIntent`、`looksLikeCasualChat`(舊 [`lib/topic-anchor.ts`](../../lib/topic-anchor.ts)、[`lib/scan-recency.ts`](../../lib/scan-recency.ts))。
|
||||||
|
|
||||||
|
### scan_posts 擴充欄位
|
||||||
|
|
||||||
|
- `placement_score`、`priority`(gold/recent/relevant)
|
||||||
|
- `product_fit_score`、`solved_by_product`
|
||||||
|
- `posted_at`、`search_tag`、`query_dimension`(relevance/recency)
|
||||||
|
- `graph_node_id`
|
||||||
|
- `replies[]`(可選,`scrape_replies: true`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 產品匹配驗收
|
||||||
|
|
||||||
|
每篇海巡結果:
|
||||||
|
|
||||||
|
- **`productFitScore`**:痛點 vs `product_brief`
|
||||||
|
- **`solvedByProduct`**:獲客留言是否對應產品能力(生成時強制檢查)
|
||||||
|
|
||||||
|
獲客台 UI:
|
||||||
|
|
||||||
|
- 預設排序:7 天內 + 產品能解決
|
||||||
|
- 標示:可置入 / 需人工 / 超出產品範圍
|
||||||
|
- 獲客留言:**島民 fill 全文,不自動送出**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 島民交接
|
||||||
|
|
||||||
|
### job.result.handoff
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"handoff": {
|
||||||
|
"flow": "placement",
|
||||||
|
"persona_id": "...",
|
||||||
|
"pain_tag_count": 9,
|
||||||
|
"summary": "12 候選 tag → 勾選 6 節點 → 38 篇;痛點 10(核心 6 + 周邊 4);7 天內 8 篇",
|
||||||
|
"pain_breakdown": { "core": 6, "peripheral": 4, "recent_7d": 8 },
|
||||||
|
"top_peripheral_hits": ["懷孕皮膚癢", "換季泛紅"],
|
||||||
|
"next_route": "/personas/:id/outreach",
|
||||||
|
"needs_supplemental_expand": false,
|
||||||
|
"connection_required": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
JobMonitor → `islanderHandoffStore`;[`buildIslanderContext`](../web/src/lib/islander/buildIslanderContext.ts) 注入【近期海巡交接】。
|
||||||
|
|
||||||
|
### Custom actions
|
||||||
|
|
||||||
|
| Action | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `expandKnowledgeGraph` | 觸發 `expand-graph`;`supplemental=true` 補充迴圈 |
|
||||||
|
| `toggleGraphNode` | 勾選/取消節點 |
|
||||||
|
| `startScan` | `dual_track=true`,只爬 `selectedForScan` 節點 |
|
||||||
|
| `generateOutreachReply` | 產獲客留言 |
|
||||||
|
| `applyDraft` | fill 留言欄位 |
|
||||||
|
|
||||||
|
### 對話路徑(流程 B)
|
||||||
|
|
||||||
|
```text
|
||||||
|
「幫我找敏感肌的痛點」
|
||||||
|
→ expand-graph(Brave knowledge_expand + AI 合成 TKG)
|
||||||
|
→ IF pain_tag_count < 8 → 島民:「要再補一輪 Brave 嗎?」→ supplemental_round
|
||||||
|
→ 研究頁:圖譜 + 雙維度 tag + productFitScore
|
||||||
|
→ 使用者手動勾選節點
|
||||||
|
→ startScan(每詞雙軌:相關 + 近期,7d/30d)
|
||||||
|
→ outreach → highlight gold/recent → generateOutreachReply + fill
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Job 模板
|
||||||
|
|
||||||
|
| Template | Steps | worker |
|
||||||
|
|----------|-------|--------|
|
||||||
|
| `expand-graph` | plan_queries → brave_knowledge → ai_synth → derive_tags → [supplemental?] → persist_tkg | go |
|
||||||
|
| `scan` | session → crawl_dual_track → replies? → store → filter → ai_fit → persist | node + go |
|
||||||
|
| `style-8d` | (既有) | node + go |
|
||||||
|
|
||||||
|
執行順序:**expand-graph → 勾選 tag → scan**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 草案
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/v1/personas/:id/knowledge-graph/expand # ?supplemental=true
|
||||||
|
GET /api/v1/personas/:id/knowledge-graph
|
||||||
|
PATCH /api/v1/personas/:id/knowledge-graph/nodes # selectedForScan
|
||||||
|
|
||||||
|
POST /api/v1/personas/:id/scan-jobs # graph_id, selected_node_ids, dual_track
|
||||||
|
GET /api/v1/personas/:id/scan-posts # recent_7d, product_fit_min, priority
|
||||||
|
|
||||||
|
POST /api/v1/personas/:id/outreach-drafts/generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Internal worker:`POST /workers/scan-posts/batch`、Brave/AI 內部端點。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端頁面
|
||||||
|
|
||||||
|
| 路徑 | 用途 | 島民 label |
|
||||||
|
|------|------|------------|
|
||||||
|
| `/personas/:id/research` | 圖譜、雙維度 tag、勾選 | 加入海巡、Brave 再擴展 |
|
||||||
|
| `/personas/:id/outreach` | 獲客貼文 + 留言 | 獲客留言、標記已處理 |
|
||||||
|
| `/personas/:id/matrix` | 流程 A(Phase 2) | 草稿內容 |
|
||||||
|
|
||||||
|
研究頁每節點展示:`relevanceQueries`、`recencyQueries`、`productFitScore`、勾選框、上次命中數。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 實作分期
|
||||||
|
|
||||||
|
### Phase 0a — 知識圖譜 + Tag 流水線
|
||||||
|
|
||||||
|
- [ ] Go Brave adapter(`knowledge_expand` / `threads_discover`)
|
||||||
|
- [ ] `expand-graph` job:plan_queries → brave → ai_synth → derive_tags
|
||||||
|
- [ ] `supplemental_round`(痛點 tag < 8)
|
||||||
|
- [ ] Mongo `topic_knowledge_graphs`(含 `derivedTags`、`painTagCount`)
|
||||||
|
- [ ] `deriveSearchTagsFromGraph`(relevance + recency 雙陣列)
|
||||||
|
- [ ] API expand / get / patch nodes
|
||||||
|
|
||||||
|
### Phase 0b — 島民 handoff
|
||||||
|
|
||||||
|
- [ ] handoff(含 `pain_tag_count`、`needs_supplemental_expand`)
|
||||||
|
- [ ] JobMonitor bridge
|
||||||
|
- [ ] custom actions + `ai.islander.system.md` 海巡專章
|
||||||
|
|
||||||
|
### Phase 1 — 雙軌 scan + 流程 B
|
||||||
|
|
||||||
|
- [ ] Node `crawl_dual_track`(每 tag 相關+近期,7d/30d)
|
||||||
|
- [ ] `productFitScore` + outreach UI
|
||||||
|
- [ ] **驗收**:敏感肌 → L2(懷孕等)→ 候選痛點 tag ≥8 → 勾選後貼文痛點 ≥8 → 7d 內 ≥5
|
||||||
|
|
||||||
|
### Phase 2 — 流程 A
|
||||||
|
|
||||||
|
- [ ] matrix + 留言收集 + 島民 fill
|
||||||
|
|
||||||
|
### Phase 3 — 自動化
|
||||||
|
|
||||||
|
- [ ] job_schedules、Brave 熔斷、Meta API 發留言
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 風險
|
||||||
|
|
||||||
|
| 議題 | 對策 |
|
||||||
|
|------|------|
|
||||||
|
| Brave 幻覺 | 節點必須有 `evidence[]` |
|
||||||
|
| 圖譜跑題 | exclusions + `productFitScore` |
|
||||||
|
| 查詢爆炸 | Brave ≤15/輪;supplemental ≤5;衍生 ≤20;只爬勾選 |
|
||||||
|
| 醫療敏感 | `disclaimer`;留言不自動發 |
|
||||||
|
| 周邊節點產品不符 | 低 productFit 預設不勾;獲客台標 ✗ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 參考
|
||||||
|
|
||||||
|
- 舊海巡:[`lib/services/scan.ts`](../../lib/services/scan.ts)
|
||||||
|
- 舊網搜:[`lib/services/scan-web-discover.ts`](../../lib/services/scan-web-discover.ts)
|
||||||
|
- 舊研究地圖:[`lib/ai/analyze-topic.ts`](../../lib/ai/analyze-topic.ts)
|
||||||
|
- Job 系統:[`docs/job-system-plan.md`](./job-system-plan.md)
|
||||||
|
- 島民:[`internal/library/prompt/files/ai.islander.system.md`](../internal/library/prompt/files/ai.islander.system.md)
|
||||||
|
- 既有 8D:[`worker/style-8d-worker.ts`](../worker/style-8d-worker.ts)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://127.0.0.1:27017
|
||||||
|
Database: haixun_dev
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: 127.0.0.1:6379
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: haixun-dev-access-secret-change-me
|
||||||
|
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: true
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://mongo:27017
|
||||||
|
Database: haixun
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: redis:6379
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: change-me-in-prod
|
||||||
|
RefreshSecret: change-me-in-prod-too
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: change-me-worker-secret
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: false
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-worker
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8891
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://mongo:27017
|
||||||
|
Database: haixun
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: redis:6379
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: change-me-in-prod
|
||||||
|
RefreshSecret: change-me-in-prod-too
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: change-me-worker-secret
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: true
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://127.0.0.1:27017
|
||||||
|
Database: haixun
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: 127.0.0.1:6379
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: haixun-dev-access-secret-change-me
|
||||||
|
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: true
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: true
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"haixun-backend/internal/config"
|
||||||
|
"haixun-backend/internal/handler"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/conf"
|
||||||
|
"github.com/zeromicro/go-zero/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configFile = flag.String("f", "etc/gateway.yaml", "config file")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var c config.Config
|
||||||
|
conf.MustLoad(*configFile, &c)
|
||||||
|
|
||||||
|
server := rest.MustNewServer(c.RestConf)
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
sc := svc.NewServiceContext(c)
|
||||||
|
defer sc.Close(context.Background())
|
||||||
|
|
||||||
|
handler.RegisterHandlers(server, sc)
|
||||||
|
|
||||||
|
fmt.Printf("Starting backend backend at %s:%d...\n", c.Host, c.Port)
|
||||||
|
server.Start()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
AIMessage {
|
||||||
|
Role string `json:"role" validate:"required,oneof=system user assistant"` // 訊息角色
|
||||||
|
Content string `json:"content" validate:"required"` // 訊息內容
|
||||||
|
}
|
||||||
|
AIChatReq {
|
||||||
|
Provider string `json:"provider" validate:"required,oneof=opencode-go xai"` // AI provider
|
||||||
|
Model string `json:"model" validate:"required"` // 模型 ID
|
||||||
|
System string `json:"system,optional"` // system prompt
|
||||||
|
Messages []AIMessage `json:"messages" validate:"required,min=1,dive"` // 對話訊息
|
||||||
|
Temperature *float64 `json:"temperature,optional"` // 溫度
|
||||||
|
MaxTokens *int `json:"max_tokens,optional"` // 最大輸出 token
|
||||||
|
}
|
||||||
|
AIProviderOption {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Streams bool `json:"streams"`
|
||||||
|
}
|
||||||
|
AIProvidersData {
|
||||||
|
Providers []AIProviderOption `json:"providers"`
|
||||||
|
}
|
||||||
|
AIProviderPath {
|
||||||
|
Provider string `path:"provider" validate:"required,oneof=opencode-go xai"` // 要查模型的 provider
|
||||||
|
}
|
||||||
|
AIProviderModelsData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
Streams bool `json:"streams"`
|
||||||
|
Error string `json:"error,optional"` // 拉 models 失敗時的摘要
|
||||||
|
}
|
||||||
|
AIChatData {
|
||||||
|
Text string `json:"text"`
|
||||||
|
FinishReason string `json:"finish_reason,optional"`
|
||||||
|
}
|
||||||
|
IslanderChatReq {
|
||||||
|
Messages []AIMessage `json:"messages" validate:"required,min=1,dive"` // 對話訊息
|
||||||
|
Context string `json:"context,optional"` // 目前頁面與站內導覽快照
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server (
|
||||||
|
group: ai
|
||||||
|
prefix: /api/v1/ai
|
||||||
|
tags: "AI - Provider Streaming"
|
||||||
|
summary: "Public AI provider catalog"
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listAiProviders
|
||||||
|
get /providers returns (AIProvidersData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@server (
|
||||||
|
group: ai
|
||||||
|
prefix: /api/v1/ai
|
||||||
|
middleware: MemberAuth
|
||||||
|
tags: "AI - Provider Streaming (member)"
|
||||||
|
summary: "Chat/stream/models; member JWT via X-Member-Authorization; provider API key via Authorization"
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listAiProviderModels
|
||||||
|
post /providers/:provider/models (AIProviderPath) returns (AIProviderModelsData)
|
||||||
|
|
||||||
|
@handler chat
|
||||||
|
post /chat (AIChatReq) returns (AIChatData)
|
||||||
|
|
||||||
|
@handler chatStream
|
||||||
|
post /chat/stream (AIChatReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
@server (
|
||||||
|
group: ai
|
||||||
|
prefix: /api/v1/ai
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "AI - Islander Guide"
|
||||||
|
summary: "Floating islander chat; member JWT via Authorization; AI key from member settings"
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler islanderChatStream
|
||||||
|
post /islander/chat/stream (IslanderChatReq)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
AuthRegisterReq {
|
||||||
|
TenantID string `json:"tenant_id" validate:"required"`
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Password string `json:"password" validate:"required,min=8,max=128"`
|
||||||
|
DisplayName string `json:"display_name,optional"`
|
||||||
|
Language string `json:"language,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthLoginReq {
|
||||||
|
TenantID string `json:"tenant_id" validate:"required"`
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Password string `json:"password" validate:"required,min=8,max=128"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthRefreshReq {
|
||||||
|
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthTokenData {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
LogoutData {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: auth
|
||||||
|
prefix: /api/v1/auth
|
||||||
|
tags: "Auth"
|
||||||
|
summary: "Native member auth and JWT token endpoints"
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler register
|
||||||
|
post /register (AuthRegisterReq) returns (AuthTokenData)
|
||||||
|
|
||||||
|
@handler login
|
||||||
|
post /login (AuthLoginReq) returns (AuthTokenData)
|
||||||
|
|
||||||
|
@handler refresh
|
||||||
|
post /refresh (AuthRefreshReq) returns (AuthTokenData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: auth
|
||||||
|
prefix: /api/v1/auth
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Auth"
|
||||||
|
summary: "Logout requires member Bearer JWT"
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler logout
|
||||||
|
post /logout returns (LogoutData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,445 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
ResearchItemData {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Snippet string `json:"snippet,omitempty"`
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ResearchMapData {
|
||||||
|
AudienceSummary string `json:"audience_summary,omitempty"`
|
||||||
|
ContentGoal string `json:"content_goal,omitempty"`
|
||||||
|
Questions []string `json:"questions,omitempty"`
|
||||||
|
Pillars []string `json:"pillars,omitempty"`
|
||||||
|
Exclusions []string `json:"exclusions,omitempty"`
|
||||||
|
ResearchItems []ResearchItemData `json:"research_items,omitempty"`
|
||||||
|
ExpandStrategy string `json:"expand_strategy,omitempty"`
|
||||||
|
PatrolKeywords []string `json:"patrol_keywords,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeGraphEvidenceData {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Snippet string `json:"snippet,omitempty"`
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BraveSourceData {
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Snippet string `json:"snippet,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandProductData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
ProductContext string `json:"product_context"`
|
||||||
|
MatchTags []string `json:"match_tags,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
TopicName string `json:"topic_name,omitempty"`
|
||||||
|
SeedQuery string `json:"seed_query,omitempty"`
|
||||||
|
Brief string `json:"brief,omitempty"`
|
||||||
|
ProductBrief string `json:"product_brief,omitempty"`
|
||||||
|
ProductContext string `json:"product_context,omitempty"`
|
||||||
|
ProductID string `json:"product_id,omitempty"`
|
||||||
|
Products []BrandProductData `json:"products,omitempty"`
|
||||||
|
TargetAudience string `json:"target_audience,omitempty"`
|
||||||
|
Goals string `json:"goals,omitempty"`
|
||||||
|
ResearchMap ResearchMapData `json:"research_map,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListBrandProductsData {
|
||||||
|
List []BrandProductData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateBrandProductReq {
|
||||||
|
Label string `json:"label" validate:"required"`
|
||||||
|
ProductContext string `json:"product_context" validate:"required"`
|
||||||
|
MatchTags []string `json:"match_tags,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBrandProductReq {
|
||||||
|
Label *string `json:"label,optional"`
|
||||||
|
ProductContext *string `json:"product_context,optional"`
|
||||||
|
MatchTags []string `json:"match_tags,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandProductPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
ProductID string `path:"productId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateBrandProductHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
CreateBrandProductReq
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBrandProductHandlerReq {
|
||||||
|
BrandProductPath
|
||||||
|
UpdateBrandProductReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListBrandsData {
|
||||||
|
List []BrandData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateBrandReq {
|
||||||
|
DisplayName string `json:"display_name,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBrandReq {
|
||||||
|
DisplayName *string `json:"display_name,optional"`
|
||||||
|
TopicName *string `json:"topic_name,optional"`
|
||||||
|
SeedQuery *string `json:"seed_query,optional"`
|
||||||
|
Brief *string `json:"brief,optional"`
|
||||||
|
ProductBrief *string `json:"product_brief,optional"`
|
||||||
|
ProductContext *string `json:"product_context,optional"`
|
||||||
|
ProductID *string `json:"product_id,optional"`
|
||||||
|
TargetAudience *string `json:"target_audience,optional"`
|
||||||
|
Goals *string `json:"goals,optional"`
|
||||||
|
AudienceSummary *string `json:"audience_summary,optional"`
|
||||||
|
ContentGoal *string `json:"content_goal,optional"`
|
||||||
|
Questions []string `json:"questions,optional"`
|
||||||
|
Pillars []string `json:"pillars,optional"`
|
||||||
|
Exclusions []string `json:"exclusions,optional"`
|
||||||
|
PatrolKeywords []string `json:"patrol_keywords,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeGraphNodeData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
NodeKind string `json:"node_kind"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Layer int `json:"layer"`
|
||||||
|
Relation string `json:"relation,omitempty"`
|
||||||
|
PlacementValue string `json:"placement_value,omitempty"`
|
||||||
|
ProductFitScore int `json:"product_fit_score"`
|
||||||
|
SelectedForScan bool `json:"selected_for_scan"`
|
||||||
|
RelevanceTags []string `json:"relevance_tags"`
|
||||||
|
RecencyTags []string `json:"recency_tags"`
|
||||||
|
Evidence []KnowledgeGraphEvidenceData `json:"evidence,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeGraphEdgeData {
|
||||||
|
From string `json:"from"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeGraphData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
BrandID string `json:"brand_id"`
|
||||||
|
Seed string `json:"seed"`
|
||||||
|
Nodes []KnowledgeGraphNodeData `json:"nodes"`
|
||||||
|
Edges []KnowledgeGraphEdgeData `json:"edges"`
|
||||||
|
BraveSources []BraveSourceData `json:"brave_sources,omitempty"`
|
||||||
|
ExpandStrategy string `json:"expand_strategy,omitempty"`
|
||||||
|
PainTagCount int `json:"pain_tag_count"`
|
||||||
|
GeneratedAt int64 `json:"generated_at"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandKnowledgeGraphReq {
|
||||||
|
SeedQuery string `json:"seed_query" validate:"required"`
|
||||||
|
Supplemental bool `json:"supplemental,optional"`
|
||||||
|
RegenerateMap bool `json:"regenerate_map,optional"`
|
||||||
|
ExpandStrategy string `json:"expand_strategy,optional"` // brave | llm | hybrid
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandKnowledgeGraphData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeGraphNodeUpdate {
|
||||||
|
NodeID string `json:"node_id" validate:"required"`
|
||||||
|
SelectedForScan *bool `json:"selected_for_scan,optional"`
|
||||||
|
RelevanceTags []string `json:"relevance_tags,optional"`
|
||||||
|
RecencyTags []string `json:"recency_tags,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchKnowledgeGraphNodesReq {
|
||||||
|
Updates []KnowledgeGraphNodeUpdate `json:"updates" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartBrandScanJobReq {
|
||||||
|
GraphID string `json:"graph_id,optional"`
|
||||||
|
NodeIDs []string `json:"node_ids,optional"`
|
||||||
|
DualTrack bool `json:"dual_track,optional"`
|
||||||
|
PatrolMode bool `json:"patrol_mode,optional"`
|
||||||
|
PatrolKeywords []string `json:"patrol_keywords,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartBrandScanJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListBrandScanPostsReq {
|
||||||
|
Priority string `form:"priority,optional"`
|
||||||
|
Recent7d bool `form:"recent_7d,optional"`
|
||||||
|
ProductFitMin int `form:"product_fit_min,optional"`
|
||||||
|
Limit int `form:"limit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanPostData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
GraphNodeID string `json:"graph_node_id"`
|
||||||
|
SearchTag string `json:"search_tag"`
|
||||||
|
QueryDimension string `json:"query_dimension"`
|
||||||
|
ExternalID string `json:"external_id"`
|
||||||
|
Permalink string `json:"permalink"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
PlacementScore int `json:"placement_score"`
|
||||||
|
ProductFitScore int `json:"product_fit_score"`
|
||||||
|
SolvedByProduct bool `json:"solved_by_product"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
ScanJobID string `json:"scan_job_id"`
|
||||||
|
OutreachStatus string `json:"outreach_status,omitempty"`
|
||||||
|
PublishedReplyID string `json:"published_reply_id,omitempty"`
|
||||||
|
PublishedPermalink string `json:"published_permalink,omitempty"`
|
||||||
|
OutreachUpdateAt int64 `json:"outreach_update_at,omitempty"`
|
||||||
|
PostedAt string `json:"posted_at,omitempty"`
|
||||||
|
Replies []ScanReplyData `json:"replies,omitempty"`
|
||||||
|
LatestDraft *GenerateOutreachDraftsData `json:"latest_draft,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListBrandScanPostsData {
|
||||||
|
List []ScanPostData `json:"list"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOutreachDraftsReq {
|
||||||
|
ScanPostID string `json:"scan_post_id" validate:"required"`
|
||||||
|
TopicID string `json:"topic_id,optional"`
|
||||||
|
Count int `json:"count,optional"`
|
||||||
|
VoicePersonaID string `json:"voice_persona_id,optional"`
|
||||||
|
ProductID string `json:"product_id,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
OutreachDraftItemData {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Angle string `json:"angle"`
|
||||||
|
Rationale string `json:"rationale"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOutreachDraftsData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ScanPostID string `json:"scan_post_id"`
|
||||||
|
Relevance float64 `json:"relevance"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Drafts []OutreachDraftItemData `json:"drafts"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishOutreachDraftReq {
|
||||||
|
ScanPostID string `json:"scan_post_id" validate:"required"`
|
||||||
|
Text string `json:"text" validate:"required"`
|
||||||
|
Confirm bool `json:"confirm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishOutreachDraftData {
|
||||||
|
ScanPostID string `json:"scan_post_id"`
|
||||||
|
ReplyID string `json:"reply_id"`
|
||||||
|
Permalink string `json:"permalink"`
|
||||||
|
OutreachStatus string `json:"outreach_status"`
|
||||||
|
PublishedPermalink string `json:"published_permalink"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchScanPostOutreachReq {
|
||||||
|
OutreachStatus *string `json:"outreach_status,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentMatrixRowData {
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
SearchTag string `json:"search_tag"`
|
||||||
|
Angle string `json:"angle"`
|
||||||
|
Hook string `json:"hook"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
ReferenceNotes string `json:"reference_notes"`
|
||||||
|
SourcePermalinks []string `json:"source_permalinks"`
|
||||||
|
Rationale string `json:"rationale"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentMatrixData {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
BrandID string `json:"brand_id"`
|
||||||
|
Rows []ContentMatrixRowData `json:"rows"`
|
||||||
|
GeneratedAt int64 `json:"generated_at"`
|
||||||
|
CreateAt int64 `json:"create_at,omitempty"`
|
||||||
|
UpdateAt int64 `json:"update_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateContentMatrixReq {
|
||||||
|
Count int `json:"count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanReplyData {
|
||||||
|
ExternalID string `json:"external_id,omitempty"`
|
||||||
|
Author string `json:"author,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Permalink string `json:"permalink,omitempty"`
|
||||||
|
LikeCount int `json:"like_count,omitempty"`
|
||||||
|
PostedAt string `json:"posted_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BrandScanScheduleData {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
BrandID string `json:"brand_id"`
|
||||||
|
Cron string `json:"cron"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
NextRunAt int64 `json:"next_run_at,omitempty"`
|
||||||
|
LastRunAt int64 `json:"last_run_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertBrandScanScheduleReq {
|
||||||
|
Cron string `json:"cron,optional"`
|
||||||
|
Timezone string `json:"timezone,optional"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBrandHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
UpdateBrandReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandKnowledgeGraphHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
ExpandKnowledgeGraphReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchKnowledgeGraphNodesHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
PatchKnowledgeGraphNodesReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartBrandScanJobHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
StartBrandScanJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListBrandScanPostsHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
ListBrandScanPostsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOutreachDraftsHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
GenerateOutreachDraftsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishOutreachDraftHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
PublishOutreachDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchScanPostOutreachHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
PostID string `path:"postId"`
|
||||||
|
PatchScanPostOutreachReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateContentMatrixHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
GenerateContentMatrixReq
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertBrandScanScheduleHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
UpsertBrandScanScheduleReq
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: brand
|
||||||
|
prefix: /api/v1/brands
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Brand"
|
||||||
|
summary: "Brand profiles for placement workflow. Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listBrands
|
||||||
|
get / returns (ListBrandsData)
|
||||||
|
|
||||||
|
@handler createBrand
|
||||||
|
post / (CreateBrandReq) returns (BrandData)
|
||||||
|
|
||||||
|
@handler getBrand
|
||||||
|
get /:id (BrandPath) returns (BrandData)
|
||||||
|
|
||||||
|
@handler updateBrand
|
||||||
|
patch /:id (UpdateBrandHandlerReq) returns (BrandData)
|
||||||
|
|
||||||
|
@handler deleteBrand
|
||||||
|
delete /:id (BrandPath)
|
||||||
|
|
||||||
|
@handler listBrandProducts
|
||||||
|
get /:id/products (BrandPath) returns (ListBrandProductsData)
|
||||||
|
|
||||||
|
@handler createBrandProduct
|
||||||
|
post /:id/products (CreateBrandProductHandlerReq) returns (BrandProductData)
|
||||||
|
|
||||||
|
@handler updateBrandProduct
|
||||||
|
patch /:id/products/:productId (UpdateBrandProductHandlerReq) returns (BrandProductData)
|
||||||
|
|
||||||
|
@handler deleteBrandProduct
|
||||||
|
delete /:id/products/:productId (BrandProductPath)
|
||||||
|
|
||||||
|
@handler expandKnowledgeGraph
|
||||||
|
post /:id/knowledge-graph/expand (ExpandKnowledgeGraphHandlerReq) returns (ExpandKnowledgeGraphData)
|
||||||
|
|
||||||
|
@handler getKnowledgeGraph
|
||||||
|
get /:id/knowledge-graph (BrandPath) returns (KnowledgeGraphData)
|
||||||
|
|
||||||
|
@handler patchKnowledgeGraphNodes
|
||||||
|
patch /:id/knowledge-graph/nodes (PatchKnowledgeGraphNodesHandlerReq) returns (KnowledgeGraphData)
|
||||||
|
|
||||||
|
@handler startBrandScanJob
|
||||||
|
post /:id/scan-jobs (StartBrandScanJobHandlerReq) returns (StartBrandScanJobData)
|
||||||
|
|
||||||
|
@handler listBrandScanPosts
|
||||||
|
get /:id/scan-posts (ListBrandScanPostsHandlerReq) returns (ListBrandScanPostsData)
|
||||||
|
|
||||||
|
@handler generateOutreachDrafts
|
||||||
|
post /:id/outreach-drafts/generate (GenerateOutreachDraftsHandlerReq) returns (GenerateOutreachDraftsData)
|
||||||
|
|
||||||
|
@handler publishOutreachDraft
|
||||||
|
post /:id/outreach-drafts/publish (PublishOutreachDraftHandlerReq) returns (PublishOutreachDraftData)
|
||||||
|
|
||||||
|
@handler patchScanPostOutreach
|
||||||
|
patch /:id/scan-posts/:postId (PatchScanPostOutreachHandlerReq) returns (ScanPostData)
|
||||||
|
|
||||||
|
@handler getBrandContentMatrix
|
||||||
|
get /:id/content-matrix (BrandPath) returns (ContentMatrixData)
|
||||||
|
|
||||||
|
@handler generateBrandContentMatrix
|
||||||
|
post /:id/content-matrix/generate (GenerateContentMatrixHandlerReq) returns (ContentMatrixData)
|
||||||
|
|
||||||
|
@handler getBrandScanSchedule
|
||||||
|
get /:id/scan-schedule (BrandPath) returns (BrandScanScheduleData)
|
||||||
|
|
||||||
|
@handler upsertBrandScanSchedule
|
||||||
|
put /:id/scan-schedule (UpsertBrandScanScheduleHandlerReq) returns (BrandScanScheduleData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type ErrorDetail {
|
||||||
|
BizCode string `json:"biz_code,optional"`
|
||||||
|
Scope int64 `json:"scope,optional"`
|
||||||
|
Category int64 `json:"category,optional"`
|
||||||
|
Detail int64 `json:"detail,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status {
|
||||||
|
Code int64 `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,optional"`
|
||||||
|
Error ErrorDetail `json:"error,optional"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
CopySuggestedTagData {
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
SearchIntent string `json:"search_intent,omitempty"`
|
||||||
|
SearchType string `json:"search_type,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopySimilarAccountData {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Confidence string `json:"confidence,omitempty"`
|
||||||
|
ProfileUrl string `json:"profile_url,omitempty"`
|
||||||
|
AuthorVerified bool `json:"author_verified,omitempty"`
|
||||||
|
FollowerCount int `json:"follower_count,omitempty"`
|
||||||
|
EngagementScore int `json:"engagement_score,omitempty"`
|
||||||
|
LikeCount int `json:"like_count,omitempty"`
|
||||||
|
ReplyCount int `json:"reply_count,omitempty"`
|
||||||
|
PostCount int `json:"post_count,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionResearchMapData {
|
||||||
|
AudienceSummary string `json:"audience_summary,omitempty"`
|
||||||
|
ContentGoal string `json:"content_goal,omitempty"`
|
||||||
|
Questions []string `json:"questions,omitempty"`
|
||||||
|
Pillars []string `json:"pillars,omitempty"`
|
||||||
|
Exclusions []string `json:"exclusions,omitempty"`
|
||||||
|
SuggestedTags []CopySuggestedTagData `json:"suggested_tags,omitempty"`
|
||||||
|
SimilarAccounts []CopySimilarAccountData `json:"similar_accounts,omitempty"`
|
||||||
|
BenchmarkNotes string `json:"benchmark_notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
SeedQuery string `json:"seed_query,omitempty"`
|
||||||
|
Brief string `json:"brief,omitempty"`
|
||||||
|
ResearchMap CopyMissionResearchMapData `json:"research_map,omitempty"`
|
||||||
|
SelectedTags []string `json:"selected_tags,omitempty"`
|
||||||
|
LastScanJobID string `json:"last_scan_job_id,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListCopyMissionsData {
|
||||||
|
List []CopyMissionData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateCopyMissionReq {
|
||||||
|
Label string `json:"label" validate:"required"`
|
||||||
|
SeedQuery string `json:"seed_query" validate:"required"`
|
||||||
|
Brief string `json:"brief" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCopyMissionReq {
|
||||||
|
Label *string `json:"label,optional"`
|
||||||
|
SeedQuery *string `json:"seed_query,optional"`
|
||||||
|
Brief *string `json:"brief,optional"`
|
||||||
|
AudienceSummary *string `json:"audience_summary,optional"`
|
||||||
|
ContentGoal *string `json:"content_goal,optional"`
|
||||||
|
Questions []string `json:"questions,optional"`
|
||||||
|
Pillars []string `json:"pillars,optional"`
|
||||||
|
Exclusions []string `json:"exclusions,optional"`
|
||||||
|
BenchmarkNotes *string `json:"benchmark_notes,optional"`
|
||||||
|
SelectedTags []string `json:"selected_tags,optional"`
|
||||||
|
Status *string `json:"status,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionScanScheduleData {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
MissionID string `json:"mission_id"`
|
||||||
|
Cron string `json:"cron"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
NextRunAt int64 `json:"next_run_at,omitempty"`
|
||||||
|
LastRunAt int64 `json:"last_run_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertCopyMissionScanScheduleReq {
|
||||||
|
Cron string `json:"cron,optional"`
|
||||||
|
Timezone string `json:"timezone,optional"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertCopyMissionScanScheduleHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
UpsertCopyMissionScanScheduleReq
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionPath {
|
||||||
|
PersonaID string `path:"personaId" validate:"required"`
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PersonaCopyMissionsPath {
|
||||||
|
PersonaID string `path:"personaId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateCopyMissionHandlerReq {
|
||||||
|
PersonaCopyMissionsPath
|
||||||
|
CreateCopyMissionReq
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCopyMissionHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
UpdateCopyMissionReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionAnalyzeJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionScanJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionMatrixJobReq {
|
||||||
|
Count int `json:"count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionMatrixJobHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
StartCopyMissionMatrixJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionMatrixJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionCopyDraftJobReq {
|
||||||
|
ScanPostID string `json:"scan_post_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionCopyDraftJobHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
StartCopyMissionCopyDraftJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartCopyMissionCopyDraftJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListCopyMissionScanPostsReq {
|
||||||
|
Limit int `form:"limit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListCopyMissionScanPostsHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
ListCopyMissionScanPostsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateCopyMissionMatrixReq {
|
||||||
|
Count int `json:"count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateCopyMissionMatrixHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
GenerateCopyMissionMatrixReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateCopyMissionMatrixData {
|
||||||
|
Drafts []CopyDraftData `json:"drafts"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListCopyMissionCopyDraftsData {
|
||||||
|
List []CopyDraftData `json:"list"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionInspirationSourceData {
|
||||||
|
Query string `json:"query,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Snippet string `json:"snippet,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionInspirationData {
|
||||||
|
Label string `json:"label"`
|
||||||
|
SeedQuery string `json:"seed_query"`
|
||||||
|
Brief string `json:"brief"`
|
||||||
|
TrendReason string `json:"trend_reason,omitempty"`
|
||||||
|
TrendKeywords []string `json:"trend_keywords,omitempty"`
|
||||||
|
Sources []CopyMissionInspirationSourceData `json:"sources,omitempty"`
|
||||||
|
WebSearchUsed bool `json:"web_search_used"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: copy_mission
|
||||||
|
prefix: /api/v1/personas
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "CopyMission"
|
||||||
|
summary: "Copy ninja missions (Flow A). Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listCopyMissions
|
||||||
|
get /:personaId/copy-missions (PersonaCopyMissionsPath) returns (ListCopyMissionsData)
|
||||||
|
|
||||||
|
@handler inspireCopyMission
|
||||||
|
post /:personaId/copy-mission-inspiration (PersonaCopyMissionsPath) returns (CopyMissionInspirationData)
|
||||||
|
|
||||||
|
@handler createCopyMission
|
||||||
|
post /:personaId/copy-missions (CreateCopyMissionHandlerReq) returns (CopyMissionData)
|
||||||
|
|
||||||
|
@handler getCopyMission
|
||||||
|
get /:personaId/copy-missions/:id (CopyMissionPath) returns (CopyMissionData)
|
||||||
|
|
||||||
|
@handler updateCopyMission
|
||||||
|
patch /:personaId/copy-missions/:id (UpdateCopyMissionHandlerReq) returns (CopyMissionData)
|
||||||
|
|
||||||
|
@handler deleteCopyMission
|
||||||
|
delete /:personaId/copy-missions/:id (CopyMissionPath)
|
||||||
|
|
||||||
|
@handler startCopyMissionAnalyzeJob
|
||||||
|
post /:personaId/copy-missions/:id/analyze-jobs (CopyMissionPath) returns (StartCopyMissionAnalyzeJobData)
|
||||||
|
|
||||||
|
@handler startCopyMissionScanJob
|
||||||
|
post /:personaId/copy-missions/:id/scan-jobs (CopyMissionPath) returns (StartCopyMissionScanJobData)
|
||||||
|
|
||||||
|
@handler listCopyMissionScanPosts
|
||||||
|
get /:personaId/copy-missions/:id/scan-posts (ListCopyMissionScanPostsHandlerReq) returns (ListPersonaViralScanPostsData)
|
||||||
|
|
||||||
|
@handler generateCopyMissionMatrix
|
||||||
|
post /:personaId/copy-missions/:id/matrix-drafts (GenerateCopyMissionMatrixHandlerReq) returns (GenerateCopyMissionMatrixData)
|
||||||
|
|
||||||
|
@handler startCopyMissionMatrixJob
|
||||||
|
post /:personaId/copy-missions/:id/matrix-jobs (StartCopyMissionMatrixJobHandlerReq) returns (StartCopyMissionMatrixJobData)
|
||||||
|
|
||||||
|
@handler startCopyMissionCopyDraftJob
|
||||||
|
post /:personaId/copy-missions/:id/copy-draft-jobs (StartCopyMissionCopyDraftJobHandlerReq) returns (StartCopyMissionCopyDraftJobData)
|
||||||
|
|
||||||
|
@handler listCopyMissionCopyDrafts
|
||||||
|
get /:personaId/copy-missions/:id/copy-drafts (CopyMissionPath) returns (ListCopyMissionCopyDraftsData)
|
||||||
|
|
||||||
|
@handler getCopyMissionScanSchedule
|
||||||
|
get /:personaId/copy-missions/:id/scan-schedule (CopyMissionPath) returns (CopyMissionScanScheduleData)
|
||||||
|
|
||||||
|
@handler upsertCopyMissionScanSchedule
|
||||||
|
put /:personaId/copy-missions/:id/scan-schedule (UpsertCopyMissionScanScheduleHandlerReq) returns (CopyMissionScanScheduleData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
info (
|
||||||
|
title: "Haixun Backend"
|
||||||
|
desc: "Haixun service-oriented backend core"
|
||||||
|
author: "haixun"
|
||||||
|
version: "0.1.0"
|
||||||
|
host: "127.0.0.1:8890"
|
||||||
|
schemes: "http,https"
|
||||||
|
consumes: "application/json"
|
||||||
|
produces: "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"common.api"
|
||||||
|
"normal.api"
|
||||||
|
"setting.api"
|
||||||
|
"ai.api"
|
||||||
|
"job.api"
|
||||||
|
"auth.api"
|
||||||
|
"member.api"
|
||||||
|
"permission.api"
|
||||||
|
"threads_account.api"
|
||||||
|
"persona.api"
|
||||||
|
"copy_mission.api"
|
||||||
|
"brand.api"
|
||||||
|
"placement_topic.api"
|
||||||
|
"worker_internal.api"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
JobTemplatePath {
|
||||||
|
Type string `path:"type" validate:"required"` // template type
|
||||||
|
}
|
||||||
|
|
||||||
|
JobIDPath {
|
||||||
|
ID string `path:"id" validate:"required"` // job run id
|
||||||
|
}
|
||||||
|
|
||||||
|
JobScheduleIDPath {
|
||||||
|
ID string `path:"id" validate:"required"` // schedule id
|
||||||
|
}
|
||||||
|
|
||||||
|
ListJobsReq {
|
||||||
|
Scope string `form:"scope,optional"` // filter by scope
|
||||||
|
ScopeID string `form:"scope_id,optional"` // filter by scope id
|
||||||
|
Page int64 `form:"page,optional"` // page number starting at 1
|
||||||
|
PageSize int64 `form:"pageSize,optional"` // page size
|
||||||
|
}
|
||||||
|
|
||||||
|
ListJobSchedulesReq {
|
||||||
|
Scope string `form:"scope,optional"` // filter by scope
|
||||||
|
ScopeID string `form:"scope_id,optional"` // filter by scope id
|
||||||
|
Page int64 `form:"page,optional"` // page number starting at 1
|
||||||
|
PageSize int64 `form:"pageSize,optional"` // page size
|
||||||
|
}
|
||||||
|
|
||||||
|
ListJobEventsReq {
|
||||||
|
ID string `path:"id" validate:"required"` // job run id
|
||||||
|
Limit int64 `form:"limit,optional"` // max events
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelJobReq {
|
||||||
|
ID string `path:"id" validate:"required"` // job run id
|
||||||
|
Reason string `json:"reason,optional"` // cancel reason
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateJobReq {
|
||||||
|
TemplateType string `json:"template_type" validate:"required"` // job template type
|
||||||
|
Scope string `json:"scope" validate:"required,oneof=user account system persona brand"` // job scope
|
||||||
|
ScopeID string `json:"scope_id" validate:"required"` // scope id
|
||||||
|
Payload map[string]interface{} `json:"payload,optional"` // job payload
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertJobTemplateReq {
|
||||||
|
Type string `path:"type" validate:"required"` // template type
|
||||||
|
Version int `json:"version,optional"` // template version
|
||||||
|
Name string `json:"name" validate:"required"` // display name
|
||||||
|
Description string `json:"description,optional"` // description
|
||||||
|
Enabled bool `json:"enabled"` // enabled flag
|
||||||
|
Repeatable bool `json:"repeatable"` // repeatable flag
|
||||||
|
ConcurrencyPolicy string `json:"concurrency_policy,optional"` // concurrency policy
|
||||||
|
DedupeKeys []string `json:"dedupe_keys,optional"` // dedupe keys
|
||||||
|
TimeoutSeconds int `json:"timeout_seconds,optional"` // timeout seconds
|
||||||
|
CancelPolicy JobCancelPolicyData `json:"cancel_policy,optional"` // cancel policy
|
||||||
|
RetryPolicy JobRetryPolicyData `json:"retry_policy,optional"` // retry policy
|
||||||
|
Steps []JobTemplateStepData `json:"steps" validate:"required,min=1,dive"` // steps
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateJobScheduleReq {
|
||||||
|
TemplateType string `json:"template_type" validate:"required"` // template type
|
||||||
|
Scope string `json:"scope" validate:"required,oneof=user account system persona brand"` // scope
|
||||||
|
ScopeID string `json:"scope_id" validate:"required"` // scope id
|
||||||
|
Cron string `json:"cron" validate:"required"` // cron expression
|
||||||
|
Timezone string `json:"timezone,optional"` // timezone
|
||||||
|
PayloadTemplate map[string]interface{} `json:"payload_template,optional"` // payload template
|
||||||
|
Enabled bool `json:"enabled"` // enabled flag
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateJobScheduleReq {
|
||||||
|
ID string `path:"id" validate:"required"` // schedule id
|
||||||
|
Cron string `json:"cron,optional"` // cron expression
|
||||||
|
Timezone string `json:"timezone,optional"` // timezone
|
||||||
|
PayloadTemplate map[string]interface{} `json:"payload_template,optional"` // payload template
|
||||||
|
Enabled *bool `json:"enabled,optional"` // enabled flag
|
||||||
|
}
|
||||||
|
|
||||||
|
JobCancelPolicyData {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
GraceSeconds int `json:"grace_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobRetryPolicyData {
|
||||||
|
MaxAttempts int `json:"max_attempts"`
|
||||||
|
BackoffSeconds []int `json:"backoff_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobTemplateStepData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
WorkerType string `json:"worker_type"`
|
||||||
|
TimeoutSeconds int `json:"timeout_seconds"`
|
||||||
|
Cancelable bool `json:"cancelable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobTemplateData {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Repeatable bool `json:"repeatable"`
|
||||||
|
ConcurrencyPolicy string `json:"concurrency_policy"`
|
||||||
|
DedupeKeys []string `json:"dedupe_keys"`
|
||||||
|
TimeoutSeconds int `json:"timeout_seconds"`
|
||||||
|
CancelPolicy JobCancelPolicyData `json:"cancel_policy"`
|
||||||
|
RetryPolicy JobRetryPolicyData `json:"retry_policy"`
|
||||||
|
Steps []JobTemplateStepData `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobTemplateListData {
|
||||||
|
List []JobTemplateData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobStepProgressData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartedAt *int64 `json:"started_at,optional"`
|
||||||
|
EndedAt *int64 `json:"ended_at,optional"`
|
||||||
|
Message string `json:"message,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobProgressData {
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Percentage int `json:"percentage"`
|
||||||
|
Steps []JobStepProgressData `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
TemplateType string `json:"template_type"`
|
||||||
|
TemplateVersion int `json:"template_version"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
ScopeID string `json:"scope_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
WorkerType string `json:"worker_type"`
|
||||||
|
Payload map[string]interface{} `json:"payload"`
|
||||||
|
Progress JobProgressData `json:"progress"`
|
||||||
|
Result map[string]interface{} `json:"result,optional"`
|
||||||
|
Error string `json:"error,optional"`
|
||||||
|
Attempt int `json:"attempt"`
|
||||||
|
MaxAttempts int `json:"max_attempts"`
|
||||||
|
CancelRequestedAt *int64 `json:"cancel_requested_at,optional"`
|
||||||
|
CancelReason string `json:"cancel_reason,optional"`
|
||||||
|
StartedAt *int64 `json:"started_at,optional"`
|
||||||
|
CompletedAt *int64 `json:"completed_at,optional"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobListData {
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []JobData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobScheduleData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
TemplateType string `json:"template_type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
ScopeID string `json:"scope_id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Cron string `json:"cron"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
PayloadTemplate map[string]interface{} `json:"payload_template"`
|
||||||
|
LastRunAt *int64 `json:"last_run_at,optional"`
|
||||||
|
NextRunAt int64 `json:"next_run_at"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobScheduleListData {
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []JobScheduleData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobEventData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
From string `json:"from,optional"`
|
||||||
|
To string `json:"to,optional"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata,optional"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
JobEventListData {
|
||||||
|
List []JobEventData `json:"list"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: job
|
||||||
|
prefix: /api/v1
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Job - Core"
|
||||||
|
summary: "Generic job templates, runs, schedules, cancel, and retry. Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listJobTemplates
|
||||||
|
get /job/templates returns (JobTemplateListData)
|
||||||
|
|
||||||
|
@handler getJobTemplate
|
||||||
|
get /job/templates/:type (JobTemplatePath) returns (JobTemplateData)
|
||||||
|
|
||||||
|
@handler upsertJobTemplate
|
||||||
|
put /job/templates/:type (UpsertJobTemplateReq) returns (JobTemplateData)
|
||||||
|
|
||||||
|
@handler createJob
|
||||||
|
post /jobs (CreateJobReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler getJob
|
||||||
|
get /jobs/:id (JobIDPath) returns (JobData)
|
||||||
|
|
||||||
|
@handler listJobs
|
||||||
|
get /jobs (ListJobsReq) returns (JobListData)
|
||||||
|
|
||||||
|
@handler listJobEvents
|
||||||
|
get /jobs/:id/events (ListJobEventsReq) returns (JobEventListData)
|
||||||
|
|
||||||
|
@handler cancelJob
|
||||||
|
post /jobs/:id/cancel (CancelJobReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler retryJob
|
||||||
|
post /jobs/:id/retry (JobIDPath) returns (JobData)
|
||||||
|
|
||||||
|
@handler listJobSchedules
|
||||||
|
get /job/schedules (ListJobSchedulesReq) returns (JobScheduleListData)
|
||||||
|
|
||||||
|
@handler createJobSchedule
|
||||||
|
post /job/schedules (CreateJobScheduleReq) returns (JobScheduleData)
|
||||||
|
|
||||||
|
@handler updateJobSchedule
|
||||||
|
put /job/schedules/:id (UpdateJobScheduleReq) returns (JobScheduleData)
|
||||||
|
|
||||||
|
@handler enableJobSchedule
|
||||||
|
post /job/schedules/:id/enable (JobScheduleIDPath) returns (JobScheduleData)
|
||||||
|
|
||||||
|
@handler disableJobSchedule
|
||||||
|
post /job/schedules/:id/disable (JobScheduleIDPath) returns (JobScheduleData)
|
||||||
|
|
||||||
|
@handler deleteJobSchedule
|
||||||
|
delete /job/schedules/:id (JobScheduleIDPath)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
MemberMeData {
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
UID string `json:"uid"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
Phone string `json:"phone,omitempty"`
|
||||||
|
Language string `json:"language,omitempty"`
|
||||||
|
Currency string `json:"currency,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Roles []string `json:"roles,omitempty"`
|
||||||
|
BusinessEmail string `json:"business_email,omitempty"`
|
||||||
|
BusinessEmailVerified bool `json:"business_email_verified"`
|
||||||
|
BusinessPhone string `json:"business_phone,omitempty"`
|
||||||
|
BusinessPhoneVerified bool `json:"business_phone_verified"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateMemberMeReq {
|
||||||
|
DisplayName string `json:"display_name,optional"`
|
||||||
|
Avatar string `json:"avatar,optional"`
|
||||||
|
Language string `json:"language,optional"`
|
||||||
|
Currency string `json:"currency,optional"`
|
||||||
|
Phone string `json:"phone,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
MemberPlacementSettingsData {
|
||||||
|
WebSearchProvider string `json:"web_search_provider"` // brave | exa
|
||||||
|
BraveAPIKey string `json:"brave_api_key,omitempty"`
|
||||||
|
BraveAPIKeyConfigured bool `json:"brave_api_key_configured"`
|
||||||
|
ExaAPIKey string `json:"exa_api_key,omitempty"`
|
||||||
|
ExaAPIKeyConfigured bool `json:"exa_api_key_configured"`
|
||||||
|
BraveCountry string `json:"brave_country"`
|
||||||
|
BraveSearchLang string `json:"brave_search_lang"`
|
||||||
|
ExaUserLocation string `json:"exa_user_location"`
|
||||||
|
ExpandStrategy string `json:"expand_strategy"` // brave | llm | hybrid
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateMemberPlacementSettingsReq {
|
||||||
|
WebSearchProvider *string `json:"web_search_provider,optional"`
|
||||||
|
BraveAPIKey *string `json:"brave_api_key,optional"`
|
||||||
|
ExaAPIKey *string `json:"exa_api_key,optional"`
|
||||||
|
BraveCountry *string `json:"brave_country,optional"`
|
||||||
|
BraveSearchLang *string `json:"brave_search_lang,optional"`
|
||||||
|
ExaUserLocation *string `json:"exa_user_location,optional"`
|
||||||
|
ExpandStrategy *string `json:"expand_strategy,optional"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: member
|
||||||
|
prefix: /api/v1/members
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Member"
|
||||||
|
summary: "Current member profile endpoints. Requires Bearer JWT or dev headers."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler getMemberMe
|
||||||
|
get /me returns (MemberMeData)
|
||||||
|
|
||||||
|
@handler updateMemberMe
|
||||||
|
patch /me (UpdateMemberMeReq) returns (MemberMeData)
|
||||||
|
|
||||||
|
@handler getMemberPlacementSettings
|
||||||
|
get /me/placement-settings returns (MemberPlacementSettingsData)
|
||||||
|
|
||||||
|
@handler updateMemberPlacementSettings
|
||||||
|
patch /me/placement-settings (UpdateMemberPlacementSettingsReq) returns (MemberPlacementSettingsData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type HealthData {
|
||||||
|
Pong string `json:"pong"`
|
||||||
|
}
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: normal
|
||||||
|
prefix: /api/v1
|
||||||
|
tags: "Normal - Public"
|
||||||
|
summary: "Health check"
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler health
|
||||||
|
get /health () returns (HealthData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
PermissionCatalogQuery {
|
||||||
|
Status string `form:"status,optional" validate:"omitempty,oneof=open close"`
|
||||||
|
Type string `form:"type,optional" validate:"omitempty,oneof=backend_user frontend_user"`
|
||||||
|
Tree bool `form:"tree,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionNode {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Parent string `json:"parent,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HTTPMethods string `json:"http_methods,omitempty"`
|
||||||
|
HTTPPath string `json:"http_path,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Children []PermissionNode `json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionCatalogData {
|
||||||
|
Tree []PermissionNode `json:"tree,omitempty"`
|
||||||
|
List []PermissionNode `json:"list,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
MePermissionsQuery {
|
||||||
|
IncludeTree bool `form:"include_tree,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
MePermissionsData {
|
||||||
|
UID string `json:"uid"`
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
Permissions map[string]string `json:"permissions"`
|
||||||
|
Tree []PermissionNode `json:"tree,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: permission
|
||||||
|
prefix: /api/v1/permissions
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Permission"
|
||||||
|
summary: "Permission catalog and current member permissions. Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler getPermissionCatalog
|
||||||
|
get /catalog (PermissionCatalogQuery) returns (PermissionCatalogData)
|
||||||
|
|
||||||
|
@handler getMePermissions
|
||||||
|
get /me (MePermissionsQuery) returns (MePermissionsData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
CopyResearchMapData {
|
||||||
|
AudienceSummary string `json:"audience_summary,omitempty"`
|
||||||
|
ContentGoal string `json:"content_goal,omitempty"`
|
||||||
|
Questions []string `json:"questions,omitempty"`
|
||||||
|
Pillars []string `json:"pillars,omitempty"`
|
||||||
|
Exclusions []string `json:"exclusions,omitempty"`
|
||||||
|
SuggestedTags []string `json:"suggested_tags,omitempty"`
|
||||||
|
BenchmarkNotes string `json:"benchmark_notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PersonaData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
Persona string `json:"persona,omitempty"`
|
||||||
|
Brief string `json:"brief,omitempty"`
|
||||||
|
StyleProfile string `json:"style_profile,omitempty"`
|
||||||
|
StyleBenchmark string `json:"style_benchmark,omitempty"`
|
||||||
|
SeedQuery string `json:"seed_query,omitempty"`
|
||||||
|
CopyResearchMap CopyResearchMapData `json:"copy_research_map,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonasData {
|
||||||
|
List []PersonaData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePersonaReq {
|
||||||
|
DisplayName string `json:"display_name,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PersonaPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePersonaReq {
|
||||||
|
DisplayName *string `json:"display_name,optional"`
|
||||||
|
Persona *string `json:"persona,optional"`
|
||||||
|
Brief *string `json:"brief,optional"`
|
||||||
|
StyleProfile *string `json:"style_profile,optional"`
|
||||||
|
StyleBenchmark *string `json:"style_benchmark,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaStyleAnalysisReq {
|
||||||
|
BenchmarkUsername string `json:"benchmark_username" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaStyleAnalysisData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePersonaHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
UpdatePersonaReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaStyleAnalysisHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
StartPersonaStyleAnalysisReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaViralScanJobReq {
|
||||||
|
Keywords []string `json:"keywords,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaViralScanJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaViralScanJobHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
StartPersonaViralScanJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaViralScanPostsReq {
|
||||||
|
Limit int `form:"limit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ViralScanPostData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SearchTag string `json:"search_tag"`
|
||||||
|
Permalink string `json:"permalink"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
AuthorVerified bool `json:"author_verified,omitempty"`
|
||||||
|
FollowerCount int `json:"follower_count,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
LikeCount int `json:"like_count"`
|
||||||
|
ReplyCount int `json:"reply_count"`
|
||||||
|
EngagementScore int `json:"engagement_score"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
ScanJobID string `json:"scan_job_id"`
|
||||||
|
Replies []ScanReplyData `json:"replies,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaViralScanPostsData {
|
||||||
|
List []ViralScanPostData `json:"list"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaViralScanPostsHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
ListPersonaViralScanPostsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyDraftData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
CopyMissionID string `json:"copy_mission_id,omitempty"`
|
||||||
|
ScanPostID string `json:"scan_post_id,omitempty"`
|
||||||
|
DraftType string `json:"draft_type"`
|
||||||
|
SortOrder int `json:"sort_order,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Angle string `json:"angle,omitempty"`
|
||||||
|
Hook string `json:"hook,omitempty"`
|
||||||
|
Rationale string `json:"rationale,omitempty"`
|
||||||
|
ReferenceNotes string `json:"reference_notes,omitempty"`
|
||||||
|
Sources []string `json:"sources,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
PublishedMediaID string `json:"published_media_id,omitempty"`
|
||||||
|
PublishedPermalink string `json:"published_permalink,omitempty"`
|
||||||
|
PublishedAt int64 `json:"published_at,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaCopyDraftsData {
|
||||||
|
List []CopyDraftData `json:"list"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePersonaCopyDraftReq {
|
||||||
|
ScanPostID string `json:"scan_post_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePersonaCopyDraftHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
GeneratePersonaCopyDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePersonaCopyDraftData {
|
||||||
|
Draft CopyDraftData `json:"draft"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyDraftPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
DraftID string `path:"draftId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCopyDraftReq {
|
||||||
|
Text *string `json:"text,optional"`
|
||||||
|
Hook *string `json:"hook,optional"`
|
||||||
|
Angle *string `json:"angle,optional"`
|
||||||
|
Status *string `json:"status,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCopyDraftHandlerReq {
|
||||||
|
CopyDraftPath
|
||||||
|
UpdateCopyDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishCopyDraftReq {
|
||||||
|
Text string `json:"text,optional"`
|
||||||
|
Confirm bool `json:"confirm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishCopyDraftHandlerReq {
|
||||||
|
CopyDraftPath
|
||||||
|
PublishCopyDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishCopyDraftData {
|
||||||
|
DraftID string `json:"draft_id"`
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
Permalink string `json:"permalink,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: persona
|
||||||
|
prefix: /api/v1/personas
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Persona"
|
||||||
|
summary: "Reusable persona profiles with 8D style strategy. Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listPersonas
|
||||||
|
get / returns (ListPersonasData)
|
||||||
|
|
||||||
|
@handler createPersona
|
||||||
|
post / (CreatePersonaReq) returns (PersonaData)
|
||||||
|
|
||||||
|
@handler getPersona
|
||||||
|
get /:id (PersonaPath) returns (PersonaData)
|
||||||
|
|
||||||
|
@handler updatePersona
|
||||||
|
patch /:id (UpdatePersonaHandlerReq) returns (PersonaData)
|
||||||
|
|
||||||
|
@handler deletePersona
|
||||||
|
delete /:id (PersonaPath)
|
||||||
|
|
||||||
|
@handler startPersonaStyleAnalysis
|
||||||
|
post /:id/style-analysis (StartPersonaStyleAnalysisHandlerReq) returns (StartPersonaStyleAnalysisData)
|
||||||
|
|
||||||
|
@handler startPersonaViralScanJob
|
||||||
|
post /:id/viral-scan-jobs (StartPersonaViralScanJobHandlerReq) returns (StartPersonaViralScanJobData)
|
||||||
|
|
||||||
|
@handler listPersonaViralScanPosts
|
||||||
|
get /:id/viral-scan-posts (ListPersonaViralScanPostsHandlerReq) returns (ListPersonaViralScanPostsData)
|
||||||
|
|
||||||
|
@handler listPersonaCopyDrafts
|
||||||
|
get /:id/copy-drafts (PersonaPath) returns (ListPersonaCopyDraftsData)
|
||||||
|
|
||||||
|
@handler generatePersonaCopyDraft
|
||||||
|
post /:id/copy-drafts/generate (GeneratePersonaCopyDraftHandlerReq) returns (GeneratePersonaCopyDraftData)
|
||||||
|
|
||||||
|
@handler updatePersonaCopyDraft
|
||||||
|
patch /:id/copy-drafts/:draftId (UpdateCopyDraftHandlerReq) returns (CopyDraftData)
|
||||||
|
|
||||||
|
@handler publishPersonaCopyDraft
|
||||||
|
post /:id/copy-drafts/:draftId/publish (PublishCopyDraftHandlerReq) returns (PublishCopyDraftData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
PlacementTopicData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
BrandID string `json:"brand_id"`
|
||||||
|
BrandDisplayName string `json:"brand_display_name,omitempty"`
|
||||||
|
TopicName string `json:"topic_name,omitempty"`
|
||||||
|
SeedQuery string `json:"seed_query,omitempty"`
|
||||||
|
Brief string `json:"brief,omitempty"`
|
||||||
|
ProductID string `json:"product_id,omitempty"`
|
||||||
|
ResearchMap ResearchMapData `json:"research_map,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPlacementTopicsData {
|
||||||
|
List []PlacementTopicData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePlacementTopicReq {
|
||||||
|
BrandID string `json:"brand_id" validate:"required"`
|
||||||
|
TopicName string `json:"topic_name" validate:"required"`
|
||||||
|
SeedQuery string `json:"seed_query" validate:"required"`
|
||||||
|
Brief string `json:"brief" validate:"required"`
|
||||||
|
ProductID string `json:"product_id,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePlacementTopicReq {
|
||||||
|
BrandID *string `json:"brand_id,optional"`
|
||||||
|
TopicName *string `json:"topic_name,optional"`
|
||||||
|
SeedQuery *string `json:"seed_query,optional"`
|
||||||
|
Brief *string `json:"brief,optional"`
|
||||||
|
ProductID *string `json:"product_id,optional"`
|
||||||
|
AudienceSummary *string `json:"audience_summary,optional"`
|
||||||
|
ContentGoal *string `json:"content_goal,optional"`
|
||||||
|
Questions []string `json:"questions,optional"`
|
||||||
|
Pillars []string `json:"pillars,optional"`
|
||||||
|
Exclusions []string `json:"exclusions,optional"`
|
||||||
|
PatrolKeywords []string `json:"patrol_keywords,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PlacementTopicPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePlacementTopicHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
UpdatePlacementTopicReq
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePlacementTopicHandlerReq {
|
||||||
|
CreatePlacementTopicReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandPlacementTopicGraphHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
ExpandKnowledgeGraphReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchPlacementTopicGraphNodesHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
PatchKnowledgeGraphNodesReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPlacementTopicScanJobHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
StartBrandScanJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPlacementTopicScanPostsHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
ListBrandScanPostsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePlacementTopicOutreachDraftsHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
GenerateOutreachDraftsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishPlacementTopicOutreachDraftHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
PublishOutreachDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchPlacementTopicScanPostOutreachHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
PostID string `path:"postId"`
|
||||||
|
PatchScanPostOutreachReq
|
||||||
|
}
|
||||||
|
|
||||||
|
DeletePlacementTopicScanPostHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
PostID string `path:"postId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchDeletePlacementTopicScanPostsReq {
|
||||||
|
PostIDs []string `json:"post_ids" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchDeletePlacementTopicScanPostsData {
|
||||||
|
DeletedCount int `json:"deleted_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchDeletePlacementTopicScanPostsHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
BatchDeletePlacementTopicScanPostsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePlacementTopicContentMatrixHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
GenerateContentMatrixReq
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertPlacementTopicScanScheduleHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
UpsertBrandScanScheduleReq
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: placement_topic
|
||||||
|
prefix: /api/v1/placement/topics
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Placement Topic"
|
||||||
|
summary: "找 TA 主題;每個主題關聯一個品牌,一個品牌可有多個主題。Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listPlacementTopics
|
||||||
|
get / returns (ListPlacementTopicsData)
|
||||||
|
|
||||||
|
@handler createPlacementTopic
|
||||||
|
post / (CreatePlacementTopicHandlerReq) returns (PlacementTopicData)
|
||||||
|
|
||||||
|
@handler getPlacementTopic
|
||||||
|
get /:id (PlacementTopicPath) returns (PlacementTopicData)
|
||||||
|
|
||||||
|
@handler updatePlacementTopic
|
||||||
|
patch /:id (UpdatePlacementTopicHandlerReq) returns (PlacementTopicData)
|
||||||
|
|
||||||
|
@handler deletePlacementTopic
|
||||||
|
delete /:id (PlacementTopicPath)
|
||||||
|
|
||||||
|
@handler expandPlacementTopicGraph
|
||||||
|
post /:id/knowledge-graph/expand (ExpandPlacementTopicGraphHandlerReq) returns (ExpandKnowledgeGraphData)
|
||||||
|
|
||||||
|
@handler getPlacementTopicGraph
|
||||||
|
get /:id/knowledge-graph (PlacementTopicPath) returns (KnowledgeGraphData)
|
||||||
|
|
||||||
|
@handler patchPlacementTopicGraphNodes
|
||||||
|
patch /:id/knowledge-graph/nodes (PatchPlacementTopicGraphNodesHandlerReq) returns (KnowledgeGraphData)
|
||||||
|
|
||||||
|
@handler startPlacementTopicScanJob
|
||||||
|
post /:id/scan-jobs (StartPlacementTopicScanJobHandlerReq) returns (StartBrandScanJobData)
|
||||||
|
|
||||||
|
@handler listPlacementTopicScanPosts
|
||||||
|
get /:id/scan-posts (ListPlacementTopicScanPostsHandlerReq) returns (ListBrandScanPostsData)
|
||||||
|
|
||||||
|
@handler generatePlacementTopicOutreachDrafts
|
||||||
|
post /:id/outreach-drafts/generate (GeneratePlacementTopicOutreachDraftsHandlerReq) returns (GenerateOutreachDraftsData)
|
||||||
|
|
||||||
|
@handler publishPlacementTopicOutreachDraft
|
||||||
|
post /:id/outreach-drafts/publish (PublishPlacementTopicOutreachDraftHandlerReq) returns (PublishOutreachDraftData)
|
||||||
|
|
||||||
|
@handler patchPlacementTopicScanPostOutreach
|
||||||
|
patch /:id/scan-posts/:postId (PatchPlacementTopicScanPostOutreachHandlerReq) returns (ScanPostData)
|
||||||
|
|
||||||
|
@handler deletePlacementTopicScanPost
|
||||||
|
delete /:id/scan-posts/:postId (DeletePlacementTopicScanPostHandlerReq)
|
||||||
|
|
||||||
|
@handler batchDeletePlacementTopicScanPosts
|
||||||
|
post /:id/scan-posts/batch-delete (BatchDeletePlacementTopicScanPostsHandlerReq) returns (BatchDeletePlacementTopicScanPostsData)
|
||||||
|
|
||||||
|
@handler getPlacementTopicContentMatrix
|
||||||
|
get /:id/content-matrix (PlacementTopicPath) returns (ContentMatrixData)
|
||||||
|
|
||||||
|
@handler generatePlacementTopicContentMatrix
|
||||||
|
post /:id/content-matrix/generate (GeneratePlacementTopicContentMatrixHandlerReq) returns (ContentMatrixData)
|
||||||
|
|
||||||
|
@handler getPlacementTopicScanSchedule
|
||||||
|
get /:id/scan-schedule (PlacementTopicPath) returns (BrandScanScheduleData)
|
||||||
|
|
||||||
|
@handler upsertPlacementTopicScanSchedule
|
||||||
|
put /:id/scan-schedule (UpsertPlacementTopicScanScheduleHandlerReq) returns (BrandScanScheduleData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
SettingPath {
|
||||||
|
Scope string `path:"scope" validate:"required,oneof=user account system"` // 設定範圍,可選 user / account / system
|
||||||
|
ScopeID string `path:"scope_id" validate:"required"` // 範圍 ID,例如 user_id、account_id 或 global
|
||||||
|
Page int64 `form:"page,optional"` // 頁碼,從 1 開始
|
||||||
|
PageSize int64 `form:"pageSize,optional"` // 每頁筆數,server 會限制最大值
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingKeyPath {
|
||||||
|
Scope string `path:"scope" validate:"required,oneof=user account system"` // 設定範圍,可選 user / account / system
|
||||||
|
ScopeID string `path:"scope_id" validate:"required"` // 範圍 ID,例如 user_id、account_id 或 global
|
||||||
|
Key string `path:"key" validate:"required"` // 設定 key,例如 ai.default
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingUpsertReq {
|
||||||
|
Scope string `path:"scope" validate:"required,oneof=user account system"` // 設定範圍,可選 user / account / system
|
||||||
|
ScopeID string `path:"scope_id" validate:"required"` // 範圍 ID,例如 user_id、account_id 或 global
|
||||||
|
Key string `path:"key" validate:"required"` // 設定 key,例如 ai.default
|
||||||
|
Value map[string]interface{} `json:"value" validate:"required"` // 設定內容 JSON object
|
||||||
|
Version int `json:"version,optional"` // schema version,未帶入時預設 1
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
ScopeID string `json:"scope_id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value map[string]interface{} `json:"value"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingListData {
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []SettingData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PaginationData {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int64 `json:"page"`
|
||||||
|
PageSize int64 `json:"pageSize"`
|
||||||
|
TotalPages int64 `json:"totalPages"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: setting
|
||||||
|
prefix: /api/v1/settings
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "Setting - General"
|
||||||
|
summary: "Manage settings by scope, scope_id, and key. Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listSettings
|
||||||
|
get /:scope/:scope_id (SettingPath) returns (SettingListData)
|
||||||
|
|
||||||
|
@handler getSetting
|
||||||
|
get /:scope/:scope_id/:key (SettingKeyPath) returns (SettingData)
|
||||||
|
|
||||||
|
@handler upsertSetting
|
||||||
|
put /:scope/:scope_id/:key (SettingUpsertReq) returns (SettingData)
|
||||||
|
|
||||||
|
@handler deleteSetting
|
||||||
|
delete /:scope/:scope_id/:key (SettingKeyPath)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
ThreadsAccountData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
ThreadsUserID string `json:"threads_user_id,omitempty"`
|
||||||
|
PersonaID string `json:"persona_id,omitempty"` // deprecated: persona is chosen per publish, not bound to account
|
||||||
|
BrowserConnected bool `json:"browser_connected"`
|
||||||
|
ApiConnected bool `json:"api_connected"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsAccountsData {
|
||||||
|
List []ThreadsAccountData `json:"list"`
|
||||||
|
ActiveAccountID string `json:"active_account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateThreadsAccountReq {
|
||||||
|
DisplayName string `json:"display_name,optional"`
|
||||||
|
Activate *bool `json:"activate,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateThreadsAccountReq {
|
||||||
|
DisplayName *string `json:"display_name,optional"`
|
||||||
|
PersonaID *string `json:"persona_id,optional"` // deprecated: use persona_id in publish payload instead
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountConnectionPrefs {
|
||||||
|
SearchViaApi bool `json:"search_via_api"`
|
||||||
|
SearchSourceMode string `json:"search_source_mode"`
|
||||||
|
PublishViaApi bool `json:"publish_via_api"`
|
||||||
|
DevMode bool `json:"dev_mode"`
|
||||||
|
ScrapeReplies bool `json:"scrape_replies"`
|
||||||
|
RepliesPerPost int `json:"replies_per_post"`
|
||||||
|
PublishHeaded bool `json:"publish_headed"`
|
||||||
|
PlaywrightDebug bool `json:"playwright_debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountConnectionData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
AccountName string `json:"account_name"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
BrowserConnected bool `json:"browser_connected"`
|
||||||
|
ApiConnected bool `json:"api_connected"`
|
||||||
|
Prefs ThreadsAccountConnectionPrefs `json:"prefs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateThreadsAccountConnectionReq {
|
||||||
|
SearchViaApi *bool `json:"search_via_api,optional"`
|
||||||
|
SearchSourceMode *string `json:"search_source_mode,optional"`
|
||||||
|
PublishViaApi *bool `json:"publish_via_api,optional"`
|
||||||
|
DevMode *bool `json:"dev_mode,optional"`
|
||||||
|
ScrapeReplies *bool `json:"scrape_replies,optional"`
|
||||||
|
RepliesPerPost *int `json:"replies_per_post,optional"`
|
||||||
|
PublishHeaded *bool `json:"publish_headed,optional"`
|
||||||
|
PlaywrightDebug *bool `json:"playwright_debug,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportThreadsAccountSessionReq {
|
||||||
|
StorageState string `json:"storageState" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportThreadsAccountSessionData {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Synced bool `json:"synced"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountAiSettingsData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
ResearchProvider string `json:"research_provider,omitempty"`
|
||||||
|
ResearchModel string `json:"research_model,omitempty"`
|
||||||
|
ApiKeys map[string]string `json:"api_keys"`
|
||||||
|
ApiKeysConfigured map[string]interface{} `json:"api_keys_configured"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateThreadsAccountAiSettingsReq {
|
||||||
|
Provider *string `json:"provider,optional"`
|
||||||
|
Model *string `json:"model,optional"`
|
||||||
|
ResearchProvider *string `json:"research_provider,optional"`
|
||||||
|
ResearchModel *string `json:"research_model,optional"`
|
||||||
|
ApiKeys map[string]string `json:"api_keys,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateThreadsAccountHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
UpdateThreadsAccountReq
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateThreadsAccountConnectionHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
UpdateThreadsAccountConnectionReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportThreadsAccountSessionHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ImportThreadsAccountSessionReq
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateThreadsAccountAiSettingsHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
UpdateThreadsAccountAiSettingsReq
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: threads_account
|
||||||
|
prefix: /api/v1/threads-accounts
|
||||||
|
middleware: AuthJWT
|
||||||
|
tags: "ThreadsAccount"
|
||||||
|
summary: "Threads operating account endpoints. Requires Bearer JWT."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler listThreadsAccounts
|
||||||
|
get / returns (ListThreadsAccountsData)
|
||||||
|
|
||||||
|
@handler createThreadsAccount
|
||||||
|
post / (CreateThreadsAccountReq) returns (ThreadsAccountData)
|
||||||
|
|
||||||
|
@handler getThreadsAccount
|
||||||
|
get /:id (ThreadsAccountPath) returns (ThreadsAccountData)
|
||||||
|
|
||||||
|
@handler updateThreadsAccount
|
||||||
|
patch /:id (UpdateThreadsAccountHandlerReq) returns (ThreadsAccountData)
|
||||||
|
|
||||||
|
@handler activateThreadsAccount
|
||||||
|
post /:id/activate (ThreadsAccountPath)
|
||||||
|
|
||||||
|
@handler getThreadsAccountConnection
|
||||||
|
get /:id/connection (ThreadsAccountPath) returns (ThreadsAccountConnectionData)
|
||||||
|
|
||||||
|
@handler updateThreadsAccountConnection
|
||||||
|
patch /:id/connection (UpdateThreadsAccountConnectionHandlerReq) returns (ThreadsAccountConnectionData)
|
||||||
|
|
||||||
|
@handler importThreadsAccountSession
|
||||||
|
post /:id/session/import (ImportThreadsAccountSessionHandlerReq) returns (ImportThreadsAccountSessionData)
|
||||||
|
|
||||||
|
@handler getThreadsAccountAiSettings
|
||||||
|
get /:id/ai-settings (ThreadsAccountPath) returns (ThreadsAccountAiSettingsData)
|
||||||
|
|
||||||
|
@handler updateThreadsAccountAiSettings
|
||||||
|
put /:id/ai-settings (UpdateThreadsAccountAiSettingsHandlerReq) returns (ThreadsAccountAiSettingsData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
type (
|
||||||
|
ClaimWorkerJobReq {
|
||||||
|
WorkerType string `json:"worker_type" validate:"required"`
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerJobPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerJobReq {
|
||||||
|
WorkerJobPath
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerHeartbeatReq {
|
||||||
|
WorkerJobPath
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
TTLSeconds int `json:"ttl_seconds,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerProgressReq {
|
||||||
|
WorkerJobPath
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
Phase string `json:"phase,optional"`
|
||||||
|
Summary string `json:"summary,optional"`
|
||||||
|
Percentage int `json:"percentage,optional"`
|
||||||
|
Steps []JobStepProgressData `json:"steps,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerCompleteReq {
|
||||||
|
WorkerJobPath
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
Result map[string]interface{} `json:"result,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerFailReq {
|
||||||
|
WorkerJobPath
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
Error string `json:"error" validate:"required"`
|
||||||
|
Phase string `json:"phase,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerCancelCheckData {
|
||||||
|
Cancelled bool `json:"cancelled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerOKData {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StorePersonaStyleProfileReq {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
TenantID string `json:"tenant_id" validate:"required"`
|
||||||
|
OwnerUID string `json:"owner_uid" validate:"required"`
|
||||||
|
StyleProfile string `json:"style_profile" validate:"required"`
|
||||||
|
StyleBenchmark string `json:"style_benchmark,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StorePersonaStyleProfileData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerThreadsAccountSessionReq {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
TenantID string `json:"tenant_id" validate:"required"`
|
||||||
|
OwnerUID string `json:"owner_uid" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkerThreadsAccountSessionData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
StorageState string `json:"storage_state"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalyzeStyle8DPostReq {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Permalink string `json:"permalink,optional"`
|
||||||
|
LikeCount int `json:"like_count,optional"`
|
||||||
|
ReplyCount int `json:"reply_count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalyzeStyle8DReq {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
WorkerID string `json:"worker_id" validate:"required"`
|
||||||
|
TenantID string `json:"tenant_id" validate:"required"`
|
||||||
|
OwnerUID string `json:"owner_uid" validate:"required"`
|
||||||
|
PersonaID string `json:"persona_id" validate:"required"`
|
||||||
|
ThreadsAccountID string `json:"threads_account_id" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Posts []AnalyzeStyle8DPostReq `json:"posts" validate:"required"`
|
||||||
|
Steps []JobStepProgressData `json:"steps,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalyzeStyle8DData {
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
PostCount int `json:"post_count"`
|
||||||
|
StyleProfile string `json:"style_profile"`
|
||||||
|
StyleBenchmark string `json:"style_benchmark"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: job
|
||||||
|
prefix: /api/v1/internal
|
||||||
|
middleware: WorkerSecret
|
||||||
|
tags: "Internal Worker"
|
||||||
|
summary: "Internal worker endpoints protected by X-Worker-Secret when InternalWorker.Secret is configured."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler claimWorkerJob
|
||||||
|
post /workers/jobs/claim (ClaimWorkerJobReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler refreshWorkerJobLock
|
||||||
|
post /workers/jobs/:id/heartbeat (WorkerHeartbeatReq) returns (WorkerOKData)
|
||||||
|
|
||||||
|
@handler checkWorkerJobCancel
|
||||||
|
post /workers/jobs/:id/cancel-check (WorkerJobReq) returns (WorkerCancelCheckData)
|
||||||
|
|
||||||
|
@handler ackWorkerJobCancel
|
||||||
|
post /workers/jobs/:id/cancel-ack (WorkerJobReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler updateWorkerJobProgress
|
||||||
|
post /workers/jobs/:id/progress (WorkerProgressReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler completeWorkerJob
|
||||||
|
post /workers/jobs/:id/complete (WorkerCompleteReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler failWorkerJob
|
||||||
|
post /workers/jobs/:id/fail (WorkerFailReq) returns (JobData)
|
||||||
|
|
||||||
|
@handler storePersonaStyleProfileFromWorker
|
||||||
|
patch /workers/personas/:id/style-profile (StorePersonaStyleProfileReq) returns (StorePersonaStyleProfileData)
|
||||||
|
|
||||||
|
@handler getWorkerThreadsAccountSession
|
||||||
|
post /workers/threads-accounts/:id/session (WorkerThreadsAccountSessionReq) returns (WorkerThreadsAccountSessionData)
|
||||||
|
|
||||||
|
@handler analyzeStyle8DFromWorker
|
||||||
|
post /workers/jobs/:id/analyze-style8d (AnalyzeStyle8DReq) returns (AnalyzeStyle8DData)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl {{.version}}
|
||||||
|
|
||||||
|
package {{.PkgName}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
{{if .HasRequest}}"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
{{end}}{{.ImportPackages}}
|
||||||
|
)
|
||||||
|
|
||||||
|
{{if .HasDoc}}{{.Doc}}{{end}}
|
||||||
|
func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
{{if .HasRequest}}var req types.{{.RequestType}}
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
|
||||||
|
{{if .HasResp}}data, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
|
||||||
|
{{if .HasResp}}response.Write(r.Context(), w, data, err){{else}}response.Write(r.Context(), w, nil, err){{end}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
module haixun-backend
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/zeromicro/go-zero v1.9.2
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4
|
||||||
|
golang.org/x/crypto v0.33.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
|
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||||
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||||
|
google.golang.org/grpc v1.65.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||||
|
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
|
||||||
|
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
||||||
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||||
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||||
|
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
|
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||||
|
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zeromicro/go-zero v1.9.2 h1:ZXOXBIcazZ1pWAMiHyVnDQ3Sxwy7DYPzjE89Qtj9vqM=
|
||||||
|
github.com/zeromicro/go-zero v1.9.2/go.mod h1:k8YBMEFZKjTd4q/qO5RCW+zDgUlNyAs5vue3P4/Kmn0=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
|
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||||
|
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||||
|
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
|
||||||
|
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
|
||||||
|
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||||
|
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||||
|
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||||
|
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||||
|
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||||
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
app "haixun-backend/internal/library/errors"
|
||||||
|
"haixun-backend/internal/library/errors/code"
|
||||||
|
"haixun-backend/internal/model/member/domain/entity"
|
||||||
|
domrepo "haixun-backend/internal/model/member/domain/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminOptions struct {
|
||||||
|
TenantID string
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureAdminMember(ctx context.Context, repo domrepo.Repository, opts AdminOptions) (*entity.Member, bool, error) {
|
||||||
|
tenantID := strings.TrimSpace(opts.TenantID)
|
||||||
|
email := normalizeEmail(opts.Email)
|
||||||
|
if tenantID == "" || email == "" || opts.Password == "" {
|
||||||
|
return nil, false, app.For(code.Member).InputMissingRequired("tenant_id, email, and password are required")
|
||||||
|
}
|
||||||
|
if len(opts.Password) < 8 {
|
||||||
|
return nil, false, app.For(code.Member).InputInvalidFormat("password must be at least 8 characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := repo.FindByEmail(ctx, tenantID, email)
|
||||||
|
if err == nil {
|
||||||
|
if err := repo.SetRoles(ctx, tenantID, existing.UID, []string{"admin"}); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
existing.Roles = []string{"admin"}
|
||||||
|
return existing, false, nil
|
||||||
|
}
|
||||||
|
if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(opts.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, app.For(code.Member).SysInternal("hash password failed").WithCause(err)
|
||||||
|
}
|
||||||
|
displayName := strings.TrimSpace(opts.DisplayName)
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = "Admin"
|
||||||
|
}
|
||||||
|
member, err := repo.Create(ctx, &entity.Member{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UID: uuid.NewString(),
|
||||||
|
Email: email,
|
||||||
|
DisplayName: displayName,
|
||||||
|
Language: "zh-TW",
|
||||||
|
Status: entity.StatusOpen,
|
||||||
|
Origin: entity.OriginNative,
|
||||||
|
PasswordHash: string(hash),
|
||||||
|
Roles: []string{"admin"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return member, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEmail(email string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(email))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
app "haixun-backend/internal/library/errors"
|
||||||
|
"haixun-backend/internal/library/errors/code"
|
||||||
|
"haixun-backend/internal/model/member/domain/entity"
|
||||||
|
domrepo "haixun-backend/internal/model/member/domain/repository"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryMemberRepo struct {
|
||||||
|
byEmail map[string]*entity.Member
|
||||||
|
}
|
||||||
|
|
||||||
|
func memberKey(tenantID, email string) string {
|
||||||
|
return tenantID + ":" + normalizeEmail(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) EnsureIndexes(context.Context) error { return nil }
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) Create(_ context.Context, member *entity.Member) (*entity.Member, error) {
|
||||||
|
if m.byEmail == nil {
|
||||||
|
m.byEmail = map[string]*entity.Member{}
|
||||||
|
}
|
||||||
|
key := memberKey(member.TenantID, member.Email)
|
||||||
|
if _, ok := m.byEmail[key]; ok {
|
||||||
|
return nil, app.For(code.Member).ResConflict("member already exists")
|
||||||
|
}
|
||||||
|
cp := *member
|
||||||
|
m.byEmail[key] = &cp
|
||||||
|
return &cp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) FindByUID(_ context.Context, tenantID, uid string) (*entity.Member, error) {
|
||||||
|
for _, item := range m.byEmail {
|
||||||
|
if item.TenantID == tenantID && item.UID == uid {
|
||||||
|
cp := *item
|
||||||
|
return &cp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, app.For(code.Member).ResNotFound("member not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) FindByEmail(_ context.Context, tenantID, email string) (*entity.Member, error) {
|
||||||
|
item, ok := m.byEmail[memberKey(tenantID, email)]
|
||||||
|
if !ok {
|
||||||
|
return nil, app.For(code.Member).ResNotFound("member not found")
|
||||||
|
}
|
||||||
|
cp := *item
|
||||||
|
return &cp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) UpdateProfile(context.Context, string, string, domrepo.ProfileUpdate) (*entity.Member, error) {
|
||||||
|
return nil, app.For(code.Member).SysNotImplemented("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) SetActiveThreadsAccountID(_ context.Context, tenantID, uid, accountID string) error {
|
||||||
|
for _, item := range m.byEmail {
|
||||||
|
if item.TenantID == tenantID && item.UID == uid {
|
||||||
|
item.ActiveThreadsAccountID = accountID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return app.For(code.Member).ResNotFound("member not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) SetRoles(_ context.Context, tenantID, uid string, roles []string) error {
|
||||||
|
for _, item := range m.byEmail {
|
||||||
|
if item.TenantID == tenantID && item.UID == uid {
|
||||||
|
item.Roles = append([]string(nil), roles...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return app.For(code.Member).ResNotFound("member not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureAdminMemberCreatesAdmin(t *testing.T) {
|
||||||
|
repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{}}
|
||||||
|
member, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{
|
||||||
|
TenantID: "default",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Password: "Admin-Pass-1!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnsureAdminMember: %v", err)
|
||||||
|
}
|
||||||
|
if !created {
|
||||||
|
t.Fatal("expected created=true")
|
||||||
|
}
|
||||||
|
if member.Roles[0] != "admin" {
|
||||||
|
t.Fatalf("roles=%v", member.Roles)
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("Admin-Pass-1!")); err != nil {
|
||||||
|
t.Fatalf("password hash mismatch: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureAdminMemberUpgradesExisting(t *testing.T) {
|
||||||
|
repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{
|
||||||
|
memberKey("default", "admin@haixun.local"): {
|
||||||
|
TenantID: "default",
|
||||||
|
UID: "uid-1",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Roles: []string{"user"},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
_, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{
|
||||||
|
TenantID: "default",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Password: "Admin-Pass-1!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnsureAdminMember: %v", err)
|
||||||
|
}
|
||||||
|
if created {
|
||||||
|
t.Fatal("expected created=false")
|
||||||
|
}
|
||||||
|
member := repo.byEmail[memberKey("default", "admin@haixun.local")]
|
||||||
|
if len(member.Roles) != 1 || member.Roles[0] != "admin" {
|
||||||
|
t.Fatalf("roles=%v", member.Roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"haixun-backend/internal/config"
|
||||||
|
libmongo "haixun-backend/internal/library/mongo"
|
||||||
|
jobrepo "haixun-backend/internal/model/job/repository"
|
||||||
|
memberrepo "haixun-backend/internal/model/member/repository"
|
||||||
|
permissionrepo "haixun-backend/internal/model/permission/repository"
|
||||||
|
permissionuc "haixun-backend/internal/model/permission/usecase"
|
||||||
|
settingrepo "haixun-backend/internal/model/setting/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InitOptions struct {
|
||||||
|
TenantID string
|
||||||
|
AdminEmail string
|
||||||
|
AdminPass string
|
||||||
|
DisplayName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitReport struct {
|
||||||
|
IndexesEnsured bool
|
||||||
|
PermissionsSeeded bool
|
||||||
|
RolePermissionsSeeded bool
|
||||||
|
AdminUID string
|
||||||
|
AdminCreated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport, error) {
|
||||||
|
if cfg.Mongo.URI == "" || cfg.Mongo.Database == "" {
|
||||||
|
return nil, fmt.Errorf("mongo URI and database are required")
|
||||||
|
}
|
||||||
|
if opts.TenantID == "" {
|
||||||
|
return nil, fmt.Errorf("tenant_id is required")
|
||||||
|
}
|
||||||
|
if opts.AdminEmail == "" || opts.AdminPass == "" {
|
||||||
|
return nil, fmt.Errorf("admin email and password are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
mongoClient, err := libmongo.NewClient(ctx, cfg.Mongo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect mongo: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = mongoClient.Close(ctx) }()
|
||||||
|
|
||||||
|
db := mongoClient.Database()
|
||||||
|
report := &InitReport{}
|
||||||
|
|
||||||
|
settingRepository := settingrepo.NewMongoRepository(db)
|
||||||
|
memberRepository := memberrepo.NewMongoRepository(db)
|
||||||
|
permissionRepository := permissionrepo.NewMongoPermissionRepository(db)
|
||||||
|
rolePermissionRepository := permissionrepo.NewMongoRolePermissionRepository(db)
|
||||||
|
jobTemplateRepository := jobrepo.NewMongoTemplateRepository(db)
|
||||||
|
jobRunRepository := jobrepo.NewMongoRunRepository(db)
|
||||||
|
jobScheduleRepository := jobrepo.NewMongoScheduleRepository(db)
|
||||||
|
jobEventRepository := jobrepo.NewMongoEventRepository(db)
|
||||||
|
|
||||||
|
repos := []struct {
|
||||||
|
name string
|
||||||
|
fn func(context.Context) error
|
||||||
|
}{
|
||||||
|
{"settings", settingRepository.EnsureIndexes},
|
||||||
|
{"members", memberRepository.EnsureIndexes},
|
||||||
|
{"permissions", permissionRepository.EnsureIndexes},
|
||||||
|
{"role_permissions", rolePermissionRepository.EnsureIndexes},
|
||||||
|
{"job_templates", jobTemplateRepository.EnsureIndexes},
|
||||||
|
{"job_runs", jobRunRepository.EnsureIndexes},
|
||||||
|
{"job_schedules", jobScheduleRepository.EnsureIndexes},
|
||||||
|
{"job_events", jobEventRepository.EnsureIndexes},
|
||||||
|
}
|
||||||
|
for _, repo := range repos {
|
||||||
|
if err := repo.fn(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure %s indexes: %w", repo.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.IndexesEnsured = true
|
||||||
|
|
||||||
|
permissionUseCase := permissionuc.NewUseCase(permissionRepository, rolePermissionRepository)
|
||||||
|
if err := permissionUseCase.EnsureDefaultPermissions(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("seed permissions catalog: %w", err)
|
||||||
|
}
|
||||||
|
report.PermissionsSeeded = true
|
||||||
|
|
||||||
|
if err := permissionUseCase.EnsureDefaultRolePermissions(ctx, opts.TenantID); err != nil {
|
||||||
|
return nil, fmt.Errorf("seed role permissions: %w", err)
|
||||||
|
}
|
||||||
|
report.RolePermissionsSeeded = true
|
||||||
|
|
||||||
|
admin, created, err := EnsureAdminMember(ctx, memberRepository, AdminOptions{
|
||||||
|
TenantID: opts.TenantID,
|
||||||
|
Email: opts.AdminEmail,
|
||||||
|
Password: opts.AdminPass,
|
||||||
|
DisplayName: opts.DisplayName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
report.AdminUID = admin.UID
|
||||||
|
report.AdminCreated = created
|
||||||
|
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/zeromicro/go-zero/rest"
|
||||||
|
|
||||||
|
type MongoConf struct {
|
||||||
|
URI string
|
||||||
|
Database string
|
||||||
|
TimeoutSeconds int `json:",default=10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisConf struct {
|
||||||
|
Addr string `json:",optional"`
|
||||||
|
DB int `json:",optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobWorkerConf struct {
|
||||||
|
Enabled bool `json:",default=true"`
|
||||||
|
WorkerType string `json:",default=go"`
|
||||||
|
WorkerID string `json:",optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobSchedulerConf struct {
|
||||||
|
Enabled bool `json:",default=true"`
|
||||||
|
IntervalSeconds int `json:",default=60"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobReaperConf struct {
|
||||||
|
Enabled bool `json:",default=true"`
|
||||||
|
IntervalSeconds int `json:",default=30"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthConf struct {
|
||||||
|
AccessSecret string `json:",optional"`
|
||||||
|
RefreshSecret string `json:",optional"`
|
||||||
|
AccessExpireSeconds int64 `json:",default=900"`
|
||||||
|
RefreshExpireSeconds int64 `json:",default=2592000"`
|
||||||
|
DevHeaderFallback bool `json:",default=true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalWorkerConf struct {
|
||||||
|
Secret string `json:",optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BraveConf struct {
|
||||||
|
APIKey string `json:",optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
rest.RestConf
|
||||||
|
Mongo MongoConf `json:",optional"`
|
||||||
|
Redis RedisConf `json:",optional"`
|
||||||
|
Auth AuthConf `json:",optional"`
|
||||||
|
InternalWorker InternalWorkerConf `json:",optional"`
|
||||||
|
JobWorker JobWorkerConf `json:",optional"`
|
||||||
|
JobScheduler JobSchedulerConf `json:",optional"`
|
||||||
|
JobReaper JobReaperConf `json:",optional"`
|
||||||
|
Brave BraveConf `json:",optional"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/ai"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChatHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AIChatReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := ai.BearerToken(r)
|
||||||
|
if err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := ai.NewChatLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.Chat(&req, token)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/ai"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChatStreamHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AIChatReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := ai.BearerToken(r)
|
||||||
|
if err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := ai.NewChatStreamLogic(r.Context(), svcCtx)
|
||||||
|
stream, err := l.ChatStream(&req, token)
|
||||||
|
if err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(fmt.Errorf("server does not support streaming")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
|
|
||||||
|
for event := range stream {
|
||||||
|
writeSSE(w, event.Type, event)
|
||||||
|
flusher.Flush()
|
||||||
|
if event.Type == "done" || event.Type == "error" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeSSE(w, "done", map[string]string{"finish_reason": "stop"})
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSSE(w http.ResponseWriter, eventName string, data any) {
|
||||||
|
payload, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
payload = []byte(`{"type":"error","error":"failed to serialize SSE payload"}`)
|
||||||
|
eventName = "error"
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, "event: %s\n", eventName)
|
||||||
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", payload)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/ai"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IslanderChatStreamHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.IslanderChatReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := ai.NewIslanderChatStreamLogic(r.Context(), svcCtx)
|
||||||
|
stream, err := l.ChatStream(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(fmt.Errorf("server does not support streaming")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
|
|
||||||
|
for event := range stream {
|
||||||
|
writeSSE(w, event.Type, event)
|
||||||
|
flusher.Flush()
|
||||||
|
if event.Type == "done" || event.Type == "error" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeSSE(w, "done", map[string]string{"finish_reason": "stop"})
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/ai"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListAiProviderModelsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AIProviderPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := ai.BearerToken(r)
|
||||||
|
if err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := ai.NewListAIProviderModelsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListAIProviderModels(&req, token)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/ai"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListAiProvidersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := ai.NewListAIProvidersLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListAIProviders()
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/auth"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AuthLoginReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := auth.NewLoginLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.Login(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/auth"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := auth.NewLogoutLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.Logout(r.Header.Get("Authorization"))
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/auth"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RefreshHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AuthRefreshReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := auth.NewRefreshLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.Refresh(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/auth"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AuthRegisterReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := auth.NewRegisterLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.Register(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateBrandHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateBrandReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewCreateBrandLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateBrand(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateBrandProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateBrandProductHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewCreateBrandProductLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateBrandProduct(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteBrandHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewDeleteBrandLogic(r.Context(), svcCtx)
|
||||||
|
err := l.DeleteBrand(&req)
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteBrandProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandProductPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewDeleteBrandProductLogic(r.Context(), svcCtx)
|
||||||
|
err := l.DeleteBrandProduct(&req)
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExpandKnowledgeGraphHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.ExpandKnowledgeGraphHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewExpandKnowledgeGraphLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ExpandKnowledgeGraph(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateBrandContentMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GenerateContentMatrixHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewGenerateBrandContentMatrixLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GenerateBrandContentMatrix(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateOutreachDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GenerateOutreachDraftsHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewGenerateOutreachDraftsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GenerateOutreachDrafts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBrandContentMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewGetBrandContentMatrixLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetBrandContentMatrix(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBrandHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewGetBrandLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetBrand(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBrandScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewGetBrandScanScheduleLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetBrandScanSchedule(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetKnowledgeGraphHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewGetKnowledgeGraphLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetKnowledgeGraph(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListBrandProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewListBrandProductsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListBrandProducts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListBrandScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.ListBrandScanPostsHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewListBrandScanPostsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListBrandScanPosts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListBrandsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := brand.NewListBrandsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListBrands()
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PatchKnowledgeGraphNodesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PatchKnowledgeGraphNodesHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewPatchKnowledgeGraphNodesLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.PatchKnowledgeGraphNodes(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PatchScanPostOutreachHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PatchScanPostOutreachHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewPatchScanPostOutreachLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.PatchScanPostOutreach(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PublishOutreachDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PublishOutreachDraftHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewPublishOutreachDraftLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.PublishOutreachDraft(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartBrandScanJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartBrandScanJobHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewStartBrandScanJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartBrandScanJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateBrandHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpdateBrandHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewUpdateBrandLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpdateBrand(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateBrandProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpdateBrandProductHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewUpdateBrandProductLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpdateBrandProduct(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package brand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/brand"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpsertBrandScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpsertBrandScanScheduleHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := brand.NewUpsertBrandScanScheduleLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpsertBrandScanSchedule(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateCopyMissionHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewCreateCopyMissionLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateCopyMission(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CopyMissionPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewDeleteCopyMissionLogic(r.Context(), svcCtx)
|
||||||
|
err := l.DeleteCopyMission(&req)
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateCopyMissionMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GenerateCopyMissionMatrixHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewGenerateCopyMissionMatrixLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GenerateCopyMissionMatrix(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CopyMissionPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewGetCopyMissionLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetCopyMission(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CopyMissionPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewGetCopyMissionScanScheduleLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GetCopyMissionScanSchedule(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InspireCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaCopyMissionsPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewInspireCopyMissionLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.InspireCopyMission(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListCopyMissionCopyDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CopyMissionPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewListCopyMissionCopyDraftsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListCopyMissionCopyDrafts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListCopyMissionScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.ListCopyMissionScanPostsHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewListCopyMissionScanPostsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListCopyMissionScanPosts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListCopyMissionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaCopyMissionsPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewListCopyMissionsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListCopyMissions(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartCopyMissionAnalyzeJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CopyMissionPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewStartCopyMissionAnalyzeJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartCopyMissionAnalyzeJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartCopyMissionCopyDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartCopyMissionCopyDraftJobHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewStartCopyMissionCopyDraftJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartCopyMissionCopyDraftJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartCopyMissionMatrixJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartCopyMissionMatrixJobHandlerReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewStartCopyMissionMatrixJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartCopyMissionMatrixJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartCopyMissionScanJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CopyMissionPath
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := svcCtx.Validator.ValidateAll(&req); err != nil {
|
||||||
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := copy_mission.NewStartCopyMissionScanJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartCopyMissionScanJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue