first commit
This commit is contained in:
parent
232111712d
commit
8b0985f604
98
Makefile
98
Makefile
|
|
@ -1,98 +0,0 @@
|
||||||
GO ?= go
|
|
||||||
GOFMT ?= gofmt
|
|
||||||
GOCTL ?= goctl
|
|
||||||
GO_ZERO_STYLE := go_zero
|
|
||||||
API_ENTRY := ./generate/api/gateway.api
|
|
||||||
GOFILES := $(shell find . -name '*.go')
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
|
|
||||||
help: ## 顯示可用指令
|
|
||||||
@echo "Haixun Backend"
|
|
||||||
@echo ""
|
|
||||||
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-12s %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
tools: ## 安裝 goctl / goimports
|
|
||||||
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install github.com/zeromicro/go-zero/tools/goctl@latest)
|
|
||||||
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
|
|
||||||
|
|
||||||
gen-api: tools ## 由 .api 生成 handler / logic / types
|
|
||||||
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
|
|
||||||
|
|
||||||
fmt: ## gofmt + goimports
|
|
||||||
$(GOFMT) -s -w $(GOFILES)
|
|
||||||
@command -v goimports >/dev/null 2>&1 && goimports -w . || true
|
|
||||||
|
|
||||||
test: ## 執行測試
|
|
||||||
$(GO) test ./...
|
|
||||||
|
|
||||||
run: ## 啟動 API(前景)
|
|
||||||
$(GO) run ./gateway.go -f etc/gateway.yaml
|
|
||||||
|
|
||||||
dev-all: ## 一鍵啟動 Mongo/Redis + API + 前端 + 8D worker(背景)
|
|
||||||
bash scripts/start-all.sh
|
|
||||||
|
|
||||||
stop-all: ## 一鍵停止全部開發服務
|
|
||||||
bash scripts/stop-all.sh
|
|
||||||
|
|
||||||
restart-all: ## 一鍵重啟全部開發服務
|
|
||||||
bash scripts/restart-all.sh
|
|
||||||
|
|
||||||
status-all: ## 查看全部開發服務狀態
|
|
||||||
bash scripts/status-all.sh
|
|
||||||
|
|
||||||
stop: stop-all ## 同 stop-all
|
|
||||||
|
|
||||||
restart: restart-all ## 同 restart-all
|
|
||||||
|
|
||||||
dev-8d: ## 一鍵啟動 API + Node 8D worker(前景,Ctrl+C 結束)
|
|
||||||
bash scripts/dev-with-style-8d.sh
|
|
||||||
|
|
||||||
CONFIG ?= etc/gateway.yaml
|
|
||||||
INIT_TENANT ?= default
|
|
||||||
INIT_EMAIL ?= admin@30cm.net
|
|
||||||
INIT_PASSWORD ?= Fafafa54088
|
|
||||||
|
|
||||||
tool-init: ## 初始化 Mongo indexes、預設權限與 admin 帳號
|
|
||||||
$(GO) run ./cmd/tool init -f $(CONFIG) -tenant $(INIT_TENANT) -email $(INIT_EMAIL) -password '$(INIT_PASSWORD)'
|
|
||||||
|
|
||||||
tool: ## 執行 cmd/tool(例:make tool ARGS="init -f etc/gateway.yaml")
|
|
||||||
$(GO) run ./cmd/tool $(ARGS)
|
|
||||||
|
|
||||||
web-install: ## 安裝前端依賴
|
|
||||||
cd web && npm install
|
|
||||||
|
|
||||||
web-dev: web-install ## 啟動前端 dev server(proxy 到 :8890)
|
|
||||||
cd web && npm run dev
|
|
||||||
|
|
||||||
extension-pack: ## 打包 Chrome 擴充為 web/public/downloads/*.zip
|
|
||||||
bash scripts/package-extension.sh
|
|
||||||
|
|
||||||
web-build: web-install extension-pack ## 建置前端靜態檔
|
|
||||||
cd web && npm run build
|
|
||||||
|
|
||||||
node-worker-style-8d: ## 啟動 Node 8D 爬蟲 worker
|
|
||||||
cd .. && npm run worker:style-8d
|
|
||||||
|
|
||||||
check: fmt test ## 格式化並測試
|
|
||||||
|
|
||||||
prod: ## 一鍵啟動 production Docker(API + Web + workers,分身數見 deploy/.env)
|
|
||||||
bash scripts/prod-up.sh
|
|
||||||
|
|
||||||
prod-update: ## 只重建/重啟 API+Web+Workers;mongo/redis 不重啟,資料留在 volume
|
|
||||||
bash scripts/prod-update.sh
|
|
||||||
|
|
||||||
prod-deps: ## 只啟動 mongo+redis(named volume 持久化)
|
|
||||||
bash scripts/prod-deps.sh
|
|
||||||
|
|
||||||
prod-down: ## 停止 stack(不刪 volume;Mongo/Redis 資料保留)
|
|
||||||
bash scripts/prod-down.sh
|
|
||||||
|
|
||||||
prod-wipe-data: ## 停止並刪除 mongo/redis volume(危險,需輸入 yes)
|
|
||||||
bash scripts/prod-wipe-data.sh
|
|
||||||
|
|
||||||
prod-logs: ## 追蹤 production logs(可傳 service 名,例:make prod-logs ARGS=api)
|
|
||||||
bash scripts/prod-logs.sh $(ARGS)
|
|
||||||
|
|
||||||
prod-build: web-build ## 建置靜態前端 + production images(不啟動)
|
|
||||||
cd deploy && docker compose -f docker-compose.prod.yml build
|
|
||||||
40
deploy/.env
40
deploy/.env
|
|
@ -1,40 +0,0 @@
|
||||||
# 複製為 deploy/.env 後再啟動:cp deploy/.env.example deploy/.env
|
|
||||||
|
|
||||||
# ── 對外埠 ──
|
|
||||||
HAIXUN_WEB_PORT=8080
|
|
||||||
|
|
||||||
# ── 前端打包模式 ──
|
|
||||||
# static = 本機 make web-build 後 nginx 只 COPY dist(預設,最快)
|
|
||||||
# docker = 在 Docker 內跑 npm build(需改 compose 用 Dockerfile.web)
|
|
||||||
# HAIXUN_WEB_BUILD_MODE=static
|
|
||||||
|
|
||||||
# ── Worker 分身數(make prod 會帶入 docker compose --scale)──
|
|
||||||
GO_WORKER_REPLICAS=5
|
|
||||||
NODE_STYLE8D_WORKER_REPLICAS=5
|
|
||||||
|
|
||||||
# ── Mongo / Redis(容器內預設,通常不用改)──
|
|
||||||
# 資料存在 Docker named volume:haixun-prod_mongo_data、haixun-prod_redis_data
|
|
||||||
# prod-down 不會刪 volume;重啟 container 資料仍在。
|
|
||||||
# 只改版程式:make prod-update(不碰 mongo/redis)
|
|
||||||
HAIXUN_MONGO_URI=mongodb://mongo:27017
|
|
||||||
HAIXUN_MONGO_DATABASE=haixun
|
|
||||||
HAIXUN_REDIS_ADDR=redis:6379
|
|
||||||
|
|
||||||
# ── 安全金鑰(正式環境務必更換)──
|
|
||||||
HAIXUN_AUTH_ACCESS_SECRET=change-me-access-secret
|
|
||||||
HAIXUN_AUTH_REFRESH_SECRET=change-me-refresh-secret
|
|
||||||
HAIXUN_WORKER_SECRET=change-me-worker-secret
|
|
||||||
|
|
||||||
# ── 首次初始化管理員(make prod 會自動跑 init;已存在則跳過建立)──
|
|
||||||
INIT_TENANT_ID=default
|
|
||||||
INIT_ADMIN_EMAIL=admin@30cm.net
|
|
||||||
INIT_ADMIN_PASSWORD=Fafafa54088
|
|
||||||
|
|
||||||
# ── Node 8D worker 選項 ──
|
|
||||||
# HAIXUN_NODE_WORKER_ID=custom-node-worker-1
|
|
||||||
# HAIXUN_WORKER_POLL_MS=3000
|
|
||||||
|
|
||||||
# ── 略過自動 init ──
|
|
||||||
# 預設:若 Mongo 已有 members 會自動跳過 init。
|
|
||||||
# 強制重跑 init:PROD_FORCE_INIT=1 make prod
|
|
||||||
# HAIXUN_SKIP_INIT=1
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
# 複製為 deploy/.env 後再啟動:cp deploy/.env.example deploy/.env
|
|
||||||
|
|
||||||
# ── 對外埠 ──
|
|
||||||
HAIXUN_WEB_PORT=8080
|
|
||||||
|
|
||||||
# ── 前端打包模式 ──
|
|
||||||
# static = 本機 make web-build 後 nginx 只 COPY dist(預設,最快)
|
|
||||||
# docker = 在 Docker 內跑 npm build(需改 compose 用 Dockerfile.web)
|
|
||||||
# HAIXUN_WEB_BUILD_MODE=static
|
|
||||||
|
|
||||||
# ── Worker 分身數(make prod 會帶入 docker compose --scale)──
|
|
||||||
GO_WORKER_REPLICAS=1
|
|
||||||
NODE_STYLE8D_WORKER_REPLICAS=1
|
|
||||||
|
|
||||||
# ── Mongo / Redis(容器內預設,通常不用改)──
|
|
||||||
# 資料存在 Docker named volume:haixun-prod_mongo_data、haixun-prod_redis_data
|
|
||||||
# prod-down 不會刪 volume;重啟 container 資料仍在。
|
|
||||||
# 只改版程式:make prod-update(不碰 mongo/redis)
|
|
||||||
HAIXUN_MONGO_URI=mongodb://mongo:27017
|
|
||||||
HAIXUN_MONGO_DATABASE=haixun
|
|
||||||
HAIXUN_REDIS_ADDR=redis:6379
|
|
||||||
|
|
||||||
# ── 安全金鑰(正式環境務必更換)──
|
|
||||||
HAIXUN_AUTH_ACCESS_SECRET=change-me-access-secret
|
|
||||||
HAIXUN_AUTH_REFRESH_SECRET=change-me-refresh-secret
|
|
||||||
HAIXUN_WORKER_SECRET=change-me-worker-secret
|
|
||||||
|
|
||||||
# ── 首次初始化管理員(make prod 會自動跑 init;已存在則跳過建立)──
|
|
||||||
INIT_TENANT_ID=default
|
|
||||||
INIT_ADMIN_EMAIL=admin@haixun.local
|
|
||||||
INIT_ADMIN_PASSWORD=Admin-Pass-1!
|
|
||||||
|
|
||||||
# ── Node 8D worker 選項 ──
|
|
||||||
# HAIXUN_NODE_WORKER_ID=custom-node-worker-1
|
|
||||||
# HAIXUN_WORKER_POLL_MS=3000
|
|
||||||
|
|
||||||
# ── 略過自動 init ──
|
|
||||||
# 預設:若 Mongo 已有 members 會自動跳過 init。
|
|
||||||
# 強制重跑 init:PROD_FORCE_INIT=1 make prod
|
|
||||||
# HAIXUN_SKIP_INIT=1
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM golang:1.22-bookworm AS builder
|
|
||||||
WORKDIR /src
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
COPY . .
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-api .
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-worker ./cmd/worker
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-tool ./cmd/tool
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates gettext-base curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /out/haixun-api /out/haixun-worker /out/haixun-tool /app/
|
|
||||||
COPY deploy/config/gateway.runtime.yaml.tpl deploy/config/gateway.worker.runtime.yaml.tpl /app/deploy/config/
|
|
||||||
COPY deploy/docker/entrypoint-api.sh deploy/docker/entrypoint-worker.sh deploy/docker/entrypoint-init.sh /app/deploy/docker/
|
|
||||||
RUN chmod +x /app/deploy/docker/entrypoint-api.sh /app/deploy/docker/entrypoint-worker.sh /app/deploy/docker/entrypoint-init.sh
|
|
||||||
EXPOSE 8890
|
|
||||||
ENTRYPOINT ["/app/deploy/docker/entrypoint-api.sh"]
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/playwright:v1.49.1-noble AS base
|
|
||||||
WORKDIR /app
|
|
||||||
COPY worker/package.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY worker/ ./
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
CMD ["npx", "tsx", "style-8d-worker.ts"]
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
# 備用:無本機 Node 時在 Docker 內編譯。預設請用 Dockerfile.web.static + make web-build。
|
|
||||||
|
|
||||||
FROM node:22-bookworm AS web-builder
|
|
||||||
WORKDIR /src/web
|
|
||||||
COPY web/package.json web/package-lock.json ./
|
|
||||||
RUN npm ci
|
|
||||||
COPY web/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
|
||||||
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY --from=web-builder /src/web/dist /usr/share/nginx/html
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
# 本機先執行 make web-build,再打包純靜態檔 + nginx(無 Node 編譯,建置最快)
|
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
|
||||||
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY web/dist /usr/share/nginx/html
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
# 本機依賴(Docker Compose)
|
|
||||||
|
|
||||||
Gateway 啟用 **Notification** / **Member OTP** 需要:
|
|
||||||
|
|
||||||
| 服務 | 用途 | 預設埠 |
|
|
||||||
|------|------|--------|
|
|
||||||
| **MongoDB** | `notifications`、`notification_dlq` collections | 27017 |
|
|
||||||
| **Redis** | 冪等、配額、異步重試佇列、member OTP challenge | 6379 |
|
|
||||||
| MailHog(選用) | 本機 SMTP 測試 | 1025 / 8025 |
|
|
||||||
| OpenLDAP(`make ldap-up` / `make k6-up`) | ZITADEL LDAP IdP 本機目錄 | 389 |
|
|
||||||
| ZITADEL(`make k6-up`) | OIDC / Social / LDAP 登入 | 8080 |
|
|
||||||
|
|
||||||
Mongo **不需要**事先手動建 collection;應用程式寫入時會自動建立。索引由 init script 或 `make mongo-index` 建立。
|
|
||||||
|
|
||||||
## 快速開始
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 啟動 Mongo + Redis
|
|
||||||
make deps-up
|
|
||||||
|
|
||||||
# 2.(選用)含 MailHog
|
|
||||||
make deps-up-smtp
|
|
||||||
|
|
||||||
# 3. 確認索引(首次 docker volume 通常已由 init 建立;可再跑一次保險)
|
|
||||||
make mongo-index
|
|
||||||
|
|
||||||
# 4. 啟動 Gateway(使用 etc/gateway.dev.yaml)
|
|
||||||
make run-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mongo collections
|
|
||||||
|
|
||||||
| Collection | 模組 | 說明 |
|
|
||||||
|------------|------|------|
|
|
||||||
| `notifications` | notification | 發送紀錄、冪等 |
|
|
||||||
| `notification_dlq` | notification | 超過 MaxRetry 的死信 |
|
|
||||||
|
|
||||||
索引定義見 [`deploy/mongo/init/01-gateway-indexes.js`](mongo/init/01-gateway-indexes.js),與 Go 的 `Index20260520001UP` 一致。
|
|
||||||
|
|
||||||
## 常用指令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make deps-up # docker compose up -d mongo redis
|
|
||||||
make deps-up-smtp # 再加上 mailhog(profile smtp)
|
|
||||||
make ldap-up # 只起 OpenLDAP(profile ldap)
|
|
||||||
make k6-up # 全棧含 OpenLDAP + ZITADEL(見 deploy/zitadel、deploy/openldap README)
|
|
||||||
make ldap-test # 確認 LDAP 測試帳號 alice/bob
|
|
||||||
make deps-down # 停止並移除容器(保留 volume)
|
|
||||||
make deps-down-v # 停止並刪除 volume(會清掉 Mongo 資料)
|
|
||||||
make deps-logs # 查看 log
|
|
||||||
make mongo-index # 手動建立/補齊索引
|
|
||||||
```
|
|
||||||
|
|
||||||
LDAP 本機測試:[deploy/openldap/README.md](openldap/README.md)
|
|
||||||
|
|
||||||
## 連線設定
|
|
||||||
|
|
||||||
設定說明:[`etc/README.md`](../etc/README.md)
|
|
||||||
|
|
||||||
| 檔案 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| [`etc/gateway.yaml`](../etc/gateway.yaml) | 預設,無需 Docker |
|
|
||||||
| [`etc/gateway.dev.example.yaml`](../etc/gateway.dev.example.yaml) | 範例(可提交) |
|
|
||||||
| `etc/gateway.dev.yaml` | 本機專用(**勿提交**,見 `.gitignore`) |
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
Name: haixun-backend
|
|
||||||
Host: 0.0.0.0
|
|
||||||
Port: 8890
|
|
||||||
Timeout: 120000
|
|
||||||
|
|
||||||
Mongo:
|
|
||||||
URI: ${HAIXUN_MONGO_URI}
|
|
||||||
Database: ${HAIXUN_MONGO_DATABASE}
|
|
||||||
TimeoutSeconds: 10
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Addr: ${HAIXUN_REDIS_ADDR}
|
|
||||||
DB: 0
|
|
||||||
|
|
||||||
Auth:
|
|
||||||
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
|
||||||
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
|
||||||
AccessExpireSeconds: 900
|
|
||||||
RefreshExpireSeconds: 2592000
|
|
||||||
DevHeaderFallback: false
|
|
||||||
|
|
||||||
InternalWorker:
|
|
||||||
Secret: ${HAIXUN_WORKER_SECRET}
|
|
||||||
|
|
||||||
JobWorker:
|
|
||||||
Enabled: false
|
|
||||||
WorkerType: go
|
|
||||||
|
|
||||||
JobScheduler:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 60
|
|
||||||
|
|
||||||
JobReaper:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 30
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
Name: haixun-worker
|
|
||||||
Host: 0.0.0.0
|
|
||||||
Port: 8891
|
|
||||||
Timeout: 120000
|
|
||||||
|
|
||||||
Mongo:
|
|
||||||
URI: ${HAIXUN_MONGO_URI}
|
|
||||||
Database: ${HAIXUN_MONGO_DATABASE}
|
|
||||||
TimeoutSeconds: 10
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Addr: ${HAIXUN_REDIS_ADDR}
|
|
||||||
DB: 0
|
|
||||||
|
|
||||||
Auth:
|
|
||||||
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
|
||||||
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
|
||||||
AccessExpireSeconds: 900
|
|
||||||
RefreshExpireSeconds: 2592000
|
|
||||||
DevHeaderFallback: false
|
|
||||||
|
|
||||||
InternalWorker:
|
|
||||||
Secret: ${HAIXUN_WORKER_SECRET}
|
|
||||||
|
|
||||||
JobWorker:
|
|
||||||
Enabled: true
|
|
||||||
WorkerType: go
|
|
||||||
|
|
||||||
JobScheduler:
|
|
||||||
Enabled: false
|
|
||||||
IntervalSeconds: 60
|
|
||||||
|
|
||||||
JobReaper:
|
|
||||||
Enabled: false
|
|
||||||
IntervalSeconds: 30
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
name: haixun-prod
|
|
||||||
|
|
||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo:7
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_DATABASE: haixun
|
|
||||||
# named volume:重啟/改版不會清資料(只有 prod-wipe-data 或 docker volume rm 才會)
|
|
||||||
volumes:
|
|
||||||
- mongo_data:/data/db
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 12
|
|
||||||
start_period: 15s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
|
||||||
# AOF + named volume:重啟後 queue/lock 狀態可從磁碟恢復
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 12
|
|
||||||
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: deploy/Dockerfile.api
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
mongo:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8890/api/v1/health >/dev/null || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 12
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
go-worker:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: deploy/Dockerfile.api
|
|
||||||
restart: unless-stopped
|
|
||||||
entrypoint: ["/app/deploy/docker/entrypoint-worker.sh"]
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
mongo:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
api:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
node-worker-style-8d:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: deploy/Dockerfile.node-worker
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
HAIXUN_BACKEND_URL: http://api:8890
|
|
||||||
HAIXUN_WORKER_SECRET: ${HAIXUN_WORKER_SECRET}
|
|
||||||
HAIXUN_NODE_WORKER_ID: ${HAIXUN_NODE_WORKER_ID:-}
|
|
||||||
HAIXUN_WORKER_POLL_MS: ${HAIXUN_WORKER_POLL_MS:-3000}
|
|
||||||
depends_on:
|
|
||||||
api:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
web:
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: deploy/Dockerfile.web.static
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${HAIXUN_WEB_PORT:-8080}:80"
|
|
||||||
depends_on:
|
|
||||||
api:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
init:
|
|
||||||
profiles: ["init"]
|
|
||||||
build:
|
|
||||||
context: ..
|
|
||||||
dockerfile: deploy/Dockerfile.api
|
|
||||||
entrypoint: ["/app/deploy/docker/entrypoint-init.sh"]
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
mongo:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mongo_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo:7
|
|
||||||
container_name: gateway-mongo
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "27017:27017"
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_DATABASE: gateway
|
|
||||||
volumes:
|
|
||||||
- mongo_data:/data/db
|
|
||||||
- ./mongo/init:/docker-entrypoint-initdb.d:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: gateway-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mongo_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
|
||||||
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
|
||||||
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
|
||||||
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
|
||||||
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
|
||||||
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
|
||||||
|
|
||||||
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
|
||||||
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
|
||||||
|
|
||||||
envsubst < /app/deploy/config/gateway.runtime.yaml.tpl > /tmp/gateway.runtime.yaml
|
|
||||||
exec /app/haixun-api -f /tmp/gateway.runtime.yaml
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
|
||||||
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
|
||||||
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
|
||||||
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
|
||||||
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
|
||||||
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
|
||||||
: "${INIT_TENANT_ID:=default}"
|
|
||||||
: "${INIT_ADMIN_EMAIL:=admin@haixun.local}"
|
|
||||||
: "${INIT_ADMIN_PASSWORD:?INIT_ADMIN_PASSWORD is required}"
|
|
||||||
|
|
||||||
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
|
||||||
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
|
||||||
|
|
||||||
envsubst < /app/deploy/config/gateway.runtime.yaml.tpl > /tmp/gateway.runtime.yaml
|
|
||||||
exec /app/haixun-tool init \
|
|
||||||
-f /tmp/gateway.runtime.yaml \
|
|
||||||
-tenant "$INIT_TENANT_ID" \
|
|
||||||
-email "$INIT_ADMIN_EMAIL" \
|
|
||||||
-password "$INIT_ADMIN_PASSWORD"
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
|
||||||
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
|
||||||
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
|
||||||
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
|
||||||
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
|
||||||
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
|
||||||
|
|
||||||
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
|
||||||
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
|
||||||
|
|
||||||
envsubst < /app/deploy/config/gateway.worker.runtime.yaml.tpl > /tmp/gateway.worker.runtime.yaml
|
|
||||||
exec /app/haixun-worker -f /tmp/gateway.worker.runtime.yaml
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
// Gateway MongoDB 初始化(僅在 data volume 首次建立時執行)
|
|
||||||
// 與 internal/model/notification/repository/* Index20260520001UP 對齊
|
|
||||||
// 既有 volume 請執行:make mongo-index
|
|
||||||
|
|
||||||
db = db.getSiblingDB('gateway');
|
|
||||||
|
|
||||||
print('Creating indexes on notifications...');
|
|
||||||
|
|
||||||
db.notifications.createIndex(
|
|
||||||
{ tenant_id: 1, kind: 1, idempotency_key: 1 },
|
|
||||||
{ unique: true, name: 'idx_notifications_tenant_kind_idempotency' }
|
|
||||||
);
|
|
||||||
|
|
||||||
db.notifications.createIndex(
|
|
||||||
{ tenant_id: 1, uid: 1, occurred_at: -1 },
|
|
||||||
{ name: 'idx_notifications_tenant_uid_occurred' }
|
|
||||||
);
|
|
||||||
|
|
||||||
db.notifications.createIndex(
|
|
||||||
{ status: 1, attempts: 1, occurred_at: 1 },
|
|
||||||
{ name: 'idx_notifications_status_attempts_occurred' }
|
|
||||||
);
|
|
||||||
|
|
||||||
print('Creating indexes on notification_dlq...');
|
|
||||||
|
|
||||||
db.notification_dlq.createIndex(
|
|
||||||
{ tenant_id: 1, occurred_at: -1 },
|
|
||||||
{ name: 'idx_notification_dlq_tenant_occurred' }
|
|
||||||
);
|
|
||||||
|
|
||||||
print('Gateway Mongo init done.');
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_comp_level 5;
|
|
||||||
gzip_min_length 256;
|
|
||||||
gzip_types
|
|
||||||
text/css
|
|
||||||
text/javascript
|
|
||||||
application/javascript
|
|
||||||
application/json
|
|
||||||
application/xml
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
# Vite 產物:檔名含 hash,可長期快取
|
|
||||||
location /assets/ {
|
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /downloads/ {
|
|
||||||
add_header Cache-Control "public, max-age=86400";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /illustrations/ {
|
|
||||||
add_header Cache-Control "public, max-age=86400";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# SPA 入口與路由:不快取,避免部署後仍載入舊版 shell
|
|
||||||
location = /index.html {
|
|
||||||
add_header Cache-Control "no-cache";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://api:8890;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# 直接對比 OpenCode upstream 與本機 gateway 的原始 JSON/SSE,方便排查空回應
|
|
||||||
#
|
|
||||||
# 用法:
|
|
||||||
# OPENCODE_TOKEN=sk-xxx ./scripts/debug-opencode-raw.sh
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8890}"
|
|
||||||
OPENCODE_TOKEN="${OPENCODE_TOKEN:-}"
|
|
||||||
MODEL="${MODEL:-deepseek-v4-flash}"
|
|
||||||
MESSAGE="${MESSAGE:-Introduce yourself in one sentence.}"
|
|
||||||
|
|
||||||
if [[ -z "$OPENCODE_TOKEN" ]]; then
|
|
||||||
echo "OPENCODE_TOKEN is required"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || { echo "missing command: $1"; exit 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd curl
|
|
||||||
require_cmd jq
|
|
||||||
|
|
||||||
payload="$(jq -n \
|
|
||||||
--arg model "$MODEL" \
|
|
||||||
--arg message "$MESSAGE" \
|
|
||||||
'{
|
|
||||||
model: $model,
|
|
||||||
messages: [{role: "user", content: $message}],
|
|
||||||
max_tokens: 2048,
|
|
||||||
thinking: {type: "disabled"}
|
|
||||||
}')"
|
|
||||||
|
|
||||||
echo "=== OpenCode upstream (non-stream) ==="
|
|
||||||
curl -sS -m 120 -X POST "https://opencode.ai/zen/go/v1/chat/completions" \
|
|
||||||
-H "Authorization: Bearer ${OPENCODE_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$payload" | jq .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Local gateway (non-stream) ==="
|
|
||||||
curl -sS -m 120 -X POST "${BASE_URL}/api/v1/ai/chat" \
|
|
||||||
-H "Authorization: Bearer ${OPENCODE_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg provider "opencode-go" \
|
|
||||||
--arg model "$MODEL" \
|
|
||||||
--arg message "$MESSAGE" \
|
|
||||||
'{provider:$provider,model:$model,messages:[{role:"user",content:$message}],max_tokens:2048}')" | jq .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== OpenCode upstream (stream, first 20 lines) ==="
|
|
||||||
curl -sS -N -m 120 -X POST "https://opencode.ai/zen/go/v1/chat/completions" \
|
|
||||||
-H "Authorization: Bearer ${OPENCODE_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: text/event-stream" \
|
|
||||||
-d "$(echo "$payload" | jq '.stream=true')" | head -n 20
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Local gateway (stream, first 20 lines) ==="
|
|
||||||
curl -sS -N -m 120 -X POST "${BASE_URL}/api/v1/ai/chat/stream" \
|
|
||||||
-H "Authorization: Bearer ${OPENCODE_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: text/event-stream" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg provider "opencode-go" \
|
|
||||||
--arg model "$MODEL" \
|
|
||||||
--arg message "$MESSAGE" \
|
|
||||||
'{provider:$provider,model:$model,messages:[{role:"user",content:$message}],max_tokens:2048}')" | head -n 20
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
BACKEND_DIR="$ROOT_DIR/haixun-backend"
|
|
||||||
CONFIG_FILE="${HAIXUN_BACKEND_CONFIG:-etc/gateway.yaml}"
|
|
||||||
BACKEND_URL="${HAIXUN_BACKEND_URL:-http://127.0.0.1:8890}"
|
|
||||||
|
|
||||||
pids=()
|
|
||||||
owned_pids=()
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
local code=$?
|
|
||||||
if ((${#owned_pids[@]} > 0)); then
|
|
||||||
echo ""
|
|
||||||
echo "[dev-8d] stopping backend and worker..."
|
|
||||||
kill "${owned_pids[@]}" 2>/dev/null || true
|
|
||||||
wait "${owned_pids[@]}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
exit "$code"
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
if curl -fsS "$BACKEND_URL/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
echo "[dev-8d] backend already running: $BACKEND_URL"
|
|
||||||
else
|
|
||||||
echo "[dev-8d] starting Go backend: $CONFIG_FILE"
|
|
||||||
(
|
|
||||||
cd "$BACKEND_DIR"
|
|
||||||
go run ./gateway.go -f "$CONFIG_FILE"
|
|
||||||
) &
|
|
||||||
pids+=("$!")
|
|
||||||
owned_pids+=("$!")
|
|
||||||
|
|
||||||
echo "[dev-8d] waiting for backend health..."
|
|
||||||
for _ in $(seq 1 30); do
|
|
||||||
if curl -fsS "$BACKEND_URL/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if ! kill -0 "${pids[0]}" 2>/dev/null; then
|
|
||||||
wait "${pids[0]}"
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
if ! curl -fsS "$BACKEND_URL/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
echo "[dev-8d] backend health check timed out: $BACKEND_URL" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[dev-8d] starting Node 8D worker"
|
|
||||||
(
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
npm run worker:style-8d
|
|
||||||
) &
|
|
||||||
pids+=("$!")
|
|
||||||
owned_pids+=("$!")
|
|
||||||
|
|
||||||
echo "[dev-8d] running pids=${pids[*]}"
|
|
||||||
echo "[dev-8d] press Ctrl+C to stop both"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
for pid in "${owned_pids[@]}"; do
|
|
||||||
if ! kill -0 "$pid" 2>/dev/null; then
|
|
||||||
wait "$pid"
|
|
||||||
exit $?
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
EXT_DIR="$ROOT_DIR/extension/haixun-threads-sync"
|
|
||||||
OUT_DIR="$ROOT_DIR/haixun-backend/web/public/downloads"
|
|
||||||
OUT_FILE="$OUT_DIR/haixun-threads-sync.zip"
|
|
||||||
|
|
||||||
if [[ ! -f "$EXT_DIR/manifest.json" ]]; then
|
|
||||||
echo "extension not found: $EXT_DIR" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
rm -f "$OUT_FILE"
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "$(dirname "$EXT_DIR")"
|
|
||||||
zip -qr "$OUT_FILE" "$(basename "$EXT_DIR")" \
|
|
||||||
-x "*.DS_Store" -x "*__MACOSX*"
|
|
||||||
)
|
|
||||||
|
|
||||||
VERSION="$(python3 -c "import json; print(json.load(open('$EXT_DIR/manifest.json'))['version'])")"
|
|
||||||
echo "packed haixun-threads-sync v$VERSION -> $OUT_FILE"
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
# Shared helpers for production Docker scripts.
|
|
||||||
|
|
||||||
_PROD_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
BACKEND_DIR="$(cd "$_PROD_SCRIPT_DIR/.." && pwd)"
|
|
||||||
DEPLOY_DIR="$BACKEND_DIR/deploy"
|
|
||||||
COMPOSE_FILE="$DEPLOY_DIR/docker-compose.prod.yml"
|
|
||||||
ENV_FILE="$DEPLOY_DIR/.env"
|
|
||||||
ENV_EXAMPLE="$DEPLOY_DIR/.env.example"
|
|
||||||
|
|
||||||
prod_common_init() {
|
|
||||||
:
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_load_env() {
|
|
||||||
prod_common_init
|
|
||||||
|
|
||||||
if [[ ! -f "$ENV_FILE" ]]; then
|
|
||||||
if [[ -f "$ENV_EXAMPLE" ]]; then
|
|
||||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
|
||||||
echo "[prod] created $ENV_FILE from .env.example — 請先修改密鑰與管理員密碼"
|
|
||||||
else
|
|
||||||
echo "[prod] missing $ENV_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
GO_REPLICAS="${GO_WORKER_REPLICAS:-1}"
|
|
||||||
NODE_REPLICAS="${NODE_STYLE8D_WORKER_REPLICAS:-1}"
|
|
||||||
WEB_PORT="${HAIXUN_WEB_PORT:-8080}"
|
|
||||||
MONGO_DB="${HAIXUN_MONGO_DATABASE:-haixun}"
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_require_docker() {
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "[prod] docker is required" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_compose() {
|
|
||||||
prod_common_init
|
|
||||||
docker compose -f "$COMPOSE_FILE" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_service_health() {
|
|
||||||
local service="$1"
|
|
||||||
prod_compose ps --format json "$service" 2>/dev/null \
|
|
||||||
| grep -o '"Health":"[^"]*"' \
|
|
||||||
| head -1 \
|
|
||||||
| cut -d'"' -f4 \
|
|
||||||
|| true
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_named_container_health() {
|
|
||||||
local name="$1"
|
|
||||||
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{if .State.Running}}running{{else}}stopped{{end}}{{end}}' \
|
|
||||||
"$name" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_named_container_running() {
|
|
||||||
local name="$1"
|
|
||||||
[[ "$(docker inspect --format '{{.State.Running}}' "$name" 2>/dev/null || echo false)" == "true" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_deps_healthy() {
|
|
||||||
local mongo_ok redis_ok
|
|
||||||
mongo_ok="$(prod_service_health mongo)"
|
|
||||||
redis_ok="$(prod_service_health redis)"
|
|
||||||
if [[ "$mongo_ok" == "healthy" && "$redis_ok" == "healthy" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# compose ps 偶發失敗時,改查實際 container(避免重複 create 撞名)
|
|
||||||
mongo_ok="$(prod_named_container_health haixun-prod-mongo-1)"
|
|
||||||
redis_ok="$(prod_named_container_health haixun-prod-redis-1)"
|
|
||||||
[[ "$mongo_ok" == "healthy" && "$redis_ok" == "healthy" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_wait_deps_healthy() {
|
|
||||||
echo "[prod] waiting for mongo/redis..."
|
|
||||||
for _ in $(seq 1 90); do
|
|
||||||
if prod_deps_healthy; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "[prod] mongo/redis did not become healthy in time" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_ensure_deps() {
|
|
||||||
if prod_deps_healthy; then
|
|
||||||
echo "[prod] mongo + redis already healthy — 略過重啟(資料在 named volume)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[prod] starting mongo + redis..."
|
|
||||||
if prod_named_container_running haixun-prod-mongo-1 && prod_named_container_running haixun-prod-redis-1; then
|
|
||||||
prod_compose start mongo redis 2>/dev/null \
|
|
||||||
|| prod_compose up -d --no-recreate mongo redis
|
|
||||||
else
|
|
||||||
prod_compose up -d mongo redis
|
|
||||||
fi
|
|
||||||
prod_wait_deps_healthy
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_mongo_has_members() {
|
|
||||||
prod_compose exec -T mongo mongosh --quiet "$MONGO_DB" --eval \
|
|
||||||
'db.members.countDocuments({})' 2>/dev/null \
|
|
||||||
| tr -d '\r' \
|
|
||||||
| grep -Eq '^[1-9][0-9]*$'
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_should_skip_init() {
|
|
||||||
if [[ "${HAIXUN_SKIP_INIT:-0}" == "1" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ "${PROD_FORCE_INIT:-0}" == "1" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if prod_mongo_has_members; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_run_init_if_needed() {
|
|
||||||
if prod_should_skip_init; then
|
|
||||||
if [[ "${HAIXUN_SKIP_INIT:-0}" == "1" ]]; then
|
|
||||||
echo "[prod] skip init (HAIXUN_SKIP_INIT=1)"
|
|
||||||
else
|
|
||||||
echo "[prod] skip init (Mongo 已有資料;若要強制重跑請設 PROD_FORCE_INIT=1)"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[prod] running bootstrap init..."
|
|
||||||
prod_compose --profile init run --rm init
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_build_web_if_static() {
|
|
||||||
prod_common_init
|
|
||||||
if [[ "${HAIXUN_WEB_BUILD_MODE:-static}" == "static" ]]; then
|
|
||||||
echo "[prod] building frontend static files (vite → web/dist)..."
|
|
||||||
(cd "$BACKEND_DIR" && make web-build)
|
|
||||||
else
|
|
||||||
echo "[prod] HAIXUN_WEB_BUILD_MODE=docker — web image will compile inside Docker"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_start_app_services() {
|
|
||||||
local build_flag=()
|
|
||||||
if [[ "${PROD_SKIP_BUILD:-0}" != "1" ]]; then
|
|
||||||
build_flag=(--build)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[prod] starting api, web, workers (go=${GO_REPLICAS}, node-style-8d=${NODE_REPLICAS})..."
|
|
||||||
prod_compose up -d "${build_flag[@]}" \
|
|
||||||
--no-deps \
|
|
||||||
--scale "go-worker=${GO_REPLICAS}" \
|
|
||||||
--scale "node-worker-style-8d=${NODE_REPLICAS}" \
|
|
||||||
api web go-worker node-worker-style-8d
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_wait_api_health() {
|
|
||||||
echo "[prod] waiting for API health..."
|
|
||||||
for _ in $(seq 1 60); do
|
|
||||||
if curl -fsS "http://127.0.0.1:${WEB_PORT}/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "[prod] API health check timed out" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_print_volume_hint() {
|
|
||||||
echo " Data: Mongo/Redis 使用 named volume(重啟 container 不會清資料)"
|
|
||||||
echo " Update app: make -C haixun-backend prod-update"
|
|
||||||
echo " Wipe data: make -C haixun-backend prod-wipe-data # 會刪除 volume"
|
|
||||||
}
|
|
||||||
|
|
||||||
prod_print_stack_summary() {
|
|
||||||
echo ""
|
|
||||||
echo "[prod] stack is up"
|
|
||||||
echo " Web: http://127.0.0.1:${WEB_PORT}"
|
|
||||||
echo " API: http://127.0.0.1:${WEB_PORT}/api/v1/health (via nginx)"
|
|
||||||
echo " Go worker: ${GO_REPLICAS} replica(s)"
|
|
||||||
echo " Node 8D: ${NODE_REPLICAS} replica(s)"
|
|
||||||
echo " Env: ${ENV_FILE}"
|
|
||||||
echo " Stop: make -C haixun-backend prod-down"
|
|
||||||
echo " Logs: make -C haixun-backend prod-logs"
|
|
||||||
prod_print_volume_hint
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# shellcheck source=scripts/prod-common.sh
|
|
||||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/prod-common.sh"
|
|
||||||
|
|
||||||
prod_load_env
|
|
||||||
prod_require_docker
|
|
||||||
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
|
|
||||||
prod_ensure_deps
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[prod] mongo + redis ready"
|
|
||||||
prod_print_volume_hint
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# shellcheck source=scripts/prod-common.sh
|
|
||||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/prod-common.sh"
|
|
||||||
|
|
||||||
prod_common_init
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
|
|
||||||
prod_compose down --remove-orphans
|
|
||||||
echo "[prod] stopped(Mongo/Redis 資料仍在 named volume,下次 prod / prod-deps 會沿用)"
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
COMPOSE_FILE="$BACKEND_DIR/deploy/docker-compose.prod.yml"
|
|
||||||
|
|
||||||
cd "$BACKEND_DIR/deploy"
|
|
||||||
docker compose -f "$COMPOSE_FILE" logs -f --tail=200 "${@:-}"
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# shellcheck source=scripts/prod-common.sh
|
|
||||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/prod-common.sh"
|
|
||||||
|
|
||||||
prod_load_env
|
|
||||||
prod_require_docker
|
|
||||||
|
|
||||||
cd "$BACKEND_DIR"
|
|
||||||
prod_build_web_if_static
|
|
||||||
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
|
|
||||||
echo "[prod] building images..."
|
|
||||||
prod_compose build
|
|
||||||
|
|
||||||
prod_ensure_deps
|
|
||||||
prod_run_init_if_needed
|
|
||||||
|
|
||||||
prod_start_app_services
|
|
||||||
prod_wait_api_health
|
|
||||||
prod_print_stack_summary
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# 只重建/重啟 API、Web、Workers;不碰 mongo/redis(資料留在 volume)。
|
|
||||||
|
|
||||||
# shellcheck source=scripts/prod-common.sh
|
|
||||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/prod-common.sh"
|
|
||||||
|
|
||||||
prod_load_env
|
|
||||||
prod_require_docker
|
|
||||||
|
|
||||||
cd "$BACKEND_DIR"
|
|
||||||
prod_build_web_if_static
|
|
||||||
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
|
|
||||||
if ! prod_deps_healthy; then
|
|
||||||
echo "[prod] mongo/redis 未在運行,先啟動依賴(不會清 volume)..."
|
|
||||||
prod_ensure_deps
|
|
||||||
else
|
|
||||||
echo "[prod] mongo + redis 維持運行 — 只更新應用層"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[prod] building app images (api, web, workers)..."
|
|
||||||
prod_compose build api web go-worker node-worker-style-8d
|
|
||||||
|
|
||||||
prod_start_app_services
|
|
||||||
prod_wait_api_health
|
|
||||||
prod_print_stack_summary
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# 危險:停止 stack 並刪除 Mongo/Redis named volume。
|
|
||||||
|
|
||||||
# shellcheck source=scripts/prod-common.sh
|
|
||||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/prod-common.sh"
|
|
||||||
|
|
||||||
prod_load_env
|
|
||||||
prod_require_docker
|
|
||||||
|
|
||||||
cd "$DEPLOY_DIR"
|
|
||||||
|
|
||||||
echo "[prod] 這會刪除 haixun-prod_mongo_data 與 haixun-prod_redis_data 內所有資料。"
|
|
||||||
read -r -p "輸入 yes 才會繼續: " confirm
|
|
||||||
if [[ "$confirm" != "yes" ]]; then
|
|
||||||
echo "[prod] cancelled"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
prod_compose down -v --remove-orphans
|
|
||||||
echo "[prod] volumes removed — 下次 make prod 會是全新資料庫"
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
|
|
||||||
bash "$BACKEND_DIR/scripts/stop-all.sh"
|
|
||||||
bash "$BACKEND_DIR/scripts/start-all.sh"
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
BACKEND_DIR="$ROOT_DIR/haixun-backend"
|
|
||||||
RUN_DIR="$BACKEND_DIR/.run"
|
|
||||||
LOG_DIR="$RUN_DIR/logs"
|
|
||||||
COMPOSE_FILE="$BACKEND_DIR/deploy/docker-compose.yml"
|
|
||||||
CONFIG_FILE="${HAIXUN_BACKEND_CONFIG:-etc/gateway.yaml}"
|
|
||||||
BACKEND_URL="${HAIXUN_BACKEND_URL:-http://127.0.0.1:8890}"
|
|
||||||
WEB_URL="${HAIXUN_WEB_URL:-http://127.0.0.1:5173}"
|
|
||||||
|
|
||||||
mkdir -p "$RUN_DIR" "$LOG_DIR"
|
|
||||||
|
|
||||||
bash "$BACKEND_DIR/scripts/stop-all.sh"
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "[start-all] docker not found; skip mongo/redis" >&2
|
|
||||||
else
|
|
||||||
echo "[start-all] starting mongo + redis..."
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[start-all] starting Go API ($CONFIG_FILE)..."
|
|
||||||
(
|
|
||||||
cd "$BACKEND_DIR"
|
|
||||||
go run ./gateway.go -f "$CONFIG_FILE"
|
|
||||||
) >"$LOG_DIR/api.log" 2>&1 &
|
|
||||||
echo $! >"$RUN_DIR/api.pid"
|
|
||||||
|
|
||||||
echo "[start-all] waiting for API health ($BACKEND_URL)..."
|
|
||||||
for _ in $(seq 1 40); do
|
|
||||||
if curl -fsS "$BACKEND_URL/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if ! kill -0 "$(cat "$RUN_DIR/api.pid")" 2>/dev/null; then
|
|
||||||
echo "[start-all] API exited early; see $LOG_DIR/api.log" >&2
|
|
||||||
tail -n 20 "$LOG_DIR/api.log" >&2 || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
if ! curl -fsS "$BACKEND_URL/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
echo "[start-all] API health check timed out; see $LOG_DIR/api.log" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -d "$BACKEND_DIR/web/node_modules" ]]; then
|
|
||||||
echo "[start-all] installing web dependencies..."
|
|
||||||
(cd "$BACKEND_DIR/web" && npm install)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[start-all] starting web dev server..."
|
|
||||||
(
|
|
||||||
cd "$BACKEND_DIR/web"
|
|
||||||
npm run dev
|
|
||||||
) >"$LOG_DIR/web.log" 2>&1 &
|
|
||||||
echo $! >"$RUN_DIR/web.pid"
|
|
||||||
|
|
||||||
echo "[start-all] starting Node 8D worker..."
|
|
||||||
(
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
npm run worker:style-8d
|
|
||||||
) >"$LOG_DIR/worker.log" 2>&1 &
|
|
||||||
echo $! >"$RUN_DIR/worker.pid"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[start-all] all services started"
|
|
||||||
echo " API: $BACKEND_URL"
|
|
||||||
echo " Web: $WEB_URL"
|
|
||||||
echo " Logs: $LOG_DIR/{api,web,worker}.log"
|
|
||||||
echo " Stop: make -C haixun-backend stop-all"
|
|
||||||
echo " Status: make -C haixun-backend status-all"
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
RUN_DIR="$BACKEND_DIR/.run"
|
|
||||||
COMPOSE_FILE="$BACKEND_DIR/deploy/docker-compose.yml"
|
|
||||||
BACKEND_URL="${HAIXUN_BACKEND_URL:-http://127.0.0.1:8890}"
|
|
||||||
WEB_URL="${HAIXUN_WEB_URL:-http://127.0.0.1:5173}"
|
|
||||||
|
|
||||||
check_pid() {
|
|
||||||
local name="$1"
|
|
||||||
local file="$RUN_DIR/${name}.pid"
|
|
||||||
if [[ -f "$file" ]]; then
|
|
||||||
local pid
|
|
||||||
pid="$(cat "$file" 2>/dev/null || true)"
|
|
||||||
if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
||||||
echo " $name: running (pid=$pid)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo " $name: stopped"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Haixun dev services"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if command -v docker >/dev/null 2>&1 && [[ -f "$COMPOSE_FILE" ]]; then
|
|
||||||
echo "Docker:"
|
|
||||||
docker compose -f "$COMPOSE_FILE" ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo " (docker compose not running)"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Processes:"
|
|
||||||
check_pid api || true
|
|
||||||
check_pid web || true
|
|
||||||
check_pid worker || true
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Health:"
|
|
||||||
if curl -fsS "$BACKEND_URL/api/v1/health" >/dev/null 2>&1; then
|
|
||||||
echo " API health: OK ($BACKEND_URL)"
|
|
||||||
else
|
|
||||||
echo " API health: down ($BACKEND_URL)"
|
|
||||||
fi
|
|
||||||
if curl -fsS "$WEB_URL" >/dev/null 2>&1; then
|
|
||||||
echo " Web: OK ($WEB_URL)"
|
|
||||||
else
|
|
||||||
echo " Web: down ($WEB_URL)"
|
|
||||||
fi
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
BACKEND_DIR="$ROOT_DIR/haixun-backend"
|
|
||||||
RUN_DIR="$BACKEND_DIR/.run"
|
|
||||||
COMPOSE_FILE="$BACKEND_DIR/deploy/docker-compose.yml"
|
|
||||||
|
|
||||||
stop_pid_file() {
|
|
||||||
local name="$1"
|
|
||||||
local file="$RUN_DIR/${name}.pid"
|
|
||||||
if [[ ! -f "$file" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local pid
|
|
||||||
pid="$(cat "$file" 2>/dev/null || true)"
|
|
||||||
if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
||||||
echo "[stop-all] stopping $name (pid=$pid)"
|
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
for _ in $(seq 1 10); do
|
|
||||||
kill -0 "$pid" 2>/dev/null || break
|
|
||||||
sleep 0.2
|
|
||||||
done
|
|
||||||
kill -9 "$pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
rm -f "$file"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "[stop-all] stopping tracked processes..."
|
|
||||||
for name in worker web api; do
|
|
||||||
stop_pid_file "$name"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "[stop-all] stopping stray processes..."
|
|
||||||
pkill -f "haixun-backend/worker/style-8d-worker" 2>/dev/null || true
|
|
||||||
pkill -f "worker:style-8d" 2>/dev/null || true
|
|
||||||
pkill -f "haixun-backend/web/node_modules/.bin/vite" 2>/dev/null || true
|
|
||||||
pkill -f "go run ./gateway.go -f etc/gateway.yaml" 2>/dev/null || true
|
|
||||||
# `go run` spawns a compiled binary child under the go-build cache (e.g.
|
|
||||||
# ~/Library/Caches/go-build/.../gateway) that is NOT killed when the parent
|
|
||||||
# wrapper dies; kill the orphan too so it stops serving stale routes on the API
|
|
||||||
# port and frees the port for the freshly built binary.
|
|
||||||
pkill -f "/gateway -f etc/gateway.yaml" 2>/dev/null || true
|
|
||||||
pkill -f "dev-with-style-8d.sh" 2>/dev/null || true
|
|
||||||
|
|
||||||
if command -v docker >/dev/null 2>&1 && [[ -f "$COMPOSE_FILE" ]]; then
|
|
||||||
echo "[stop-all] stopping docker compose (mongo + redis)..."
|
|
||||||
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[stop-all] done"
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# 示範 job 建立與取消:running 時會透過 Redis jobs:cancel:<id> 戳 worker
|
|
||||||
#
|
|
||||||
# 用法:
|
|
||||||
# ./scripts/test-job-cancel.sh
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8890}"
|
|
||||||
SCOPE="${SCOPE:-user}"
|
|
||||||
SCOPE_ID="${SCOPE_ID:-demo_user_1}"
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || { echo "missing command: $1"; exit 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd curl
|
|
||||||
require_cmd jq
|
|
||||||
|
|
||||||
echo "== create demo_long_task =="
|
|
||||||
CREATE_BODY="$(curl -sS -X POST "${BASE_URL}/api/v1/jobs" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n --arg scope "$SCOPE" --arg scope_id "$SCOPE_ID" '{
|
|
||||||
template_type: "demo_long_task",
|
|
||||||
scope: $scope,
|
|
||||||
scope_id: $scope_id,
|
|
||||||
payload: {target: "demo"}
|
|
||||||
}')")"
|
|
||||||
|
|
||||||
echo "$CREATE_BODY" | jq .
|
|
||||||
JOB_ID="$(echo "$CREATE_BODY" | jq -r '.data.id // empty')"
|
|
||||||
if [[ -z "$JOB_ID" ]]; then
|
|
||||||
echo "failed to create job"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "== wait until running (max 10s) =="
|
|
||||||
for _ in $(seq 1 20); do
|
|
||||||
STATUS="$(curl -sS "${BASE_URL}/api/v1/jobs/${JOB_ID}" | jq -r '.data.status')"
|
|
||||||
echo "status: $STATUS"
|
|
||||||
if [[ "$STATUS" == "running" || "$STATUS" == "cancel_requested" ]]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "$STATUS" == "succeeded" || "$STATUS" == "cancelled" ]]; then
|
|
||||||
echo "job finished before cancel test: $STATUS"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "== cancel while worker is active =="
|
|
||||||
CANCEL_BODY="$(curl -sS -X POST "${BASE_URL}/api/v1/jobs/${JOB_ID}/cancel" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"reason":"demo cancel from script"}')"
|
|
||||||
echo "$CANCEL_BODY" | jq .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "== poll until cancelled (max 20s) =="
|
|
||||||
for _ in $(seq 1 40); do
|
|
||||||
BODY="$(curl -sS "${BASE_URL}/api/v1/jobs/${JOB_ID}")"
|
|
||||||
STATUS="$(echo "$BODY" | jq -r '.data.status')"
|
|
||||||
PHASE="$(echo "$BODY" | jq -r '.data.phase')"
|
|
||||||
SUMMARY="$(echo "$BODY" | jq -r '.data.progress.summary')"
|
|
||||||
echo "status=$STATUS phase=$PHASE summary=$SUMMARY"
|
|
||||||
if [[ "$STATUS" == "cancelled" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "ok: worker acknowledged cancel"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "timeout waiting for cancelled status"
|
|
||||||
exit 1
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Verify scheme B: same scope_id+target blocks concurrent runs; different targets allow parallel.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/test-job-concurrency.sh
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8890}"
|
|
||||||
SCOPE="${SCOPE:-user}"
|
|
||||||
SCOPE_ID="${SCOPE_ID:-concurrency_demo_user}"
|
|
||||||
TEMPLATE_TYPE="${TEMPLATE_TYPE:-demo_long_task}"
|
|
||||||
TARGET_A="${TARGET_A:-building_A}"
|
|
||||||
TARGET_B="${TARGET_B:-building_B}"
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || { echo "missing command: $1"; exit 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd curl
|
|
||||||
require_cmd jq
|
|
||||||
|
|
||||||
create_job() {
|
|
||||||
local target="$1"
|
|
||||||
curl -sS -X POST "${BASE_URL}/api/v1/jobs" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n --arg scope "$SCOPE" --arg scope_id "$SCOPE_ID" --arg template "$TEMPLATE_TYPE" --arg target "$target" '{
|
|
||||||
template_type: $template,
|
|
||||||
scope: $scope,
|
|
||||||
scope_id: $scope_id,
|
|
||||||
payload: {target: $target}
|
|
||||||
}')"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "== configure template for scheme B =="
|
|
||||||
PUT_BODY="$(curl -sS -X PUT "${BASE_URL}/api/v1/job/templates/${TEMPLATE_TYPE}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n '{
|
|
||||||
name: "Demo Long Task",
|
|
||||||
enabled: true,
|
|
||||||
repeatable: true,
|
|
||||||
concurrency_policy: "allow_parallel",
|
|
||||||
dedupe_keys: ["scope_id", "target"],
|
|
||||||
timeout_seconds: 600,
|
|
||||||
cancel_policy: {supported: true, mode: "cooperative", grace_seconds: 30},
|
|
||||||
retry_policy: {max_attempts: 2, backoff_seconds: [30, 120]},
|
|
||||||
steps: [
|
|
||||||
{id: "prepare", name: "Prepare", worker_type: "go", timeout_seconds: 60, cancelable: true},
|
|
||||||
{id: "execute", name: "Execute", worker_type: "go", timeout_seconds: 300, cancelable: true},
|
|
||||||
{id: "finalize", name: "Finalize", worker_type: "go", timeout_seconds: 30, cancelable: false}
|
|
||||||
]
|
|
||||||
}')")"
|
|
||||||
echo "$PUT_BODY" | jq .
|
|
||||||
if [[ "$(echo "$PUT_BODY" | jq -r '.code')" != "102000" ]]; then
|
|
||||||
echo "failed to upsert template"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "== same target: first create should succeed =="
|
|
||||||
FIRST="$(create_job "$TARGET_A")"
|
|
||||||
echo "$FIRST" | jq .
|
|
||||||
if [[ "$(echo "$FIRST" | jq -r '.code')" != "102000" ]]; then
|
|
||||||
echo "first create failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "== same target: second create should fail while first is active =="
|
|
||||||
SECOND="$(create_job "$TARGET_A")"
|
|
||||||
echo "$SECOND" | jq .
|
|
||||||
if [[ "$(echo "$SECOND" | jq -r '.code')" == "102000" ]]; then
|
|
||||||
echo "expected duplicate create to fail"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "== different target: should succeed in parallel =="
|
|
||||||
THIRD="$(create_job "$TARGET_B")"
|
|
||||||
echo "$THIRD" | jq .
|
|
||||||
if [[ "$(echo "$THIRD" | jq -r '.code')" != "102000" ]]; then
|
|
||||||
echo "different target create failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "ok: scheme B concurrency behaves as expected"
|
|
||||||
Loading…
Reference in New Issue