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