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
|
.PHONY: help
|
||||||
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}'
|
| 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
|
.PHONY: gen-api
|
||||||
gen-api: ## 重新產生後端 API handler / logic / types
|
gen-api: ## 重新產生後端 API handler / logic / types
|
||||||
$(MAKE) -C $(BACKEND_DIR) gen-api
|
$(MAKE) -C $(BACKEND_DIR) gen-api
|
||||||
|
|
@ -28,6 +32,10 @@ fmt: ## 格式化後端 Go 程式
|
||||||
test: ## 執行後端測試
|
test: ## 執行後端測試
|
||||||
$(MAKE) -C $(BACKEND_DIR) 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
|
.PHONY: run
|
||||||
run: ## 啟動後端 API
|
run: ## 啟動後端 API
|
||||||
$(MAKE) -C $(BACKEND_DIR) run
|
$(MAKE) -C $(BACKEND_DIR) run
|
||||||
|
|
@ -62,22 +70,42 @@ dev-infra: ## 啟動本機 dev 資料服務(Mongo / Redis 容器,讀 deploy/
|
||||||
dev-infra-down: ## 關閉本機 dev 資料服務(保留資料 volume)
|
dev-infra-down: ## 關閉本機 dev 資料服務(保留資料 volume)
|
||||||
$(INFRA_DEV_COMPOSE) down
|
$(INFRA_DEV_COMPOSE) down
|
||||||
|
|
||||||
.PHONY: prod-install
|
.PHONY: dev-infra-recreate
|
||||||
prod-install: ## 一鍵安裝 prod:複製 .env.prod、啟動 infra、init、啟動 app
|
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
|
@test -f $(DEPLOY_DIR)/.env.prod || cp $(DEPLOY_DIR)/.env.prod.example $(DEPLOY_DIR)/.env.prod
|
||||||
@echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.prod 再執行後續指令" >&2
|
@echo ">>> 請先編輯 $(DEPLOY_DIR)/.env.prod 再執行後續指令" >&2
|
||||||
$(INFRA_PROD_COMPOSE) up -d
|
$(INFRA_PROD_COMPOSE) up -d
|
||||||
$(INFRA_PROD_COMPOSE) ps
|
$(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) up -d
|
||||||
$(DEPLOY_COMPOSE) --profile init run --rm init
|
$(DEPLOY_COMPOSE) --profile init run --rm init
|
||||||
$(DEPLOY_COMPOSE) ps
|
$(DEPLOY_COMPOSE) ps
|
||||||
|
|
||||||
.PHONY: prod-rebuild
|
.PHONY: prod-rebuild
|
||||||
prod-rebuild: ## 重新拉取 image 並重建 prod stack(infra + app)
|
prod-rebuild: ## 重新 build 本地 image 並重建 prod app stack(infra 不影響)
|
||||||
$(INFRA_PROD_COMPOSE) pull
|
$(DEPLOY_COMPOSE) build
|
||||||
$(INFRA_PROD_COMPOSE) up -d
|
|
||||||
$(DEPLOY_COMPOSE) pull
|
|
||||||
$(DEPLOY_COMPOSE) up -d
|
$(DEPLOY_COMPOSE) up -d
|
||||||
$(DEPLOY_COMPOSE) ps
|
$(DEPLOY_COMPOSE) ps
|
||||||
|
|
||||||
|
|
@ -89,3 +117,12 @@ prod-stop: ## 關閉 prod app stack(保留 infra 與資料)
|
||||||
prod-stop-all: ## 關閉 prod app + infra(保留資料 volume)
|
prod-stop-all: ## 關閉 prod app + infra(保留資料 volume)
|
||||||
$(DEPLOY_COMPOSE) down
|
$(DEPLOY_COMPOSE) down
|
||||||
$(INFRA_PROD_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_JWT_ACCESS_SECRET HAIXUN_JWT_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
export HAIXUN_SECRETS_KEY HAIXUN_BACKEND_URL
|
export HAIXUN_SECRETS_KEY HAIXUN_BACKEND_URL
|
||||||
export HAIXUN_WORKER_POLL_MS HAIXUN_8D_TARGET_SAMPLES PLAYWRIGHT_HEADLESS
|
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
|
endif
|
||||||
|
|
||||||
export HAIXUN_JOB_WORKER_TYPE ?= go-demo
|
export HAIXUN_JOB_WORKER_TYPE ?= go-demo
|
||||||
|
|
@ -36,6 +37,9 @@ run:
|
||||||
run-dev:
|
run-dev:
|
||||||
go run gateway.go -f etc/gateway.yaml
|
go run gateway.go -f etc/gateway.yaml
|
||||||
|
|
||||||
|
init:
|
||||||
|
go run ./cmd/tool/main.go init -f etc/gateway.yaml
|
||||||
|
|
||||||
web-dev:
|
web-dev:
|
||||||
cd web && npm install && npm run 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
|
return nil, false, err
|
||||||
}
|
}
|
||||||
existing.Roles = []string{"admin"}
|
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
|
return existing, false, nil
|
||||||
}
|
}
|
||||||
if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound {
|
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")
|
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 {
|
func (m *memoryMemberRepo) SetRoles(_ context.Context, tenantID, uid string, roles []string) error {
|
||||||
for _, item := range m.byEmail {
|
for _, item := range m.byEmail {
|
||||||
if item.TenantID == tenantID && item.UID == uid {
|
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" {
|
if len(member.Roles) != 1 || member.Roles[0] != "admin" {
|
||||||
t.Fatalf("roles=%v", member.Roles)
|
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
|
package style8d
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
@ -440,7 +441,23 @@ func extractJSONObject(raw string) ([]byte, error) {
|
||||||
if start < 0 || end <= start {
|
if start < 0 || end <= start {
|
||||||
return nil, fmt.Errorf("LLM response does not contain JSON object")
|
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 {
|
func parseDimension(root map[string]json.RawMessage, keys ...string) Dimension {
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ func (l *AnalyzeStyle8DFromWorkerLogic) AnalyzeStyle8DFromWorker(req *types.Anal
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, app.For(code.AI).SysInternal("prompt config load failed")
|
return nil, app.For(code.AI).SysInternal("prompt config load failed")
|
||||||
}
|
}
|
||||||
|
maxTokens := 8192
|
||||||
result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{
|
result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{
|
||||||
Provider: providerID,
|
Provider: providerID,
|
||||||
Model: credential.Model,
|
Model: credential.Model,
|
||||||
|
|
@ -109,6 +110,7 @@ func (l *AnalyzeStyle8DFromWorkerLogic) AnalyzeStyle8DFromWorker(req *types.Anal
|
||||||
Messages: []domai.Message{
|
Messages: []domai.Message{
|
||||||
{Role: "user", Content: input.BuildUserPrompt()},
|
{Role: "user", Content: input.BuildUserPrompt()},
|
||||||
},
|
},
|
||||||
|
MaxTokens: &maxTokens,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "HTTP 401") {
|
if strings.Contains(err.Error(), "HTTP 401") {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ func (l *StartPersonaStyleAnalysisFromTextLogic) StartPersonaStyleAnalysisFromTe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, app.For(code.AI).SysInternal("prompt config load failed")
|
return nil, app.For(code.AI).SysInternal("prompt config load failed")
|
||||||
}
|
}
|
||||||
|
maxTokens := 8192
|
||||||
result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{
|
result, err := l.svcCtx.AI.GenerateText(l.ctx, domai.GenerateRequest{
|
||||||
Provider: providerID,
|
Provider: providerID,
|
||||||
Model: credential.Model,
|
Model: credential.Model,
|
||||||
|
|
@ -93,6 +94,7 @@ func (l *StartPersonaStyleAnalysisFromTextLogic) StartPersonaStyleAnalysisFromTe
|
||||||
Messages: []domai.Message{
|
Messages: []domai.Message{
|
||||||
{Role: "user", Content: input.BuildUserPrompt()},
|
{Role: "user", Content: input.BuildUserPrompt()},
|
||||||
},
|
},
|
||||||
|
MaxTokens: &maxTokens,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "HTTP 401") {
|
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)
|
UpdateProfile(ctx context.Context, tenantID, uid string, update ProfileUpdate) (*entity.Member, error)
|
||||||
SetRoles(ctx context.Context, tenantID, uid string, roles []string) error
|
SetRoles(ctx context.Context, tenantID, uid string, roles []string) error
|
||||||
SetActiveThreadsAccountID(ctx context.Context, tenantID, uid, accountID 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
|
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) {
|
func (r *mongoRepository) findOne(ctx context.Context, filter bson.M) (*entity.Member, error) {
|
||||||
if r.collection == nil {
|
if r.collection == nil {
|
||||||
return nil, app.For(code.Member).DBUnavailable("Mongo is not configured")
|
return nil, app.For(code.Member).DBUnavailable("Mongo is not configured")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
# 巡樓 Prod 環境(deploy/docker-compose.yml 讀這份)
|
# 巡樓 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 密碼必須一致)---
|
# --- infra compose(Mongo / Redis 容器,與下方 HAIXUN_MONGO_URI / HAIXUN_REDIS_PASSWORD 密碼必須一致)---
|
||||||
MONGO_PORT=27017
|
MONGO_PORT=27017
|
||||||
|
|
@ -20,12 +23,11 @@ NODE_WORKER_TAG=v0.0.1
|
||||||
|
|
||||||
WEB_PORT=80
|
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_MONGO_DB=haixun
|
||||||
HAIXUN_REDIS_ADDR=redis:6379
|
HAIXUN_REDIS_ADDR=redis:6379
|
||||||
HAIXUN_REDIS_PASSWORD=CHANGE_ME
|
HAIXUN_REDIS_PASSWORD=CHANGE_ME
|
||||||
# ⚠ HAIXUN_MONGO_URI 的密碼必須 URL-encode 且等於上方 MONGO_ROOT_PASSWORD;
|
|
||||||
# HAIXUN_REDIS_PASSWORD 必須等於上方 REDIS_PASSWORD。
|
|
||||||
|
|
||||||
# openssl rand -base64 48
|
# openssl rand -base64 48
|
||||||
HAIXUN_JWT_ACCESS_SECRET=CHANGE_ME
|
HAIXUN_JWT_ACCESS_SECRET=CHANGE_ME
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ x-worker-service: &worker-service
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: backend/web/Dockerfile
|
||||||
image: ${WEB_IMAGE:-ghcr.io/kupi-cat/haixun-web}:${WEB_TAG:-v0.0.1}
|
image: ${WEB_IMAGE:-ghcr.io/kupi-cat/haixun-web}:${WEB_TAG:-v0.0.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -50,6 +53,9 @@ services:
|
||||||
- app
|
- app
|
||||||
|
|
||||||
gateway:
|
gateway:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
image: ${BACKEND_IMAGE:-ghcr.io/kupi-cat/haixun-backend}:${BACKEND_TAG:-v0.0.1}
|
image: ${BACKEND_IMAGE:-ghcr.io/kupi-cat/haixun-backend}:${BACKEND_TAG:-v0.0.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["/app/gateway", "-f", "/app/etc/gateway.prod.yaml"]
|
command: ["/app/gateway", "-f", "/app/etc/gateway.prod.yaml"]
|
||||||
|
|
@ -175,6 +181,9 @@ services:
|
||||||
HAIXUN_JOB_WORKER_ID: go-refill-publish-inventory
|
HAIXUN_JOB_WORKER_ID: go-refill-publish-inventory
|
||||||
|
|
||||||
node-worker-style-8d:
|
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}
|
image: ${NODE_WORKER_IMAGE:-ghcr.io/kupi-cat/haixun-node-worker}:${NODE_WORKER_TAG:-v0.0.1}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["npm", "run", "style-8d"]
|
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
|
# 巡樓資料服務:Mongo + Redis
|
||||||
# 只綁 127.0.0.1,給同主機上以 systemd 跑的 Go gateway / worker 連線。
|
# 只綁 127.0.0.1,給同主機上以 systemd 跑的 Go gateway / worker 連線。
|
||||||
# 啟動(dev):make dev-infra # 讀 deploy/.env.dev
|
# 啟動(dev):make dev-infra # 讀 deploy/.env.dev
|
||||||
# 啟動(prod):make prod-install # 讀 deploy/.env.prod
|
# 啟動(prod):make prod-infra # 讀 deploy/.env.prod
|
||||||
name: haixun-infra
|
name: haixun-infra
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue