diff --git a/backend/internal/types/types.go b/backend/internal/types/types.go index 539c7c4..47efae0 100644 --- a/backend/internal/types/types.go +++ b/backend/internal/types/types.go @@ -1305,6 +1305,28 @@ type ThreadsAccountAiSettingsData struct { ApiKeysConfigured map[string]interface{} `json:"api_keys_configured"` } +type ThreadsAccountAiProviderPath struct { + ID string `path:"id" validate:"required"` + Provider string `path:"provider" validate:"required,oneof=opencode-go xai"` +} + +type ListThreadsAccountAiProviderModelsReq struct { + ApiKey string `json:"api_key,optional"` +} + +type ThreadsAccountAiProviderModelsHandlerReq struct { + ThreadsAccountAiProviderPath + ListThreadsAccountAiProviderModelsReq +} + +type ThreadsAccountAiProviderModelsData struct { + ID string `json:"id"` + Label string `json:"label"` + Models []string `json:"models"` + Streams bool `json:"streams"` + Error string `json:"error,optional"` +} + type ThreadsAccountConnectionData struct { AccountID string `json:"account_id"` AccountName string `json:"account_name"` diff --git a/infra/.env.example b/infra/.env.example new file mode 100644 index 0000000..4780b12 --- /dev/null +++ b/infra/.env.example @@ -0,0 +1,20 @@ +# Infra docker compose 環境變數範本 +# 複製成 infra/.env 後填入實際值;.env 不要 commit。 + +# --- Mongo --- +MONGO_PORT=27017 +MONGO_ROOT_USER=haixun +MONGO_ROOT_PASSWORD=1qaz@WSX3edc$RFV +MONGO_DATABASE=haixun + +# --- Redis --- +REDIS_PORT=6379 +REDIS_PASSWORD=change-me-redis-pass + +INIT_ADMIN_EMAIL=admin@30cm.net +PASSWORD=Fafafafa54088 + +HAIXUN_JWT_ACCESS_SECRET=1qaz@WSX3edc$RFV +HAIXUN_JWT_REFRESH_SECRET=1qaz@WSX3edc$RFV +HAIXUN_SECRETS_KEY=1qaz@WSX3edc$RFV +HAIXUN_WORKER_SECRET=1qaz@WSX3edc$RFV diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..afb57fd --- /dev/null +++ b/infra/README.md @@ -0,0 +1,103 @@ +# 巡樓部署 (infra) + +部署拓樸: + +``` +瀏覽器 → nginx(systemd, :80/:443) + ├─ 靜態前端 /var/www/haixun (frontend/dist) + └─ /api 反向代理 → Go gateway (systemd, 127.0.0.1:8890) +Go gateway / Go worker (systemd) → Mongo / Redis (docker compose, 綁 127.0.0.1) +Node playwright worker (systemd) → 透過 HTTP 打 gateway +``` + +- 資料服務(Mongo/Redis)用 docker compose,只綁 `127.0.0.1`。 +- Go gateway / Go worker / Node worker 都是 systemd 原生服務。 +- secret 一律放 `/opt/haixun/etc/haixun.env`(不進 repo),yaml 用 `${VAR}` 讀取。 + +## 目錄 + +``` +infra/ + docker-compose.yml # mongo + redis + .env.example # compose 用環境變數 + etc/haixun.env.example # systemd EnvironmentFile 範本(secret) + nginx/haixun.conf # 靜態前端 + /api 反代 + SSE + systemd/ + haixun-gateway.service + haixun-worker.service + haixun-node-worker.service +``` + +## 1. 起資料服務 (docker) + +```bash +cd infra +cp .env.example .env # 填入 Mongo/Redis 密碼 +docker compose --env-file .env up -d +docker compose ps +``` + +## 2. 建置產物(本機或 CI) + +```bash +make build # 前端 dist + 兩個 linux Go binary(backend/bin/) +``` + +## 3. 安裝到目標主機 + +於目標主機(需 root): + +```bash +sudo make install +``` + +`make install` 會: + +1. 建立使用者 `haixun` 與目錄 `/opt/haixun/{bin,etc,node-worker}`、`/var/www/haixun`。 +2. 複製 `backend/bin/{gateway,worker}`、`backend/etc/gateway.prod.yaml`、`backend/etc/gateway.worker.prod.yaml`。 +3. 複製 `frontend/dist/*` → `/var/www/haixun`。 +4. 複製 `backend/worker/*`(Node worker)→ `/opt/haixun/node-worker`,並 `npm ci` + `npx playwright install`。 +5. 安裝 `infra/systemd/*.service` 與 `infra/nginx/haixun.conf`。 + +接著手動建立 secret 檔(**只做一次**): + +```bash +sudo cp infra/etc/haixun.env.example /opt/haixun/etc/haixun.env +sudo chmod 600 /opt/haixun/etc/haixun.env +sudoedit /opt/haixun/etc/haixun.env # 填入實際 secret +``` + +## 4. 初始化資料庫與 admin 帳號(只做一次) + +Mongo 起來、secret 填好後,建立索引 / 權限 catalog / role_permissions,並建立第一個 admin: + +```bash +# 可在 haixun.env 內設定 INIT_ADMIN_EMAIL / INIT_ADMIN_PASSWORD,或在這裡用環境變數覆寫 +sudo make prod-init +# 等同:source /opt/haixun/etc/haixun.env 後執行 /opt/haixun/bin/tool init -f /opt/haixun/etc/gateway.prod.yaml +``` + +之後一般使用者可走 `POST /api/v1/auth/register` 自助註冊(前端登入頁)。 + +## 5. 啟用服務 + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now haixun-gateway haixun-worker haixun-node-worker +sudo nginx -t && sudo systemctl reload nginx +``` + +## 6. 健康檢查 + +```bash +curl http://127.0.0.1:8890/api/v1/health +sudo systemctl status haixun-gateway haixun-worker haixun-node-worker +journalctl -u haixun-gateway -f +``` + +## 產生 secret + +```bash +openssl rand -base64 48 # JWT access / refresh / worker secret +openssl rand -base64 32 # HAIXUN_SECRETS_KEY(機敏資料落地加密) +``` diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..fec1b16 --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,42 @@ +# 巡樓資料服務:Mongo + Redis +# 只綁 127.0.0.1,給同主機上以 systemd 跑的 Go gateway / worker 連線。 +# 啟動:docker compose -f infra/docker-compose.yml --env-file infra/.env up -d +name: haixun-infra + +services: + mongo: + image: mongo:7 + restart: unless-stopped + ports: + - "127.0.0.1:${MONGO_PORT:-27017}:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-haixun} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD is required} + MONGO_INITDB_DATABASE: ${MONGO_DATABASE:-haixun} + volumes: + - mongo_data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s + + redis: + image: redis:7 + restart: unless-stopped + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}", "--appendonly", "yes"] + ports: + - "127.0.0.1:${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +volumes: + mongo_data: + redis_data: diff --git a/infra/etc/haixun.env.example b/infra/etc/haixun.env.example new file mode 100644 index 0000000..903366c --- /dev/null +++ b/infra/etc/haixun.env.example @@ -0,0 +1,23 @@ +# 部署到目標主機的 /opt/haixun/etc/haixun.env(chmod 600,不要 commit 實值) +# gateway.prod.yaml / gateway.worker.yaml 用 ${VAR} 讀取這些值(go-zero conf.UseEnv)。 + +# Mongo(含 docker compose 設定的帳密;authSource=admin) +HAIXUN_MONGO_URI=mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin +HAIXUN_MONGO_DB=haixun + +# Redis +HAIXUN_REDIS_ADDR=127.0.0.1:6379 +HAIXUN_REDIS_PASSWORD=change-me-redis-pass + +# JWT secret(請用 openssl rand -base64 48 產生,兩把不同) +HAIXUN_JWT_ACCESS_SECRET=replace-with-strong-random +HAIXUN_JWT_REFRESH_SECRET=replace-with-another-strong-random + +# 內部 worker secret(gateway 與 node worker 必須一致) +HAIXUN_WORKER_SECRET=replace-with-strong-random + +# 機敏資料落地加密金鑰(base64 編碼的 32 bytes;openssl rand -base64 32) +HAIXUN_SECRETS_KEY=replace-with-base64-32-bytes + +# Node worker 連線設定 +HAIXUN_BACKEND_URL=http://127.0.0.1:8890 diff --git a/infra/nginx/haixun.conf b/infra/nginx/haixun.conf new file mode 100644 index 0000000..69db9e0 --- /dev/null +++ b/infra/nginx/haixun.conf @@ -0,0 +1,56 @@ +# 巡樓 Console nginx 設定 +# 安裝:cp infra/nginx/haixun.conf /etc/nginx/conf.d/haixun.conf && nginx -t && systemctl reload nginx +# 前端靜態檔部署在 /var/www/haixun(make install 會放 frontend/dist 內容)。 +# /api 反向代理到本機 systemd 跑的 Go gateway (127.0.0.1:8890),含 SSE 串流設定。 + +upstream haixun_gateway { + server 127.0.0.1:8890; + keepalive 32; +} + +server { + listen 80; + listen [::]:80; + server_name _; + + root /var/www/haixun; + index index.html; + + # 安全標頭 + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header X-XSS-Protection "0" always; + + # 靜態資源快取(vite build 帶 hash 檔名) + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # API 反向代理(一般 JSON 與 SSE 共用) + location /api/ { + proxy_pass http://haixun_gateway; + 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; + + # Authorization(provider token)與 X-Member-Authorization(會員 JWT)由 nginx 預設轉發; + # 以下確保 SSE 串流不被緩衝、長連線不被提前中斷。 + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # SPA:所有非檔案路徑都回 index.html,交給 react-router + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/infra/systemd/haixun-gateway.service b/infra/systemd/haixun-gateway.service new file mode 100644 index 0000000..9030f3f --- /dev/null +++ b/infra/systemd/haixun-gateway.service @@ -0,0 +1,25 @@ +[Unit] +Description=Haixun Gateway API (go-zero) +After=network-online.target docker.service +Wants=network-online.target + +[Service] +Type=simple +User=haixun +Group=haixun +WorkingDirectory=/opt/haixun +# secrets(JWT / Mongo URI / Redis 密碼 / worker secret / 加密金鑰)放這裡,不進 repo +EnvironmentFile=/opt/haixun/etc/haixun.env +ExecStart=/opt/haixun/bin/gateway -f /opt/haixun/etc/gateway.prod.yaml +Restart=always +RestartSec=5 +LimitNOFILE=65535 + +# 加固 +NoNewPrivileges=true +ProtectSystem=full +ProtectHome=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/infra/systemd/haixun-node-worker.service b/infra/systemd/haixun-node-worker.service new file mode 100644 index 0000000..7221767 --- /dev/null +++ b/infra/systemd/haixun-node-worker.service @@ -0,0 +1,23 @@ +[Unit] +Description=Haixun Node Playwright Worker (style-8d) +After=network-online.target haixun-gateway.service +Wants=network-online.target + +[Service] +Type=simple +User=haixun +Group=haixun +WorkingDirectory=/opt/haixun/node-worker +# 至少需要 HAIXUN_BACKEND_URL 與 HAIXUN_WORKER_SECRET(與 gateway 的 InternalWorker.Secret 一致) +EnvironmentFile=/opt/haixun/etc/haixun.env +ExecStart=/usr/bin/npx tsx style-8d-worker.ts +Restart=always +RestartSec=10 +LimitNOFILE=65535 + +NoNewPrivileges=true +ProtectSystem=full +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/infra/systemd/haixun-worker.service b/infra/systemd/haixun-worker.service new file mode 100644 index 0000000..df7a3e6 --- /dev/null +++ b/infra/systemd/haixun-worker.service @@ -0,0 +1,23 @@ +[Unit] +Description=Haixun Go Job Worker +After=network-online.target docker.service haixun-gateway.service +Wants=network-online.target + +[Service] +Type=simple +User=haixun +Group=haixun +WorkingDirectory=/opt/haixun +EnvironmentFile=/opt/haixun/etc/haixun.env +ExecStart=/opt/haixun/bin/worker -f /opt/haixun/etc/gateway.worker.prod.yaml +Restart=always +RestartSec=5 +LimitNOFILE=65535 + +NoNewPrivileges=true +ProtectSystem=full +ProtectHome=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/start-prod.sh b/start-prod.sh new file mode 100755 index 0000000..a25f15a --- /dev/null +++ b/start-prod.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -e + +echo "=== 巡樓 Production 啟動 ===" + +# 1. 確認 nginx 不會被 default site 搶走 +echo "[1/7] 確保 nginx config..." +if [ -f /etc/nginx/sites-enabled/default ]; then + sudo rm /etc/nginx/sites-enabled/default +fi + +# 2. 起 infra (Mongo + Redis) +echo "[2/7] 啟動 Mongo + Redis..." +docker compose -f infra/docker-compose.yml --env-file infra/.env up -d + +# 3. 確保 Playwright 瀏覽器已安裝 +echo "[3/7] 確認 Playwright chromium..." +if [ ! -d /opt/haixun/.cache/ms-playwright/chromium-1228 ]; then + sudo -u haixun bash -c 'cd /opt/haixun/node-worker && npx playwright install chromium' +fi + +# 4. 清除 systemd 失敗計數(之前 crash loop 的話需要 reset) +echo "[4/7] 重置 systemd 狀態..." +for svc in haixun-gateway haixun-worker haixun-node-worker; do + sudo systemctl reset-failed "$svc" 2>/dev/null || true +done + +# 5. 啟動 systemd 服務 +echo "[5/7] 啟動 backend gateway..." +sudo systemctl start haixun-gateway +echo "[5/7] 啟動 worker..." +sudo systemctl start haixun-worker +echo "[5/7] 啟動 node worker..." +sudo systemctl start haixun-node-worker + +# 6. 初始化 DB(indexes + admin,冪等) +echo "[6/7] 初始化 DB(admin: admin@30cm.net)..." +set -a; source /opt/haixun/etc/haixun.env; set +a +sudo -E /opt/haixun/bin/tool init -f /opt/haixun/etc/gateway.prod.yaml + +# 7. 確認 nginx 執行 + reload config +echo "[7/7] 確認 nginx..." +sudo nginx -t && sudo systemctl start nginx && sudo systemctl reload nginx + +echo "" +echo "=== 啟動完成 ===" +echo " nginx: $(systemctl is-active nginx)" +echo " gateway: $(systemctl is-active haixun-gateway)" +echo " worker: $(systemctl is-active haixun-worker)" +echo " node-wkr: $(systemctl is-active haixun-node-worker)" +echo " docker: $(docker compose -f infra/docker-compose.yml ps --status running 2>/dev/null | wc -l) containers running" +echo "" +echo "健康檢查:" +echo " curl http://localhost/api/v1/health" +echo "" +echo "查看日誌:" +echo " journalctl -u haixun-gateway -n 50 --no-pager -f" +echo " journalctl -u haixun-worker -n 50 --no-pager -f" +echo " docker compose -f infra/docker-compose.yml logs -f" diff --git a/status-prod.sh b/status-prod.sh new file mode 100755 index 0000000..f0c0cc6 --- /dev/null +++ b/status-prod.sh @@ -0,0 +1,20 @@ +#!/bin/bash +echo "=== 巡樓 Production 狀態 ===" +echo "" +echo "--- Nginx ---" +systemctl status nginx --no-pager -n 5 2>&1 | head -5 +echo "" +echo "--- Gateway ---" +systemctl status haixun-gateway --no-pager -n 5 2>&1 | head -5 +echo "" +echo "--- Worker ---" +systemctl status haixun-worker --no-pager -n 5 2>&1 | head -5 +echo "" +echo "--- Node Worker ---" +systemctl status haixun-node-worker --no-pager -n 5 2>&1 | head -5 +echo "" +echo "--- Docker Infra ---" +docker compose -f /home/daniel/thread-master/infra/docker-compose.yml ps 2>&1 +echo "" +echo "--- Port 8890 ---" +ss -tlnp | grep 8890 || echo "gateway not listening" diff --git a/stop-prod.sh b/stop-prod.sh new file mode 100755 index 0000000..daca685 --- /dev/null +++ b/stop-prod.sh @@ -0,0 +1,6 @@ +#!/bin/bash +echo "=== 巡樓 Production 停止 ===" +sudo systemctl stop haixun-worker +sudo systemctl stop haixun-gateway +docker compose -f /home/daniel/thread-master/infra/docker-compose.yml down +echo "done"