From 27a8a71d00d4e613046637637c6c5fc2a2a026a1 Mon Sep 17 00:00:00 2001 From: wang Date: Mon, 6 Jul 2026 03:16:29 +0000 Subject: [PATCH] fix --- Makefile | 53 +++++++-- backend/Makefile | 4 + backend/internal/bootstrap/admin.go | 12 ++ backend/internal/bootstrap/admin_test.go | 78 +++++++++++++ backend/internal/library/style8d/analyze.go | 19 ++- .../job/analyze_style8_d_from_worker_logic.go | 2 + ..._persona_style_analysis_from_text_logic.go | 2 + .../model/member/domain/repository/member.go | 1 + .../internal/model/member/repository/mongo.go | 21 ++++ deploy/.env.prod.example | 10 +- deploy/docker-compose.yml | 9 ++ deploy/setup.sh | 108 ++++++++++++++++++ infra/docker-compose.yml | 2 +- 13 files changed, 307 insertions(+), 14 deletions(-) create mode 100755 deploy/setup.sh diff --git a/Makefile b/Makefile index c81c64b..d322066 100644 --- a/Makefile +++ b/Makefile @@ -13,9 +13,13 @@ DEPLOY_COMPOSE := docker compose --env-file $(DEPLOY_DIR)/.env.prod -f $(DEPLOY_ .PHONY: help help: ## 顯示可用指令 - @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + @grep -E '^[a-zA-Z0-9_-]+:.*## .*$$' $(MAKEFILE_LIST) \ | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' +.PHONY: setup-env +setup-env: ## 互動式產生 deploy/.env.prod(自動 URL-encode MongoDB 密碼) + @$(DEPLOY_DIR)/setup.sh + .PHONY: gen-api gen-api: ## 重新產生後端 API handler / logic / types $(MAKE) -C $(BACKEND_DIR) gen-api @@ -28,6 +32,10 @@ fmt: ## 格式化後端 Go 程式 test: ## 執行後端測試 $(MAKE) -C $(BACKEND_DIR) test +.PHONY: init +init: ## 初始化系統(建索引、種權限、建立 admin,讀 deploy/.env.prod) + $(MAKE) -C $(BACKEND_DIR) ENV_FILE=../deploy/.env.prod init + .PHONY: run run: ## 啟動後端 API $(MAKE) -C $(BACKEND_DIR) run @@ -62,22 +70,42 @@ dev-infra: ## 啟動本機 dev 資料服務(Mongo / Redis 容器,讀 deploy/ dev-infra-down: ## 關閉本機 dev 資料服務(保留資料 volume) $(INFRA_DEV_COMPOSE) down -.PHONY: prod-install -prod-install: ## 一鍵安裝 prod:複製 .env.prod、啟動 infra、init、啟動 app +.PHONY: dev-infra-recreate +dev-infra-recreate: ## 清除 dev 資料 volume 並重啟(Mongo / Redis 全新狀態) + $(INFRA_DEV_COMPOSE) down -v + $(INFRA_DEV_COMPOSE) up -d + $(INFRA_DEV_COMPOSE) ps + +.PHONY: prod-infra +prod-infra: ## 啟動 prod 資料服務(Mongo / Redis,讀 deploy/.env.prod) @test -f $(DEPLOY_DIR)/.env.prod || cp $(DEPLOY_DIR)/.env.prod.example $(DEPLOY_DIR)/.env.prod @echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.prod 再執行後續指令" >&2 $(INFRA_PROD_COMPOSE) up -d $(INFRA_PROD_COMPOSE) ps - $(DEPLOY_COMPOSE) pull + +.PHONY: prod-infra-down +prod-infra-down: ## 關閉 prod 資料服務(保留資料 volume) + $(INFRA_PROD_COMPOSE) down + +.PHONY: prod-infra-recreate +prod-infra-recreate: ## 清除 prod 資料 volume 並重啟(Mongo / Redis 全新狀態) + $(INFRA_PROD_COMPOSE) down -v + $(INFRA_PROD_COMPOSE) up -d + $(INFRA_PROD_COMPOSE) ps + +.PHONY: prod-install +prod-install: ## 一鍵部署 prod:複製 .env.prod、build 本地 image、啟動 app、init(需先啟動 infra) + @test -f $(DEPLOY_DIR)/.env.prod || cp $(DEPLOY_DIR)/.env.prod.example $(DEPLOY_DIR)/.env.prod + @echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.prod 再執行後續指令" >&2 + @echo ">>> 請確認 infra 已啟動(make prod-infra)" >&2 + $(DEPLOY_COMPOSE) build $(DEPLOY_COMPOSE) up -d $(DEPLOY_COMPOSE) --profile init run --rm init $(DEPLOY_COMPOSE) ps .PHONY: prod-rebuild -prod-rebuild: ## 重新拉取 image 並重建 prod stack(infra + app) - $(INFRA_PROD_COMPOSE) pull - $(INFRA_PROD_COMPOSE) up -d - $(DEPLOY_COMPOSE) pull +prod-rebuild: ## 重新 build 本地 image 並重建 prod app stack(infra 不影響) + $(DEPLOY_COMPOSE) build $(DEPLOY_COMPOSE) up -d $(DEPLOY_COMPOSE) ps @@ -89,3 +117,12 @@ prod-stop: ## 關閉 prod app stack(保留 infra 與資料) prod-stop-all: ## 關閉 prod app + infra(保留資料 volume) $(DEPLOY_COMPOSE) down $(INFRA_PROD_COMPOSE) down + +.PHONY: prod-recreate +prod-recreate: ## 完整清除 prod 全部資料 volume + 重建啟動(app + infra + init) + $(DEPLOY_COMPOSE) down -v + $(INFRA_PROD_COMPOSE) down -v + $(INFRA_PROD_COMPOSE) up -d + $(DEPLOY_COMPOSE) up -d + $(DEPLOY_COMPOSE) --profile init run --rm init + $(DEPLOY_COMPOSE) ps diff --git a/backend/Makefile b/backend/Makefile index 2be65f9..d9ca395 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -13,6 +13,7 @@ export HAIXUN_MONGO_URI HAIXUN_MONGO_DB HAIXUN_REDIS_ADDR HAIXUN_REDIS_PASSWORD export HAIXUN_JWT_ACCESS_SECRET HAIXUN_JWT_REFRESH_SECRET HAIXUN_WORKER_SECRET export HAIXUN_SECRETS_KEY HAIXUN_BACKEND_URL export HAIXUN_WORKER_POLL_MS HAIXUN_8D_TARGET_SAMPLES PLAYWRIGHT_HEADLESS +export INIT_TENANT_ID INIT_ADMIN_EMAIL INIT_ADMIN_PASSWORD INIT_ADMIN_DISPLAY_NAME endif export HAIXUN_JOB_WORKER_TYPE ?= go-demo @@ -36,6 +37,9 @@ run: run-dev: go run gateway.go -f etc/gateway.yaml +init: + go run ./cmd/tool/main.go init -f etc/gateway.yaml + web-dev: cd web && npm install && npm run dev diff --git a/backend/internal/bootstrap/admin.go b/backend/internal/bootstrap/admin.go index a820eae..2ab36d3 100644 --- a/backend/internal/bootstrap/admin.go +++ b/backend/internal/bootstrap/admin.go @@ -36,6 +36,18 @@ func EnsureAdminMember(ctx context.Context, repo domrepo.Repository, opts AdminO return nil, false, err } existing.Roles = []string{"admin"} + + if err := bcrypt.CompareHashAndPassword([]byte(existing.PasswordHash), []byte(opts.Password)); err != nil { + hash, hashErr := bcrypt.GenerateFromPassword([]byte(opts.Password), bcrypt.DefaultCost) + if hashErr != nil { + return nil, false, app.For(code.Member).SysInternal("rehash password failed").WithCause(hashErr) + } + if updateErr := repo.UpdatePassword(ctx, tenantID, existing.UID, string(hash)); updateErr != nil { + return nil, false, updateErr + } + existing.PasswordHash = string(hash) + } + return existing, false, nil } if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound { diff --git a/backend/internal/bootstrap/admin_test.go b/backend/internal/bootstrap/admin_test.go index b36df77..06b0f20 100644 --- a/backend/internal/bootstrap/admin_test.go +++ b/backend/internal/bootstrap/admin_test.go @@ -68,6 +68,16 @@ func (m *memoryMemberRepo) SetActiveThreadsAccountID(_ context.Context, tenantID return app.For(code.Member).ResNotFound("member not found") } +func (m *memoryMemberRepo) UpdatePassword(_ context.Context, tenantID, uid, passwordHash string) error { + for _, item := range m.byEmail { + if item.TenantID == tenantID && item.UID == uid { + item.PasswordHash = passwordHash + return nil + } + } + return app.For(code.Member).ResNotFound("member not found") +} + func (m *memoryMemberRepo) SetRoles(_ context.Context, tenantID, uid string, roles []string) error { for _, item := range m.byEmail { if item.TenantID == tenantID && item.UID == uid { @@ -123,4 +133,72 @@ func TestEnsureAdminMemberUpgradesExisting(t *testing.T) { if len(member.Roles) != 1 || member.Roles[0] != "admin" { t.Fatalf("roles=%v", member.Roles) } + if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("Admin-Pass-1!")); err != nil { + t.Fatalf("password should have been synced: %v", err) + } +} + +func TestEnsureAdminMemberSyncsPasswordWhenChanged(t *testing.T) { + oldHash, err := bcrypt.GenerateFromPassword([]byte("Old-Pass-1!"), bcrypt.DefaultCost) + if err != nil { + t.Fatal(err) + } + repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{ + memberKey("default", "admin@haixun.local"): { + TenantID: "default", + UID: "uid-1", + Email: "admin@haixun.local", + Roles: []string{"admin"}, + PasswordHash: string(oldHash), + }, + }} + _, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{ + TenantID: "default", + Email: "admin@haixun.local", + Password: "New-Pass-2@", + }) + if err != nil { + t.Fatalf("EnsureAdminMember: %v", err) + } + if created { + t.Fatal("expected created=false") + } + member := repo.byEmail[memberKey("default", "admin@haixun.local")] + if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("Old-Pass-1!")); err == nil { + t.Fatal("password should have been updated, but old hash still matches") + } + if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte("New-Pass-2@")); err != nil { + t.Fatalf("new password hash does not match: %v", err) + } +} + +func TestEnsureAdminMemberKeepsPasswordWhenSame(t *testing.T) { + hash, err := bcrypt.GenerateFromPassword([]byte("Same-Pass-1!"), bcrypt.DefaultCost) + if err != nil { + t.Fatal(err) + } + repo := &memoryMemberRepo{byEmail: map[string]*entity.Member{ + memberKey("default", "admin@haixun.local"): { + TenantID: "default", + UID: "uid-1", + Email: "admin@haixun.local", + Roles: []string{"admin"}, + PasswordHash: string(hash), + }, + }} + _, created, err := EnsureAdminMember(context.Background(), repo, AdminOptions{ + TenantID: "default", + Email: "admin@haixun.local", + Password: "Same-Pass-1!", + }) + if err != nil { + t.Fatalf("EnsureAdminMember: %v", err) + } + if created { + t.Fatal("expected created=false") + } + member := repo.byEmail[memberKey("default", "admin@haixun.local")] + if member.PasswordHash != string(hash) { + t.Fatal("password hash should not change when password is the same") + } } diff --git a/backend/internal/library/style8d/analyze.go b/backend/internal/library/style8d/analyze.go index 8e2746a..9d75a6e 100644 --- a/backend/internal/library/style8d/analyze.go +++ b/backend/internal/library/style8d/analyze.go @@ -1,6 +1,7 @@ package style8d import ( + "bytes" "encoding/json" "fmt" "math" @@ -440,7 +441,23 @@ func extractJSONObject(raw string) ([]byte, error) { if start < 0 || end <= start { return nil, fmt.Errorf("LLM response does not contain JSON object") } - return []byte(text[start : end+1]), nil + return repairTruncatedJSON([]byte(text[start : end+1])), nil +} + +// repairTruncatedJSON adds missing closing braces/brackets when the LLM output +// was cut off mid-JSON. Returns nil if no repair was needed or possible. +func repairTruncatedJSON(raw []byte) []byte { + opens := bytes.Count(raw, []byte("{")) + closes := bytes.Count(raw, []byte("}")) + if closes < opens { + raw = append(raw, bytes.Repeat([]byte("}"), opens-closes)...) + } + bracketOpens := bytes.Count(raw, []byte("[")) + bracketCloses := bytes.Count(raw, []byte("]")) + if bracketCloses < bracketOpens { + raw = append(raw, bytes.Repeat([]byte("]"), bracketOpens-bracketCloses)...) + } + return raw } func parseDimension(root map[string]json.RawMessage, keys ...string) Dimension { diff --git a/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go b/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go index 927fb33..c51bf43 100644 --- a/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go +++ b/backend/internal/logic/job/analyze_style8_d_from_worker_logic.go @@ -99,6 +99,7 @@ func (l *AnalyzeStyle8DFromWorkerLogic) AnalyzeStyle8DFromWorker(req *types.Anal if err != nil { return nil, app.For(code.AI).SysInternal("prompt config load failed") } + maxTokens := 8192 result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, @@ -109,6 +110,7 @@ func (l *AnalyzeStyle8DFromWorkerLogic) AnalyzeStyle8DFromWorker(req *types.Anal Messages: []domai.Message{ {Role: "user", Content: input.BuildUserPrompt()}, }, + MaxTokens: &maxTokens, }) if err != nil { if strings.Contains(err.Error(), "HTTP 401") { diff --git a/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go b/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go index 969013d..e806988 100644 --- a/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go +++ b/backend/internal/logic/persona/start_persona_style_analysis_from_text_logic.go @@ -83,6 +83,7 @@ func (l *StartPersonaStyleAnalysisFromTextLogic) StartPersonaStyleAnalysisFromTe if err != nil { return nil, app.For(code.AI).SysInternal("prompt config load failed") } + maxTokens := 8192 result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{ Provider: providerID, Model: credential.Model, @@ -93,6 +94,7 @@ func (l *StartPersonaStyleAnalysisFromTextLogic) StartPersonaStyleAnalysisFromTe Messages: []domai.Message{ {Role: "user", Content: input.BuildUserPrompt()}, }, + MaxTokens: &maxTokens, }) if err != nil { if strings.Contains(err.Error(), "HTTP 401") { diff --git a/backend/internal/model/member/domain/repository/member.go b/backend/internal/model/member/domain/repository/member.go index e4734f7..d01a182 100644 --- a/backend/internal/model/member/domain/repository/member.go +++ b/backend/internal/model/member/domain/repository/member.go @@ -22,4 +22,5 @@ type Repository interface { UpdateProfile(ctx context.Context, tenantID, uid string, update ProfileUpdate) (*entity.Member, error) SetRoles(ctx context.Context, tenantID, uid string, roles []string) error SetActiveThreadsAccountID(ctx context.Context, tenantID, uid, accountID string) error + UpdatePassword(ctx context.Context, tenantID, uid, passwordHash string) error } diff --git a/backend/internal/model/member/repository/mongo.go b/backend/internal/model/member/repository/mongo.go index d28ef09..162ddd2 100644 --- a/backend/internal/model/member/repository/mongo.go +++ b/backend/internal/model/member/repository/mongo.go @@ -148,6 +148,27 @@ func (r *mongoRepository) UpdateProfile(ctx context.Context, tenantID, uid strin return &out, err } +func (r *mongoRepository) UpdatePassword(ctx context.Context, tenantID, uid, passwordHash string) error { + if r.collection == nil { + return app.For(code.Member).DBUnavailable("Mongo is not configured") + } + if tenantID == "" || uid == "" { + return app.For(code.Member).InputMissingRequired("tenant_id and uid are required") + } + res, err := r.collection.UpdateOne( + ctx, + bson.M{"tenant_id": tenantID, "uid": uid}, + bson.M{"$set": bson.M{"password_hash": passwordHash, "update_at": clock.NowUnixNano()}}, + ) + if err != nil { + return err + } + if res.MatchedCount == 0 { + return app.For(code.Member).ResNotFound("member not found") + } + return nil +} + func (r *mongoRepository) findOne(ctx context.Context, filter bson.M) (*entity.Member, error) { if r.collection == nil { return nil, app.For(code.Member).DBUnavailable("Mongo is not configured") diff --git a/deploy/.env.prod.example b/deploy/.env.prod.example index 8da4bc5..2e3d859 100644 --- a/deploy/.env.prod.example +++ b/deploy/.env.prod.example @@ -1,5 +1,8 @@ # 巡樓 Prod 環境(deploy/docker-compose.yml 讀這份) -# 複製成 deploy/.env.prod 再填值;.env.prod 不要 commit。 +# 不要手動編輯!請執行以下指令自動產生: +# make setup-env +# 或直接執行: +# bash deploy/setup.sh # --- infra compose(Mongo / Redis 容器,與下方 HAIXUN_MONGO_URI / HAIXUN_REDIS_PASSWORD 密碼必須一致)--- MONGO_PORT=27017 @@ -20,12 +23,11 @@ NODE_WORKER_TAG=v0.0.1 WEB_PORT=80 -HAIXUN_MONGO_URI=mongodb://haixun:CHANGE_ME_URL_ENCODED@mongo:27017/?authSource=admin +# ⚠ 以下由 setup.sh 自動填入,手動改很容易忘記 URL-encode +HAIXUN_MONGO_URI=mongodb://haixun:CHANGE_ME@mongo:27017/?authSource=admin HAIXUN_MONGO_DB=haixun HAIXUN_REDIS_ADDR=redis:6379 HAIXUN_REDIS_PASSWORD=CHANGE_ME -# ⚠ HAIXUN_MONGO_URI 的密碼必須 URL-encode 且等於上方 MONGO_ROOT_PASSWORD; -# HAIXUN_REDIS_PASSWORD 必須等於上方 REDIS_PASSWORD。 # openssl rand -base64 48 HAIXUN_JWT_ACCESS_SECRET=CHANGE_ME diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 2cac27e..365bd05 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -40,6 +40,9 @@ x-worker-service: &worker-service services: web: + build: + context: .. + dockerfile: backend/web/Dockerfile image: ${WEB_IMAGE:-ghcr.io/kupi-cat/haixun-web}:${WEB_TAG:-v0.0.1} restart: unless-stopped ports: @@ -50,6 +53,9 @@ services: - app gateway: + build: + context: .. + dockerfile: backend/Dockerfile image: ${BACKEND_IMAGE:-ghcr.io/kupi-cat/haixun-backend}:${BACKEND_TAG:-v0.0.1} restart: unless-stopped command: ["/app/gateway", "-f", "/app/etc/gateway.prod.yaml"] @@ -175,6 +181,9 @@ services: HAIXUN_JOB_WORKER_ID: go-refill-publish-inventory node-worker-style-8d: + build: + context: .. + dockerfile: backend/worker/Dockerfile image: ${NODE_WORKER_IMAGE:-ghcr.io/kupi-cat/haixun-node-worker}:${NODE_WORKER_TAG:-v0.0.1} restart: unless-stopped command: ["npm", "run", "style-8d"] diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100755 index 0000000..7380624 --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT="$SCRIPT_DIR/.env.prod" + +echo "==============================================" +echo " 巡樓 Prod 環境設定" +echo "==============================================" +echo "" + +# --- Mongo --- +read -p "Mongo root password (留空自動產生): " MONGO_ROOT_PASSWORD +if [ -z "$MONGO_ROOT_PASSWORD" ]; then + MONGO_ROOT_PASSWORD=$(openssl rand -base64 32) + echo " 自動產生: $MONGO_ROOT_PASSWORD" +fi +MONGO_ROOT_PASSWORD_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MONGO_ROOT_PASSWORD', safe=''))") + +# --- Redis --- +read -p "Redis password (留空自動產生): " REDIS_PASSWORD +if [ -z "$REDIS_PASSWORD" ]; then + REDIS_PASSWORD=$(openssl rand -base64 32) + echo " 自動產生: $REDIS_PASSWORD" +fi + +# --- Admin --- +read -p "Admin email [admin@example.com]: " ADMIN_EMAIL +ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} + +while true; do + read -sp "Admin password (至少 8 字): " ADMIN_PASSWORD + echo "" + if [ ${#ADMIN_PASSWORD} -ge 8 ]; then + break + fi + echo " 密碼太短,再試一次" +done + +# --- Secrets (auto-generate) --- +JWT_ACCESS_SECRET=$(openssl rand -base64 48) +JWT_REFRESH_SECRET=$(openssl rand -base64 48) +WORKER_SECRET=$(openssl rand -base64 48) +SECRETS_KEY=$(openssl rand -base64 32) + +# --- Write .env.prod --- +cat > "$OUTPUT" << ENDOFFILE +# 巡樓 Prod 環境(由 deploy/setup.sh 自動產生,請勿手動編輯) +# infra compose(Mongo / Redis 容器,與下方 HAIXUN_MONGO_URI / HAIXUN_REDIS_PASSWORD 密碼一致) +MONGO_PORT=27017 +MONGO_ROOT_USER=haixun +MONGO_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD} +MONGO_DATABASE=haixun + +REDIS_PORT=6379 +REDIS_PASSWORD=${REDIS_PASSWORD} + +# app compose +WEB_IMAGE=ghcr.io/kupi-cat/haixun-web +WEB_TAG=v0.0.1 +BACKEND_IMAGE=ghcr.io/kupi-cat/haixun-backend +BACKEND_TAG=v0.0.1 +NODE_WORKER_IMAGE=ghcr.io/kupi-cat/haixun-node-worker +NODE_WORKER_TAG=v0.0.1 + +WEB_PORT=80 + +HAIXUN_MONGO_URI=mongodb://haixun:${MONGO_ROOT_PASSWORD_ENCODED}@mongo:27017/?authSource=admin +HAIXUN_MONGO_DB=haixun +HAIXUN_REDIS_ADDR=redis:6379 +HAIXUN_REDIS_PASSWORD=${REDIS_PASSWORD} + +HAIXUN_JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} +HAIXUN_JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} +HAIXUN_WORKER_SECRET=${WORKER_SECRET} +HAIXUN_SECRETS_KEY=${SECRETS_KEY} + +THREADS_APP_ID= +THREADS_APP_SECRET= +THREADS_REDIRECT_URI=http://your-domain.example/api/v1/threads-accounts/oauth/callback +THREADS_OAUTH_SUCCESS_REDIRECT=http://your-domain.example + +INIT_TENANT_ID=default +INIT_ADMIN_EMAIL=${ADMIN_EMAIL} +INIT_ADMIN_PASSWORD=${ADMIN_PASSWORD} +INIT_ADMIN_DISPLAY_NAME=Admin + +HAIXUN_WORKER_POLL_MS=3000 +HAIXUN_8D_TARGET_SAMPLES=4 + +# Worker / sweeper 行為 +HAIXUN_JOB_SCHEDULER_ENABLED=false +HAIXUN_JOB_SCHEDULER_INTERVAL_SECONDS=60 +HAIXUN_JOB_REAPER_ENABLED=false +HAIXUN_JOB_REAPER_INTERVAL_SECONDS=30 +HAIXUN_THREADS_TOKEN_REFRESH_ENABLED=false +HAIXUN_THREADS_TOKEN_REFRESH_INTERVAL_SECONDS=3600 +HAIXUN_THREADS_TOKEN_REFRESH_LEAD_DAYS=7 +HAIXUN_THREADS_PUBLISH_QUEUE_ENABLED=false +HAIXUN_THREADS_PUBLISH_QUEUE_INTERVAL_SECONDS=60 +HAIXUN_THREADS_PUBLISH_QUEUE_BATCH_SIZE=20 +ENDOFFILE + +echo "" +echo "==============================================" +echo " 已完成!產出: $OUTPUT" +echo " 接著執行: make prod-infra && make prod-install" +echo "==============================================" diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index b7995eb..ece083e 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -1,7 +1,7 @@ # 巡樓資料服務:Mongo + Redis # 只綁 127.0.0.1,給同主機上以 systemd 跑的 Go gateway / worker 連線。 # 啟動(dev):make dev-infra # 讀 deploy/.env.dev -# 啟動(prod):make prod-install # 讀 deploy/.env.prod +# 啟動(prod):make prod-infra # 讀 deploy/.env.prod name: haixun-infra services: