fix
This commit is contained in:
parent
d4fcdfedb6
commit
27a8a71d00
53
Makefile
53
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 "=============================================="
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue