feat/rebone #3
|
|
@ -2,7 +2,11 @@
|
||||||
.run
|
.run
|
||||||
.cursor
|
.cursor
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
**/.env
|
||||||
backend/bin
|
backend/bin
|
||||||
|
backend/web/dist
|
||||||
|
backend/web/public/downloads/*.zip
|
||||||
|
backend/dev-console/dist
|
||||||
frontend/dist
|
frontend/dist
|
||||||
**/*_test.go
|
**/*_test.go
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
# --- secrets / env(不要 commit 實值)---
|
# --- secrets / env ---
|
||||||
deploy/.env
|
deploy/.env
|
||||||
|
deploy/.env.dev
|
||||||
|
deploy/.env.prod
|
||||||
|
infra/.env
|
||||||
|
*.env
|
||||||
|
**/.env
|
||||||
|
!.env.example
|
||||||
|
!*.env.example
|
||||||
|
|
||||||
# --- build 產物 ---
|
# --- runtime / logs ---
|
||||||
backend/bin/
|
.run/
|
||||||
frontend/dist/
|
|
||||||
|
|
||||||
# --- 依賴 ---
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# --- 其他 ---
|
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# --- build artifacts ---
|
||||||
|
backend/bin/
|
||||||
|
backend/web/dist/
|
||||||
|
backend/dev-console/dist/
|
||||||
|
frontend/dist/
|
||||||
|
dist/
|
||||||
|
backend/web/public/downloads/*.zip
|
||||||
|
|
||||||
|
# --- dependencies ---
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
|
||||||
|
# --- OS / editor ---
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.cursor/
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
72708
|
|
||||||
2857
.run/logs/api.log
2857
.run/logs/api.log
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
> haixun-web@0.1.0 dev
|
|
||||||
> vite
|
|
||||||
|
|
||||||
|
|
||||||
VITE v6.4.3 ready in 187 ms
|
|
||||||
|
|
||||||
➜ Local: http://localhost:5173/
|
|
||||||
➜ Network: use --host to expose
|
|
||||||
1217
.run/logs/worker.log
1217
.run/logs/worker.log
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +0,0 @@
|
||||||
72713
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
72714
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
## 設計文件
|
## 設計文件
|
||||||
|
|
||||||
- `docs/job-system-plan.md`:通用 job system 規劃,包含 template、run、schedule、Redis queue/lock、取消語意與 API 草案。
|
- `docs/job-system-plan.md`:通用 job system 規劃,包含 template、run、schedule、Redis queue/lock、取消語意與 API 草案。
|
||||||
|
- `docs/post-project-plan.md`:發文計畫(文案任務 Flow A 砍掉重做)— 主題發想、風格分析、矩陣產 N 篇草稿、定時發送。
|
||||||
- `docs/scan-placement-plan.md`:海巡獲客(流程 B)— 知識圖譜、Brave 擴展、雙軌爬取(7 天重點 / 30 天補充)、產品匹配、島民交接。
|
- `docs/scan-placement-plan.md`:海巡獲客(流程 B)— 知識圖譜、Brave 擴展、雙軌爬取(7 天重點 / 30 天補充)、產品匹配、島民交接。
|
||||||
|
|
||||||
## 新增 API 流程
|
## 新增 API 流程
|
||||||
|
|
|
||||||
294
Makefile
294
Makefile
|
|
@ -1,200 +1,128 @@
|
||||||
# 巡樓 monorepo Makefile
|
|
||||||
# 兩種模式:
|
|
||||||
# dev - 本機開發(docker 起 Mongo/Redis,go run / vite dev 在本機跑)
|
|
||||||
# docker - 全棧一鍵啟動(Mongo/Redis/gateway/worker/node-worker/web 全在 docker)
|
|
||||||
#
|
|
||||||
# 常用:
|
|
||||||
# make up # 一鍵:build + 啟動整套(含前後端、worker、DB)
|
|
||||||
# make down # 停掉整套
|
|
||||||
# make logs # 追全部日誌
|
|
||||||
# make dev-infra # 本機開發:只起 Mongo/Redis
|
|
||||||
# make dev-backend # 本機開發:go run gateway (:8890)
|
|
||||||
# make dev-frontend # 本機開發:vite dev (:5173)
|
|
||||||
|
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
|
|
||||||
# --- 路徑 ---
|
BACKEND_DIR := backend
|
||||||
BACKEND_DIR := backend
|
WEB_DIR := backend/web
|
||||||
FRONTEND_DIR := frontend
|
INFRA_DIR := infra
|
||||||
|
DEPLOY_DIR := deploy
|
||||||
|
|
||||||
# --- docker compose(prod 全棧)---
|
INFRA_DEV_COMPOSE := docker compose --env-file $(DEPLOY_DIR)/.env.dev -f $(INFRA_DIR)/docker-compose.yml
|
||||||
COMPOSE_FILE := deploy/docker-compose.yml
|
INFRA_PROD_COMPOSE := docker compose --env-file $(DEPLOY_DIR)/.env.prod -f $(INFRA_DIR)/docker-compose.yml
|
||||||
ENV_FILE := deploy/.env
|
DEPLOY_COMPOSE := docker compose --env-file $(DEPLOY_DIR)/.env.prod -f $(DEPLOY_DIR)/docker-compose.yml
|
||||||
COMPOSE := docker compose -f $(COMPOSE_FILE) --env-file $(ENV_FILE)
|
|
||||||
|
|
||||||
# --- docker compose(本地測試;base + dev override + dev env)---
|
|
||||||
DEV_ENV_FILE := deploy/.env.dev
|
|
||||||
DEV_COMPOSE := docker compose -f $(COMPOSE_FILE) -f deploy/docker-compose.dev.yml --env-file $(DEV_ENV_FILE)
|
|
||||||
# go worker 用 gateway 內嵌,dev 不啟動獨立 worker 容器
|
|
||||||
DEV_SERVICES := mongo redis init gateway node-worker web
|
|
||||||
|
|
||||||
# --- dev 用 worker secret(對應 etc/gateway.yaml 的 InternalWorker.Secret)---
|
|
||||||
DEV_WORKER_SECRET := haixun-dev-worker-secret
|
|
||||||
DEV_BACKEND_URL := http://127.0.0.1:8890
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## 顯示可用指令
|
help: ## 顯示可用指令
|
||||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
@grep -E '^[a-zA-Z0-9_-]+:.*## .*$$' $(MAKEFILE_LIST) \
|
||||||
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
$(ENV_FILE):
|
.PHONY: setup-env
|
||||||
@test -f $(ENV_FILE) || (cp deploy/.env.example $(ENV_FILE) && echo "已從 deploy/.env.example 建立 $(ENV_FILE),請填入密碼與 secret 後再 make up")
|
setup-env: ## 互動式產生 deploy/.env.prod(自動 URL-encode MongoDB 密碼)
|
||||||
|
@$(DEPLOY_DIR)/setup.sh
|
||||||
# ============================================================
|
|
||||||
# DOCKER(全棧一鍵)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: up
|
|
||||||
up: $(ENV_FILE) ## [docker] 一鍵啟動整套(build + up,背景)
|
|
||||||
$(COMPOSE) up -d --build
|
|
||||||
@echo ""
|
|
||||||
@echo "=== 啟動中 ==="
|
|
||||||
@echo " 前端: http://localhost:$$(grep -E '^WEB_PORT=' $(ENV_FILE) | cut -d= -f2)"
|
|
||||||
@echo " 健康檢查: curl http://localhost:$$(grep -E '^WEB_PORT=' $(ENV_FILE) | cut -d= -f2)/api/v1/health"
|
|
||||||
@echo " 看狀態: make ps 看日誌: make logs"
|
|
||||||
|
|
||||||
.PHONY: down
|
|
||||||
down: ## [docker] 停掉整套(保留資料 volume)
|
|
||||||
$(COMPOSE) down
|
|
||||||
|
|
||||||
.PHONY: down-clean
|
|
||||||
down-clean: ## [docker] 停掉整套並刪除資料 volume(清空 Mongo/Redis)
|
|
||||||
$(COMPOSE) down -v
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: $(ENV_FILE) ## [docker] 只 build image,不啟動
|
|
||||||
$(COMPOSE) build
|
|
||||||
|
|
||||||
.PHONY: rebuild
|
|
||||||
rebuild: $(ENV_FILE) ## [docker] 無快取重新 build 並重啟
|
|
||||||
$(COMPOSE) build --no-cache
|
|
||||||
$(COMPOSE) up -d
|
|
||||||
|
|
||||||
.PHONY: restart
|
|
||||||
restart: ## [docker] 重啟所有服務
|
|
||||||
$(COMPOSE) restart
|
|
||||||
|
|
||||||
.PHONY: ps
|
|
||||||
ps: ## [docker] 看服務狀態
|
|
||||||
$(COMPOSE) ps
|
|
||||||
|
|
||||||
.PHONY: logs
|
|
||||||
logs: ## [docker] 追全部日誌(Ctrl-C 離開)
|
|
||||||
$(COMPOSE) logs -f
|
|
||||||
|
|
||||||
.PHONY: logs-gateway
|
|
||||||
logs-gateway: ## [docker] 只看 gateway 日誌
|
|
||||||
$(COMPOSE) logs -f gateway
|
|
||||||
|
|
||||||
.PHONY: init
|
|
||||||
init: $(ENV_FILE) ## [docker] 重跑一次 DB 初始化(索引/權限/admin,冪等)
|
|
||||||
$(COMPOSE) run --rm init
|
|
||||||
|
|
||||||
.PHONY: config
|
|
||||||
config: $(ENV_FILE) ## [docker] 驗證 compose 設定
|
|
||||||
$(COMPOSE) config >/dev/null && echo "docker compose config OK"
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# DEV(本地測試,全走 docker)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: dev-up
|
|
||||||
dev-up: ## [dev] 一鍵:本地 docker 測試(gateway 開 :8890,web 開 :8080,go worker 內嵌)
|
|
||||||
$(DEV_COMPOSE) up -d --build $(DEV_SERVICES)
|
|
||||||
@echo ""
|
|
||||||
@echo "=== 本地測試啟動中 ==="
|
|
||||||
@echo " 前端: http://localhost:$$(grep -E '^WEB_PORT=' $(DEV_ENV_FILE) | cut -d= -f2)"
|
|
||||||
@echo " API: http://localhost:8890/api/v1/health"
|
|
||||||
@echo " DevHeaderFallback 已開(可用 X-Tenant-ID / X-UID 模擬登入)"
|
|
||||||
|
|
||||||
.PHONY: dev-down
|
|
||||||
dev-down: ## [dev] 停掉本地 docker(保留資料)
|
|
||||||
$(DEV_COMPOSE) down
|
|
||||||
|
|
||||||
.PHONY: dev-down-clean
|
|
||||||
dev-down-clean: ## [dev] 停掉本地 docker 並清空資料 volume
|
|
||||||
$(DEV_COMPOSE) down -v
|
|
||||||
|
|
||||||
.PHONY: dev-rebuild
|
|
||||||
dev-rebuild: ## [dev] 改 code 後重新 build 並重啟本地 docker
|
|
||||||
$(DEV_COMPOSE) up -d --build $(DEV_SERVICES)
|
|
||||||
|
|
||||||
.PHONY: dev-restart
|
|
||||||
dev-restart: ## [dev] 只重啟(改了 etc/*.yaml 後不用 rebuild,掛載即生效)
|
|
||||||
$(DEV_COMPOSE) restart gateway node-worker
|
|
||||||
|
|
||||||
.PHONY: dev-ps
|
|
||||||
dev-ps: ## [dev] 看本地 docker 狀態
|
|
||||||
$(DEV_COMPOSE) ps
|
|
||||||
|
|
||||||
.PHONY: dev-logs
|
|
||||||
dev-logs: ## [dev] 追本地 docker 日誌
|
|
||||||
$(DEV_COMPOSE) logs -f
|
|
||||||
|
|
||||||
.PHONY: dev-init
|
|
||||||
dev-init: ## [dev] 重跑一次 DB 初始化(索引/權限/admin,冪等)
|
|
||||||
$(DEV_COMPOSE) run --rm init
|
|
||||||
|
|
||||||
.PHONY: dev
|
|
||||||
dev: ## [dev] 本地 docker 測試說明
|
|
||||||
@echo "本地測試(全 docker):"
|
|
||||||
@echo " make dev-up # 一鍵起整套(前端 :8080、API :8890)"
|
|
||||||
@echo " make dev-logs # 看日誌"
|
|
||||||
@echo " make dev-restart # 改 etc/*.yaml 後重啟(免 rebuild)"
|
|
||||||
@echo " make dev-rebuild # 改 Go/前端 code 後重 build"
|
|
||||||
@echo " make dev-down # 停(保留資料)"
|
|
||||||
|
|
||||||
# --- 進階:不進 docker,直接本機 go run / vite(需自備 Go/Node)---
|
|
||||||
.PHONY: local-infra
|
|
||||||
local-infra: ## [local] 只起 Mongo + Redis(docker),其餘本機跑
|
|
||||||
$(DEV_COMPOSE) up -d mongo redis
|
|
||||||
$(DEV_COMPOSE) ps
|
|
||||||
|
|
||||||
.PHONY: local-backend
|
|
||||||
local-backend: ## [local] 本機 go run gateway (:8890)
|
|
||||||
cd $(BACKEND_DIR) && go run . -f etc/gateway.yaml
|
|
||||||
|
|
||||||
.PHONY: local-worker
|
|
||||||
local-worker: ## [local] 本機 go run Go job worker (:8891)
|
|
||||||
cd $(BACKEND_DIR) && go run ./cmd/worker -f etc/gateway.worker.yaml
|
|
||||||
|
|
||||||
.PHONY: local-node-worker
|
|
||||||
local-node-worker: ## [local] 本機跑 Node playwright worker (style-8d)
|
|
||||||
cd $(BACKEND_DIR)/worker && npm install && \
|
|
||||||
HAIXUN_WORKER_SECRET=$(DEV_WORKER_SECRET) HAIXUN_BACKEND_URL=$(DEV_BACKEND_URL) npm run style-8d
|
|
||||||
|
|
||||||
.PHONY: local-frontend
|
|
||||||
local-frontend: ## [local] 本機跑前端 dev server (:5173)
|
|
||||||
cd $(FRONTEND_DIR) && npm install && npm run dev
|
|
||||||
|
|
||||||
.PHONY: local-init
|
|
||||||
local-init: ## [local] 本機初始化 DB(索引/權限)並建立 admin
|
|
||||||
cd $(BACKEND_DIR) && go run ./cmd/tool init -f etc/gateway.yaml
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 驗證 / 維護
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: tidy
|
|
||||||
tidy: ## go mod tidy
|
|
||||||
cd $(BACKEND_DIR) && go mod tidy
|
|
||||||
|
|
||||||
.PHONY: gen-api
|
.PHONY: gen-api
|
||||||
gen-api: ## 用 goctl 從 .api 重新產生 handler/logic/types/routes
|
gen-api: ## 重新產生後端 API handler / logic / types
|
||||||
cd $(BACKEND_DIR) && goctl api go -api generate/api/gateway.api -dir . -home generate/goctl -style go_zero
|
$(MAKE) -C $(BACKEND_DIR) gen-api
|
||||||
|
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: ## gofmt 後端
|
fmt: ## 格式化後端 Go 程式
|
||||||
cd $(BACKEND_DIR) && gofmt -w .
|
$(MAKE) -C $(BACKEND_DIR) fmt
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: ## 跑後端測試
|
test: ## 執行後端測試
|
||||||
cd $(BACKEND_DIR) && go test ./...
|
$(MAKE) -C $(BACKEND_DIR) test
|
||||||
|
|
||||||
|
.PHONY: init
|
||||||
|
init: ## 初始化系統(建索引、種權限、建立 admin,讀 deploy/.env.prod)
|
||||||
|
$(MAKE) -C $(BACKEND_DIR) ENV_FILE=../deploy/.env.prod init
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: ## 啟動後端 API
|
||||||
|
$(MAKE) -C $(BACKEND_DIR) run
|
||||||
|
|
||||||
|
.PHONY: dev-all
|
||||||
|
dev-all: ## 啟動本機完整 dev stack(API、Go workers、Node workers、web)
|
||||||
|
./scripts/dev-all.sh
|
||||||
|
|
||||||
|
.PHONY: dev-all-stop
|
||||||
|
dev-all-stop: ## 停止本機 dev stack(釋放 8890 / 9101 / 9102 / web 等埠)
|
||||||
|
./scripts/dev-all-stop.sh
|
||||||
|
|
||||||
|
.PHONY: web-dev
|
||||||
|
web-dev: ## 啟動正式前端 dev server
|
||||||
|
cd $(WEB_DIR) && npm install && npm run dev
|
||||||
|
|
||||||
|
.PHONY: web-build
|
||||||
|
web-build: ## 建置正式前端
|
||||||
|
cd $(WEB_DIR) && npm install && npm run build
|
||||||
|
|
||||||
.PHONY: verify
|
.PHONY: verify
|
||||||
verify: $(ENV_FILE) ## 後端 build/test + 前端 build + compose 語法
|
verify: test web-build ## 後端測試與前端建置
|
||||||
cd $(BACKEND_DIR) && go build ./... && go test ./...
|
|
||||||
cd $(FRONTEND_DIR) && npm ci && npm run build
|
.PHONY: dev-infra
|
||||||
$(COMPOSE) config >/dev/null && echo "docker compose config OK"
|
dev-infra: ## 啟動本機 dev 資料服務(Mongo / Redis 容器,讀 deploy/.env.dev)
|
||||||
|
@test -f $(DEPLOY_DIR)/.env.dev || cp $(DEPLOY_DIR)/.env.dev.example $(DEPLOY_DIR)/.env.dev
|
||||||
|
@echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.dev 再執行後續指令" >&2
|
||||||
|
$(INFRA_DEV_COMPOSE) up -d
|
||||||
|
$(INFRA_DEV_COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: dev-infra-down
|
||||||
|
dev-infra-down: ## 關閉本機 dev 資料服務(保留資料 volume)
|
||||||
|
$(INFRA_DEV_COMPOSE) down
|
||||||
|
|
||||||
|
.PHONY: dev-infra-recreate
|
||||||
|
dev-infra-recreate: ## 清除 dev 資料 volume 並重啟(Mongo / Redis 全新狀態)
|
||||||
|
$(INFRA_DEV_COMPOSE) down -v
|
||||||
|
$(INFRA_DEV_COMPOSE) up -d
|
||||||
|
$(INFRA_DEV_COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: prod-infra
|
||||||
|
prod-infra: ## 啟動 prod 資料服務(Mongo / Redis,讀 deploy/.env.prod)
|
||||||
|
@test -f $(DEPLOY_DIR)/.env.prod || cp $(DEPLOY_DIR)/.env.prod.example $(DEPLOY_DIR)/.env.prod
|
||||||
|
@echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.prod 再執行後續指令" >&2
|
||||||
|
$(INFRA_PROD_COMPOSE) up -d
|
||||||
|
$(INFRA_PROD_COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: prod-infra-down
|
||||||
|
prod-infra-down: ## 關閉 prod 資料服務(保留資料 volume)
|
||||||
|
$(INFRA_PROD_COMPOSE) down
|
||||||
|
|
||||||
|
.PHONY: prod-infra-recreate
|
||||||
|
prod-infra-recreate: ## 清除 prod 資料 volume 並重啟(Mongo / Redis 全新狀態)
|
||||||
|
$(INFRA_PROD_COMPOSE) down -v
|
||||||
|
$(INFRA_PROD_COMPOSE) up -d
|
||||||
|
$(INFRA_PROD_COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: prod-install
|
||||||
|
prod-install: ## 一鍵部署 prod:複製 .env.prod、build 本地 image、啟動 app、init(需先啟動 infra)
|
||||||
|
@test -f $(DEPLOY_DIR)/.env.prod || cp $(DEPLOY_DIR)/.env.prod.example $(DEPLOY_DIR)/.env.prod
|
||||||
|
@echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.prod 再執行後續指令" >&2
|
||||||
|
@echo ">>> 請確認 infra 已啟動(make prod-infra)" >&2
|
||||||
|
$(DEPLOY_COMPOSE) build
|
||||||
|
$(DEPLOY_COMPOSE) up -d
|
||||||
|
$(DEPLOY_COMPOSE) --profile init run --rm init
|
||||||
|
$(DEPLOY_COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: prod-rebuild
|
||||||
|
prod-rebuild: ## 重新 build 本地 image 並重建 prod app stack(infra 不影響)
|
||||||
|
$(DEPLOY_COMPOSE) build
|
||||||
|
$(DEPLOY_COMPOSE) up -d
|
||||||
|
$(DEPLOY_COMPOSE) ps
|
||||||
|
|
||||||
|
.PHONY: prod-stop
|
||||||
|
prod-stop: ## 關閉 prod app stack(保留 infra 與資料)
|
||||||
|
$(DEPLOY_COMPOSE) down
|
||||||
|
|
||||||
|
.PHONY: prod-stop-all
|
||||||
|
prod-stop-all: ## 關閉 prod app + infra(保留資料 volume)
|
||||||
|
$(DEPLOY_COMPOSE) down
|
||||||
|
$(INFRA_PROD_COMPOSE) down
|
||||||
|
|
||||||
|
.PHONY: prod-recreate
|
||||||
|
prod-recreate: ## 完整清除 prod 全部資料 volume + 重建啟動(app + infra + init)
|
||||||
|
$(DEPLOY_COMPOSE) down -v
|
||||||
|
$(INFRA_PROD_COMPOSE) down -v
|
||||||
|
$(INFRA_PROD_COMPOSE) up -d
|
||||||
|
$(DEPLOY_COMPOSE) up -d
|
||||||
|
$(DEPLOY_COMPOSE) --profile init run --rm init
|
||||||
|
$(DEPLOY_COMPOSE) ps
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -0,0 +1,24 @@
|
||||||
|
FROM golang:1.24-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
RUN apk add --no-cache ca-certificates git
|
||||||
|
COPY backend/go.mod backend/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY backend/ ./
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/gateway ./gateway.go \
|
||||||
|
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/worker ./cmd/worker \
|
||||||
|
&& CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/tool ./cmd/tool
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata \
|
||||||
|
&& addgroup -S haixun \
|
||||||
|
&& adduser -S -G haixun -h /app haixun
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /out/gateway /app/gateway
|
||||||
|
COPY --from=build /out/worker /app/worker
|
||||||
|
COPY --from=build /out/tool /app/tool
|
||||||
|
COPY backend/etc /app/etc
|
||||||
|
USER haixun
|
||||||
|
EXPOSE 8890
|
||||||
|
CMD ["/app/gateway", "-f", "/app/etc/gateway.prod.yaml"]
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
.PHONY: gen-api fmt test run web-dev web-build
|
||||||
|
|
||||||
|
API_FILE := generate/api/gateway.api
|
||||||
|
GOCTL_HOME := generate/goctl
|
||||||
|
GOCTL ?= $(shell go env GOPATH)/bin/goctl
|
||||||
|
|
||||||
|
ENV_FILE ?= ../deploy/.env.dev
|
||||||
|
|
||||||
|
ifneq ($(wildcard $(ENV_FILE)),)
|
||||||
|
# 把 deploy/.env.dev 的值 export 出來(給 make run / native binary 用;dev-all.sh 自行 source 故不走這條)
|
||||||
|
include $(ENV_FILE)
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DB HAIXUN_REDIS_ADDR HAIXUN_REDIS_PASSWORD
|
||||||
|
export HAIXUN_JWT_ACCESS_SECRET HAIXUN_JWT_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
export HAIXUN_SECRETS_KEY HAIXUN_BACKEND_URL
|
||||||
|
export HAIXUN_WORKER_POLL_MS HAIXUN_8D_TARGET_SAMPLES PLAYWRIGHT_HEADLESS
|
||||||
|
export INIT_TENANT_ID INIT_ADMIN_EMAIL INIT_ADMIN_PASSWORD INIT_ADMIN_DISPLAY_NAME
|
||||||
|
endif
|
||||||
|
|
||||||
|
export HAIXUN_JOB_WORKER_TYPE ?= go-demo
|
||||||
|
export HAIXUN_JOB_WORKER_ID ?= local-go-demo
|
||||||
|
|
||||||
|
# 修改 generate/api/*.api 後執行,重新產生 routes / types / handler(logic 已存在則保留實作)
|
||||||
|
gen-api:
|
||||||
|
$(GOCTL) api go -api $(API_FILE) -dir . -style go_zero -home $(GOCTL_HOME)
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
run:
|
||||||
|
mkdir -p bin
|
||||||
|
go build -o bin/gateway gateway.go
|
||||||
|
./bin/gateway -f etc/gateway.yaml
|
||||||
|
|
||||||
|
run-dev:
|
||||||
|
go run gateway.go -f etc/gateway.yaml
|
||||||
|
|
||||||
|
init:
|
||||||
|
go run ./cmd/tool/main.go init -f etc/gateway.yaml
|
||||||
|
|
||||||
|
web-dev:
|
||||||
|
cd web && npm install && npm run dev
|
||||||
|
|
||||||
|
web-build:
|
||||||
|
cd web && npm install && npm run build
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
- `auth`:native email/password 登入、JWT access/refresh token、logout revoke。
|
- `auth`:native email/password 登入、JWT access/refresh token、logout revoke。
|
||||||
- `member`:目前登入會員的 profile 讀寫。
|
- `member`:目前登入會員的 profile 讀寫。
|
||||||
- `permission`:permission catalog 與目前會員權限查詢。
|
- `permission`:permission catalog 與目前會員權限查詢。
|
||||||
|
- `threads automation`:Threads 帳號、OAuth/API 診斷、發文 queue、補庫存、智慧時段、頻率護欄、成效追蹤與語調庫。
|
||||||
|
|
||||||
暫時不包含 template-monorepo 裡較重的 OAuth / OTP / MFA / Zitadel 整合,也不包含 notification、Playwright worker。這些之後要接時再按服務邊界新增。
|
暫時不包含 template-monorepo 裡較重的 OAuth / OTP / MFA / Zitadel 整合,也不包含 notification、Playwright worker。這些之後要接時再按服務邊界新增。
|
||||||
|
|
||||||
|
|
@ -23,6 +24,13 @@ go mod download
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
正式前端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make web-dev # Vite dev server :5173,proxy 到 :8890
|
||||||
|
make web-build # TypeScript + Vite build
|
||||||
|
```
|
||||||
|
|
||||||
預設服務:
|
預設服務:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -61,10 +69,10 @@ HAIXUN_8D_MIN_SAMPLES=1 # 驗證期預設 1;要嚴格一點可調高
|
||||||
前端在人設詳情頁按「開始 8D 分析」後,任務會進入:
|
前端在人設詳情頁按「開始 8D 分析」後,任務會進入:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
確認連線 -> 抓取樣本 -> AI 8D -> 儲存策略
|
準備爬蟲 -> 抓取公開樣本 -> AI 8D -> 儲存策略
|
||||||
```
|
```
|
||||||
|
|
||||||
目前 Node worker 先用 Playwright 抓 Threads 公開頁樣本並產生可驗證的 8D 結構;若公開頁無法讀到足夠樣本,job 會標記為 `failed` 並顯示原因,不會停在等待狀態。
|
人設 8D 分析**不需** Chrome extension session;Node worker 以匿名 Playwright 讀取對標帳號的公開個人頁。若公開頁無法讀到足夠樣本,job 會標記為 `failed` 並顯示原因,不會停在等待狀態。AI 分析使用會員設定頁的研究用 provider / API key(有選定經營帳號時沿用該帳號設定)。
|
||||||
|
|
||||||
## 專案結構
|
## 專案結構
|
||||||
|
|
||||||
|
|
@ -72,6 +80,8 @@ HAIXUN_8D_MIN_SAMPLES=1 # 驗證期預設 1;要嚴格一點可調高
|
||||||
haixun-backend/
|
haixun-backend/
|
||||||
gateway.go # go-zero server 入口
|
gateway.go # go-zero server 入口
|
||||||
Makefile # gen-api / fmt / test / run
|
Makefile # gen-api / fmt / test / run
|
||||||
|
web/ # 正式巡樓 Console(Vite + React + TypeScript)
|
||||||
|
dev-console/ # 本機診斷用最小開發面板,不是產品 UI
|
||||||
etc/ # runtime config
|
etc/ # runtime config
|
||||||
generate/
|
generate/
|
||||||
api/ # goctl .api 定義
|
api/ # goctl .api 定義
|
||||||
|
|
@ -87,6 +97,11 @@ haixun-backend/
|
||||||
auth/ # JWT token issue/refresh/logout + Redis revoke store
|
auth/ # JWT token issue/refresh/logout + Redis revoke store
|
||||||
member/ # Native member profile + password hash
|
member/ # Native member profile + password hash
|
||||||
permission/ # Permission catalog + role permission mapping
|
permission/ # Permission catalog + role permission mapping
|
||||||
|
publish_queue/ # Threads 發文 queue、guarded transition、retry/missed
|
||||||
|
publish_inventory/ # 自動補庫存 policy
|
||||||
|
publish_guard/ # 發文頻率護欄與 pause/resume
|
||||||
|
publish_queue_event/ # Queue 狀態轉移與告警事件
|
||||||
|
style_preset/ # 人設語調 preset、CTA、禁用詞
|
||||||
worker/ # 常駐背景 worker / scheduler / reaper
|
worker/ # 常駐背景 worker / scheduler / reaper
|
||||||
library/ # 最小 runtime library
|
library/ # 最小 runtime library
|
||||||
response/ # 統一 JSON response envelope
|
response/ # 統一 JSON response envelope
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
bootstrap.LoadInfraEnv()
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
printUsage()
|
printUsage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -37,7 +39,7 @@ func runInit(args []string) error {
|
||||||
configFile := fs.String("f", "etc/gateway.yaml", "config file")
|
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")
|
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@30cm.net"), "bootstrap admin email")
|
email := fs.String("email", envOr("INIT_ADMIN_EMAIL", "admin@30cm.net"), "bootstrap admin email")
|
||||||
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Fafafa54088"), "bootstrap admin password")
|
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", ""), "bootstrap admin password")
|
||||||
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"haixun-backend/internal/bootstrap"
|
||||||
"haixun-backend/internal/config"
|
"haixun-backend/internal/config"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
|
|
||||||
|
|
@ -18,11 +19,12 @@ var configFile = flag.String("f", "etc/gateway.worker.yaml", "config file")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
bootstrap.LoadInfraEnv()
|
||||||
|
|
||||||
var c config.Config
|
var c config.Config
|
||||||
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
||||||
if !c.JobWorker.Enabled {
|
if !c.JobWorker.Enabled && !c.JobScheduler.Enabled && !c.JobReaper.Enabled && !c.ThreadsTokenRefresh.Enabled && !c.ThreadsPublishQueue.Enabled {
|
||||||
fmt.Fprintln(os.Stderr, "[worker] JobWorker.Enabled must be true")
|
fmt.Fprintln(os.Stderr, "[worker] at least one worker, scheduler, reaper, token sweeper, or publish queue sweeper must be enabled")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,10 +32,13 @@ func main() {
|
||||||
defer sc.Close(context.Background())
|
defer sc.Close(context.Background())
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"[worker] started type=%s (scheduler=%v reaper=%v)\n",
|
"[worker] started type=%s (job=%v scheduler=%v reaper=%v token_sweeper=%v publish_sweeper=%v)\n",
|
||||||
c.JobWorker.WorkerType,
|
c.JobWorker.WorkerType,
|
||||||
|
c.JobWorker.Enabled,
|
||||||
c.JobScheduler.Enabled,
|
c.JobScheduler.Enabled,
|
||||||
c.JobReaper.Enabled,
|
c.JobReaper.Enabled,
|
||||||
|
c.ThreadsTokenRefresh.Enabled,
|
||||||
|
c.ThreadsPublishQueue.Enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
ch := make(chan os.Signal, 1)
|
ch := make(chan os.Signal, 1)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# 巡樓 Dev Console
|
||||||
|
|
||||||
|
重做正式前端前的**最小開發面板**:登入、人設 CRUD、8D 公開爬蟲任務與 job 輪詢。
|
||||||
|
|
||||||
|
## 啟動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 後端 API(另開終端)
|
||||||
|
cd backend
|
||||||
|
go run gateway.go -f etc/gateway.yaml
|
||||||
|
|
||||||
|
# 2. Node 8D worker(另開終端,人設 8D 需要)
|
||||||
|
cd backend/worker
|
||||||
|
npm install
|
||||||
|
npm run style-8d
|
||||||
|
|
||||||
|
# 3. Dev Console
|
||||||
|
cd backend/dev-console
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
瀏覽器開啟 http://127.0.0.1:5173 。API 透過 Vite proxy 轉到 `http://127.0.0.1:8890`,無需處理 CORS。
|
||||||
|
|
||||||
|
## 使用順序
|
||||||
|
|
||||||
|
1. 註冊或登入(tenant 預設 `default`)
|
||||||
|
2. **AI 設定**:選研究 provider → 貼 API key → **讀取模型列表** → 選 model → **儲存**
|
||||||
|
3. 建立人設 → 點選列表中的一筆
|
||||||
|
4. 填對標 Threads 帳號 → **開始 8D**
|
||||||
|
5. 自動輪詢 job;完成後下方人設詳情會出現 `style_profile`
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 8D 分析使用**研究用** provider / model + API key(與 Chrome session 無關)
|
||||||
|
- 若尚無經營帳號,Dev Console 會自動建立「Dev Console」帳號(AI key 實際存會員 scope)
|
||||||
|
- 僅供本機開發,不是產品 UI
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,225 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-Hant">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>巡樓 Dev Console</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f4f6f8;
|
||||||
|
--card: #fff;
|
||||||
|
--ink: #1a2332;
|
||||||
|
--muted: #5a6578;
|
||||||
|
--line: #d8dee8;
|
||||||
|
--brand: #2a9d8f;
|
||||||
|
--danger: #c0392b;
|
||||||
|
--ok: #1e7a4c;
|
||||||
|
--mono: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||||
|
--sans: system-ui, -apple-system, sans-serif;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--sans);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
background: var(--card);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
header h1 { margin: 0; font-size: 1.1rem; }
|
||||||
|
header p { margin: 0.15rem 0 0; color: var(--muted); font-size: 0.85rem; }
|
||||||
|
main {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1.25rem 3rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
}
|
||||||
|
section h2 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
label { font-size: 0.82rem; color: var(--muted); min-width: 5.5rem; }
|
||||||
|
input, select, textarea {
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.45rem 0.6rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
input { flex: 1; }
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.45rem 0.85rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover { border-color: var(--brand); color: var(--brand); }
|
||||||
|
button.primary {
|
||||||
|
background: var(--brand);
|
||||||
|
border-color: var(--brand);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
button.primary:hover { filter: brightness(1.05); color: #fff; }
|
||||||
|
button.danger { color: var(--danger); }
|
||||||
|
.status { font-size: 0.82rem; color: var(--muted); }
|
||||||
|
.status.ok { color: var(--ok); }
|
||||||
|
.status.err { color: var(--danger); }
|
||||||
|
.list {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list li {
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.list li:last-child { border-bottom: none; }
|
||||||
|
.list li:hover { background: #f0faf8; }
|
||||||
|
.list li.active { background: #e6f5f2; font-weight: 600; }
|
||||||
|
.list li .meta { color: var(--muted); font-size: 0.78rem; }
|
||||||
|
pre, .log {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #0f1419;
|
||||||
|
color: #d6e2f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 280px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.steps { margin: 0.5rem 0 0; font-size: 0.85rem; }
|
||||||
|
.steps div { padding: 0.2rem 0; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
background: #eef2f7;
|
||||||
|
}
|
||||||
|
.badge.running { background: #fff3cd; }
|
||||||
|
.badge.succeeded, .badge.completed { background: #d4edda; }
|
||||||
|
.badge.failed { background: #f8d7da; }
|
||||||
|
.badge.queued, .badge.pending { background: #e8eef5; }
|
||||||
|
textarea { width: 100%; min-height: 120px; font-family: var(--mono); font-size: 0.78rem; }
|
||||||
|
.hint { font-size: 0.8rem; color: var(--muted); margin-top: 0.35rem; }
|
||||||
|
</style>
|
||||||
|
<script type="module" crossorigin src="/assets/index-56hNfpGK.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<h1>巡樓 Dev Console</h1>
|
||||||
|
<p>暫用開發面板 · 人設 + 8D 公開爬蟲 · 重做前端前夠用就好</p>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="connStatus">尚未連線</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<h2>1. 登入</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label for="tenantId">Tenant</label>
|
||||||
|
<input id="tenantId" value="default" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input id="email" type="email" placeholder="you@example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label for="password">密碼</label>
|
||||||
|
<input id="password" type="password" placeholder="至少 8 碼" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="primary" id="btnLogin">登入</button>
|
||||||
|
<button id="btnRegister">註冊</button>
|
||||||
|
<button id="btnLogout" class="danger">登出</button>
|
||||||
|
<span class="status" id="authStatus"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>2. 人設</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label for="displayName">名稱</label>
|
||||||
|
<input id="displayName" placeholder="新人設" />
|
||||||
|
<button class="primary" id="btnCreatePersona">建立</button>
|
||||||
|
<button id="btnRefreshPersonas">重新載入</button>
|
||||||
|
</div>
|
||||||
|
<ul class="list" id="personaList"></ul>
|
||||||
|
<p class="hint">點選一筆人設後,下方可跑 8D 分析。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>3. 8D 風格分析(公開爬蟲,不需 session)</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label for="benchmark">對標帳號</label>
|
||||||
|
<input id="benchmark" placeholder="@username" />
|
||||||
|
<button class="primary" id="btnStart8d" disabled>開始 8D</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">需先啟動 Go API(:8890)與 Node worker(style-8d)。設定頁要有研究用 AI API key。</p>
|
||||||
|
<div class="status" id="jobHint"></div>
|
||||||
|
<div class="row" style="margin-top: 0.5rem">
|
||||||
|
<label for="jobId">Job ID</label>
|
||||||
|
<input id="jobId" placeholder="自動填入或手動貼上" />
|
||||||
|
<button id="btnPollJob">查詢一次</button>
|
||||||
|
<button id="btnAutoPoll">自動輪詢</button>
|
||||||
|
</div>
|
||||||
|
<div class="steps" id="jobSteps"></div>
|
||||||
|
<pre id="jobDetail">(尚無任務)</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>4. 人設詳情</h2>
|
||||||
|
<div class="row">
|
||||||
|
<button id="btnReloadPersona" disabled>重新載入選中人設</button>
|
||||||
|
<span class="status" id="personaMeta"></span>
|
||||||
|
</div>
|
||||||
|
<textarea id="personaDetail" readonly placeholder="選中人設後顯示 style_profile 等欄位"></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Log</h2>
|
||||||
|
<pre class="log" id="log"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "haixun-dev-console",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
server: {
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
# 全 Phase 新增功能說明
|
|
||||||
|
|
||||||
## Phase 1 — TA 精準度
|
|
||||||
|
|
||||||
### 1. Semantic Embedding Reranker
|
|
||||||
|
|
||||||
**ScanPost entity 新增欄位**(`internal/model/scan_post/domain/entity/post.go`):
|
|
||||||
|
|
||||||
| 欄位 | 型別 | 用途 |
|
|
||||||
|------|------|------|
|
|
||||||
| `Embedding` | `[]float32` | OpenAI text-embedding-3-small 向量 |
|
|
||||||
| `SemanticScore` | `int` | 語意相似度分數(0–100) |
|
|
||||||
| `EngagementPredicted` | `int` | 預測互動率 |
|
|
||||||
| `AudienceQualityScore` | `int` | 受眾品質分數(0–100) |
|
|
||||||
| `AuthorID` | `string` | 作者 ID(Threads user ID) |
|
|
||||||
| `AuthorAvatar` | `string` | 作者頭像 URL |
|
|
||||||
| `AuthorFollowers` | `int` | 作者粉絲數 |
|
|
||||||
|
|
||||||
`ScanCandidate` struct 同步新增對應欄位(`internal/library/placement/dual_track.go`)。
|
|
||||||
|
|
||||||
### 2. Embedding Client
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/library/embedding/client.go
|
|
||||||
func NewClient(apiKey, model string) *Client
|
|
||||||
func (c *Client) Generate(ctx context.Context, text string) ([]float32, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
- 預設使用 OpenAI `text-embedding-3-small`(可降級 local BGE-M3)
|
|
||||||
- Scorer: `CosineSimilarity(a, b []float32) float64`
|
|
||||||
|
|
||||||
### 3. Placement Score V2
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/library/placement/dual_track.go
|
|
||||||
func computePlacementScore(post *ScanCandidate, member productContext, brand productMeta,
|
|
||||||
semanticScore, predictedEngagement, audienceQuality *int) int
|
|
||||||
```
|
|
||||||
|
|
||||||
整合三項 pointer 參數(nil 表示欄位不存在,跳過該項):
|
|
||||||
|
|
||||||
- **語意相似度**(`SemanticScore`):Embedding cosine → 0–100
|
|
||||||
- **預測互動率**(`EngagementPredicted`):like+reply → 對數正規化 0–100
|
|
||||||
- **受眾品質**(`AudienceQualityScore`):`computeAudienceQuality()` heuristic
|
|
||||||
|
|
||||||
權重:語意 20% / 互動 30% / 品質 10% / 舊有 productFit + solvedBy + recency 40%。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func computeAudienceQuality(followers int) int
|
|
||||||
```
|
|
||||||
|
|
||||||
Heuristic:followers 在 500–50k 之間品質最佳,過低或過高遞減。
|
|
||||||
|
|
||||||
### 4. Creator Lookalike
|
|
||||||
|
|
||||||
**端點**: `POST /api/v1/scan-post/lookalike`(需 JWT)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "source_post_id": "uuid", "brand_id": "...", "limit": 10 }
|
|
||||||
|
|
||||||
→ { "list": [{ "post_id", "author_id", "author_name", "similarity", ... }] }
|
|
||||||
```
|
|
||||||
|
|
||||||
**演算法**: cosine similarity(門檻 ≥0.3),同 brand 內找 embedding 最接近的貼文回傳。
|
|
||||||
|
|
||||||
| 檔案 | |
|
|
||||||
|------|--|
|
|
||||||
| Handler | `internal/handler/scan_post/lookalike_handler.go` |
|
|
||||||
| Logic | `internal/logic/scan_post/lookalike_logic.go` |
|
|
||||||
| `cosineSimilarity` | `internal/logic/scan_post/lookalike_logic.go` — 全 Go 實作 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 — 營運流程
|
|
||||||
|
|
||||||
### 2.1 Creator CRM
|
|
||||||
|
|
||||||
**完整四層 model** `crm_contact`:
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/model/crm_contact/
|
|
||||||
domain/entity/entity.go
|
|
||||||
domain/repository/repository.go
|
|
||||||
domain/usecase/usecase.go
|
|
||||||
usecase/usecase.go
|
|
||||||
repository/mongo.go
|
|
||||||
```
|
|
||||||
|
|
||||||
**端點**: `/api/v1/crm/contacts`(需 JWT)
|
|
||||||
|
|
||||||
| Method | Path | 用途 |
|
|
||||||
|--------|------|------|
|
|
||||||
| GET | `/` | List(`page`/`pageSize`/`brand_id`/`status`/`tag`) |
|
|
||||||
| POST | `/` | 新增聯絡人(`author_id` + `author_name`) |
|
|
||||||
| GET | `/:id` | 取得單筆 |
|
|
||||||
| PATCH | `/:id` | 更新(`notes`/`tags`/`status`) |
|
|
||||||
| DELETE | `/:id` | 刪除 |
|
|
||||||
|
|
||||||
**CRMContact 狀態機**: `lead` → `contacted` → `responded` → `converted` / `archived`
|
|
||||||
|
|
||||||
### 2.2 Publish Analytics
|
|
||||||
|
|
||||||
**完整四層 model** `publish_analytics`:
|
|
||||||
|
|
||||||
**Checkpoint 常數**(`internal/model/publish_analytics/usecase/usecase.go`): `+1h`、`+24h`、`+7d`
|
|
||||||
|
|
||||||
**方法**:
|
|
||||||
```go
|
|
||||||
RecordCheckpoint(ctx, tenantID, ownerUID, summary) (*PublishAnalyticsSummary, error)
|
|
||||||
ListByMedia(ctx, tenantID, ownerUID, mediaID) ([]PublishAnalyticsSummary, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Worker handler** `publish-analytics`(step `check`):
|
|
||||||
|
|
||||||
呼叫 Threads API `GET /{media-id}?fields=like_count,reply_count,repost_count,quote_count` → 存入 `publish_analytics`。
|
|
||||||
|
|
||||||
### 2.3 Threads API Token 刷新
|
|
||||||
|
|
||||||
**底層 API**(`internal/library/threadsapi/media.go`):
|
|
||||||
```go
|
|
||||||
RefreshAccessToken(ctx, currentToken) (*TokenRefreshResult, error)
|
|
||||||
// GET /v1.0/refresh_access_token?grant_type=th_refresh_token
|
|
||||||
```
|
|
||||||
|
|
||||||
**儲存 Token**:
|
|
||||||
```go
|
|
||||||
// Repository
|
|
||||||
SaveAPIAccessToken(ctx, accountID, accessToken, expiresAt) (*entity.Secrets, error)
|
|
||||||
// Usecase
|
|
||||||
SaveAPIAccessToken(ctx, accountID, accessToken, expiresAt) (*SecretsData, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Worker handler** `refresh-threads-token`(step `refresh`):讀取 secrets → refresh → 儲存。
|
|
||||||
|
|
||||||
### 2.4 Rate Limiter
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/library/ratelimit/token_bucket.go
|
|
||||||
func NewTokenBucket(rate float64, burst int) *TokenBucket
|
|
||||||
func (tb *TokenBucket) Allow() bool
|
|
||||||
func (tb *TokenBucket) AllowN(n int) bool
|
|
||||||
```
|
|
||||||
|
|
||||||
- In-memory token bucket(非 Redis),thread-safe
|
|
||||||
- `rate` = tokens/sec,`burst` = 最大 burst
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Infrastructure
|
|
||||||
|
|
||||||
### Job Templates
|
|
||||||
|
|
||||||
`internal/model/job/usecase/usecase.go` 新增兩個 template constants + 對應 `Ensure*` methods + `template()` functions:
|
|
||||||
|
|
||||||
| Template Type | Steps | Timeout | Retry |
|
|
||||||
|---------------|-------|---------|-------|
|
|
||||||
| `refresh-threads-token` | `refresh`(go) | 120s | 3 次(30/120/300s) |
|
|
||||||
| `publish-analytics` | `check`(go) | 120s | 2 次(60s) |
|
|
||||||
|
|
||||||
### Worker Handlers 註冊
|
|
||||||
|
|
||||||
`internal/svc/service_context.go` 中註冊:
|
|
||||||
|
|
||||||
```go
|
|
||||||
jobworker.RegisterRefreshThreadsTokenHandler(runner, RefreshThreadsTokenDeps{...})
|
|
||||||
jobworker.RegisterPublishAnalyticsHandler(runner, PublishAnalyticsDeps{...})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Worker Helper
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/worker/job/expand_graph.go
|
|
||||||
func int64Field(payload map[string]any, key string) int64
|
|
||||||
```
|
|
||||||
|
|
||||||
從 job payload 讀 `int64`,支援 `float64` / `int64` / `int` / `json.Number`。
|
|
||||||
|
|
@ -1,512 +0,0 @@
|
||||||
# 海巡獲客計畫:知識圖譜 + 雙軌爬取 + 島民交接
|
|
||||||
|
|
||||||
> 在既有「背景 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)
|
|
||||||
|
|
@ -1,307 +0,0 @@
|
||||||
# 相似帳號 / 對標帳號 / 找 TA — 改進計畫
|
|
||||||
|
|
||||||
## ⚠ 換手進度(2026-06-28)
|
|
||||||
|
|
||||||
### ✅ Phase 1 已完成(全部測試通過)
|
|
||||||
`make fmt && make test` 全 ok;`make build-frontend` 通過。`go build ./...` 通過。
|
|
||||||
|
|
||||||
已完成項目:
|
|
||||||
- **1.1 名額放寬**:`MaxSimilarAccounts` 5 → 10(`backend/internal/library/viral/discover_accounts.go:15`)。
|
|
||||||
- **1.2 停止清空**:`analyze_copy_mission.go` 不再 `SimilarAccounts = nil`,改用 `similarAccountsFromSummary(mission.ResearchMap.SimilarAccounts)` 保留前次(`backend/internal/worker/job/analyze_copy_mission.go:128`)。
|
|
||||||
- **1.3 合併取代覆蓋**:`scan_viral.go` 改 `libviral.MergeSimilarAccounts(prev, new)`(`backend/internal/worker/job/scan_viral.go:195`)。
|
|
||||||
- **1.4 排序權重抽出**:`reference_accounts.go` 新增 `ReferenceRankWeights` + `DefaultReferenceRankWeights` + `rankScore`(log-scale follower bucket 1k/10k/100k/1M、topicHits 加權),排序用加權和;`postTopicRelevant` 改為 `topicTopicHits` 回傳 hits 並用 `normalisedTopicTerms` lower-case 取代 `topicTerms`。
|
|
||||||
- **1.5 web search fallback**:`scan_viral.go:170` `BuildReferenceAccountsFromScan` 後若 `len(referenceAccounts) < MaxSimilarAccounts` 且 `placement.WebSearchAvailable(research)` → 呼 `supplementSimilarAccountsFromWeb`(`scan_viral.go:369`),透過 `websearch.New(memberCtx.WebSearchConfig())` 建 client → `DiscoverSimilarAccounts` → `MergeSimilarAccounts` 串接。失敗安全:web search 失敗不讓 job fail。
|
|
||||||
- **1.6 ProfileURL helper**:新檔 `backend/internal/library/threadsapi/url.go` `ProfileURLFromPermalink(permalink, username)`,優先用 permalink 反推、fallback `https://www.threads.com/@<u>`(`.com` 為 canonical host)。`reference_accounts.go:137`、`enrich_accounts.go`、`discover_accounts.go` 已改用。
|
|
||||||
- **1.7 Brave StartPublishedDate**:`brave.SearchOptions` 加 `StartPublishedDate string` 欄位(forward-compat,目前 brave client 尚未傳給 API);`websearch/client.go` braveAdapter 把 `opts.StartPublishedDate` 塞進 libbrave.SearchOptions。Exa 原本就有支援。
|
|
||||||
- **1.8 前端顯示**:`frontend/src/types/copyMission.ts` `CopySimilarAccountData` 加 `matched_source?: string[]` + `similarAccountSourceTier`/`similarAccountSourceLabel` helper;`frontend/src/lib/viralSignals.ts` 加 `similarAccountTone`;`CopyMissionDetailPage.tsx:837-878` row 加「來源」badge(scan+web/sky/success、web/sky、scan/brand)、「置信度」小字。
|
|
||||||
- **schema**:`missionentity.SimilarAccount` 加 `MatchedSource []string`(`backend/internal/model/copy_mission/domain/entity/mission.go:27`);`domusecase.SimilarAccountSummary` 同步;`usecase.toSummary` 帶 matched;`types.CopySimilarAccountData` 同步;`logic/copy_mission/mapper.go` entity↔API 兩處對映帶 matched;`generate/api/copy_mission.api` 的 `CopySimilarAccountData` 同步(手動補,**未跑 `make gen-api`**——必要時下一手可跑 gen-api 重新產生以對齊)。
|
|
||||||
- 新檔:
|
|
||||||
- `backend/internal/library/viral/merge_accounts.go` `MergeSimilarAccounts` + `unionMatchedSource`。
|
|
||||||
- `backend/internal/library/threadsapi/url.go` `ProfileURLFromPermalink`。
|
|
||||||
- `backend/internal/worker/job/similar_accounts.go` `similarAccountsFromSummary`(summary→entity 轉換 helper,給 worker 跨 rerun 保留 Previous)。
|
|
||||||
|
|
||||||
### 未做、給下一手
|
|
||||||
- **Phase 2**:相似帳號變一等公民(`Status`/`TopicRelevance`/`LastSeenAt` schema + PATCH API + worker 尊重 excluded/pinned + 前端釘選/排除按鈕 + Placement 維持不動)。詳見下方 Phase 2 章節。
|
|
||||||
- **Phase 3**:TA 樣本(留言作者採樣,`AudienceSample` + `audience.go` + worker 接入 + PATCH API + 前端「TA 樣本」區)。詳見下方 Phase 3 章節。
|
|
||||||
- **Phase 4**:跨任務記憶(後續,未規劃細節)。
|
|
||||||
- **重新 gen-api**:`generate/api/copy_mission.api` 只有 `CopySimilarAccountData` 加 `matched_source` 一處,types.go 已手動同步。若下一手要擴 API schema(Phase 2),建議跑 `make gen-api` 重新產 handler/logic/types 確保對齊。
|
|
||||||
- **新測試**:Phase 1 沒額外加新 test case。建議下一手補:`viral/merge_accounts_test.go`(`MergeSimilarAccounts` merges、preserves prev、truncates to limit、unionMatchedSource dedup)、`viral/reference_accounts_test.go` 新排序 case(topicHits 加權、log-scale follower bucket)、`worker/job/scan_viral_test.go` web fallback case(mock websearch client)。這些 test 是 Phase 1 的「補強」層,不阻塞 Phase 2 進行。
|
|
||||||
|
|
||||||
### 已動的檔案清單(Phase 1)
|
|
||||||
**Go 後端**
|
|
||||||
- `backend/internal/library/viral/discover_accounts.go`(常數、ProfileURL helper、accountCandidate.permalink)
|
|
||||||
- `backend/internal/library/viral/reference_accounts.go`(ReferenceRankWeights、topicHits、MatchedSource、ProfileURL helper、刪 topicTerms)
|
|
||||||
- `backend/internal/library/viral/enrich_accounts.go`(MatchedSource、ProfileURL helper、authorScore.permalink)
|
|
||||||
- `backend/internal/library/viral/merge_accounts.go`(**新檔**)
|
|
||||||
- `backend/internal/library/threadsapi/url.go`(**新檔**)
|
|
||||||
- `backend/internal/library/websearch/client.go`(braveAdapter 傳 StartPublishedDate)
|
|
||||||
- `backend/internal/library/brave/client.go`(SearchOptions 加 StartPublishedDate)
|
|
||||||
- `backend/internal/worker/job/scan_viral.go`(web fallback + MergeSimilarAccounts + supplementSimilarAccountsFromWeb)
|
|
||||||
- `backend/internal/worker/job/analyze_copy_mission.go`(停止清空 SimilarAccounts)
|
|
||||||
- `backend/internal/worker/job/similar_accounts.go`(**新檔**:similarAccountsFromSummary)
|
|
||||||
- `backend/internal/model/copy_mission/domain/entity/mission.go`(SimilarAccount 加 MatchedSource)
|
|
||||||
- `backend/internal/model/copy_mission/domain/usecase/usecase.go`(SimilarAccountSummary 加 MatchedSource)
|
|
||||||
- `backend/internal/model/copy_mission/usecase/usecase.go`(toSummary 帶 matched)
|
|
||||||
- `backend/internal/types/types.go`(CopySimilarAccountData 加 matched_source)
|
|
||||||
- `backend/generate/api/copy_mission.api`(CopySimilarAccountData 加 matched_source)
|
|
||||||
- `backend/internal/logic/copy_mission/mapper.go`(兩處對映帶 matched)
|
|
||||||
|
|
||||||
**前端**
|
|
||||||
- `frontend/src/types/copyMission.ts`(CopySimilarAccountData 加 matched_source + helper)
|
|
||||||
- `frontend/src/lib/viralSignals.ts`(similarAccountTone)
|
|
||||||
- `frontend/src/pages/CopyMissionDetailPage.tsx`(row 顯示 source badge、置信度小字)
|
|
||||||
|
|
||||||
### 設計選擇備忘
|
|
||||||
- `similarAccountsFromSummary` 只帶 summary 可見欄位(沒 `TopicRelevance/LastSeenAt/Status`,那些是 Phase 2)。Phase 1 schema 只補 `MatchedSource`,其他 Phase 2 欄位別在 Phase 1 先加,避免污染。
|
|
||||||
- 排序權重改成 log-scale follower bucket(1k/10k/100k/1M cut),避免 mega 帳號壓過 niche 高相關作者。`ReferenceRankWeights` 結構在 `reference_accounts.go` 內部,未注入 input,未來要調整再加。
|
|
||||||
- `ProfileURL` 從 `https://www.threads.net/@` 改 `https://www.threads.com/@`(threads.com 現在是 canonical host,前端 `frontend/src/lib/threadsLinks.ts` 用 `threadsProfileUrl(username, profileUrl)` 已能接受 `profile_url` injected,不需改)。
|
|
||||||
- `MergeSimilarAccounts` 串接 priority:新(scan+web)> 舊(保舊不被清掉),命中兩條 source 的 username `MatchedSource` 會 union 含 scan 與 web,但 **confidence 不會自動升 high**(confidence 由 `BuildReferenceAccountsFromScan` 內 strict/relaxed gate + verified + bestEngagement 算出來)。若未來要把「跨來源」當 high confidence 的訊號,需在 `MergeSimilarAccounts` 或 mapper 後處理;目前只透過前端 `MatchedSource` badge 視覺呈現。
|
|
||||||
- web search fallback 只在 `< MaxSimilarAccounts(=10)` 時觸發,避免每次 scan 都額外打 web 浪費 quota;也避免 web 來的帳號擠掉 scan 來的(因為 new 排前面)。
|
|
||||||
- Brave `StartPublishedDate` 欄位加了但**沒 map 到 Brave 的 `freshness` 參數**(Brave API freshness 接受 `d/w/m/y` 而非 ISO date,要做轉換)。Phase 1 只做資訊不丟失;實際生效需要第二階段處理 Brave adapter 的 `freshness` mapping。Exa 已有支援。
|
|
||||||
|
|
||||||
### 沒動的檔案(確認 boundary)
|
|
||||||
- Placement / Persona / Brand schema 完全沒動。
|
|
||||||
- 沒碰 `internal/handler/routes.go`、`internal/svc/service_context.go`、`internal/response`、`internal/middleware`。
|
|
||||||
- 沒動 `internal/model/copy_mission/repository`、`internal/model/copy_mission/domain/repository`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> 範圍:僅 CopyMission flow。Placement(B 流海巡獲客)刻意維持「無對標帳號」設計,本計畫不動其 schema。
|
|
||||||
> 上限:`MaxSimilarAccounts` 自 5 放寬到 10。
|
|
||||||
> 遵循:`code/message/data` envelope、`page/pageSize`、`SSCCCDDD` 錯誤碼、UTC+0 nanoseconds、Redis `workerID` lock、不裸 `Update` job 狀態。
|
|
||||||
> 驗證:`go mod tidy && make fmt && go test ./...`;前端動到另執行 `make web-build`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 背景:threads-api-skill repo 對位
|
|
||||||
|
|
||||||
參考 repo `madebypan/threads-api-skill` 是**純發布工具包**(容器→publish 30s wait、reply_to_id 串貼、catbox.moe 圖床、60 天 token refresh、Chrome 自動化 setup),**沒有「找相似帳號 / 找對標 / 找 TA」功能**。它整理的 Threads Graph API endpoint 與踩坑,可用來校正下列事實:
|
|
||||||
|
|
||||||
- `me?fields=id,name,username,...` 取 self 資訊 → 目前 `threadsapi/client.go` 完全沒用,無法驗 token 對應 username。
|
|
||||||
- `keyword_search`(TOP/RECENT,50/page,無 cursor)→ 已實作;目前沒做翻頁,召回量受限。
|
|
||||||
- `media/{id}/replies` → 已實作,目前只當 outreach 標的,**沒把留言作者彙整成 TA 樣本**。
|
|
||||||
- Threads Graph API **沒有 endpoint 查任意別人的 follower / 受眾輪廓** → 「找 TA」必須繞道:把留言作者/互動者彙整成潛在受眾樣本。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目前狀態(viral 流程摸整)
|
|
||||||
|
|
||||||
- 唯一活著的相似帳號產生路徑:`scan_viral.go` job → `libviral.RunDiscover`(抓 threads 貼文)→ `BuildReferenceAccountsFromScan`(by author 聚合 + verified/follower/engagement 排序,strict gate→relaxed fallback)→ 寫入 `Mission.ResearchMap.SimilarAccounts`(`reference_accounts.go:43`)。
|
|
||||||
- `DiscoverSimilarAccounts` / `EnrichSimilarAccounts` / `ToEntitySimilarAccounts` 是 dead code(只有測試呼叫)。
|
|
||||||
- `analyze_copy_mission.go:138` 顯式 `researchMap.SimilarAccounts = nil` → 重新跑研究地圖 job 會清空相似帳號,前端沒警告。
|
|
||||||
- API 對相似帳號純唯讀:`UpdateCopyMissionReq`(`copy_mission.api:60-72`)沒 `similar_accounts` 欄位,使用者無法排除/釘選。
|
|
||||||
- 「找 TA」目前只是 LLM 產出的一段文字 `AudienceSummary`,**沒有任何「找出受眾樣本帳號」的機制**,連結構都沒有。
|
|
||||||
- 驗證缺位:`ProfileURL` 一律 `https://www.threads.net/@<username>` 拼字串(`reference_accounts.go:137`、`enrich_accounts.go:77`、`discover_accounts.go:119`),不驗真。
|
|
||||||
- 純 Threads API 模式下 `AuthorVerified=false, FollowerCount=0` 恆成立,排序權重失衡。
|
|
||||||
- 排序權重寫死:`reference_accounts.go:108-119` 固定 verified→follower→engagement,沒有主題 pillar 相似度、沒有跨任務記憶、沒有使用者可調。
|
|
||||||
- `postTopicRelevant`(`reference_accounts.go:166-180`)用裸 `strings.Contains`,中文長 label 命中精度低。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 — 資料品質低風險修正(先做)
|
|
||||||
|
|
||||||
### 1.1 名額放寬
|
|
||||||
- `backend/internal/library/viral/discover_accounts.go:14`:`MaxSimilarAccounts = 5` → `10`。
|
|
||||||
- `scan_viral.go:177`:`Limit: libviral.MaxSimilarAccounts` 維持引用常數。
|
|
||||||
- 前端已用 list render,不需改。
|
|
||||||
|
|
||||||
### 1.2 停止 `analyze_copy_mission` 清空相似帳號
|
|
||||||
- `backend/internal/worker/job/analyze_copy_mission.go:138`:刪 `researchMap.SimilarAccounts = nil`。
|
|
||||||
- 確認 `model/copy_mission/usecase/usecase.go` dot-path 更新其他 research_map 欄位時不會覆蓋 `similar_accounts`(worker 才會用 `Patch.ResearchMap` 整包,且 worker 現在會保留 SimilarAccounts)。
|
|
||||||
|
|
||||||
### 1.3 scan 結果改合併而非覆蓋
|
|
||||||
- `scan_viral.go:189-197`:`SimilarAccounts: referenceAccounts` → `SimilarAccounts: mergeSimilarAccounts(mission.ResearchMap.SimilarAccounts, referenceAccounts)`。
|
|
||||||
- 新檔 `backend/internal/library/viral/merge_accounts.go`:by username 合併,新來的更新統計、舊有保留;Phase 1 schema 還沒 `Status`,先純保舊不被清掉。
|
|
||||||
- `mergeSimilarAccounts(prev, new []missionentity.SimilarAccount) []missionentity.SimilarAccount`:
|
|
||||||
- key = lowercase username;新>舊(更新統計欄位),舊沒被新覆蓋就保留。
|
|
||||||
- 排序:保留新來秩序在前 N 個,舊的接在後面直到達 `MaxSimilarAccounts`。
|
|
||||||
|
|
||||||
### 1.4 排序權重抽出 + 主題相關改用命中詞數
|
|
||||||
- `reference_accounts.go:108-119`:抽出 `ReferenceRankWeights` struct(`VerifiedW/FollowerW/TotalEngagementW/BestEngagementW/TopicRelevanceW`,預設 `4/2/1/1/2`),排序 key = 加權和。
|
|
||||||
- `reference_accounts.go:166-180` `postTopicRelevant`:仍維持「命中詞數 >= 1 才納入」,但額外回傳 `topicHitCount` 給排序用。
|
|
||||||
- `referenceAuthorAgg` 加 `topicHits int`;`BuildReferenceAccountsFromScan` 內部迴圈累加。
|
|
||||||
-硼切詞暫不引入 jieba:對 CJK 用 `strings.Fields` + 拆字元組(>=2 字才當 term)即可,避免新依賴。
|
|
||||||
|
|
||||||
### 1.5 web search 補洞(復活 dead code 線)
|
|
||||||
- `scan_viral.go:172` `BuildReferenceAccountsFromScan` 之後,若 `len(referenceAccounts) < 5` 且 `memberCtx.WebSearchEnabled()`:
|
|
||||||
```go
|
|
||||||
extra, err := libviral.DiscoverSimilarAccounts(ctx, websearchClient, libviral.DiscoverAccountsInput{
|
|
||||||
SeedQuery: mission.SeedQuery,
|
|
||||||
Brief: mission.Brief,
|
|
||||||
Pillars: mission.ResearchMap.Pillars,
|
|
||||||
})
|
|
||||||
if err == nil && len(extra) > 0 {
|
|
||||||
referenceAccounts = libviral.EnrichSimilarAccounts(referenceAccounts, nil, libviral.MaxSimilarAccounts)
|
|
||||||
// 把 web 找到的也 by username 合併進去
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- `enrich_accounts.go`:`Source` 改成集合概念:`viral.SimilarAccount` 與 `missionentity.SimilarAccount` 都加 `MatchedSource []string`,命中 web 也命中 scan = `confidence="high"`。
|
|
||||||
- web search 失敗**不可讓 scan job 失敗**,只跳過並 log。
|
|
||||||
|
|
||||||
### 1.6 threadsapi profile URL helper
|
|
||||||
- 新檔 `backend/internal/library/threadsapi/url.go`:`ProfileURLFromPermalink(permalink, username) string`:
|
|
||||||
- regex `threads\.(?:com|net)/@([^/]+)/post/` 從 permalink 抓 author root `https://www.threads.com/@<u>/`。
|
|
||||||
- fallback `https://www.threads.com/@<username>`(注意 `www.threads.com` 而非 `www.threads.net` 較新且穩)。
|
|
||||||
- `reference_accounts.go:137`、`enrich_accounts.go:77`、`discover_accounts.go:119` 改用此 helper。
|
|
||||||
|
|
||||||
### 1.7 Brave adapter 補 StartPublishedDate
|
|
||||||
- `backend/internal/library/websearch/client.go:114-127` Brave adapter:把 `SearchOptions.StartPublishedDate`(即便 Brave API 不支援)也讀進 `libbrave.SearchOptions`,讓未來 Brave 端支援時自動生效。實作細節:libbrave.SearchOptions 若沒對應欄位就先無效果,但頂層 client 不能丟失資訊。
|
|
||||||
|
|
||||||
### 1.8 前端顯示新增資訊
|
|
||||||
- `frontend/src/pages/CopyMissionDetailPage.tsx:837-875` row:加 `source` badge(已有 `AccountSource` 文字時顯示 `brand`/`neutral`)、`confidence` 用 `text-muted` 小字。
|
|
||||||
- `frontend/src/lib/viralSignals.ts:42-48` `viralAccountRowClass`:依 Phase 1 schema 補 `MatchedSource` → 範含 `scan` 與 `web` 用 `--verified-hot` 樣式。
|
|
||||||
- `frontend/src/types/copyMission.ts`:`SimilarAccount` 加 `matched_source?: string[]`(Phase 1 只補這欄位,舊資料缺少不影響渲染)。
|
|
||||||
- 執行 `make web-build`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 — 相似帳號變一等公民(動 API,CopyMission only)
|
|
||||||
|
|
||||||
### 2.1 schema 變更
|
|
||||||
`backend/internal/model/copy_mission/domain/entity/mission.go:23-35` `SimilarAccount` 加:
|
|
||||||
```go
|
|
||||||
Status string `bson:"status,omitempty" json:"status,omitempty"` // recommended|pinned|excluded|promoted
|
|
||||||
TopicRelevance float64 `bson:"topic_relevance,omitempty" json:"topic_relevance,omitempty"`
|
|
||||||
LastSeenAt int64 `bson:"last_seen_at,omitempty" json:"last_seen_at,omitempty"`
|
|
||||||
MatchedSource []string `bson:"matched_source,omitempty" json:"matched_source,omitempty"`
|
|
||||||
```
|
|
||||||
`internal/types/types.go` 同步(重新 gen-api)。
|
|
||||||
|
|
||||||
### 2.2 API 新增
|
|
||||||
`backend/generate/api/copy_mission.api` 加:
|
|
||||||
```
|
|
||||||
@handler patchCopyMissionSimilarAccount
|
|
||||||
patch /personas/:persona_id/copy-missions/:copy_mission_id/similar-accounts/:username (PatchSimilarAccountReq) returns (CommonRes)
|
|
||||||
|
|
||||||
type PatchSimilarAccountReq {
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
跑 `make gen-api` 重新產 handler/logic/types。logic 走 dot-path:
|
|
||||||
```go
|
|
||||||
out[fmt.Sprintf("research_map.similar_accounts.%s.status", username)] = strings.TrimSpace(req.Status)
|
|
||||||
```
|
|
||||||
錯誤碼:CopyMission 目前沒有專屬 scope,**Phase 2 用 `Facade` (10)** 暫時頂著,待新增 `CopyMission Scope=41` 後再回頭改。**禁止裸寫數字**,一律 `errs.For(code.Facade).ResInvalidState("similar account status not allowed")`。
|
|
||||||
|
|
||||||
### 2.3 worker 尊重 status
|
|
||||||
- `reference_accounts.go:54` 開頭:`if isExcluded(in.ExcludedUsernames, post.Author) { continue }`。
|
|
||||||
- `scan_viral.go:170`:把 `mission.ResearchMap.SimilarAccounts` 中 `status=excluded` 的 username 傳進 `ReferenceAccountInput.ExcludedUsernames`。
|
|
||||||
- `merge_accounts.go`:`status=pinned` 永遠置頂,本次沒掃到也保留。
|
|
||||||
|
|
||||||
### 2.4 前端互動
|
|
||||||
- `CopyMissionDetailPage.tsx` row 加兩按鈕「釘選」「排除」,呼 `api.patch(.../similar-accounts/:username, {status})`,本地 optimistic update。
|
|
||||||
- 新檔 `frontend/src/lib/copyMissionSimilarAccounts.ts` 包 API client。
|
|
||||||
- `CopyMissionResearchOverview.tsx` 加「相似帳號」子區(從 detail 頁整合進來)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 — TA 樣本(留言作者採樣,全新功能)
|
|
||||||
|
|
||||||
Threads API 拿不到任意別人的受眾 → 繞道:用 `MediaReplies` 的 `ReplyCandidate.Author` 聚合成潛在受眾樣本。
|
|
||||||
|
|
||||||
### 3.1 schema 變更
|
|
||||||
`backend/internal/model/copy_mission/domain/entity/mission.go` `ResearchMap` 加:
|
|
||||||
```go
|
|
||||||
AudienceSamples []AudienceSample `bson:"audience_samples,omitempty" json:"audience_samples,omitempty"`
|
|
||||||
```
|
|
||||||
新 struct:
|
|
||||||
```go
|
|
||||||
type AudienceSample struct {
|
|
||||||
Username string `bson:"username" json:"username"`
|
|
||||||
SamplePostID string `bson:"sample_post_id,omitempty" json:"sample_post_id,omitempty"`
|
|
||||||
SampleText string `bson:"sample_text,omitempty" json:"sample_text,omitempty"`
|
|
||||||
ReplyLikeCount int `bson:"reply_like_count,omitempty" json:"reply_like_count,omitempty"`
|
|
||||||
Appearances int `bson:"appearances,omitempty" json:"appearances,omitempty"`
|
|
||||||
FirstSeenAt int64 `bson:"first_seen_at" json:"first_seen_at"`
|
|
||||||
LastSeenAt int64 `bson:"last_seen_at,omitempty" json:"last_seen_at,omitempty"`
|
|
||||||
Status string `bson:"status,omitempty" json:"status,omitempty"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`generate/api/copy_mission.api` 在 `CopyResearchMap` 加 `AudienceSamples []CopyAudienceSampleData`;`make gen-api`。
|
|
||||||
|
|
||||||
### 3.2 新 library
|
|
||||||
`backend/internal/library/viral/audience.go`:
|
|
||||||
```go
|
|
||||||
func BuildAudienceSamplesFromReplies(replies []placement.ReplyCandidate, opts AudienceOpts) []missionentity.AudienceSample
|
|
||||||
```
|
|
||||||
- `appearance = by username.Count`
|
|
||||||
- 過濾:自己 mission 的 author(即 SimilarAccount source)排除、純連結/數字罐頭 reply 文字過濾(`len < 5 chars` or URL-only)。
|
|
||||||
- 排序 `appearance desc → replyLikeCount desc → postedAt desc`,前 15 筆。
|
|
||||||
- confidence(前端呈現用,不入 struct):`appearance >= 3` high、`= 2` medium、`= 1` low。
|
|
||||||
|
|
||||||
### 3.3 worker 接入
|
|
||||||
`scan_viral.go` 在 `AttachReplies` 完成後:
|
|
||||||
```go
|
|
||||||
var allReplies []placement.ReplyCandidate
|
|
||||||
for _, p := range postsWithReplies {
|
|
||||||
allReplies = append(allReplies, p.Replies...)
|
|
||||||
}
|
|
||||||
samples := libviral.BuildAudienceSamplesFromReplies(allReplies, libviral.AudienceOpts{
|
|
||||||
Max: 15,
|
|
||||||
ExcludeAuthors: similarAccountUsernames,
|
|
||||||
})
|
|
||||||
researchMap.AudienceSamples = mergeAudienceSamples(prev.Samples, samples)
|
|
||||||
```
|
|
||||||
合併邏輯與 `merge_accounts.go` 同款。
|
|
||||||
|
|
||||||
### 3.4 API
|
|
||||||
- `GET /copy-missions/:id` 已含(在 `research_map.audience_samples`)。
|
|
||||||
- `PATCH /copy-missions/:id/audience-samples/:username` body `{status}`,dot-path 更新 `research_map.audience_samples.<username>.status`。
|
|
||||||
- `make gen-api` + 邏輯同 2.2。
|
|
||||||
|
|
||||||
### 3.5 前端
|
|
||||||
- `CopyMissionResearchOverview.tsx` 新區「TA 樣本」(在「相似帳號」之後):
|
|
||||||
- 每筆 `@username`(連 `threadsProfileUrl`)、出現 N 篇、最近一次樣本文字。
|
|
||||||
- 按鈕「收藏為長期受眾」(status=pinned)、「忽略」(status=excluded)。
|
|
||||||
- 新檔 `frontend/src/lib/copyMissionAudience.ts` 包 API client。
|
|
||||||
- `frontend/src/index.css` 加 `.hx-audience-samples*` token,沿用 `hx-copy-similar-accounts` 結構。
|
|
||||||
|
|
||||||
### 3.6 Phase 3 驗收
|
|
||||||
- `make web-build` 過、`go test ./...` 過。
|
|
||||||
- E2E:開 Threads API 連線 → 跑 scan job → detail page 顯示「TA 樣本」最多 15 筆、@username 連結不 404、可釘選後重跑 scan 不消失。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4 — 跨任務記憶(後續,本計畫不實作)
|
|
||||||
|
|
||||||
`ThreadsAccount` 層加 `KnownAccounts map[username]AccountProfile{ FirstSeenAt, LastSeenAt, Missions[], PersonaIds[], Tags[], AvgEngagement, Status }`,scan job 結束 upsert。同一 username 在多 mission 出現 → confidence 累積、auto-suggest 為 persona benchmark;已 excluded 跨任務繼承。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 移交檢查表
|
|
||||||
|
|
||||||
### 要動的檔案(Phase 1)
|
|
||||||
**Go 後端**
|
|
||||||
- `internal/library/viral/discover_accounts.go`(常數、ProfileURL helper)
|
|
||||||
- `internal/library/viral/reference_accounts.go`(排序權重、topicHits)
|
|
||||||
- `internal/library/viral/enrich_accounts.go`(MatchedSource 集合)
|
|
||||||
- `internal/library/viral/merge_accounts.go`(新檔)
|
|
||||||
- `internal/library/threadsapi/url.go`(新檔,profileURL helper)
|
|
||||||
- `internal/library/websearch/client.go`(Brave 補 StartPublishedDate)
|
|
||||||
- `internal/worker/job/scan_viral.go`(web 補洞、merge 取代覆蓋)
|
|
||||||
- `internal/worker/job/analyze_copy_mission.go:138`(停止清空)
|
|
||||||
- `internal/model/copy_mission/domain/entity/mission.go`(SimilarAccount 加 MatchedSource)
|
|
||||||
- `internal/types/types.go` 同步(gen-api 或手動加)
|
|
||||||
|
|
||||||
**前端(Phase 1.8)**
|
|
||||||
- `src/pages/CopyMissionDetailPage.tsx`(顯示 source/confidence)
|
|
||||||
- `src/lib/viralSignals.ts`(MatchedSource badge 對應)
|
|
||||||
- `src/types/copyMission.ts`(加 matched_source)
|
|
||||||
|
|
||||||
### 要動的檔案(Phase 2-3,後續 agent)
|
|
||||||
- `model/copy_mission/domain/entity/mission.go`(Status、TopicRelevance、LastSeenAt、AudienceSample、AudienceSamples)
|
|
||||||
- `generate/api/copy_mission.api`(兩支 PATCH endpoint + types)
|
|
||||||
- `internal/logic/copy_mission/patch_similar_account_logic.go` + `patch_audience_sample_logic.go`(gen 產出後手寫)
|
|
||||||
- `internal/library/viral/audience.go`(新檔)
|
|
||||||
- `internal/worker/job/scan_viral.go`(excluded 傳遞、audience samples、pinned 保留)
|
|
||||||
- `frontend/src/components/CopyMissionResearchOverview.tsx`(整合區)
|
|
||||||
- `frontend/src/lib/copyMissionSimilarAccounts.ts`、`copyMissionAudience.ts`(新)
|
|
||||||
- `frontend/src/index.css`(`.hx-audience-samples*`)
|
|
||||||
|
|
||||||
### 錯誤碼配置
|
|
||||||
- Phase 2 patch similar 錯誤:CopyMission 無專屬 scope → 先用 `errs.For(code.Facade).ResInvalidState("similar account status not allowed")`;待新增 `CopyMission=41` 後再回頭改。**禁止裸寫數字**。
|
|
||||||
|
|
||||||
### 失敗安全
|
|
||||||
- 任何 web search 呼叫失敗不能讓 scan job fail,只跳過。
|
|
||||||
- `mergeSimilarAccounts` 必須處理 prev=nil。
|
|
||||||
- `AudienceSamples`(Phase 3)在無 Threads API token 時直接跳過整段,不報錯。
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Threads 自動營運補功能計畫
|
||||||
|
|
||||||
|
## 目前狀態
|
||||||
|
|
||||||
|
P1/P2 已落地到後端 API、worker 與正式前端:
|
||||||
|
|
||||||
|
- 自動補庫存:`publish_inventory_policies` + `refill-publish-inventory` job template/worker。
|
||||||
|
- 智慧時段:`GET /publish-slot-insights` 從 publish health / checkpoints 聚合推薦時段。
|
||||||
|
- 草稿批量排程:Persona 與 Copy Mission 草稿可批量排到 `publish_queue`,草稿回寫 `scheduled + publish_queue_id`。
|
||||||
|
- Retry / 告警 / 漏發:`publish_queue_events` 記錄狀態轉移;sweeper 會標記 missed;failed/cancelled/missed 可 retry。
|
||||||
|
- 頻率護欄:`publish_guard_policies` 支援每日上限、最小間隔、連續失敗 pause / resume。
|
||||||
|
- P2:內容日曆 date range query、多帳號 dashboard summary、style preset 語調庫、OAuth/API diagnostics。
|
||||||
|
|
||||||
|
## 目標
|
||||||
|
|
||||||
|
把現有「海巡找題、AI 文案、手動排程、成效追蹤」串成自動營運閉環,讓巡樓 Console 不只是一個排程工具,而是可持續補庫存、控制風險、追蹤成效的 Threads 工作台。
|
||||||
|
|
||||||
|
## P1:自動補庫存
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 每個 Threads account 可設定每日/每週目標篇數。
|
||||||
|
- 可設定 weekday/time slots 與 timezone;timezone 僅用於 cron/排程解讀,儲存仍用 unix nanoseconds UTC。
|
||||||
|
- 指定內容來源:persona、copy mission、brand scan posts、manual seed。
|
||||||
|
- 當 `publish_queue` 未來 N 天庫存低於門檻時,自動建立 copy generation job,完成後排入 queue。
|
||||||
|
|
||||||
|
### 後端草案
|
||||||
|
|
||||||
|
- 新增 model:`internal/model/publish_inventory/`。
|
||||||
|
- 新增設定 entity:
|
||||||
|
- `account_id`
|
||||||
|
- `target_daily_count`
|
||||||
|
- `low_stock_threshold`
|
||||||
|
- `lookahead_days`
|
||||||
|
- `timezone`
|
||||||
|
- `slots`
|
||||||
|
- `source_refs`
|
||||||
|
- `enabled`
|
||||||
|
- 新增 job step:`refill_publish_inventory`。
|
||||||
|
- 補 API:
|
||||||
|
- `GET /api/v1/threads-accounts/:accountId/publish-inventory-policy`
|
||||||
|
- `PUT /api/v1/threads-accounts/:accountId/publish-inventory-policy`
|
||||||
|
- `POST /api/v1/threads-accounts/:accountId/publish-inventory/refill-jobs`
|
||||||
|
|
||||||
|
## P1:智慧時段
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 第一版以規則為主:帳號的 weekday/time slots + 最小間隔。
|
||||||
|
- 用 publish health 的 1h/24h/7d checkpoints 回填每個 slot 的平均互動。
|
||||||
|
- 前端顯示「推薦時段」與「低成效時段」。
|
||||||
|
|
||||||
|
### 後端草案
|
||||||
|
|
||||||
|
- 新增 summary usecase,從 `publish_analytics` 聚合 account slot performance。
|
||||||
|
- 補 API:
|
||||||
|
- `GET /api/v1/threads-accounts/:accountId/publish-slot-insights`
|
||||||
|
- 回傳:
|
||||||
|
- `slots[]`
|
||||||
|
- `avg_likes_1h/24h/7d`
|
||||||
|
- `avg_replies_1h/24h/7d`
|
||||||
|
- `sample_size`
|
||||||
|
- `recommendation`
|
||||||
|
|
||||||
|
## P1:草稿批量排程
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 使用者在 copy drafts 中選多篇,一次排到未來 slots。
|
||||||
|
- 支援根據智慧時段自動分配 `scheduled_at`。
|
||||||
|
- draft 排入 queue 後狀態改為 `scheduled`,保留 queue item id。
|
||||||
|
|
||||||
|
### 後端草案
|
||||||
|
|
||||||
|
- 補 API:
|
||||||
|
- `POST /api/v1/personas/:personaId/copy-drafts/schedule`
|
||||||
|
- `POST /api/v1/personas/:personaId/copy-missions/:missionId/copy-drafts/schedule`
|
||||||
|
- request:
|
||||||
|
- `account_id`
|
||||||
|
- `draft_ids`
|
||||||
|
- `start_at`
|
||||||
|
- `timezone`
|
||||||
|
- `slots`
|
||||||
|
- `mode`: `manual` / `recommended`
|
||||||
|
|
||||||
|
## P1:發文 retry / 告警 / 漏發偵測
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- failed queue item 可重試,保留失敗歷史。
|
||||||
|
- sweeper 偵測到超過 grace period 的 scheduled item 未處理,標記為 missed 或重新 enqueue。
|
||||||
|
- 連續失敗超過門檻時 pause account publish,避免 token 或 rate limit 問題擴大。
|
||||||
|
|
||||||
|
### 後端草案
|
||||||
|
|
||||||
|
- `publish_queue_events` 記錄 queue 狀態轉移與錯誤分類。
|
||||||
|
- `retry` 使用 guarded update,只允許 `failed/cancelled` 回到 `scheduled`。
|
||||||
|
- 補 API:
|
||||||
|
- `POST /api/v1/threads-accounts/:accountId/publish-queue/:queueId/retry`
|
||||||
|
- `GET /api/v1/threads-accounts/:accountId/publish-queue/:queueId/events`
|
||||||
|
- `GET /api/v1/threads-accounts/:accountId/publish-alerts`
|
||||||
|
|
||||||
|
## P1:頻率護欄
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 每帳號每日上限。
|
||||||
|
- 最小發文間隔。
|
||||||
|
- 連續失敗自動 pause。
|
||||||
|
- 手動解除 pause。
|
||||||
|
|
||||||
|
### 後端草案
|
||||||
|
|
||||||
|
- 擴充 Threads account connection prefs 或新增 `publish_guard_policy`。
|
||||||
|
- sweeper dispatch 前檢查 policy;違反則延後排程,不直接發文。
|
||||||
|
- 補 API:
|
||||||
|
- `GET /api/v1/threads-accounts/:accountId/publish-guard-policy`
|
||||||
|
- `PUT /api/v1/threads-accounts/:accountId/publish-guard-policy`
|
||||||
|
- `POST /api/v1/threads-accounts/:accountId/publish-guard/resume`
|
||||||
|
|
||||||
|
## P2
|
||||||
|
|
||||||
|
- 內容日曆:補 queue date range query,前端做週/月視圖。
|
||||||
|
- 多帳號 dashboard:補跨帳號 summary endpoint,避免前端 N+1 requests。
|
||||||
|
- 語調庫:把 `style_profile`、CTA、禁用詞、品牌口吻抽成 reusable preset。
|
||||||
|
- OAuth/API 診斷:把 smoke test、token expiry、permission scope 做成標準診斷報告。
|
||||||
|
|
||||||
|
## 驗證策略
|
||||||
|
|
||||||
|
- 所有列表維持 `page/pageSize` 與 `pagination/list`。
|
||||||
|
- 所有時間欄位寫入 unix nanoseconds。
|
||||||
|
- queue/job 狀態轉移都使用 guarded update。
|
||||||
|
- 長任務必須 heartbeat,取消走既有 cooperative cancel 語意。
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
Name: haixun-backend
|
|
||||||
Host: 0.0.0.0
|
|
||||||
Port: 8890
|
|
||||||
Timeout: 120000
|
|
||||||
|
|
||||||
# 本地「docker 測試」設定(make dev-up 使用)。
|
|
||||||
# 連線走 compose service name(mongo / redis),密碼對齊 deploy/.env.dev。
|
|
||||||
# 純本機 go run(不進 docker)請改用 etc/gateway.yaml(連 127.0.0.1)。
|
|
||||||
Mongo:
|
|
||||||
URI: mongodb://haixun:haixun-dev-pass@mongo:27017/?authSource=admin
|
|
||||||
Database: haixun
|
|
||||||
TimeoutSeconds: 10
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Addr: redis:6379
|
|
||||||
Password: haixun-dev-redis
|
|
||||||
DB: 0
|
|
||||||
|
|
||||||
Auth:
|
|
||||||
AccessSecret: haixun-dev-access-secret-change-me
|
|
||||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
|
||||||
AccessExpireSeconds: 900
|
|
||||||
RefreshExpireSeconds: 2592000
|
|
||||||
# 僅本地開發開啟:允許用 X-Tenant-ID / X-UID header 模擬登入。正式環境必須 false。
|
|
||||||
DevHeaderFallback: true
|
|
||||||
|
|
||||||
Secrets:
|
|
||||||
# 留空 = 機敏資料不加密(本地測試方便)。正式環境用 ${HAIXUN_SECRETS_KEY}。
|
|
||||||
EncryptionKey: ""
|
|
||||||
|
|
||||||
InternalWorker:
|
|
||||||
Secret: haixun-dev-worker-secret
|
|
||||||
|
|
||||||
# dev 用 gateway 內嵌 go worker(勿另開 worker 容器,避免兩個 worker 搶 job)。
|
|
||||||
JobWorker:
|
|
||||||
Enabled: true
|
|
||||||
WorkerType: go
|
|
||||||
|
|
||||||
JobScheduler:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 60
|
|
||||||
|
|
||||||
JobReaper:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 30
|
|
||||||
|
|
@ -3,8 +3,6 @@ Host: 0.0.0.0
|
||||||
Port: 8890
|
Port: 8890
|
||||||
Timeout: 120000
|
Timeout: 120000
|
||||||
|
|
||||||
# 連線字串與所有 secret 都從環境變數注入(systemd EnvironmentFile=/opt/haixun/etc/haixun.env)。
|
|
||||||
# go-zero 以 conf.UseEnv() + os.ExpandEnv 展開 ${VAR};未設定的變數會展開為空字串並讓服務 fail fast。
|
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: ${HAIXUN_MONGO_URI}
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
Database: ${HAIXUN_MONGO_DB}
|
Database: ${HAIXUN_MONGO_DB}
|
||||||
|
|
@ -30,12 +28,28 @@ InternalWorker:
|
||||||
|
|
||||||
JobWorker:
|
JobWorker:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
WorkerType: go
|
WorkerType: disabled
|
||||||
|
|
||||||
JobScheduler:
|
JobScheduler:
|
||||||
Enabled: true
|
Enabled: false
|
||||||
IntervalSeconds: 60
|
IntervalSeconds: 60
|
||||||
|
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: true
|
Enabled: false
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: 30
|
||||||
|
|
||||||
|
ThreadsOAuth:
|
||||||
|
AppID: "${THREADS_APP_ID}"
|
||||||
|
AppSecret: "${THREADS_APP_SECRET}"
|
||||||
|
RedirectURI: "${THREADS_REDIRECT_URI}"
|
||||||
|
SuccessRedirect: "${THREADS_OAUTH_SUCCESS_REDIRECT}"
|
||||||
|
|
||||||
|
ThreadsTokenRefresh:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 3600
|
||||||
|
LeadDays: 7
|
||||||
|
|
||||||
|
ThreadsPublishQueue:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 60
|
||||||
|
BatchSize: 20
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,30 @@ InternalWorker:
|
||||||
Secret: ${HAIXUN_WORKER_SECRET}
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
JobWorker:
|
JobWorker:
|
||||||
Enabled: true
|
Enabled: ${HAIXUN_JOB_WORKER_ENABLED}
|
||||||
WorkerType: go
|
WorkerType: ${HAIXUN_JOB_WORKER_TYPE}
|
||||||
|
WorkerID: ${HAIXUN_JOB_WORKER_ID}
|
||||||
|
|
||||||
JobScheduler:
|
JobScheduler:
|
||||||
Enabled: false
|
Enabled: ${HAIXUN_JOB_SCHEDULER_ENABLED}
|
||||||
IntervalSeconds: 60
|
IntervalSeconds: ${HAIXUN_JOB_SCHEDULER_INTERVAL_SECONDS}
|
||||||
|
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: false
|
Enabled: ${HAIXUN_JOB_REAPER_ENABLED}
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: ${HAIXUN_JOB_REAPER_INTERVAL_SECONDS}
|
||||||
|
|
||||||
|
ThreadsOAuth:
|
||||||
|
AppID: "${THREADS_APP_ID}"
|
||||||
|
AppSecret: "${THREADS_APP_SECRET}"
|
||||||
|
RedirectURI: "${THREADS_REDIRECT_URI}"
|
||||||
|
SuccessRedirect: "${THREADS_OAUTH_SUCCESS_REDIRECT}"
|
||||||
|
|
||||||
|
ThreadsTokenRefresh:
|
||||||
|
Enabled: ${HAIXUN_THREADS_TOKEN_REFRESH_ENABLED}
|
||||||
|
IntervalSeconds: ${HAIXUN_THREADS_TOKEN_REFRESH_INTERVAL_SECONDS}
|
||||||
|
LeadDays: ${HAIXUN_THREADS_TOKEN_REFRESH_LEAD_DAYS}
|
||||||
|
|
||||||
|
ThreadsPublishQueue:
|
||||||
|
Enabled: ${HAIXUN_THREADS_PUBLISH_QUEUE_ENABLED}
|
||||||
|
IntervalSeconds: ${HAIXUN_THREADS_PUBLISH_QUEUE_INTERVAL_SECONDS}
|
||||||
|
BatchSize: ${HAIXUN_THREADS_PUBLISH_QUEUE_BATCH_SIZE}
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,32 @@ Timeout: 120000
|
||||||
|
|
||||||
# 本機開發 worker 設定(go worker)。搭配 `make dev-infra` 的 Mongo/Redis。
|
# 本機開發 worker 設定(go worker)。搭配 `make dev-infra` 的 Mongo/Redis。
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
Database: haixun
|
Database: ${HAIXUN_MONGO_DB}
|
||||||
TimeoutSeconds: 10
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Addr: 127.0.0.1:6379
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
Password: change-me-redis-pass
|
Password: ${HAIXUN_REDIS_PASSWORD}
|
||||||
DB: 0
|
DB: 0
|
||||||
|
|
||||||
Auth:
|
Auth:
|
||||||
AccessSecret: haixun-dev-access-secret-change-me
|
AccessSecret: ${HAIXUN_JWT_ACCESS_SECRET}
|
||||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
RefreshSecret: ${HAIXUN_JWT_REFRESH_SECRET}
|
||||||
AccessExpireSeconds: 900
|
AccessExpireSeconds: 900
|
||||||
RefreshExpireSeconds: 2592000
|
RefreshExpireSeconds: 2592000
|
||||||
DevHeaderFallback: false
|
DevHeaderFallback: false
|
||||||
|
|
||||||
Secrets:
|
Secrets:
|
||||||
EncryptionKey: ""
|
EncryptionKey: ${HAIXUN_SECRETS_KEY}
|
||||||
|
|
||||||
InternalWorker:
|
InternalWorker:
|
||||||
Secret: haixun-dev-worker-secret
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
JobWorker:
|
JobWorker:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
WorkerType: go
|
WorkerType: ${HAIXUN_JOB_WORKER_TYPE}
|
||||||
|
WorkerID: ${HAIXUN_JOB_WORKER_ID}
|
||||||
|
|
||||||
JobScheduler:
|
JobScheduler:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
@ -38,3 +39,13 @@ JobScheduler:
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: 30
|
||||||
|
|
||||||
|
ThreadsTokenRefresh:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 3600
|
||||||
|
LeadDays: 7
|
||||||
|
|
||||||
|
ThreadsPublishQueue:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 60
|
||||||
|
BatchSize: 20
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,35 @@ Host: 0.0.0.0
|
||||||
Port: 8890
|
Port: 8890
|
||||||
Timeout: 120000
|
Timeout: 120000
|
||||||
|
|
||||||
# 本機開發設定。預設搭配 `make dev-infra`(infra/docker-compose.yml)跑的 Mongo/Redis,
|
# 本機開發設定。預設搭配 `make dev-infra`(infra/docker-compose.yml,讀 deploy/.env.dev)跑的 Mongo/Redis。
|
||||||
# 帳密對應 infra/.env.example 的預設值。若你改了 .env 密碼,這裡也要同步。
|
# 帳密對應 deploy/.env.dev 的值;若改 .env.dev 密碼,這裡不需動(${} 會重讀)。
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
Database: haixun
|
Database: ${HAIXUN_MONGO_DB}
|
||||||
TimeoutSeconds: 10
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Addr: 127.0.0.1:6379
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
Password: change-me-redis-pass
|
Password: ${HAIXUN_REDIS_PASSWORD}
|
||||||
DB: 0
|
DB: 0
|
||||||
|
|
||||||
Auth:
|
Auth:
|
||||||
AccessSecret: haixun-dev-access-secret-change-me
|
AccessSecret: ${HAIXUN_JWT_ACCESS_SECRET}
|
||||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
RefreshSecret: ${HAIXUN_JWT_REFRESH_SECRET}
|
||||||
AccessExpireSeconds: 900
|
AccessExpireSeconds: 900
|
||||||
RefreshExpireSeconds: 2592000
|
RefreshExpireSeconds: 2592000
|
||||||
# 僅本機開發開啟:允許用 X-Tenant-ID / X-UID header 模擬登入。正式環境必須為 false。
|
# 僅本機開發開啟:允許用 X-Tenant-ID / X-UID header 模擬登入。正式環境必須為 false。
|
||||||
DevHeaderFallback: true
|
DevHeaderFallback: true
|
||||||
|
|
||||||
Secrets:
|
Secrets:
|
||||||
# 留空 = 不加密(本機開發方便)。正式環境用 ${HAIXUN_SECRETS_KEY}。
|
EncryptionKey: ${HAIXUN_SECRETS_KEY}
|
||||||
EncryptionKey: ""
|
|
||||||
|
|
||||||
InternalWorker:
|
InternalWorker:
|
||||||
Secret: haixun-dev-worker-secret
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
JobWorker:
|
JobWorker:
|
||||||
Enabled: true
|
Enabled: false
|
||||||
WorkerType: go
|
WorkerType: disabled
|
||||||
|
|
||||||
JobScheduler:
|
JobScheduler:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
@ -41,3 +40,23 @@ JobScheduler:
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: 30
|
||||||
|
|
||||||
|
# Meta Threads OAuth(App Dashboard → 用戶端 OAuth 設定須登記 Redirect URI)
|
||||||
|
ThreadsOAuth:
|
||||||
|
AppID: "${THREADS_APP_ID}"
|
||||||
|
AppSecret: "${THREADS_APP_SECRET}"
|
||||||
|
RedirectURI: "${THREADS_REDIRECT_URI}"
|
||||||
|
# OAuth 完成後瀏覽器導回(現在指向 https://threads-tool.30cm.net )
|
||||||
|
SuccessRedirect: "${THREADS_OAUTH_SUCCESS_REDIRECT}"
|
||||||
|
|
||||||
|
# 系統任務:在 token 到期前自動 enqueue refresh-threads-token job
|
||||||
|
ThreadsTokenRefresh:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 3600
|
||||||
|
LeadDays: 7
|
||||||
|
|
||||||
|
# 排程發文佇列 sweeper(掃描 scheduled_at 到期項目並 PublishText)
|
||||||
|
ThreadsPublishQueue:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
BatchSize: 20
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"haixun-backend/internal/bootstrap"
|
||||||
"haixun-backend/internal/config"
|
"haixun-backend/internal/config"
|
||||||
"haixun-backend/internal/handler"
|
"haixun-backend/internal/handler"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
|
|
@ -18,6 +19,8 @@ var configFile = flag.String("f", "etc/gateway.yaml", "config file")
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
bootstrap.LoadInfraEnv()
|
||||||
|
|
||||||
var c config.Config
|
var c config.Config
|
||||||
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
||||||
|
|
||||||
|
|
@ -28,6 +31,7 @@ func main() {
|
||||||
defer sc.Close(context.Background())
|
defer sc.Close(context.Background())
|
||||||
|
|
||||||
handler.RegisterHandlers(server, sc)
|
handler.RegisterHandlers(server, sc)
|
||||||
|
handler.RegisterExtraHandlers(server, sc)
|
||||||
|
|
||||||
fmt.Printf("Starting backend backend at %s:%d...\n", c.Host, c.Port)
|
fmt.Printf("Starting backend backend at %s:%d...\n", c.Host, c.Port)
|
||||||
server.Start()
|
server.Start()
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ type (
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
ProductContext string `json:"product_context"`
|
ProductContext string `json:"product_context"`
|
||||||
|
PlacementURL string `json:"placement_url,omitempty"`
|
||||||
MatchTags []string `json:"match_tags,omitempty"`
|
MatchTags []string `json:"match_tags,omitempty"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
UpdateAt int64 `json:"update_at"`
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
|
@ -65,12 +66,14 @@ type (
|
||||||
CreateBrandProductReq {
|
CreateBrandProductReq {
|
||||||
Label string `json:"label" validate:"required"`
|
Label string `json:"label" validate:"required"`
|
||||||
ProductContext string `json:"product_context" validate:"required"`
|
ProductContext string `json:"product_context" validate:"required"`
|
||||||
|
PlacementURL string `json:"placement_url,optional"`
|
||||||
MatchTags []string `json:"match_tags,optional"`
|
MatchTags []string `json:"match_tags,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateBrandProductReq {
|
UpdateBrandProductReq {
|
||||||
Label *string `json:"label,optional"`
|
Label *string `json:"label,optional"`
|
||||||
ProductContext *string `json:"product_context,optional"`
|
ProductContext *string `json:"product_context,optional"`
|
||||||
|
PlacementURL string `json:"placement_url,optional"`
|
||||||
MatchTags []string `json:"match_tags,optional"`
|
MatchTags []string `json:"match_tags,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +186,7 @@ type (
|
||||||
NodeIDs []string `json:"node_ids,optional"`
|
NodeIDs []string `json:"node_ids,optional"`
|
||||||
DualTrack bool `json:"dual_track,optional"`
|
DualTrack bool `json:"dual_track,optional"`
|
||||||
PatrolMode bool `json:"patrol_mode,optional"`
|
PatrolMode bool `json:"patrol_mode,optional"`
|
||||||
|
TestPatrol bool `json:"test_patrol,optional"`
|
||||||
PatrolKeywords []string `json:"patrol_keywords,optional"`
|
PatrolKeywords []string `json:"patrol_keywords,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,6 +223,7 @@ type (
|
||||||
PublishedPermalink string `json:"published_permalink,omitempty"`
|
PublishedPermalink string `json:"published_permalink,omitempty"`
|
||||||
OutreachUpdateAt int64 `json:"outreach_update_at,omitempty"`
|
OutreachUpdateAt int64 `json:"outreach_update_at,omitempty"`
|
||||||
PostedAt string `json:"posted_at,omitempty"`
|
PostedAt string `json:"posted_at,omitempty"`
|
||||||
|
RecencyDays int `json:"recency_days,omitempty"`
|
||||||
Replies []ScanReplyData `json:"replies,omitempty"`
|
Replies []ScanReplyData `json:"replies,omitempty"`
|
||||||
LatestDraft *GenerateOutreachDraftsData `json:"latest_draft,omitempty"`
|
LatestDraft *GenerateOutreachDraftsData `json:"latest_draft,omitempty"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
|
|
@ -235,6 +240,7 @@ type (
|
||||||
Count int `json:"count,optional"`
|
Count int `json:"count,optional"`
|
||||||
VoicePersonaID string `json:"voice_persona_id,optional"`
|
VoicePersonaID string `json:"voice_persona_id,optional"`
|
||||||
ProductID string `json:"product_id,optional"`
|
ProductID string `json:"product_id,optional"`
|
||||||
|
Regenerate bool `json:"regenerate,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
OutreachDraftItemData {
|
OutreachDraftItemData {
|
||||||
|
|
@ -258,6 +264,11 @@ type (
|
||||||
Confirm bool `json:"confirm"`
|
Confirm bool `json:"confirm"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateOutreachDraftReq {
|
||||||
|
DraftIndex int `json:"draft_index"`
|
||||||
|
Text string `json:"text" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
PublishOutreachDraftData {
|
PublishOutreachDraftData {
|
||||||
ScanPostID string `json:"scan_post_id"`
|
ScanPostID string `json:"scan_post_id"`
|
||||||
ReplyID string `json:"reply_id"`
|
ReplyID string `json:"reply_id"`
|
||||||
|
|
@ -355,6 +366,12 @@ type (
|
||||||
PublishOutreachDraftReq
|
PublishOutreachDraftReq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateOutreachDraftHandlerReq {
|
||||||
|
BrandPath
|
||||||
|
DraftID string `path:"draftId" validate:"required"`
|
||||||
|
UpdateOutreachDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
PatchScanPostOutreachHandlerReq {
|
PatchScanPostOutreachHandlerReq {
|
||||||
BrandPath
|
BrandPath
|
||||||
PostID string `path:"postId"`
|
PostID string `path:"postId"`
|
||||||
|
|
@ -428,6 +445,9 @@ service gateway {
|
||||||
@handler publishOutreachDraft
|
@handler publishOutreachDraft
|
||||||
post /:id/outreach-drafts/publish (PublishOutreachDraftHandlerReq) returns (PublishOutreachDraftData)
|
post /:id/outreach-drafts/publish (PublishOutreachDraftHandlerReq) returns (PublishOutreachDraftData)
|
||||||
|
|
||||||
|
@handler updateOutreachDraft
|
||||||
|
patch /:id/outreach-drafts/:draftId (UpdateOutreachDraftHandlerReq) returns (GenerateOutreachDraftsData)
|
||||||
|
|
||||||
@handler patchScanPostOutreach
|
@handler patchScanPostOutreach
|
||||||
patch /:id/scan-posts/:postId (PatchScanPostOutreachHandlerReq) returns (ScanPostData)
|
patch /:id/scan-posts/:postId (PatchScanPostOutreachHandlerReq) returns (ScanPostData)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,10 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyMissionInspirationReq {
|
CopyMissionInspirationReq {
|
||||||
Keyword string `json:"keyword,optional"`
|
Keyword string `json:"keyword,optional"`
|
||||||
|
ContentDirection string `json:"content_direction,optional"`
|
||||||
|
UseWebSearch bool `json:"use_web_search,optional"`
|
||||||
|
AvoidTopics []string `json:"avoid_topics,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyMissionInspirationHandlerReq {
|
CopyMissionInspirationHandlerReq {
|
||||||
|
|
@ -267,6 +270,26 @@ type (
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CopyMissionScheduleCopyDraftsReq {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
DraftIDs []string `json:"draft_ids" validate:"required,min=1"`
|
||||||
|
StartAt int64 `json:"start_at,optional"`
|
||||||
|
Timezone string `json:"timezone,optional"`
|
||||||
|
Slots []PublishSlotData `json:"slots,optional"`
|
||||||
|
Mode string `json:"mode,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleCopyMissionDraftsHandlerReq {
|
||||||
|
CopyMissionPath
|
||||||
|
CopyMissionScheduleCopyDraftsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyMissionScheduleCopyDraftsData {
|
||||||
|
Scheduled int `json:"scheduled"`
|
||||||
|
List []PublishQueueItemData `json:"list"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
CopyMissionInspirationSourceData {
|
CopyMissionInspirationSourceData {
|
||||||
Query string `json:"query,omitempty"`
|
Query string `json:"query,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
|
|
@ -275,11 +298,22 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyMissionInspirationData {
|
CopyMissionInspirationData {
|
||||||
|
TopicCandidateID string `json:"topic_candidate_id,omitempty"`
|
||||||
|
ContentPlanID string `json:"content_plan_id,omitempty"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
SeedQuery string `json:"seed_query"`
|
SeedQuery string `json:"seed_query"`
|
||||||
Brief string `json:"brief"`
|
Brief string `json:"brief"`
|
||||||
TrendReason string `json:"trend_reason,omitempty"`
|
TrendReason string `json:"trend_reason,omitempty"`
|
||||||
TrendKeywords []string `json:"trend_keywords,omitempty"`
|
TrendKeywords []string `json:"trend_keywords,omitempty"`
|
||||||
|
Angles []string `json:"angles,omitempty"`
|
||||||
|
Mission string `json:"mission,omitempty"`
|
||||||
|
TargetAudience string `json:"target_audience,omitempty"`
|
||||||
|
OpeningType string `json:"opening_type,omitempty"`
|
||||||
|
BodyType string `json:"body_type,omitempty"`
|
||||||
|
Emotion string `json:"emotion,omitempty"`
|
||||||
|
CtaType string `json:"cta_type,omitempty"`
|
||||||
|
RiskLevel string `json:"risk_level,omitempty"`
|
||||||
|
Avoid []string `json:"avoid,omitempty"`
|
||||||
Sources []CopyMissionInspirationSourceData `json:"sources,omitempty"`
|
Sources []CopyMissionInspirationSourceData `json:"sources,omitempty"`
|
||||||
WebSearchUsed bool `json:"web_search_used"`
|
WebSearchUsed bool `json:"web_search_used"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
|
@ -336,6 +370,9 @@ service gateway {
|
||||||
@handler deleteCopyMissionMatrixDrafts
|
@handler deleteCopyMissionMatrixDrafts
|
||||||
post /:personaId/copy-missions/:id/matrix-drafts/delete (DeleteCopyMissionMatrixDraftsHandlerReq) returns (DeleteCopyMissionMatrixDraftsData)
|
post /:personaId/copy-missions/:id/matrix-drafts/delete (DeleteCopyMissionMatrixDraftsHandlerReq) returns (DeleteCopyMissionMatrixDraftsData)
|
||||||
|
|
||||||
|
@handler scheduleCopyMissionDrafts
|
||||||
|
post /:personaId/copy-missions/:id/copy-drafts/schedule (ScheduleCopyMissionDraftsHandlerReq) returns (CopyMissionScheduleCopyDraftsData)
|
||||||
|
|
||||||
@handler getCopyMissionScanSchedule
|
@handler getCopyMissionScanSchedule
|
||||||
get /:personaId/copy-missions/:id/scan-schedule (CopyMissionPath) returns (CopyMissionScanScheduleData)
|
get /:personaId/copy-missions/:id/scan-schedule (CopyMissionPath) returns (CopyMissionScanScheduleData)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ type (
|
||||||
BraveSearchLang string `json:"brave_search_lang"`
|
BraveSearchLang string `json:"brave_search_lang"`
|
||||||
ExaUserLocation string `json:"exa_user_location"`
|
ExaUserLocation string `json:"exa_user_location"`
|
||||||
ExpandStrategy string `json:"expand_strategy"` // brave | llm | hybrid
|
ExpandStrategy string `json:"expand_strategy"` // brave | llm | hybrid
|
||||||
|
DevModeEnabled bool `json:"dev_mode_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateMemberPlacementSettingsReq {
|
UpdateMemberPlacementSettingsReq {
|
||||||
|
|
@ -49,6 +50,7 @@ type (
|
||||||
BraveSearchLang *string `json:"brave_search_lang,optional"`
|
BraveSearchLang *string `json:"brave_search_lang,optional"`
|
||||||
ExaUserLocation *string `json:"exa_user_location,optional"`
|
ExaUserLocation *string `json:"exa_user_location,optional"`
|
||||||
ExpandStrategy *string `json:"expand_strategy,optional"`
|
ExpandStrategy *string `json:"expand_strategy,optional"`
|
||||||
|
DevModeEnabled *bool `json:"dev_mode_enabled,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
MemberCapabilitiesData {
|
MemberCapabilitiesData {
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,23 @@ type (
|
||||||
StartPersonaStyleAnalysisReq
|
StartPersonaStyleAnalysisReq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StartPersonaStyleAnalysisFromTextReq {
|
||||||
|
ReferenceTexts []string `json:"reference_texts,optional"`
|
||||||
|
RawText string `json:"raw_text,optional"`
|
||||||
|
SourceLabel string `json:"source_label,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaStyleAnalysisFromTextData {
|
||||||
|
Persona PersonaData `json:"persona"`
|
||||||
|
PostCount int `json:"post_count"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaStyleAnalysisFromTextHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
StartPersonaStyleAnalysisFromTextReq
|
||||||
|
}
|
||||||
|
|
||||||
StartPersonaViralScanJobReq {
|
StartPersonaViralScanJobReq {
|
||||||
Keywords []string `json:"keywords,optional"`
|
Keywords []string `json:"keywords,optional"`
|
||||||
}
|
}
|
||||||
|
|
@ -113,17 +130,29 @@ type (
|
||||||
CopyDraftData {
|
CopyDraftData {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
PersonaID string `json:"persona_id"`
|
PersonaID string `json:"persona_id"`
|
||||||
|
ContentPlanID string `json:"content_plan_id,omitempty"`
|
||||||
CopyMissionID string `json:"copy_mission_id,omitempty"`
|
CopyMissionID string `json:"copy_mission_id,omitempty"`
|
||||||
ScanPostID string `json:"scan_post_id,omitempty"`
|
ScanPostID string `json:"scan_post_id,omitempty"`
|
||||||
|
FormulaID string `json:"formula_id,omitempty"`
|
||||||
DraftType string `json:"draft_type"`
|
DraftType string `json:"draft_type"`
|
||||||
SortOrder int `json:"sort_order,omitempty"`
|
SortOrder int `json:"sort_order,omitempty"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
TopicTag string `json:"topic_tag,omitempty"`
|
||||||
Angle string `json:"angle,omitempty"`
|
Angle string `json:"angle,omitempty"`
|
||||||
Hook string `json:"hook,omitempty"`
|
Hook string `json:"hook,omitempty"`
|
||||||
Rationale string `json:"rationale,omitempty"`
|
Rationale string `json:"rationale,omitempty"`
|
||||||
ReferenceNotes string `json:"reference_notes,omitempty"`
|
ReferenceNotes string `json:"reference_notes,omitempty"`
|
||||||
Sources []string `json:"sources,omitempty"`
|
Sources []string `json:"sources,omitempty"`
|
||||||
|
AiScore int `json:"ai_score,omitempty"`
|
||||||
|
FormulaScore int `json:"formula_score,omitempty"`
|
||||||
|
BrandFitScore int `json:"brand_fit_score,omitempty"`
|
||||||
|
RiskScore int `json:"risk_score,omitempty"`
|
||||||
|
SimilarityScore int `json:"similarity_score,omitempty"`
|
||||||
|
EngagementPotential int `json:"engagement_potential,omitempty"`
|
||||||
|
FreshnessScore int `json:"freshness_score,omitempty"`
|
||||||
|
ReviewSuggestion string `json:"review_suggestion,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
PublishQueueID string `json:"publish_queue_id,omitempty"`
|
||||||
PublishedMediaID string `json:"published_media_id,omitempty"`
|
PublishedMediaID string `json:"published_media_id,omitempty"`
|
||||||
PublishedPermalink string `json:"published_permalink,omitempty"`
|
PublishedPermalink string `json:"published_permalink,omitempty"`
|
||||||
PublishedAt int64 `json:"published_at,omitempty"`
|
PublishedAt int64 `json:"published_at,omitempty"`
|
||||||
|
|
@ -155,10 +184,202 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateCopyDraftReq {
|
UpdateCopyDraftReq {
|
||||||
Text *string `json:"text,optional"`
|
Text *string `json:"text,optional"`
|
||||||
Hook *string `json:"hook,optional"`
|
TopicTag *string `json:"topic_tag,optional"`
|
||||||
Angle *string `json:"angle,optional"`
|
Hook *string `json:"hook,optional"`
|
||||||
Status *string `json:"status,optional"`
|
Angle *string `json:"angle,optional"`
|
||||||
|
Rationale *string `json:"rationale,optional"`
|
||||||
|
Status *string `json:"status,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
TopicCandidateData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
SeedQuery string `json:"seed_query,omitempty"`
|
||||||
|
TrendReason string `json:"trend_reason,omitempty"`
|
||||||
|
TrendKeywords []string `json:"trend_keywords,omitempty"`
|
||||||
|
HeatScore int `json:"heat_score,omitempty"`
|
||||||
|
FitScore int `json:"fit_score,omitempty"`
|
||||||
|
InteractionScore int `json:"interaction_score,omitempty"`
|
||||||
|
ExtendScore int `json:"extend_score,omitempty"`
|
||||||
|
RiskScore int `json:"risk_score,omitempty"`
|
||||||
|
FinalScore float64 `json:"final_score,omitempty"`
|
||||||
|
RecommendedMissions []string `json:"recommended_missions,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentPlanData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
TopicCandidateID string `json:"topic_candidate_id,omitempty"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Mission string `json:"mission"`
|
||||||
|
TargetAudience string `json:"target_audience,omitempty"`
|
||||||
|
Angle string `json:"angle,omitempty"`
|
||||||
|
OpeningType string `json:"opening_type,omitempty"`
|
||||||
|
BodyType string `json:"body_type,omitempty"`
|
||||||
|
Emotion string `json:"emotion,omitempty"`
|
||||||
|
EndingType string `json:"ending_type,omitempty"`
|
||||||
|
CtaType string `json:"cta_type,omitempty"`
|
||||||
|
RiskLevel string `json:"risk_level,omitempty"`
|
||||||
|
RequiresHumanReview bool `json:"requires_human_review,omitempty"`
|
||||||
|
Avoid []string `json:"avoid,omitempty"`
|
||||||
|
SelectedKnowledge []string `json:"selected_knowledge,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedbackEventData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
ContentPlanID string `json:"content_plan_id,omitempty"`
|
||||||
|
DraftID string `json:"draft_id,omitempty"`
|
||||||
|
Decision string `json:"decision"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
Snapshot string `json:"snapshot,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeSourceData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
SourceType string `json:"source_type"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
ParsedStatus string `json:"parsed_status,omitempty"`
|
||||||
|
ChunkCount int `json:"chunk_count,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeChunkData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
SourceID string `json:"source_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Topics []string `json:"topics,omitempty"`
|
||||||
|
StyleTags []string `json:"style_tags,omitempty"`
|
||||||
|
RiskLevel string `json:"risk_level,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FormulaPoolData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Pattern string `json:"pattern,omitempty"`
|
||||||
|
UseCases []string `json:"use_cases,omitempty"`
|
||||||
|
Avoid []string `json:"avoid,omitempty"`
|
||||||
|
Weight int `json:"weight,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateKnowledgeSourceReq {
|
||||||
|
SourceType string `json:"source_type" validate:"required"`
|
||||||
|
Title string `json:"title,optional"`
|
||||||
|
Filename string `json:"filename,optional"`
|
||||||
|
Content string `json:"content,optional"`
|
||||||
|
ContentBase64 string `json:"content_base64,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateKnowledgeSourceHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
CreateKnowledgeSourceReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListKnowledgeSourcesData { List []KnowledgeSourceData `json:"list"` }
|
||||||
|
ListKnowledgeChunksData { List []KnowledgeChunkData `json:"list"` }
|
||||||
|
ListFormulaPoolsData { List []FormulaPoolData `json:"list"` }
|
||||||
|
|
||||||
|
CreateFormulaPoolReq {
|
||||||
|
Type string `json:"type" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Pattern string `json:"pattern,optional"`
|
||||||
|
UseCases []string `json:"use_cases,optional"`
|
||||||
|
Avoid []string `json:"avoid,optional"`
|
||||||
|
Weight int `json:"weight,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateFormulaPoolHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
CreateFormulaPoolReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTopicCandidatesData {
|
||||||
|
List []TopicCandidateData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListContentPlansData {
|
||||||
|
List []ContentPlanData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateContentPlanReq {
|
||||||
|
TopicCandidateID string `json:"topic_candidate_id,optional"`
|
||||||
|
Topic string `json:"topic" validate:"required"`
|
||||||
|
Mission string `json:"mission" validate:"required"`
|
||||||
|
TargetAudience string `json:"target_audience,optional"`
|
||||||
|
Angle string `json:"angle,optional"`
|
||||||
|
OpeningType string `json:"opening_type,optional"`
|
||||||
|
BodyType string `json:"body_type,optional"`
|
||||||
|
Emotion string `json:"emotion,optional"`
|
||||||
|
EndingType string `json:"ending_type,optional"`
|
||||||
|
CtaType string `json:"cta_type,optional"`
|
||||||
|
RiskLevel string `json:"risk_level,optional"`
|
||||||
|
RequiresHumanReview bool `json:"requires_human_review,optional"`
|
||||||
|
Avoid []string `json:"avoid,optional"`
|
||||||
|
SelectedKnowledge []string `json:"selected_knowledge,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateContentPlanHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
CreateContentPlanReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentPlanPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
ContentPlanID string `path:"contentPlanId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateContentPlanReq {
|
||||||
|
Topic *string `json:"topic,optional"`
|
||||||
|
Mission *string `json:"mission,optional"`
|
||||||
|
TargetAudience *string `json:"target_audience,optional"`
|
||||||
|
Angle *string `json:"angle,optional"`
|
||||||
|
OpeningType *string `json:"opening_type,optional"`
|
||||||
|
BodyType *string `json:"body_type,optional"`
|
||||||
|
Emotion *string `json:"emotion,optional"`
|
||||||
|
EndingType *string `json:"ending_type,optional"`
|
||||||
|
CtaType *string `json:"cta_type,optional"`
|
||||||
|
RiskLevel *string `json:"risk_level,optional"`
|
||||||
|
RequiresHumanReview *bool `json:"requires_human_review,optional"`
|
||||||
|
Avoid []string `json:"avoid,optional"`
|
||||||
|
SelectedKnowledge []string `json:"selected_knowledge,optional"`
|
||||||
|
Status *string `json:"status,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateContentPlanHandlerReq {
|
||||||
|
ContentPlanPath
|
||||||
|
UpdateContentPlanReq
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateFeedbackEventReq {
|
||||||
|
ContentPlanID string `json:"content_plan_id,optional"`
|
||||||
|
DraftID string `json:"draft_id,optional"`
|
||||||
|
Decision string `json:"decision" validate:"required"`
|
||||||
|
Note string `json:"note,optional"`
|
||||||
|
Snapshot string `json:"snapshot,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateFeedbackEventHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
CreateFeedbackEventReq
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateCopyDraftHandlerReq {
|
UpdateCopyDraftHandlerReq {
|
||||||
|
|
@ -167,8 +388,9 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
PublishCopyDraftReq {
|
PublishCopyDraftReq {
|
||||||
Text string `json:"text,optional"`
|
Text string `json:"text,optional"`
|
||||||
Confirm bool `json:"confirm"`
|
TopicTag string `json:"topic_tag,optional"`
|
||||||
|
Confirm bool `json:"confirm"`
|
||||||
}
|
}
|
||||||
|
|
||||||
PublishCopyDraftHandlerReq {
|
PublishCopyDraftHandlerReq {
|
||||||
|
|
@ -184,10 +406,216 @@ type (
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScheduleCopyDraftsReq {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
DraftIDs []string `json:"draft_ids" validate:"required,min=1"`
|
||||||
|
StartAt int64 `json:"start_at,optional"`
|
||||||
|
Timezone string `json:"timezone,optional"`
|
||||||
|
Slots []PublishSlotData `json:"slots,optional"`
|
||||||
|
Mode string `json:"mode,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleCopyDraftsData {
|
||||||
|
Scheduled int `json:"scheduled"`
|
||||||
|
List []PublishQueueItemData `json:"list"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SchedulePersonaDraftsHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
ScheduleCopyDraftsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StylePresetData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PersonaID string `json:"persona_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Tone string `json:"tone,omitempty"`
|
||||||
|
CTA []string `json:"cta,omitempty"`
|
||||||
|
BannedWords []string `json:"banned_words,omitempty"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListStylePresetsData {
|
||||||
|
List []StylePresetData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertStylePresetReq {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Tone string `json:"tone,optional"`
|
||||||
|
CTA []string `json:"cta,optional"`
|
||||||
|
BannedWords []string `json:"banned_words,optional"`
|
||||||
|
Notes string `json:"notes,optional"`
|
||||||
|
Apply bool `json:"apply,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StylePresetPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
PresetID string `path:"presetId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertStylePresetHandlerReq {
|
||||||
|
StylePresetPath
|
||||||
|
UpsertStylePresetReq
|
||||||
|
}
|
||||||
|
|
||||||
DeleteCopyDraftData {
|
DeleteCopyDraftData {
|
||||||
DraftID string `json:"draft_id"`
|
DraftID string `json:"draft_id"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PrunePersonaCopyDraftsReq {
|
||||||
|
Keep int `json:"keep,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PrunePersonaCopyDraftsHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
PrunePersonaCopyDraftsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PrunePersonaCopyDraftsData {
|
||||||
|
Deleted int64 `json:"deleted"`
|
||||||
|
Kept int `json:"kept"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaContentInboxReq {
|
||||||
|
Page int64 `form:"page,optional"`
|
||||||
|
PageSize int64 `form:"pageSize,optional"`
|
||||||
|
Status string `form:"status,optional"`
|
||||||
|
RangeStart int64 `form:"rangeStart,optional"`
|
||||||
|
RangeEnd int64 `form:"rangeEnd,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaContentInboxHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
ListPersonaContentInboxReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentInboxItemData {
|
||||||
|
CopyDraftData
|
||||||
|
ScheduledAt int64 `json:"scheduled_at,omitempty"`
|
||||||
|
Lifecycle string `json:"lifecycle"`
|
||||||
|
GroupDate int64 `json:"group_date"`
|
||||||
|
FormulaLabel string `json:"formula_label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPersonaContentInboxData {
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []ContentInboxItemData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SchedulePersonaCopyDraftReq {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
TopicTag string `json:"topic_tag,optional"`
|
||||||
|
ScheduledAt int64 `json:"scheduled_at,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SchedulePersonaCopyDraftHandlerReq {
|
||||||
|
CopyDraftPath
|
||||||
|
SchedulePersonaCopyDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentFormulaPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
FormulaID string `path:"formulaId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateFromContentFormulaReq {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
Topic string `json:"topic" validate:"required"`
|
||||||
|
Brief string `json:"brief,optional"`
|
||||||
|
UseWebSearch bool `json:"use_web_search,optional"`
|
||||||
|
DraftCount int `json:"draft_count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateFromContentFormulaHandlerReq {
|
||||||
|
ContentFormulaPath
|
||||||
|
GenerateFromContentFormulaReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateFromContentFormulaData {
|
||||||
|
List []CopyDraftData `json:"list"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePersonaTopicMatrixReq {
|
||||||
|
Topic string `json:"topic" validate:"required"`
|
||||||
|
ContentPlanID string `json:"content_plan_id,optional"`
|
||||||
|
Brief string `json:"brief,optional"`
|
||||||
|
UseWebSearch bool `json:"use_web_search,optional"`
|
||||||
|
DraftCount int `json:"draft_count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePersonaTopicMatrixHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
GeneratePersonaTopicMatrixReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneratePersonaTopicMatrixData {
|
||||||
|
List []CopyDraftData `json:"list"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaTopicMatrixJobHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
GeneratePersonaTopicMatrixReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaTopicMatrixJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
AiProvider string `json:"ai_provider"`
|
||||||
|
AiModel string `json:"ai_model"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaFormulaDraftJobReq {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
FormulaID string `json:"formula_id" validate:"required"`
|
||||||
|
Topic string `json:"topic" validate:"required"`
|
||||||
|
Brief string `json:"brief,optional"`
|
||||||
|
UseWebSearch bool `json:"use_web_search,optional"`
|
||||||
|
DraftCount int `json:"draft_count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaFormulaDraftJobHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
StartPersonaFormulaDraftJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaFormulaDraftJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
AiProvider string `json:"ai_provider"`
|
||||||
|
AiModel string `json:"ai_model"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaRewriteDraftJobReq {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
ReferenceText string `json:"reference_text" validate:"required"`
|
||||||
|
Topic string `json:"topic" validate:"required"`
|
||||||
|
Brief string `json:"brief,optional"`
|
||||||
|
SaveLabel string `json:"save_label,optional"`
|
||||||
|
UseWebSearch bool `json:"use_web_search,optional"`
|
||||||
|
DraftCount int `json:"draft_count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaRewriteDraftJobHandlerReq {
|
||||||
|
PersonaPath
|
||||||
|
StartPersonaRewriteDraftJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPersonaRewriteDraftJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
AiProvider string `json:"ai_provider"`
|
||||||
|
AiModel string `json:"ai_model"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server(
|
@server(
|
||||||
|
|
@ -216,6 +644,9 @@ service gateway {
|
||||||
@handler startPersonaStyleAnalysis
|
@handler startPersonaStyleAnalysis
|
||||||
post /:id/style-analysis (StartPersonaStyleAnalysisHandlerReq) returns (StartPersonaStyleAnalysisData)
|
post /:id/style-analysis (StartPersonaStyleAnalysisHandlerReq) returns (StartPersonaStyleAnalysisData)
|
||||||
|
|
||||||
|
@handler startPersonaStyleAnalysisFromText
|
||||||
|
post /:id/style-analysis-from-text (StartPersonaStyleAnalysisFromTextHandlerReq) returns (StartPersonaStyleAnalysisFromTextData)
|
||||||
|
|
||||||
@handler startPersonaViralScanJob
|
@handler startPersonaViralScanJob
|
||||||
post /:id/viral-scan-jobs (StartPersonaViralScanJobHandlerReq) returns (StartPersonaViralScanJobData)
|
post /:id/viral-scan-jobs (StartPersonaViralScanJobHandlerReq) returns (StartPersonaViralScanJobData)
|
||||||
|
|
||||||
|
|
@ -225,6 +656,39 @@ service gateway {
|
||||||
@handler listPersonaCopyDrafts
|
@handler listPersonaCopyDrafts
|
||||||
get /:id/copy-drafts (PersonaPath) returns (ListPersonaCopyDraftsData)
|
get /:id/copy-drafts (PersonaPath) returns (ListPersonaCopyDraftsData)
|
||||||
|
|
||||||
|
@handler listPersonaContentInbox
|
||||||
|
get /:id/content-inbox (ListPersonaContentInboxHandlerReq) returns (ListPersonaContentInboxData)
|
||||||
|
|
||||||
|
@handler listTopicCandidates
|
||||||
|
get /:id/topic-candidates (PersonaPath) returns (ListTopicCandidatesData)
|
||||||
|
|
||||||
|
@handler listContentPlans
|
||||||
|
get /:id/content-plans (PersonaPath) returns (ListContentPlansData)
|
||||||
|
|
||||||
|
@handler createContentPlan
|
||||||
|
post /:id/content-plans (CreateContentPlanHandlerReq) returns (ContentPlanData)
|
||||||
|
|
||||||
|
@handler updateContentPlan
|
||||||
|
patch /:id/content-plans/:contentPlanId (UpdateContentPlanHandlerReq) returns (ContentPlanData)
|
||||||
|
|
||||||
|
@handler createFeedbackEvent
|
||||||
|
post /:id/feedback-events (CreateFeedbackEventHandlerReq) returns (FeedbackEventData)
|
||||||
|
|
||||||
|
@handler createKnowledgeSource
|
||||||
|
post /:id/knowledge-sources (CreateKnowledgeSourceHandlerReq) returns (KnowledgeSourceData)
|
||||||
|
|
||||||
|
@handler listKnowledgeSources
|
||||||
|
get /:id/knowledge-sources (PersonaPath) returns (ListKnowledgeSourcesData)
|
||||||
|
|
||||||
|
@handler listKnowledgeChunks
|
||||||
|
get /:id/knowledge-chunks (PersonaPath) returns (ListKnowledgeChunksData)
|
||||||
|
|
||||||
|
@handler listFormulaPools
|
||||||
|
get /:id/formula-pools (PersonaPath) returns (ListFormulaPoolsData)
|
||||||
|
|
||||||
|
@handler createFormulaPool
|
||||||
|
post /:id/formula-pools (CreateFormulaPoolHandlerReq) returns (FormulaPoolData)
|
||||||
|
|
||||||
@handler generatePersonaCopyDraft
|
@handler generatePersonaCopyDraft
|
||||||
post /:id/copy-drafts/generate (GeneratePersonaCopyDraftHandlerReq) returns (GeneratePersonaCopyDraftData)
|
post /:id/copy-drafts/generate (GeneratePersonaCopyDraftHandlerReq) returns (GeneratePersonaCopyDraftData)
|
||||||
|
|
||||||
|
|
@ -234,6 +698,39 @@ service gateway {
|
||||||
@handler publishPersonaCopyDraft
|
@handler publishPersonaCopyDraft
|
||||||
post /:id/copy-drafts/:draftId/publish (PublishCopyDraftHandlerReq) returns (PublishCopyDraftData)
|
post /:id/copy-drafts/:draftId/publish (PublishCopyDraftHandlerReq) returns (PublishCopyDraftData)
|
||||||
|
|
||||||
|
@handler schedulePersonaDrafts
|
||||||
|
post /:id/copy-drafts/schedule (SchedulePersonaDraftsHandlerReq) returns (ScheduleCopyDraftsData)
|
||||||
|
|
||||||
|
@handler schedulePersonaCopyDraft
|
||||||
|
post /:id/copy-drafts/:draftId/schedule (SchedulePersonaCopyDraftHandlerReq) returns (ScheduleCopyDraftsData)
|
||||||
|
|
||||||
|
@handler generateFromContentFormula
|
||||||
|
post /:id/content-formulas/:formulaId/generate (GenerateFromContentFormulaHandlerReq) returns (GenerateFromContentFormulaData)
|
||||||
|
|
||||||
|
@handler generatePersonaTopicMatrix
|
||||||
|
post /:id/topic-matrix/generate (GeneratePersonaTopicMatrixHandlerReq) returns (GeneratePersonaTopicMatrixData)
|
||||||
|
|
||||||
|
@handler startPersonaTopicMatrixJob
|
||||||
|
post /:id/topic-matrix/jobs (StartPersonaTopicMatrixJobHandlerReq) returns (StartPersonaTopicMatrixJobData)
|
||||||
|
|
||||||
|
@handler startPersonaFormulaDraftJob
|
||||||
|
post /:id/formula-draft/jobs (StartPersonaFormulaDraftJobHandlerReq) returns (StartPersonaFormulaDraftJobData)
|
||||||
|
|
||||||
|
@handler startPersonaRewriteDraftJob
|
||||||
|
post /:id/rewrite-draft/jobs (StartPersonaRewriteDraftJobHandlerReq) returns (StartPersonaRewriteDraftJobData)
|
||||||
|
|
||||||
@handler deletePersonaCopyDraft
|
@handler deletePersonaCopyDraft
|
||||||
delete /:id/copy-drafts/:draftId (CopyDraftPath) returns (DeleteCopyDraftData)
|
delete /:id/copy-drafts/:draftId (CopyDraftPath) returns (DeleteCopyDraftData)
|
||||||
}
|
|
||||||
|
@handler prunePersonaCopyDrafts
|
||||||
|
post /:id/copy-drafts/prune (PrunePersonaCopyDraftsHandlerReq) returns (PrunePersonaCopyDraftsData)
|
||||||
|
|
||||||
|
@handler listStylePresets
|
||||||
|
get /:id/style-presets (PersonaPath) returns (ListStylePresetsData)
|
||||||
|
|
||||||
|
@handler upsertStylePreset
|
||||||
|
put /:id/style-presets/:presetId (UpsertStylePresetHandlerReq) returns (StylePresetData)
|
||||||
|
|
||||||
|
@handler deleteStylePreset
|
||||||
|
delete /:id/style-presets/:presetId (StylePresetPath)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,12 @@ type (
|
||||||
PublishOutreachDraftReq
|
PublishOutreachDraftReq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdatePlacementTopicOutreachDraftHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
DraftID string `path:"draftId" validate:"required"`
|
||||||
|
UpdateOutreachDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
PatchPlacementTopicScanPostOutreachHandlerReq {
|
PatchPlacementTopicScanPostOutreachHandlerReq {
|
||||||
PlacementTopicPath
|
PlacementTopicPath
|
||||||
PostID string `path:"postId"`
|
PostID string `path:"postId"`
|
||||||
|
|
@ -116,6 +122,25 @@ type (
|
||||||
PlacementTopicPath
|
PlacementTopicPath
|
||||||
UpsertBrandScanScheduleReq
|
UpsertBrandScanScheduleReq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StartPlacementTopicOutreachDraftJobReq {
|
||||||
|
ScanPostID string `json:"scan_post_id,optional"`
|
||||||
|
ScanPostIDs []string `json:"scan_post_ids,optional"`
|
||||||
|
Count int `json:"count,optional"`
|
||||||
|
VoicePersonaID string `json:"voice_persona_id,optional"`
|
||||||
|
ProductID string `json:"product_id,optional"`
|
||||||
|
Regenerate bool `json:"regenerate,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPlacementTopicOutreachDraftJobHandlerReq {
|
||||||
|
PlacementTopicPath
|
||||||
|
StartPlacementTopicOutreachDraftJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPlacementTopicOutreachDraftJobsData {
|
||||||
|
Jobs []StartBrandScanJobData `json:"jobs"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server(
|
@server(
|
||||||
|
|
@ -159,9 +184,15 @@ service gateway {
|
||||||
@handler generatePlacementTopicOutreachDrafts
|
@handler generatePlacementTopicOutreachDrafts
|
||||||
post /:id/outreach-drafts/generate (GeneratePlacementTopicOutreachDraftsHandlerReq) returns (GenerateOutreachDraftsData)
|
post /:id/outreach-drafts/generate (GeneratePlacementTopicOutreachDraftsHandlerReq) returns (GenerateOutreachDraftsData)
|
||||||
|
|
||||||
|
@handler startPlacementTopicOutreachDraftJob
|
||||||
|
post /:id/outreach-draft-jobs (StartPlacementTopicOutreachDraftJobHandlerReq) returns (StartPlacementTopicOutreachDraftJobsData)
|
||||||
|
|
||||||
@handler publishPlacementTopicOutreachDraft
|
@handler publishPlacementTopicOutreachDraft
|
||||||
post /:id/outreach-drafts/publish (PublishPlacementTopicOutreachDraftHandlerReq) returns (PublishOutreachDraftData)
|
post /:id/outreach-drafts/publish (PublishPlacementTopicOutreachDraftHandlerReq) returns (PublishOutreachDraftData)
|
||||||
|
|
||||||
|
@handler updatePlacementTopicOutreachDraft
|
||||||
|
patch /:id/outreach-drafts/:draftId (UpdatePlacementTopicOutreachDraftHandlerReq) returns (GenerateOutreachDraftsData)
|
||||||
|
|
||||||
@handler patchPlacementTopicScanPostOutreach
|
@handler patchPlacementTopicScanPostOutreach
|
||||||
patch /:id/scan-posts/:postId (PatchPlacementTopicScanPostOutreachHandlerReq) returns (ScanPostData)
|
patch /:id/scan-posts/:postId (PatchPlacementTopicScanPostOutreachHandlerReq) returns (ScanPostData)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ type (
|
||||||
ThreadsUserID string `json:"threads_user_id,omitempty"`
|
ThreadsUserID string `json:"threads_user_id,omitempty"`
|
||||||
PersonaID string `json:"persona_id,omitempty"` // deprecated: persona is chosen per publish, not bound to account
|
PersonaID string `json:"persona_id,omitempty"` // deprecated: persona is chosen per publish, not bound to account
|
||||||
BrowserConnected bool `json:"browser_connected"`
|
BrowserConnected bool `json:"browser_connected"`
|
||||||
ApiConnected bool `json:"api_connected"`
|
ApiConnected bool `json:"api_connected"`
|
||||||
Status string `json:"status"`
|
APITokenExpiresAt int64 `json:"api_token_expires_at,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
UpdateAt int64 `json:"update_at"`
|
UpdateAt int64 `json:"update_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -19,6 +20,12 @@ type (
|
||||||
ActiveAccountID string `json:"active_account_id"`
|
ActiveAccountID string `json:"active_account_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeleteThreadsAccountData {
|
||||||
|
DeletedID string `json:"deleted_id"`
|
||||||
|
ActiveAccountID string `json:"active_account_id,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
CreateThreadsAccountReq {
|
CreateThreadsAccountReq {
|
||||||
DisplayName string `json:"display_name,optional"`
|
DisplayName string `json:"display_name,optional"`
|
||||||
Activate *bool `json:"activate,optional"`
|
Activate *bool `json:"activate,optional"`
|
||||||
|
|
@ -137,6 +144,711 @@ type (
|
||||||
Streams bool `json:"streams"`
|
Streams bool `json:"streams"`
|
||||||
Error string `json:"error,optional"`
|
Error string `json:"error,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StartThreadsOAuthQuery {
|
||||||
|
AccountID string `form:"account_id,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartThreadsOAuthData {
|
||||||
|
AuthorizeURL string `json:"authorize_url"`
|
||||||
|
State string `json:"state"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsOAuthCallbackQuery {
|
||||||
|
Code string `form:"code,optional"`
|
||||||
|
State string `form:"state,optional"`
|
||||||
|
Error string `form:"error,optional"`
|
||||||
|
ErrorReason string `form:"error_reason,optional"`
|
||||||
|
ErrorDescription string `form:"error_description,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsOAuthLogItem {
|
||||||
|
At int64 `json:"at"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Stage string `json:"stage"`
|
||||||
|
AccountID string `json:"account_id,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsOAuthLogsData {
|
||||||
|
List []ThreadsOAuthLogItem `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsOAuthConfigData {
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
SuccessRedirect string `json:"success_redirect"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
RequiresHTTPS bool `json:"requires_https"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAPISmokeTestItem {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
RunThreadsAPISmokeTestData {
|
||||||
|
List []ThreadsAPISmokeTestItem `json:"list"`
|
||||||
|
Passed int `json:"passed"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Skipped int `json:"skipped"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
RunThreadsAPIPlaygroundReq {
|
||||||
|
Action string `json:"action" validate:"required"`
|
||||||
|
Query string `json:"query,optional"`
|
||||||
|
Username string `json:"username,optional"`
|
||||||
|
MediaID string `json:"media_id,optional"`
|
||||||
|
ReplyID string `json:"reply_id,optional"`
|
||||||
|
ReplyToID string `json:"reply_to_id,optional"`
|
||||||
|
Text string `json:"text,optional"`
|
||||||
|
Permalink string `json:"permalink,optional"`
|
||||||
|
HintText string `json:"hint_text,optional"`
|
||||||
|
SkipPrime bool `json:"skip_prime,optional"`
|
||||||
|
LocationQuery string `json:"location_query,optional"`
|
||||||
|
Limit int `json:"limit,optional"`
|
||||||
|
Hide bool `json:"hide,optional"`
|
||||||
|
SearchType string `json:"search_type,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
RunThreadsAPIPlaygroundHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
RunThreadsAPIPlaygroundReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAPIPlaygroundData {
|
||||||
|
Action string `json:"action"`
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data string `json:"data,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsPostPerformanceQuery {
|
||||||
|
Limit int `form:"limit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsPostPerformanceHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ListThreadsPostPerformanceQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPostMetricCapability {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Metrics []string `json:"metrics"`
|
||||||
|
Trackable bool `json:"trackable"`
|
||||||
|
Note string `json:"note,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsInsightMetricItem {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value int `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsAccountInsightsBlock {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message,optional"`
|
||||||
|
Metrics []ThreadsInsightMetricItem `json:"metrics,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPostPerformanceItem {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
Text string `json:"text,optional"`
|
||||||
|
Permalink string `json:"permalink,optional"`
|
||||||
|
Timestamp string `json:"timestamp,optional"`
|
||||||
|
MediaType string `json:"media_type,optional"`
|
||||||
|
MediaURL string `json:"media_url,optional"`
|
||||||
|
ThumbnailURL string `json:"thumbnail_url,optional"`
|
||||||
|
TopicTag string `json:"topic_tag,optional"`
|
||||||
|
Shortcode string `json:"shortcode,optional"`
|
||||||
|
LikeCount int `json:"like_count"`
|
||||||
|
ReplyCount int `json:"reply_count"`
|
||||||
|
RepostCount int `json:"repost_count,optional"`
|
||||||
|
QuoteCount int `json:"quote_count,optional"`
|
||||||
|
Views int `json:"views,optional"`
|
||||||
|
Shares int `json:"shares,optional"`
|
||||||
|
InsightsStatus string `json:"insights_status,optional"`
|
||||||
|
InsightsMessage string `json:"insights_message,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPostPerformanceData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Username string `json:"username,optional"`
|
||||||
|
ThreadsUserID string `json:"threads_user_id,optional"`
|
||||||
|
FetchedAt int64 `json:"fetched_at"`
|
||||||
|
Capabilities []ThreadsPostMetricCapability `json:"capabilities"`
|
||||||
|
AccountInsights ThreadsAccountInsightsBlock `json:"account_insights"`
|
||||||
|
Posts []ThreadsPostPerformanceItem `json:"posts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueItemPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
QID string `path:"qid" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPublishQueueQuery {
|
||||||
|
Status string `form:"status,optional"`
|
||||||
|
Page int `form:"page,optional"`
|
||||||
|
PageSize int `form:"pageSize,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPublishQueueHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ListPublishQueueQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePublishQueueReq {
|
||||||
|
Text string `json:"text" validate:"required"`
|
||||||
|
TopicTag string `json:"topic_tag,optional"`
|
||||||
|
ScheduledAt int64 `json:"scheduled_at,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatePublishQueueHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
CreatePublishQueueReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchPublishQueueReq {
|
||||||
|
Text *string `json:"text,optional"`
|
||||||
|
TopicTag *string `json:"topic_tag,optional"`
|
||||||
|
ScheduledAt *int64 `json:"scheduled_at,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchPublishQueueHandlerReq {
|
||||||
|
PublishQueueItemPath
|
||||||
|
PatchPublishQueueReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueItemData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
PersonaID string `json:"persona_id,omitempty"`
|
||||||
|
CopyMissionID string `json:"copy_mission_id,omitempty"`
|
||||||
|
CopyDraftID string `json:"copy_draft_id,omitempty"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
TopicTag string `json:"topic_tag,omitempty"`
|
||||||
|
ScheduledAt int64 `json:"scheduled_at"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MediaID string `json:"media_id,optional"`
|
||||||
|
Permalink string `json:"permalink,optional"`
|
||||||
|
PublishedAt int64 `json:"published_at,optional"`
|
||||||
|
ErrorMessage string `json:"error_message,optional"`
|
||||||
|
RetryCount int `json:"retry_count,omitempty"`
|
||||||
|
LastAttemptAt int64 `json:"last_attempt_at,omitempty"`
|
||||||
|
NextAttemptAt int64 `json:"next_attempt_at,omitempty"`
|
||||||
|
MissedAt int64 `json:"missed_at,omitempty"`
|
||||||
|
PausedReason string `json:"paused_reason,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueListData {
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []PublishQueueItemData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsPublishHealthQuery {
|
||||||
|
Page int `form:"page,optional"`
|
||||||
|
PageSize int `form:"pageSize,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListThreadsPublishHealthHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ListThreadsPublishHealthQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPublishHealthSummary {
|
||||||
|
PendingScheduled int64 `json:"pending_scheduled"`
|
||||||
|
FailedCount int64 `json:"failed_count"`
|
||||||
|
Published7d int64 `json:"published_7d"`
|
||||||
|
TotalLikes7d int `json:"total_likes_7d"`
|
||||||
|
TotalReplies7d int `json:"total_replies_7d"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPublishHealthCheckpoint {
|
||||||
|
Checkpoint string `json:"checkpoint"`
|
||||||
|
LikeCount int `json:"like_count"`
|
||||||
|
ReplyCount int `json:"reply_count"`
|
||||||
|
RepostCount int `json:"repost_count"`
|
||||||
|
QuoteCount int `json:"quote_count"`
|
||||||
|
Views int `json:"views,optional"`
|
||||||
|
CheckedAt int64 `json:"checked_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPublishHealthItem {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
Permalink string `json:"permalink,optional"`
|
||||||
|
Text string `json:"text,optional"`
|
||||||
|
PublishedAt int64 `json:"published_at"`
|
||||||
|
LikeCount int `json:"like_count"`
|
||||||
|
ReplyCount int `json:"reply_count"`
|
||||||
|
RepostCount int `json:"repost_count,optional"`
|
||||||
|
QuoteCount int `json:"quote_count,optional"`
|
||||||
|
Views int `json:"views,optional"`
|
||||||
|
Shares int `json:"shares,optional"`
|
||||||
|
Checkpoints []ThreadsPublishHealthCheckpoint `json:"checkpoints"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsPublishHealthData {
|
||||||
|
Summary ThreadsPublishHealthSummary `json:"summary"`
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []ThreadsPublishHealthItem `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSlotData {
|
||||||
|
Weekday int `json:"weekday"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSourceRefData {
|
||||||
|
Type string `json:"type"`
|
||||||
|
PersonaID string `json:"persona_id,omitempty"`
|
||||||
|
CopyMissionID string `json:"copy_mission_id,omitempty"`
|
||||||
|
ScanPostID string `json:"scan_post_id,omitempty"`
|
||||||
|
ManualSeed string `json:"manual_seed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishInventoryPolicyData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
TargetDailyCount int `json:"target_daily_count"`
|
||||||
|
LowStockThreshold int `json:"low_stock_threshold"`
|
||||||
|
LookaheadDays int `json:"lookahead_days"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
Slots []PublishSlotData `json:"slots"`
|
||||||
|
SourceRefs []PublishSourceRefData `json:"source_refs"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertPublishInventoryPolicyReq {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
TargetDailyCount int `json:"target_daily_count,optional"`
|
||||||
|
LowStockThreshold int `json:"low_stock_threshold,optional"`
|
||||||
|
LookaheadDays int `json:"lookahead_days,optional"`
|
||||||
|
Timezone string `json:"timezone,optional"`
|
||||||
|
Slots []PublishSlotData `json:"slots,optional"`
|
||||||
|
SourceRefs []PublishSourceRefData `json:"source_refs,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertPublishInventoryPolicyHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
UpsertPublishInventoryPolicyReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPublishInventoryRefillJobReq {
|
||||||
|
Count int `json:"count,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPublishInventoryRefillJobHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
StartPublishInventoryRefillJobReq
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPublishInventoryRefillJobData {
|
||||||
|
JobID string `json:"job_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSlotInsightData {
|
||||||
|
Weekday int `json:"weekday"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
AvgLikes1h int `json:"avg_likes_1h"`
|
||||||
|
AvgReplies1h int `json:"avg_replies_1h"`
|
||||||
|
AvgLikes24h int `json:"avg_likes_24h"`
|
||||||
|
AvgReplies24h int `json:"avg_replies_24h"`
|
||||||
|
AvgLikes7d int `json:"avg_likes_7d"`
|
||||||
|
AvgReplies7d int `json:"avg_replies_7d"`
|
||||||
|
SampleSize int `json:"sample_size"`
|
||||||
|
Recommendation string `json:"recommendation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSlotInsightsData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
Slots []PublishSlotInsightData `json:"slots"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishGuardPolicyData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
MaxDailyPosts int `json:"max_daily_posts"`
|
||||||
|
MinIntervalMinutes int `json:"min_interval_minutes"`
|
||||||
|
ConsecutiveFailurePauseLimit int `json:"consecutive_failure_pause_limit"`
|
||||||
|
Paused bool `json:"paused"`
|
||||||
|
PausedReason string `json:"paused_reason,omitempty"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertPublishGuardPolicyReq {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
MaxDailyPosts int `json:"max_daily_posts,optional"`
|
||||||
|
MinIntervalMinutes int `json:"min_interval_minutes,optional"`
|
||||||
|
ConsecutiveFailurePauseLimit int `json:"consecutive_failure_pause_limit,optional"`
|
||||||
|
Paused *bool `json:"paused,optional"`
|
||||||
|
PausedReason string `json:"paused_reason,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertPublishGuardPolicyHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
UpsertPublishGuardPolicyReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueEventData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
QueueID string `json:"queue_id"`
|
||||||
|
EventType string `json:"event_type"`
|
||||||
|
FromStatus string `json:"from_status,omitempty"`
|
||||||
|
ToStatus string `json:"to_status,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueEventsData {
|
||||||
|
List []PublishQueueEventData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishAlertData {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
QueueID string `json:"queue_id,omitempty"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishAlertsData {
|
||||||
|
List []PublishAlertData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueRangeQuery {
|
||||||
|
StartAt int64 `form:"startAt,optional"`
|
||||||
|
EndAt int64 `form:"endAt,optional"`
|
||||||
|
Status string `form:"status,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishQueueRangeHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
PublishQueueRangeQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishDashboardAccountSummary {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
AccountName string `json:"account_name"`
|
||||||
|
PendingScheduled int64 `json:"pending_scheduled"`
|
||||||
|
FailedCount int64 `json:"failed_count"`
|
||||||
|
Published7d int64 `json:"published_7d"`
|
||||||
|
TotalLikes7d int `json:"total_likes_7d"`
|
||||||
|
TotalReplies7d int `json:"total_replies_7d"`
|
||||||
|
Paused bool `json:"paused"`
|
||||||
|
BestSlot string `json:"best_slot,omitempty"`
|
||||||
|
LowSlot string `json:"low_slot,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishDashboardSummaryData {
|
||||||
|
List []PublishDashboardAccountSummary `json:"list"`
|
||||||
|
TotalPending int64 `json:"total_pending"`
|
||||||
|
TotalFailed int64 `json:"total_failed"`
|
||||||
|
TotalPublished7d int64 `json:"total_published_7d"`
|
||||||
|
TotalLikes7d int `json:"total_likes_7d"`
|
||||||
|
TotalReplies7d int `json:"total_replies_7d"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOwnPostReplyDraftReq {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
PersonaID string `json:"persona_id,optional"`
|
||||||
|
ReplyToID string `json:"reply_to_id,optional"`
|
||||||
|
PostText string `json:"post_text,optional"`
|
||||||
|
ReplyText string `json:"reply_text,optional"`
|
||||||
|
ThreadPostText string `json:"thread_post_text,optional"`
|
||||||
|
ParentReplyText string `json:"parent_reply_text,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOwnPostReplyDraftHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
GenerateOwnPostReplyDraftReq
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOwnPostReplyDraftData {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
MentionInboxItemData {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
AuthorUsername string `json:"author_username,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Permalink string `json:"permalink,omitempty"`
|
||||||
|
Shortcode string `json:"shortcode,omitempty"`
|
||||||
|
Timestamp string `json:"timestamp,omitempty"`
|
||||||
|
MediaType string `json:"media_type,omitempty"`
|
||||||
|
IsReply bool `json:"is_reply"`
|
||||||
|
IsQuotePost bool `json:"is_quote_post"`
|
||||||
|
HasReplies bool `json:"has_replies"`
|
||||||
|
RootPostID string `json:"root_post_id,omitempty"`
|
||||||
|
ParentID string `json:"parent_id,omitempty"`
|
||||||
|
ThreadPostText string `json:"thread_post_text,omitempty"`
|
||||||
|
ParentReplyText string `json:"parent_reply_text,omitempty"`
|
||||||
|
ReplyReady bool `json:"reply_ready"`
|
||||||
|
ReplyReadyMsg string `json:"reply_ready_msg,omitempty"`
|
||||||
|
SyncedAt int64 `json:"synced_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncMentionInboxReq {
|
||||||
|
Limit int `json:"limit,optional"`
|
||||||
|
MaxPages int `json:"max_pages,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncMentionInboxHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
SyncMentionInboxReq
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncMentionInboxData {
|
||||||
|
Synced int `json:"synced"`
|
||||||
|
Ready int `json:"ready"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListMentionInboxQuery {
|
||||||
|
Page int `form:"page,optional"`
|
||||||
|
PageSize int `form:"pageSize,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListMentionInboxHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ListMentionInboxQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
ListMentionInboxData {
|
||||||
|
List []MentionInboxItemData `json:"list"`
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
ReadyTotal int64 `json:"ready_total"`
|
||||||
|
NewestSyncedAt int64 `json:"newest_synced_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishMentionReplyReq {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishMentionReplyPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
MediaID string `path:"media_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishMentionReplyHandlerReq {
|
||||||
|
PublishMentionReplyPath
|
||||||
|
PublishMentionReplyReq
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishMentionReplyData {
|
||||||
|
MediaID string `json:"media_id,omitempty"`
|
||||||
|
Permalink string `json:"permalink,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOwnPostFormulaReq {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
PersonaID string `json:"persona_id,optional"`
|
||||||
|
PostText string `json:"post_text,optional"`
|
||||||
|
TopicTag string `json:"topic_tag,optional"`
|
||||||
|
MediaType string `json:"media_type,optional"`
|
||||||
|
Timestamp string `json:"timestamp,optional"`
|
||||||
|
LikeCount int `json:"like_count,optional"`
|
||||||
|
ReplyCount int `json:"reply_count,optional"`
|
||||||
|
Views int `json:"views,optional"`
|
||||||
|
RepostCount int `json:"repost_count,optional"`
|
||||||
|
QuoteCount int `json:"quote_count,optional"`
|
||||||
|
Shares int `json:"shares,optional"`
|
||||||
|
Force bool `json:"force,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOwnPostFormulaHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
GenerateOwnPostFormulaReq
|
||||||
|
}
|
||||||
|
|
||||||
|
OwnPostFormulaReviewData {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
|
ReviewedAt int64 `json:"reviewed_at"`
|
||||||
|
FromCache bool `json:"from_cache,omitempty"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Wins []string `json:"wins"`
|
||||||
|
Improvements []string `json:"improvements"`
|
||||||
|
Formula string `json:"formula"`
|
||||||
|
PostTemplate string `json:"post_template"`
|
||||||
|
HookPattern string `json:"hook_pattern"`
|
||||||
|
Structure string `json:"structure"`
|
||||||
|
ReplicationTips []string `json:"replication_tips"`
|
||||||
|
Avoid []string `json:"avoid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOwnPostFormulaData {
|
||||||
|
OwnPostFormulaReviewData
|
||||||
|
}
|
||||||
|
|
||||||
|
ListOwnPostFormulasHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
}
|
||||||
|
|
||||||
|
ListOwnPostFormulasData {
|
||||||
|
List []OwnPostFormulaReviewData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadsDiagnosticsData {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
CheckedAt int64 `json:"checked_at"`
|
||||||
|
ApiConnected bool `json:"api_connected"`
|
||||||
|
TokenExpiresAt int64 `json:"token_expires_at,omitempty"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
Items []ThreadsAPISmokeTestItem `json:"items"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentFormulaSourceMetricsData {
|
||||||
|
LikeCount int `json:"like_count,omitempty"`
|
||||||
|
ReplyCount int `json:"reply_count,omitempty"`
|
||||||
|
Views int `json:"views,omitempty"`
|
||||||
|
RepostCount int `json:"repost_count,omitempty"`
|
||||||
|
QuoteCount int `json:"quote_count,omitempty"`
|
||||||
|
Shares int `json:"shares,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentFormulaData {
|
||||||
|
ID string `json:"id"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
SourceType string `json:"source_type"`
|
||||||
|
SourceRef string `json:"source_ref,omitempty"`
|
||||||
|
SourcePostText string `json:"source_post_text,omitempty"`
|
||||||
|
SourceAuthor string `json:"source_author,omitempty"`
|
||||||
|
SourcePermalink string `json:"source_permalink,omitempty"`
|
||||||
|
SourceMetrics *ContentFormulaSourceMetricsData `json:"source_metrics,omitempty"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Wins []string `json:"wins,omitempty"`
|
||||||
|
Improvements []string `json:"improvements,omitempty"`
|
||||||
|
Formula string `json:"formula"`
|
||||||
|
PostTemplate string `json:"post_template"`
|
||||||
|
HookPattern string `json:"hook_pattern"`
|
||||||
|
Structure string `json:"structure"`
|
||||||
|
ReplicationTips []string `json:"replication_tips,omitempty"`
|
||||||
|
Avoid []string `json:"avoid,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
UpdateAt int64 `json:"update_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListContentFormulasReq {
|
||||||
|
Page int64 `form:"page,optional"`
|
||||||
|
PageSize int64 `form:"pageSize,optional"`
|
||||||
|
SourceType string `form:"source_type,optional"`
|
||||||
|
Tag string `form:"tag,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ListContentFormulasHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ListContentFormulasReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ListContentFormulasData {
|
||||||
|
Pagination PaginationData `json:"pagination"`
|
||||||
|
List []ContentFormulaData `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentFormulaItemPath {
|
||||||
|
ID string `path:"id" validate:"required"`
|
||||||
|
FormulaID string `path:"formulaId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
GetContentFormulaHandlerReq {
|
||||||
|
ContentFormulaItemPath
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalyzeContentFormulaReq {
|
||||||
|
SourceType string `json:"source_type" validate:"required"`
|
||||||
|
PersonaID string `json:"persona_id,optional"`
|
||||||
|
PostText string `json:"post_text,optional"`
|
||||||
|
MediaID string `json:"media_id,optional"`
|
||||||
|
ScanPostID string `json:"scan_post_id,optional"`
|
||||||
|
Keyword string `json:"keyword,optional"`
|
||||||
|
SaveLabel string `json:"save_label,optional"`
|
||||||
|
AuthorName string `json:"author_name,optional"`
|
||||||
|
Permalink string `json:"permalink,optional"`
|
||||||
|
LikeCount int `json:"like_count,optional"`
|
||||||
|
ReplyCount int `json:"reply_count,optional"`
|
||||||
|
Views int `json:"views,optional"`
|
||||||
|
RepostCount int `json:"repost_count,optional"`
|
||||||
|
QuoteCount int `json:"quote_count,optional"`
|
||||||
|
Shares int `json:"shares,optional"`
|
||||||
|
ForceRefresh bool `json:"force_refresh,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalyzeContentFormulaHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
AnalyzeContentFormulaReq
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalyzeContentFormulaData {
|
||||||
|
Formula ContentFormulaData `json:"formula"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchContentFormulaPostsReq {
|
||||||
|
Keyword string `json:"keyword" validate:"required"`
|
||||||
|
SearchType string `json:"search_type,optional"`
|
||||||
|
Limit int `json:"limit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchContentFormulaPostsHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
SearchContentFormulaPostsReq
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentFormulaSearchPostData {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Permalink string `json:"permalink,omitempty"`
|
||||||
|
MediaID string `json:"media_id,omitempty"`
|
||||||
|
LikeCount int `json:"like_count"`
|
||||||
|
ReplyCount int `json:"reply_count"`
|
||||||
|
EngagementScore int `json:"engagement_score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchContentFormulaPostsData {
|
||||||
|
List []ContentFormulaSearchPostData `json:"list"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchContentFormulaReq {
|
||||||
|
Label *string `json:"label,optional"`
|
||||||
|
Formula *string `json:"formula,optional"`
|
||||||
|
PostTemplate *string `json:"post_template,optional"`
|
||||||
|
HookPattern *string `json:"hook_pattern,optional"`
|
||||||
|
Structure *string `json:"structure,optional"`
|
||||||
|
Tags []string `json:"tags,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchContentFormulaHandlerReq {
|
||||||
|
ContentFormulaItemPath
|
||||||
|
PatchContentFormulaReq
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteContentFormulaData {
|
||||||
|
FormulaID string `json:"formula_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportOwnPostFormulaReq {
|
||||||
|
MediaID string `json:"media_id" validate:"required"`
|
||||||
|
Label string `json:"label,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportOwnPostFormulaHandlerReq {
|
||||||
|
ThreadsAccountPath
|
||||||
|
ImportOwnPostFormulaReq
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server(
|
@server(
|
||||||
|
|
@ -159,6 +871,9 @@ service gateway {
|
||||||
@handler updateThreadsAccount
|
@handler updateThreadsAccount
|
||||||
patch /:id (UpdateThreadsAccountHandlerReq) returns (ThreadsAccountData)
|
patch /:id (UpdateThreadsAccountHandlerReq) returns (ThreadsAccountData)
|
||||||
|
|
||||||
|
@handler deleteThreadsAccount
|
||||||
|
delete /:id (ThreadsAccountPath) returns (DeleteThreadsAccountData)
|
||||||
|
|
||||||
@handler activateThreadsAccount
|
@handler activateThreadsAccount
|
||||||
post /:id/activate (ThreadsAccountPath)
|
post /:id/activate (ThreadsAccountPath)
|
||||||
|
|
||||||
|
|
@ -179,4 +894,135 @@ service gateway {
|
||||||
|
|
||||||
@handler listThreadsAccountAiProviderModels
|
@handler listThreadsAccountAiProviderModels
|
||||||
post /:id/ai-settings/providers/:provider/models (ThreadsAccountAiProviderModelsHandlerReq) returns (ThreadsAccountAiProviderModelsData)
|
post /:id/ai-settings/providers/:provider/models (ThreadsAccountAiProviderModelsHandlerReq) returns (ThreadsAccountAiProviderModelsData)
|
||||||
}
|
|
||||||
|
@handler startThreadsOauth
|
||||||
|
get /oauth/start (StartThreadsOAuthQuery) returns (StartThreadsOAuthData)
|
||||||
|
|
||||||
|
@handler listThreadsOauthLogs
|
||||||
|
get /oauth/logs returns (ListThreadsOAuthLogsData)
|
||||||
|
|
||||||
|
@handler getThreadsOauthConfig
|
||||||
|
get /oauth/config returns (ThreadsOAuthConfigData)
|
||||||
|
|
||||||
|
@handler disconnectThreadsApi
|
||||||
|
post /:id/api/disconnect (ThreadsAccountPath) returns (ThreadsAccountData)
|
||||||
|
|
||||||
|
@handler runThreadsApiSmokeTest
|
||||||
|
post /:id/api/smoke-test (ThreadsAccountPath) returns (RunThreadsAPISmokeTestData)
|
||||||
|
|
||||||
|
@handler runThreadsApiPlayground
|
||||||
|
post /:id/api/playground (RunThreadsAPIPlaygroundHandlerReq) returns (ThreadsAPIPlaygroundData)
|
||||||
|
|
||||||
|
@handler listThreadsPostPerformance
|
||||||
|
get /:id/post-performance (ListThreadsPostPerformanceHandlerReq) returns (ThreadsPostPerformanceData)
|
||||||
|
|
||||||
|
@handler createPublishQueueItem
|
||||||
|
post /:id/publish-queue (CreatePublishQueueHandlerReq) returns (PublishQueueItemData)
|
||||||
|
|
||||||
|
@handler listPublishQueueItems
|
||||||
|
get /:id/publish-queue (ListPublishQueueHandlerReq) returns (PublishQueueListData)
|
||||||
|
|
||||||
|
@handler getPublishQueueItem
|
||||||
|
get /:id/publish-queue/:qid (PublishQueueItemPath) returns (PublishQueueItemData)
|
||||||
|
|
||||||
|
@handler patchPublishQueueItem
|
||||||
|
patch /:id/publish-queue/:qid (PatchPublishQueueHandlerReq) returns (PublishQueueItemData)
|
||||||
|
|
||||||
|
@handler cancelPublishQueueItem
|
||||||
|
post /:id/publish-queue/:qid/cancel (PublishQueueItemPath) returns (PublishQueueItemData)
|
||||||
|
|
||||||
|
@handler deletePublishQueueItem
|
||||||
|
delete /:id/publish-queue/:qid (PublishQueueItemPath)
|
||||||
|
|
||||||
|
@handler listThreadsPublishHealth
|
||||||
|
get /:id/publish-health (ListThreadsPublishHealthHandlerReq) returns (ThreadsPublishHealthData)
|
||||||
|
|
||||||
|
@handler getPublishInventoryPolicy
|
||||||
|
get /:id/publish-inventory-policy (ThreadsAccountPath) returns (PublishInventoryPolicyData)
|
||||||
|
|
||||||
|
@handler upsertPublishInventoryPolicy
|
||||||
|
put /:id/publish-inventory-policy (UpsertPublishInventoryPolicyHandlerReq) returns (PublishInventoryPolicyData)
|
||||||
|
|
||||||
|
@handler startPublishInventoryRefillJob
|
||||||
|
post /:id/publish-inventory/refill-jobs (StartPublishInventoryRefillJobHandlerReq) returns (StartPublishInventoryRefillJobData)
|
||||||
|
|
||||||
|
@handler getPublishSlotInsights
|
||||||
|
get /:id/publish-slot-insights (ThreadsAccountPath) returns (PublishSlotInsightsData)
|
||||||
|
|
||||||
|
@handler getPublishGuardPolicy
|
||||||
|
get /:id/publish-guard-policy (ThreadsAccountPath) returns (PublishGuardPolicyData)
|
||||||
|
|
||||||
|
@handler upsertPublishGuardPolicy
|
||||||
|
put /:id/publish-guard-policy (UpsertPublishGuardPolicyHandlerReq) returns (PublishGuardPolicyData)
|
||||||
|
|
||||||
|
@handler resumePublishGuard
|
||||||
|
post /:id/publish-guard/resume (ThreadsAccountPath) returns (PublishGuardPolicyData)
|
||||||
|
|
||||||
|
@handler retryPublishQueueItem
|
||||||
|
post /:id/publish-queue/:qid/retry (PublishQueueItemPath) returns (PublishQueueItemData)
|
||||||
|
|
||||||
|
@handler listPublishQueueEvents
|
||||||
|
get /:id/publish-queue/:qid/events (PublishQueueItemPath) returns (PublishQueueEventsData)
|
||||||
|
|
||||||
|
@handler listPublishAlerts
|
||||||
|
get /:id/publish-alerts (ThreadsAccountPath) returns (PublishAlertsData)
|
||||||
|
|
||||||
|
@handler listPublishQueueRange
|
||||||
|
get /:id/publish-calendar (PublishQueueRangeHandlerReq) returns (PublishQueueListData)
|
||||||
|
|
||||||
|
@handler getThreadsDiagnostics
|
||||||
|
get /:id/diagnostics (ThreadsAccountPath) returns (ThreadsDiagnosticsData)
|
||||||
|
|
||||||
|
@handler generateOwnPostReplyDraft
|
||||||
|
post /:id/own-post-reply-draft (GenerateOwnPostReplyDraftHandlerReq) returns (GenerateOwnPostReplyDraftData)
|
||||||
|
|
||||||
|
@handler syncMentionInbox
|
||||||
|
post /:id/mention-inbox/sync (SyncMentionInboxHandlerReq) returns (SyncMentionInboxData)
|
||||||
|
|
||||||
|
@handler listMentionInbox
|
||||||
|
get /:id/mention-inbox (ListMentionInboxHandlerReq) returns (ListMentionInboxData)
|
||||||
|
|
||||||
|
@handler publishMentionReply
|
||||||
|
post /:id/mention-inbox/:media_id/reply (PublishMentionReplyHandlerReq) returns (PublishMentionReplyData)
|
||||||
|
|
||||||
|
@handler generateOwnPostFormula
|
||||||
|
post /:id/own-post-formula (GenerateOwnPostFormulaHandlerReq) returns (GenerateOwnPostFormulaData)
|
||||||
|
|
||||||
|
@handler listOwnPostFormulas
|
||||||
|
get /:id/own-post-formulas (ListOwnPostFormulasHandlerReq) returns (ListOwnPostFormulasData)
|
||||||
|
|
||||||
|
@handler getPublishDashboardSummary
|
||||||
|
get /publish-dashboard-summary returns (PublishDashboardSummaryData)
|
||||||
|
|
||||||
|
@handler listContentFormulas
|
||||||
|
get /:id/content-formulas (ListContentFormulasHandlerReq) returns (ListContentFormulasData)
|
||||||
|
|
||||||
|
@handler getContentFormula
|
||||||
|
get /:id/content-formulas/:formulaId (GetContentFormulaHandlerReq) returns (ContentFormulaData)
|
||||||
|
|
||||||
|
@handler analyzeContentFormula
|
||||||
|
post /:id/content-formulas/analyze (AnalyzeContentFormulaHandlerReq) returns (AnalyzeContentFormulaData)
|
||||||
|
|
||||||
|
@handler searchContentFormulaPosts
|
||||||
|
post /:id/content-formulas/search-posts (SearchContentFormulaPostsHandlerReq) returns (SearchContentFormulaPostsData)
|
||||||
|
|
||||||
|
@handler patchContentFormula
|
||||||
|
patch /:id/content-formulas/:formulaId (PatchContentFormulaHandlerReq) returns (ContentFormulaData)
|
||||||
|
|
||||||
|
@handler deleteContentFormula
|
||||||
|
delete /:id/content-formulas/:formulaId (GetContentFormulaHandlerReq) returns (DeleteContentFormulaData)
|
||||||
|
|
||||||
|
@handler importOwnPostToContentFormula
|
||||||
|
post /:id/content-formulas/import-own-post (ImportOwnPostFormulaHandlerReq) returns (AnalyzeContentFormulaData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@server(
|
||||||
|
group: threads_account
|
||||||
|
prefix: /api/v1/threads-accounts
|
||||||
|
tags: "ThreadsAccount"
|
||||||
|
summary: "Threads OAuth callback (public redirect from Meta)."
|
||||||
|
)
|
||||||
|
service gateway {
|
||||||
|
@handler threadsOauthCallback
|
||||||
|
get /oauth/callback (ThreadsOAuthCallbackQuery)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
|
github.com/go-shiori/go-readability v0.0.0-20251205110129-5db1dc9836f0
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
|
|
@ -27,7 +28,6 @@ require (
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
|
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
|
||||||
github.com/go-shiori/go-readability v0.0.0-20251205110129-5db1dc9836f0 // indirect
|
|
||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||||
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,18 @@ func EnsureAdminMember(ctx context.Context, repo domrepo.Repository, opts AdminO
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
existing.Roles = []string{"admin"}
|
existing.Roles = []string{"admin"}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(existing.PasswordHash), []byte(opts.Password)); err != nil {
|
||||||
|
hash, hashErr := bcrypt.GenerateFromPassword([]byte(opts.Password), bcrypt.DefaultCost)
|
||||||
|
if hashErr != nil {
|
||||||
|
return nil, false, app.For(code.Member).SysInternal("rehash password failed").WithCause(hashErr)
|
||||||
|
}
|
||||||
|
if updateErr := repo.UpdatePassword(ctx, tenantID, existing.UID, string(hash)); updateErr != nil {
|
||||||
|
return nil, false, updateErr
|
||||||
|
}
|
||||||
|
existing.PasswordHash = string(hash)
|
||||||
|
}
|
||||||
|
|
||||||
return existing, false, nil
|
return existing, false, nil
|
||||||
}
|
}
|
||||||
if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound {
|
if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,16 @@ func (m *memoryMemberRepo) SetActiveThreadsAccountID(_ context.Context, tenantID
|
||||||
return app.For(code.Member).ResNotFound("member not found")
|
return app.For(code.Member).ResNotFound("member not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *memoryMemberRepo) UpdatePassword(_ context.Context, tenantID, uid, passwordHash string) error {
|
||||||
|
for _, item := range m.byEmail {
|
||||||
|
if item.TenantID == tenantID && item.UID == uid {
|
||||||
|
item.PasswordHash = passwordHash
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return app.For(code.Member).ResNotFound("member not found")
|
||||||
|
}
|
||||||
|
|
||||||
func (m *memoryMemberRepo) SetRoles(_ context.Context, tenantID, uid string, roles []string) error {
|
func (m *memoryMemberRepo) SetRoles(_ context.Context, tenantID, uid string, roles []string) error {
|
||||||
for _, item := range m.byEmail {
|
for _, item := range m.byEmail {
|
||||||
if item.TenantID == tenantID && item.UID == uid {
|
if item.TenantID == tenantID && item.UID == uid {
|
||||||
|
|
@ -123,4 +133,72 @@ func TestEnsureAdminMemberUpgradesExisting(t *testing.T) {
|
||||||
if len(member.Roles) != 1 || member.Roles[0] != "admin" {
|
if len(member.Roles) != 1 || member.Roles[0] != "admin" {
|
||||||
t.Fatalf("roles=%v", member.Roles)
|
t.Fatalf("roles=%v", member.Roles)
|
||||||
}
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("Admin-Pass-1!")); err != nil {
|
||||||
|
t.Fatalf("password should have been synced: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureAdminMemberSyncsPasswordWhenChanged(t *testing.T) {
|
||||||
|
oldHash, err := bcrypt.GenerateFromPassword([]byte("Old-Pass-1!"), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{
|
||||||
|
memberKey("default", "admin@haixun.local"): {
|
||||||
|
TenantID: "default",
|
||||||
|
UID: "uid-1",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Roles: []string{"admin"},
|
||||||
|
PasswordHash: string(oldHash),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
_, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{
|
||||||
|
TenantID: "default",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Password: "New-Pass-2@",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnsureAdminMember: %v", err)
|
||||||
|
}
|
||||||
|
if created {
|
||||||
|
t.Fatal("expected created=false")
|
||||||
|
}
|
||||||
|
member := repo.byEmail[memberKey("default", "admin@haixun.local")]
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("Old-Pass-1!")); err == nil {
|
||||||
|
t.Fatal("password should have been updated, but old hash still matches")
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("New-Pass-2@")); err != nil {
|
||||||
|
t.Fatalf("new password hash does not match: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureAdminMemberKeepsPasswordWhenSame(t *testing.T) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte("Same-Pass-1!"), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{
|
||||||
|
memberKey("default", "admin@haixun.local"): {
|
||||||
|
TenantID: "default",
|
||||||
|
UID: "uid-1",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Roles: []string{"admin"},
|
||||||
|
PasswordHash: string(hash),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
_, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{
|
||||||
|
TenantID: "default",
|
||||||
|
Email: "admin@haixun.local",
|
||||||
|
Password: "Same-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 member.PasswordHash != string(hash) {
|
||||||
|
t.Fatal("password hash should not change when password is the same")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadInfraEnv reads local env files into process env (only keys not already set).
|
||||||
|
// Search order: HAIXUN_ENV_FILE, deploy/.env.dev (for native dev run).
|
||||||
|
func LoadInfraEnv() {
|
||||||
|
if path := strings.TrimSpace(os.Getenv("HAIXUN_ENV_FILE")); path != "" {
|
||||||
|
_ = loadEnvFile(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
candidates := []string{
|
||||||
|
filepath.Join(cwd, "deploy", ".env.dev"),
|
||||||
|
filepath.Join(cwd, "..", "deploy", ".env.dev"),
|
||||||
|
filepath.Join(cwd, "..", "..", "deploy", ".env.dev"),
|
||||||
|
}
|
||||||
|
for _, path := range candidates {
|
||||||
|
if loadEnvFile(path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEnvFile(path string) bool {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value, ok := strings.Cut(line, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := os.LookupEnv(key); exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) >= 2 {
|
||||||
|
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
||||||
|
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadEnvFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, ".env")
|
||||||
|
content := `# comment
|
||||||
|
HAIXUN_JWT_ACCESS_SECRET=from-file
|
||||||
|
THREADS_APP_ID="1566509781118049"
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("HAIXUN_JWT_REFRESH_SECRET", "preset")
|
||||||
|
unset := []string{"HAIXUN_JWT_ACCESS_SECRET", "THREADS_APP_ID"}
|
||||||
|
for _, key := range unset {
|
||||||
|
_ = os.Unsetenv(key)
|
||||||
|
}
|
||||||
|
if !loadEnvFile(path) {
|
||||||
|
t.Fatal("loadEnvFile returned false")
|
||||||
|
}
|
||||||
|
if got := os.Getenv("HAIXUN_JWT_ACCESS_SECRET"); got != "from-file" {
|
||||||
|
t.Fatalf("access secret = %q", got)
|
||||||
|
}
|
||||||
|
if got := os.Getenv("HAIXUN_JWT_REFRESH_SECRET"); got != "preset" {
|
||||||
|
t.Fatalf("refresh secret should stay preset, got %q", got)
|
||||||
|
}
|
||||||
|
if got := os.Getenv("THREADS_APP_ID"); got != "1566509781118049" {
|
||||||
|
t.Fatalf("threads app id = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,19 +8,31 @@ import (
|
||||||
libcrypto "haixun-backend/internal/library/crypto"
|
libcrypto "haixun-backend/internal/library/crypto"
|
||||||
libmongo "haixun-backend/internal/library/mongo"
|
libmongo "haixun-backend/internal/library/mongo"
|
||||||
brandrepo "haixun-backend/internal/model/brand/repository"
|
brandrepo "haixun-backend/internal/model/brand/repository"
|
||||||
|
contentformularepo "haixun-backend/internal/model/content_formula/repository"
|
||||||
cmatrixrepo "haixun-backend/internal/model/content_matrix/repository"
|
cmatrixrepo "haixun-backend/internal/model/content_matrix/repository"
|
||||||
|
contentopsrepo "haixun-backend/internal/model/content_ops/repository"
|
||||||
copydraftrepo "haixun-backend/internal/model/copy_draft/repository"
|
copydraftrepo "haixun-backend/internal/model/copy_draft/repository"
|
||||||
copymissionrepo "haixun-backend/internal/model/copy_mission/repository"
|
copymissionrepo "haixun-backend/internal/model/copy_mission/repository"
|
||||||
|
crmcontactrepo "haixun-backend/internal/model/crm_contact/repository"
|
||||||
jobrepo "haixun-backend/internal/model/job/repository"
|
jobrepo "haixun-backend/internal/model/job/repository"
|
||||||
|
jobusecase "haixun-backend/internal/model/job/usecase"
|
||||||
kgrepo "haixun-backend/internal/model/knowledge_graph/repository"
|
kgrepo "haixun-backend/internal/model/knowledge_graph/repository"
|
||||||
memberrepo "haixun-backend/internal/model/member/repository"
|
memberrepo "haixun-backend/internal/model/member/repository"
|
||||||
|
mentioninboxrepo "haixun-backend/internal/model/mention_inbox/repository"
|
||||||
outreachdraftrepo "haixun-backend/internal/model/outreach_draft/repository"
|
outreachdraftrepo "haixun-backend/internal/model/outreach_draft/repository"
|
||||||
|
ownpostformularepo "haixun-backend/internal/model/own_post_formula/repository"
|
||||||
permissionrepo "haixun-backend/internal/model/permission/repository"
|
permissionrepo "haixun-backend/internal/model/permission/repository"
|
||||||
permissionuc "haixun-backend/internal/model/permission/usecase"
|
permissionuc "haixun-backend/internal/model/permission/usecase"
|
||||||
personarepo "haixun-backend/internal/model/persona/repository"
|
personarepo "haixun-backend/internal/model/persona/repository"
|
||||||
placementtopicrepo "haixun-backend/internal/model/placement_topic/repository"
|
placementtopicrepo "haixun-backend/internal/model/placement_topic/repository"
|
||||||
|
publishanalyticsrepo "haixun-backend/internal/model/publish_analytics/repository"
|
||||||
|
publishguardrepo "haixun-backend/internal/model/publish_guard/repository"
|
||||||
|
publishinventoryrepo "haixun-backend/internal/model/publish_inventory/repository"
|
||||||
|
publishqueuerepo "haixun-backend/internal/model/publish_queue/repository"
|
||||||
|
publishqueueeventrepo "haixun-backend/internal/model/publish_queue_event/repository"
|
||||||
scanpostrepo "haixun-backend/internal/model/scan_post/repository"
|
scanpostrepo "haixun-backend/internal/model/scan_post/repository"
|
||||||
settingrepo "haixun-backend/internal/model/setting/repository"
|
settingrepo "haixun-backend/internal/model/setting/repository"
|
||||||
|
stylepresetrepo "haixun-backend/internal/model/style_preset/repository"
|
||||||
threadsaccountrepo "haixun-backend/internal/model/threads_account/repository"
|
threadsaccountrepo "haixun-backend/internal/model/threads_account/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -97,6 +109,17 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport
|
||||||
{"placement_topics", placementtopicrepo.NewMongoRepository(db).EnsureIndexes},
|
{"placement_topics", placementtopicrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
{"threads_accounts", threadsaccountrepo.NewMongoRepository(db).EnsureIndexes},
|
{"threads_accounts", threadsaccountrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
{"threads_account_secrets", threadsaccountrepo.NewSecretsMongoRepository(db, secretsCipher).EnsureIndexes},
|
{"threads_account_secrets", threadsaccountrepo.NewSecretsMongoRepository(db, secretsCipher).EnsureIndexes},
|
||||||
|
{"publish_analytics", publishanalyticsrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"publish_queue", publishqueuerepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"publish_inventory", publishinventoryrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"publish_guard", publishguardrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"publish_queue_events", publishqueueeventrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"style_presets", stylepresetrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"own_post_formulas", ownpostformularepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"content_formulas", contentformularepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"content_ops", contentopsrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"mention_inbox", mentioninboxrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
|
{"crm_contacts", crmcontactrepo.NewMongoRepository(db).EnsureIndexes},
|
||||||
}
|
}
|
||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
if err := repo.fn(ctx); err != nil {
|
if err := repo.fn(ctx); err != nil {
|
||||||
|
|
@ -105,6 +128,26 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport
|
||||||
}
|
}
|
||||||
report.IndexesEnsured = true
|
report.IndexesEnsured = true
|
||||||
|
|
||||||
|
jobUseCase := jobusecase.NewUseCase(jobTemplateRepository, jobRunRepository, jobScheduleRepository, jobEventRepository, nil)
|
||||||
|
jobTemplates := []struct {
|
||||||
|
name string
|
||||||
|
fn func(context.Context) error
|
||||||
|
}{
|
||||||
|
{"demo", jobUseCase.EnsureDemoTemplate},
|
||||||
|
{"style_8d", jobUseCase.EnsureStyle8DTemplate},
|
||||||
|
{"expand_graph", jobUseCase.EnsureExpandGraphTemplate},
|
||||||
|
{"placement_scan", jobUseCase.EnsurePlacementScanTemplate},
|
||||||
|
{"scan_viral", jobUseCase.EnsureScanViralTemplate},
|
||||||
|
{"generate_outreach_draft", jobUseCase.EnsureGenerateOutreachDraftTemplate},
|
||||||
|
{"refresh_threads_token", jobUseCase.EnsureRefreshThreadsTokenTemplate},
|
||||||
|
{"publish_analytics", jobUseCase.EnsurePublishAnalyticsTemplate},
|
||||||
|
}
|
||||||
|
for _, template := range jobTemplates {
|
||||||
|
if err := template.fn(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("seed %s job template: %w", template.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
permissionUseCase := permissionuc.NewUseCase(permissionRepository, rolePermissionRepository)
|
permissionUseCase := permissionuc.NewUseCase(permissionRepository, rolePermissionRepository)
|
||||||
if err := permissionUseCase.EnsureDefaultPermissions(ctx); err != nil {
|
if err := permissionUseCase.EnsureDefaultPermissions(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("seed permissions catalog: %w", err)
|
return nil, fmt.Errorf("seed permissions catalog: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,37 @@ type BraveConf struct {
|
||||||
APIKey string `json:",optional"`
|
APIKey string `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ThreadsOAuthConf struct {
|
||||||
|
AppID string `json:",optional"`
|
||||||
|
AppSecret string `json:",optional"`
|
||||||
|
RedirectURI string `json:",optional"`
|
||||||
|
SuccessRedirect string `json:",optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThreadsTokenRefreshConf struct {
|
||||||
|
Enabled bool `json:",default=true"`
|
||||||
|
IntervalSeconds int `json:",default=3600"`
|
||||||
|
LeadDays int `json:",default=7"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThreadsPublishQueueConf struct {
|
||||||
|
Enabled bool `json:",default=true"`
|
||||||
|
IntervalSeconds int `json:",default=60"`
|
||||||
|
BatchSize int `json:",default=20"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
rest.RestConf
|
rest.RestConf
|
||||||
Mongo MongoConf `json:",optional"`
|
Mongo MongoConf `json:",optional"`
|
||||||
Redis RedisConf `json:",optional"`
|
Redis RedisConf `json:",optional"`
|
||||||
Auth AuthConf `json:",optional"`
|
Auth AuthConf `json:",optional"`
|
||||||
Secrets SecretsConf `json:",optional"`
|
Secrets SecretsConf `json:",optional"`
|
||||||
InternalWorker InternalWorkerConf `json:",optional"`
|
InternalWorker InternalWorkerConf `json:",optional"`
|
||||||
JobWorker JobWorkerConf `json:",optional"`
|
JobWorker JobWorkerConf `json:",optional"`
|
||||||
JobScheduler JobSchedulerConf `json:",optional"`
|
JobScheduler JobSchedulerConf `json:",optional"`
|
||||||
JobReaper JobReaperConf `json:",optional"`
|
JobReaper JobReaperConf `json:",optional"`
|
||||||
Brave BraveConf `json:",optional"`
|
Brave BraveConf `json:",optional"`
|
||||||
|
ThreadsOAuth ThreadsOAuthConf `json:",optional"`
|
||||||
|
ThreadsTokenRefresh ThreadsTokenRefreshConf `json:",optional"`
|
||||||
|
ThreadsPublishQueue ThreadsPublishQueueConf `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package scan_post
|
package brand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"haixun-backend/internal/logic/scan_post"
|
"haixun-backend/internal/logic/brand"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
@ -11,9 +11,9 @@ import (
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LookalikeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func UpdateOutreachDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var req types.LookalikeReq
|
var req types.UpdateOutreachDraftHandlerReq
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
return
|
return
|
||||||
|
|
@ -22,8 +22,9 @@ func LookalikeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l := scan_post.NewLookalikeLogic(r.Context(), svcCtx)
|
|
||||||
data, err := l.Lookalike(&req)
|
l := brand.NewUpdateOutreachDraftLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpdateOutreachDraft(&req)
|
||||||
response.Write(r.Context(), w, data, err)
|
response.Write(r.Context(), w, data, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func CreateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DeleteCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func DeleteCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateCopyMissionMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func GenerateCopyMissionMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func GetCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func GetCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
package copy_mission
|
package copy_mission
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func InspireCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func InspireCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListCopyMissionCopyDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func ListCopyMissionCopyDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListCopyMissionScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func ListCopyMissionScanPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListCopyMissionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func ListCopyMissionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package copy_mission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ScheduleCopyMissionDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.ScheduleCopyMissionDraftsHandlerReq
|
||||||
|
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.NewScheduleCopyMissionDraftsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ScheduleCopyMissionDrafts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartCopyMissionAnalyzeJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func StartCopyMissionAnalyzeJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
package copy_mission
|
package copy_mission
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartCopyMissionCopyDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func StartCopyMissionCopyDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
package copy_mission
|
package copy_mission
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartCopyMissionMatrixJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func StartCopyMissionMatrixJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartCopyMissionScanJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func StartCopyMissionScanJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func UpdateCopyMissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ package copy_mission
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
"haixun-backend/internal/logic/copy_mission"
|
"haixun-backend/internal/logic/copy_mission"
|
||||||
"haixun-backend/internal/response"
|
"haixun-backend/internal/response"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
"haixun-backend/internal/types"
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpsertCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
func UpsertCopyMissionScanScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package crm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"haixun-backend/internal/logic/crm"
|
|
||||||
"haixun-backend/internal/response"
|
|
||||||
"haixun-backend/internal/svc"
|
|
||||||
"haixun-backend/internal/types"
|
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/rest/httpx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DeleteCrmContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req types.CrmContactIDPath
|
|
||||||
if err := httpx.Parse(r, &req); err != nil {
|
|
||||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l := crm.NewDeleteCrmContactLogic(r.Context(), svcCtx)
|
|
||||||
err := l.DeleteCrmContact(&req)
|
|
||||||
response.Write(r.Context(), w, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateContentPlanHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateContentPlanHandlerReq
|
||||||
|
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 := persona.NewCreateContentPlanLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateContentPlan(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateFeedbackEventHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateFeedbackEventHandlerReq
|
||||||
|
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 := persona.NewCreateFeedbackEventLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateFeedbackEvent(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateFormulaPoolHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateFormulaPoolHandlerReq
|
||||||
|
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 := persona.NewCreateFormulaPoolLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateFormulaPool(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateKnowledgeSourceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreateKnowledgeSourceHandlerReq
|
||||||
|
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 := persona.NewCreateKnowledgeSourceLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreateKnowledgeSource(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteStylePresetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StylePresetPath
|
||||||
|
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 := persona.NewDeleteStylePresetLogic(r.Context(), svcCtx)
|
||||||
|
err := l.DeleteStylePreset(&req)
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateFromContentFormulaHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GenerateFromContentFormulaHandlerReq
|
||||||
|
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 := persona.NewGenerateFromContentFormulaLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GenerateFromContentFormula(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GeneratePersonaTopicMatrixHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GeneratePersonaTopicMatrixHandlerReq
|
||||||
|
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 := persona.NewGeneratePersonaTopicMatrixLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.GeneratePersonaTopicMatrix(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListContentPlansHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaPath
|
||||||
|
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 := persona.NewListContentPlansLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListContentPlans(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListFormulaPoolsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaPath
|
||||||
|
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 := persona.NewListFormulaPoolsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListFormulaPools(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListKnowledgeChunksHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaPath
|
||||||
|
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 := persona.NewListKnowledgeChunksLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListKnowledgeChunks(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListKnowledgeSourcesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaPath
|
||||||
|
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 := persona.NewListKnowledgeSourcesLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListKnowledgeSources(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListPersonaContentInboxHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.ListPersonaContentInboxHandlerReq
|
||||||
|
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 := persona.NewListPersonaContentInboxLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListPersonaContentInbox(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListStylePresetsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaPath
|
||||||
|
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 := persona.NewListStylePresetsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListStylePresets(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListTopicCandidatesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PersonaPath
|
||||||
|
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 := persona.NewListTopicCandidatesLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.ListTopicCandidates(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrunePersonaCopyDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PrunePersonaCopyDraftsHandlerReq
|
||||||
|
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 := persona.NewPrunePersonaCopyDraftsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.PrunePersonaCopyDrafts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SchedulePersonaCopyDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.SchedulePersonaCopyDraftHandlerReq
|
||||||
|
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 := persona.NewSchedulePersonaCopyDraftLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.SchedulePersonaCopyDraft(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SchedulePersonaDraftsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.SchedulePersonaDraftsHandlerReq
|
||||||
|
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 := persona.NewSchedulePersonaDraftsLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.SchedulePersonaDrafts(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartPersonaFormulaDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartPersonaFormulaDraftJobHandlerReq
|
||||||
|
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 := persona.NewStartPersonaFormulaDraftJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartPersonaFormulaDraftJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartPersonaRewriteDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartPersonaRewriteDraftJobHandlerReq
|
||||||
|
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 := persona.NewStartPersonaRewriteDraftJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartPersonaRewriteDraftJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartPersonaStyleAnalysisFromTextHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartPersonaStyleAnalysisFromTextHandlerReq
|
||||||
|
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 := persona.NewStartPersonaStyleAnalysisFromTextLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartPersonaStyleAnalysisFromText(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartPersonaTopicMatrixJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartPersonaTopicMatrixJobHandlerReq
|
||||||
|
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 := persona.NewStartPersonaTopicMatrixJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartPersonaTopicMatrixJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateContentPlanHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpdateContentPlanHandlerReq
|
||||||
|
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 := persona.NewUpdateContentPlanLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpdateContentPlan(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package persona
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/persona"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpsertStylePresetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpsertStylePresetHandlerReq
|
||||||
|
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 := persona.NewUpsertStylePresetLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpsertStylePreset(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package placement_topic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/placement_topic"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartPlacementTopicOutreachDraftJobHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.StartPlacementTopicOutreachDraftJobHandlerReq
|
||||||
|
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 := placement_topic.NewStartPlacementTopicOutreachDraftJobLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.StartPlacementTopicOutreachDraftJob(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package placement_topic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/placement_topic"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdatePlacementTopicOutreachDraftHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpdatePlacementTopicOutreachDraftHandlerReq
|
||||||
|
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 := placement_topic.NewUpdatePlacementTopicOutreachDraftLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.UpdatePlacementTopicOutreachDraft(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/handler/static"
|
||||||
|
threads_account "haixun-backend/internal/handler/threads_account"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterExtraHandlers mounts routes that are not generated by goctl.
|
||||||
|
func RegisterExtraHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
|
server.AddRoutes(
|
||||||
|
[]rest.Route{
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/downloads/haixun-threads-sync.zip",
|
||||||
|
Handler: static.ExtensionDownloadHandler(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rest.WithPrefix("/api/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Legacy Meta redirect URI (deploy/.env.dev 曾誤設為 /api/threads/oauth/callback)。
|
||||||
|
server.AddRoutes(
|
||||||
|
[]rest.Route{
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/oauth/callback",
|
||||||
|
Handler: threads_account.ThreadsOauthCallbackHandler(serverCtx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rest.WithPrefix("/api/threads"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -10,14 +10,12 @@ import (
|
||||||
auth "haixun-backend/internal/handler/auth"
|
auth "haixun-backend/internal/handler/auth"
|
||||||
brand "haixun-backend/internal/handler/brand"
|
brand "haixun-backend/internal/handler/brand"
|
||||||
copy_mission "haixun-backend/internal/handler/copy_mission"
|
copy_mission "haixun-backend/internal/handler/copy_mission"
|
||||||
crm "haixun-backend/internal/handler/crm"
|
|
||||||
job "haixun-backend/internal/handler/job"
|
job "haixun-backend/internal/handler/job"
|
||||||
member "haixun-backend/internal/handler/member"
|
member "haixun-backend/internal/handler/member"
|
||||||
normal "haixun-backend/internal/handler/normal"
|
normal "haixun-backend/internal/handler/normal"
|
||||||
permission "haixun-backend/internal/handler/permission"
|
permission "haixun-backend/internal/handler/permission"
|
||||||
persona "haixun-backend/internal/handler/persona"
|
persona "haixun-backend/internal/handler/persona"
|
||||||
placement_topic "haixun-backend/internal/handler/placement_topic"
|
placement_topic "haixun-backend/internal/handler/placement_topic"
|
||||||
scan_post "haixun-backend/internal/handler/scan_post"
|
|
||||||
setting "haixun-backend/internal/handler/setting"
|
setting "haixun-backend/internal/handler/setting"
|
||||||
threads_account "haixun-backend/internal/handler/threads_account"
|
threads_account "haixun-backend/internal/handler/threads_account"
|
||||||
"haixun-backend/internal/svc"
|
"haixun-backend/internal/svc"
|
||||||
|
|
@ -164,6 +162,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id/knowledge-graph/nodes",
|
Path: "/:id/knowledge-graph/nodes",
|
||||||
Handler: brand.PatchKnowledgeGraphNodesHandler(serverCtx),
|
Handler: brand.PatchKnowledgeGraphNodesHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPatch,
|
||||||
|
Path: "/:id/outreach-drafts/:draftId",
|
||||||
|
Handler: brand.UpdateOutreachDraftHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/outreach-drafts/generate",
|
Path: "/:id/outreach-drafts/generate",
|
||||||
|
|
@ -278,6 +281,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:personaId/copy-missions/:id/copy-drafts",
|
Path: "/:personaId/copy-missions/:id/copy-drafts",
|
||||||
Handler: copy_mission.ListCopyMissionCopyDraftsHandler(serverCtx),
|
Handler: copy_mission.ListCopyMissionCopyDraftsHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:personaId/copy-missions/:id/copy-drafts/schedule",
|
||||||
|
Handler: copy_mission.ScheduleCopyMissionDraftsHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/:personaId/copy-missions/:id/knowledge-graph",
|
Path: "/:personaId/copy-missions/:id/knowledge-graph",
|
||||||
|
|
@ -574,6 +582,31 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id",
|
Path: "/:id",
|
||||||
Handler: persona.DeletePersonaHandler(serverCtx),
|
Handler: persona.DeletePersonaHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/content-formulas/:formulaId/generate",
|
||||||
|
Handler: persona.GenerateFromContentFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/content-inbox",
|
||||||
|
Handler: persona.ListPersonaContentInboxHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/content-plans",
|
||||||
|
Handler: persona.ListContentPlansHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/content-plans",
|
||||||
|
Handler: persona.CreateContentPlanHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPatch,
|
||||||
|
Path: "/:id/content-plans/:contentPlanId",
|
||||||
|
Handler: persona.UpdateContentPlanHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/:id/copy-drafts",
|
Path: "/:id/copy-drafts",
|
||||||
|
|
@ -594,16 +627,106 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id/copy-drafts/:draftId/publish",
|
Path: "/:id/copy-drafts/:draftId/publish",
|
||||||
Handler: persona.PublishPersonaCopyDraftHandler(serverCtx),
|
Handler: persona.PublishPersonaCopyDraftHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/copy-drafts/:draftId/schedule",
|
||||||
|
Handler: persona.SchedulePersonaCopyDraftHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/copy-drafts/generate",
|
Path: "/:id/copy-drafts/generate",
|
||||||
Handler: persona.GeneratePersonaCopyDraftHandler(serverCtx),
|
Handler: persona.GeneratePersonaCopyDraftHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/copy-drafts/prune",
|
||||||
|
Handler: persona.PrunePersonaCopyDraftsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/copy-drafts/schedule",
|
||||||
|
Handler: persona.SchedulePersonaDraftsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/feedback-events",
|
||||||
|
Handler: persona.CreateFeedbackEventHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/formula-draft/jobs",
|
||||||
|
Handler: persona.StartPersonaFormulaDraftJobHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/formula-pools",
|
||||||
|
Handler: persona.ListFormulaPoolsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/formula-pools",
|
||||||
|
Handler: persona.CreateFormulaPoolHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/knowledge-chunks",
|
||||||
|
Handler: persona.ListKnowledgeChunksHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/knowledge-sources",
|
||||||
|
Handler: persona.CreateKnowledgeSourceHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/knowledge-sources",
|
||||||
|
Handler: persona.ListKnowledgeSourcesHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/rewrite-draft/jobs",
|
||||||
|
Handler: persona.StartPersonaRewriteDraftJobHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/style-analysis",
|
Path: "/:id/style-analysis",
|
||||||
Handler: persona.StartPersonaStyleAnalysisHandler(serverCtx),
|
Handler: persona.StartPersonaStyleAnalysisHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/style-analysis-from-text",
|
||||||
|
Handler: persona.StartPersonaStyleAnalysisFromTextHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/style-presets",
|
||||||
|
Handler: persona.ListStylePresetsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPut,
|
||||||
|
Path: "/:id/style-presets/:presetId",
|
||||||
|
Handler: persona.UpsertStylePresetHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
Path: "/:id/style-presets/:presetId",
|
||||||
|
Handler: persona.DeleteStylePresetHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/topic-candidates",
|
||||||
|
Handler: persona.ListTopicCandidatesHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/topic-matrix/generate",
|
||||||
|
Handler: persona.GeneratePersonaTopicMatrixHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/topic-matrix/jobs",
|
||||||
|
Handler: persona.StartPersonaTopicMatrixJobHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/viral-scan-jobs",
|
Path: "/:id/viral-scan-jobs",
|
||||||
|
|
@ -673,6 +796,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id/knowledge-graph/nodes",
|
Path: "/:id/knowledge-graph/nodes",
|
||||||
Handler: placement_topic.PatchPlacementTopicGraphNodesHandler(serverCtx),
|
Handler: placement_topic.PatchPlacementTopicGraphNodesHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/outreach-draft-jobs",
|
||||||
|
Handler: placement_topic.StartPlacementTopicOutreachDraftJobHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPatch,
|
||||||
|
Path: "/:id/outreach-drafts/:draftId",
|
||||||
|
Handler: placement_topic.UpdatePlacementTopicOutreachDraftHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/outreach-drafts/generate",
|
Path: "/:id/outreach-drafts/generate",
|
||||||
|
|
@ -752,54 +885,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
rest.WithPrefix("/api/v1/settings"),
|
rest.WithPrefix("/api/v1/settings"),
|
||||||
)
|
)
|
||||||
|
|
||||||
server.AddRoutes(
|
|
||||||
rest.WithMiddlewares(
|
|
||||||
[]rest.Middleware{serverCtx.AuthJWT},
|
|
||||||
[]rest.Route{
|
|
||||||
{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
Path: "/",
|
|
||||||
Handler: crm.ListCrmContactsHandler(serverCtx),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Method: http.MethodPost,
|
|
||||||
Path: "/",
|
|
||||||
Handler: crm.CreateCrmContactHandler(serverCtx),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Method: http.MethodGet,
|
|
||||||
Path: "/:id",
|
|
||||||
Handler: crm.GetCrmContactHandler(serverCtx),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Method: http.MethodPatch,
|
|
||||||
Path: "/:id",
|
|
||||||
Handler: crm.UpdateCrmContactHandler(serverCtx),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Method: http.MethodDelete,
|
|
||||||
Path: "/:id",
|
|
||||||
Handler: crm.DeleteCrmContactHandler(serverCtx),
|
|
||||||
},
|
|
||||||
}...,
|
|
||||||
),
|
|
||||||
rest.WithPrefix("/api/v1/crm/contacts"),
|
|
||||||
)
|
|
||||||
|
|
||||||
server.AddRoutes(
|
|
||||||
rest.WithMiddlewares(
|
|
||||||
[]rest.Middleware{serverCtx.AuthJWT},
|
|
||||||
[]rest.Route{
|
|
||||||
{
|
|
||||||
Method: http.MethodPost,
|
|
||||||
Path: "/lookalike",
|
|
||||||
Handler: scan_post.LookalikeHandler(serverCtx),
|
|
||||||
},
|
|
||||||
}...,
|
|
||||||
),
|
|
||||||
rest.WithPrefix("/api/v1/scan-post"),
|
|
||||||
)
|
|
||||||
|
|
||||||
server.AddRoutes(
|
server.AddRoutes(
|
||||||
rest.WithMiddlewares(
|
rest.WithMiddlewares(
|
||||||
[]rest.Middleware{serverCtx.AuthJWT},
|
[]rest.Middleware{serverCtx.AuthJWT},
|
||||||
|
|
@ -824,6 +909,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id",
|
Path: "/:id",
|
||||||
Handler: threads_account.UpdateThreadsAccountHandler(serverCtx),
|
Handler: threads_account.UpdateThreadsAccountHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
Path: "/:id",
|
||||||
|
Handler: threads_account.DeleteThreadsAccountHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/activate",
|
Path: "/:id/activate",
|
||||||
|
|
@ -844,6 +934,21 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id/ai-settings/providers/:provider/models",
|
Path: "/:id/ai-settings/providers/:provider/models",
|
||||||
Handler: threads_account.ListThreadsAccountAiProviderModelsHandler(serverCtx),
|
Handler: threads_account.ListThreadsAccountAiProviderModelsHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/api/disconnect",
|
||||||
|
Handler: threads_account.DisconnectThreadsApiHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/api/playground",
|
||||||
|
Handler: threads_account.RunThreadsApiPlaygroundHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/api/smoke-test",
|
||||||
|
Handler: threads_account.RunThreadsApiSmokeTestHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/:id/connection",
|
Path: "/:id/connection",
|
||||||
|
|
@ -854,13 +959,209 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||||
Path: "/:id/connection",
|
Path: "/:id/connection",
|
||||||
Handler: threads_account.UpdateThreadsAccountConnectionHandler(serverCtx),
|
Handler: threads_account.UpdateThreadsAccountConnectionHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/content-formulas",
|
||||||
|
Handler: threads_account.ListContentFormulasHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/content-formulas/:formulaId",
|
||||||
|
Handler: threads_account.GetContentFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPatch,
|
||||||
|
Path: "/:id/content-formulas/:formulaId",
|
||||||
|
Handler: threads_account.PatchContentFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
Path: "/:id/content-formulas/:formulaId",
|
||||||
|
Handler: threads_account.DeleteContentFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/content-formulas/analyze",
|
||||||
|
Handler: threads_account.AnalyzeContentFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/content-formulas/import-own-post",
|
||||||
|
Handler: threads_account.ImportOwnPostToContentFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/content-formulas/search-posts",
|
||||||
|
Handler: threads_account.SearchContentFormulaPostsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/diagnostics",
|
||||||
|
Handler: threads_account.GetThreadsDiagnosticsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/mention-inbox",
|
||||||
|
Handler: threads_account.ListMentionInboxHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/mention-inbox/:media_id/reply",
|
||||||
|
Handler: threads_account.PublishMentionReplyHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/mention-inbox/sync",
|
||||||
|
Handler: threads_account.SyncMentionInboxHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/own-post-formula",
|
||||||
|
Handler: threads_account.GenerateOwnPostFormulaHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/own-post-formulas",
|
||||||
|
Handler: threads_account.ListOwnPostFormulasHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/own-post-reply-draft",
|
||||||
|
Handler: threads_account.GenerateOwnPostReplyDraftHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/post-performance",
|
||||||
|
Handler: threads_account.ListThreadsPostPerformanceHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-alerts",
|
||||||
|
Handler: threads_account.ListPublishAlertsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-calendar",
|
||||||
|
Handler: threads_account.ListPublishQueueRangeHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-guard-policy",
|
||||||
|
Handler: threads_account.GetPublishGuardPolicyHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPut,
|
||||||
|
Path: "/:id/publish-guard-policy",
|
||||||
|
Handler: threads_account.UpsertPublishGuardPolicyHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/publish-guard/resume",
|
||||||
|
Handler: threads_account.ResumePublishGuardHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-health",
|
||||||
|
Handler: threads_account.ListThreadsPublishHealthHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-inventory-policy",
|
||||||
|
Handler: threads_account.GetPublishInventoryPolicyHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPut,
|
||||||
|
Path: "/:id/publish-inventory-policy",
|
||||||
|
Handler: threads_account.UpsertPublishInventoryPolicyHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/publish-inventory/refill-jobs",
|
||||||
|
Handler: threads_account.StartPublishInventoryRefillJobHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/publish-queue",
|
||||||
|
Handler: threads_account.CreatePublishQueueItemHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-queue",
|
||||||
|
Handler: threads_account.ListPublishQueueItemsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-queue/:qid",
|
||||||
|
Handler: threads_account.GetPublishQueueItemHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPatch,
|
||||||
|
Path: "/:id/publish-queue/:qid",
|
||||||
|
Handler: threads_account.PatchPublishQueueItemHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
Path: "/:id/publish-queue/:qid",
|
||||||
|
Handler: threads_account.DeletePublishQueueItemHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/publish-queue/:qid/cancel",
|
||||||
|
Handler: threads_account.CancelPublishQueueItemHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-queue/:qid/events",
|
||||||
|
Handler: threads_account.ListPublishQueueEventsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/:id/publish-queue/:qid/retry",
|
||||||
|
Handler: threads_account.RetryPublishQueueItemHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/:id/publish-slot-insights",
|
||||||
|
Handler: threads_account.GetPublishSlotInsightsHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/:id/session/import",
|
Path: "/:id/session/import",
|
||||||
Handler: threads_account.ImportThreadsAccountSessionHandler(serverCtx),
|
Handler: threads_account.ImportThreadsAccountSessionHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/oauth/config",
|
||||||
|
Handler: threads_account.GetThreadsOauthConfigHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/oauth/logs",
|
||||||
|
Handler: threads_account.ListThreadsOauthLogsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/oauth/start",
|
||||||
|
Handler: threads_account.StartThreadsOauthHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/publish-dashboard-summary",
|
||||||
|
Handler: threads_account.GetPublishDashboardSummaryHandler(serverCtx),
|
||||||
|
},
|
||||||
}...,
|
}...,
|
||||||
),
|
),
|
||||||
rest.WithPrefix("/api/v1/threads-accounts"),
|
rest.WithPrefix("/api/v1/threads-accounts"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
server.AddRoutes(
|
||||||
|
[]rest.Route{
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/oauth/callback",
|
||||||
|
Handler: threads_account.ThreadsOauthCallbackHandler(serverCtx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rest.WithPrefix("/api/v1/threads-accounts"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package static
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExtensionDownloadHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path, ok := resolveExtensionZipPath()
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "extension package not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
w.Header().Set("Content-Disposition", `attachment; filename="haixun-threads-sync.zip"`)
|
||||||
|
http.ServeFile(w, r, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveExtensionZipPath() (string, bool) {
|
||||||
|
candidates := []string{
|
||||||
|
filepath.Join("web", "public", "downloads", "haixun-threads-sync.zip"),
|
||||||
|
filepath.Join("backend", "web", "public", "downloads", "haixun-threads-sync.zip"),
|
||||||
|
filepath.Join("..", "web", "public", "downloads", "haixun-threads-sync.zip"),
|
||||||
|
filepath.Join("extension", "haixun-threads-sync.zip"),
|
||||||
|
}
|
||||||
|
if wd, err := os.Getwd(); err == nil {
|
||||||
|
candidates = append(candidates,
|
||||||
|
filepath.Join(wd, "web", "public", "downloads", "haixun-threads-sync.zip"),
|
||||||
|
filepath.Join(wd, "backend", "web", "public", "downloads", "haixun-threads-sync.zip"),
|
||||||
|
filepath.Join(wd, "..", "extension", "haixun-threads-sync.zip"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && info.Size() > 0 {
|
||||||
|
return candidate, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package threads_account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/threads_account"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AnalyzeContentFormulaHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AnalyzeContentFormulaHandlerReq
|
||||||
|
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 := threads_account.NewAnalyzeContentFormulaLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.AnalyzeContentFormula(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package threads_account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/threads_account"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CancelPublishQueueItemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PublishQueueItemPath
|
||||||
|
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 := threads_account.NewCancelPublishQueueItemLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CancelPublishQueueItem(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package threads_account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/threads_account"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatePublishQueueItemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.CreatePublishQueueHandlerReq
|
||||||
|
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 := threads_account.NewCreatePublishQueueItemLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.CreatePublishQueueItem(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package threads_account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"haixun-backend/internal/logic/threads_account"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteContentFormulaHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GetContentFormulaHandlerReq
|
||||||
|
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 := threads_account.NewDeleteContentFormulaLogic(r.Context(), svcCtx)
|
||||||
|
data, err := l.DeleteContentFormula(&req)
|
||||||
|
response.Write(r.Context(), w, data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package threads_account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"haixun-backend/internal/logic/threads_account"
|
||||||
|
"haixun-backend/internal/response"
|
||||||
|
"haixun-backend/internal/svc"
|
||||||
|
"haixun-backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeletePublishQueueItemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.PublishQueueItemPath
|
||||||
|
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 := threads_account.NewDeletePublishQueueItemLogic(r.Context(), svcCtx)
|
||||||
|
err := l.DeletePublishQueueItem(&req)
|
||||||
|
response.Write(r.Context(), w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue