From fe0bb3a50379bbc16398e5b7661ea6c2d25709d9 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 13 Sep 2026 02:55:18 +0000 Subject: [PATCH] fix(config): layer root and env compose files with expanded overrides Load agent/cursor env overrides in config loader; pass root .env plus environment-specific env files in env-service; align compose env_file mounts. Co-authored-by: Cursor --- .dockerignore | 2 ++ .env.example | 31 ++++++++-------- configs/.env.dev.example | 22 ++---------- configs/.env.main.example | 18 +++++----- configs/.env.shared.example | 5 +-- configs/.env.test.example | 8 ++--- docker-compose.dev.yml | 26 +++++++++++--- docker-compose.main.yml | 29 ++++++++++++--- docker-compose.shared.yml | 4 +-- docker-compose.test.yml | 28 ++++++++++++--- internal/config/config.go | 65 ++++++++++++++++++++++------------ internal/config/config_test.go | 41 +++++++++++++++++++++ scripts/env-service.sh | 29 +++++++-------- 13 files changed, 203 insertions(+), 105 deletions(-) diff --git a/.dockerignore b/.dockerignore index a0cf2f22..37225ead 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,8 @@ .doc docs data +.regulations +.tmp web/node_modules web/dist web/.vite diff --git a/.env.example b/.env.example index 43d0a573..d2d9a40d 100644 --- a/.env.example +++ b/.env.example @@ -3,14 +3,12 @@ # # 使用说明: # 1. 复制为 .env 并填写密钥:cp .env.example .env && chmod 600 .env -# 2. 本文件供 Go 后端 (bin/server)、launch.sh 默认 compose 变量替换使用 -# 3. 若通过 ./scripts/env-service.sh dev start 启容器,还需同步密钥到 configs/.env.dev -# (见 configs/.env.dev.example 第 6 节) +# 2. 本文件供 Go 后端、launch.sh 与 env-service.sh 作为公共配置源 +# 3. configs/.env.<环境> 只保存端口、数据库等环境差异,且优先级高于本文件 # ============================================================================= # --- 1. 运行环境与基础目录 --- ENV=dev -COMPOSE_PROJECT_NAME=agent-dev DATA_DIR=./data/dev APP_PUBLISH_HOST=127.0.0.1 HTTP_PORT=8080 @@ -42,7 +40,7 @@ PI_AGENT_RUNNER_PORT=18865 # 宿主 bin/server 直连本机 pi-agent 容器时启用;容器内 app 服务由 compose 注入 http://pi-agent:18865 # PI_AGENT_RUNNER_URL=http://127.0.0.1:18865 # Runner 服务间认证(main 必填;dev 可留空) -# RUNNER_AUTH_TOKEN= +RUNNER_AUTH_TOKEN= # --- 7. 本机 Pi CLI 回退(不使用容器 runner 时)--- PI_AGENT_BIN=/opt/homebrew/bin/pi @@ -54,17 +52,15 @@ PI_AGENT_MODEL=MiniMax-M2.7-highspeed KB_DB_HOST=127.0.0.1 KB_DB_PORT=5432 KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass +KB_DB_PASSWORD= KB_DB_NAME=regulations -# --- 9. 向量 Embedding / Reranker(Go 宿主 KB 检索;容器 app 当前未透传,宿主模式必填)--- +# --- 9. 向量 Embedding / Reranker(Go 宿主与容器 app 的 KB 检索)--- EMBEDDING_ENDPOINT_URL=https://www.dmxapi.cn/v1/embeddings -EMBEDDING_PROVIDER=openai_compatible EMBEDDING_MODEL=bge-m3 EMBEDDING_API_KEY= RERANKER_ENDPOINT_URL=https://www.dmxapi.cn/v1/rerank -RERANKER_PROVIDER=openai_compatible RERANKER_MODEL=bge-reranker-v2-m3-free RERANKER_API_KEY= @@ -87,8 +83,15 @@ ADMIN_PASSWORD=admin-secret-password-123 AUTH_REGISTRATION_ENABLED=true AUTH_REQUIRE_APPROVAL=false -# --- 12. 可选:CORS / 内存降级 --- -# dev/test 未配置时 CORS 由代码填充 localhost 默认值 -# CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:8080 -# 生产必须为 false;main 启动时若 true 将拒绝加载 -# ALLOW_IN_MEMORY=false +# --- 12. 安全、缓存与意图路由开关 --- +# 留空时 dev/test 由代码补 localhost;生产留空代表不允许跨 Origin +CORS_ALLOWED_ORIGINS= +# 生产强制 false;main compose 不接受覆盖 +ALLOW_IN_MEMORY=false +CACHE_ENABLED=true +INTENT_V2_SHADOW=false +INTENT_V2_DECISION=false +INTENT_V2_DECISION_TIER=none +WORKFLOW_INTENTS_ENABLED=true +MULTI_INTENT_ENABLED=true +SEMANTIC_FALLBACK_ENABLED=false diff --git a/configs/.env.dev.example b/configs/.env.dev.example index 936c8b14..ecc5c863 100644 --- a/configs/.env.dev.example +++ b/configs/.env.dev.example @@ -1,25 +1,18 @@ # ============================================================================= # 开发环境 Compose 专用配置模板 (configs/.env.dev) -# 作用域:./scripts/env-service.sh dev * 与 docker compose --env-file configs/.env.dev +# 作用域:./scripts/env-service.sh dev * 的环境差异覆盖 # -# 重要:本文件与根目录 .env 分工不同 -# - 根 .env → Go bin/server、launch.sh 默认 compose -# - 本文件 → env-service.sh 启停 dev 容器组 -# 密钥(MINIMAX_API_KEY / TAVILY_API_KEYS 等)须与根 .env 保持一致,否则 pi-agent 容器无 Token! +# 根 .env 提供公共模型、检索、认证配置;本文件只覆盖 dev 差异。 # ============================================================================= ENV=dev -COMPOSE_PROJECT_NAME=agent-dev # 1. 业务 HTTP 服务 HTTP_PORT=8080 APP_PUBLISH_HOST=127.0.0.1 -ADMIN_TOKEN=dev-admin-token-change-me -ALLOW_INSECURE_ADMIN=1 # 2. 对话数据与工作区(禁止跨环境共享) DATA_DIR=./data/dev -AUX_FILE_DSN=file:/var/lib/feishu/outbox_dev.db?_journal_mode=WAL&_busy_timeout=5000 # 3. 开发环境专属数据库 SYSTEM_DB_USER=app @@ -29,7 +22,6 @@ SYSTEM_DB_PUBLISH_PORT=5433 # 4. 共享知识库(容器 app 经 shuimu-kb-shared 访问) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_DB_HOST=shuimu-kb-shared KB_DB_PORT=5432 @@ -40,12 +32,4 @@ PI_AGENT_RUNNER_PORT=18865 AGENT_BACKEND=pi-agent AGENT_TIMEOUT=60m -# 6. Pi-Agent / MiniMax / Tavily(pi-agent 容器必填;从根 .env 复制) -MINIMAX_ENDPOINT_URL=https://api.minimaxi.com/v1 -MINIMAX_API_KEY= -MINIMAX_MODEL_ID=MiniMax-M2.7-highspeed -MINIMAX_API_TYPE=openai-compatible -PI_AGENT_PROVIDER=minimax -PI_AGENT_MODEL=MiniMax-M2.7-highspeed -TAVILY_API_KEYS= -# RUNNER_AUTH_TOKEN= +# 6. Pi-Agent / MiniMax / Tavily / Runner 密钥统一读取根 .env diff --git a/configs/.env.main.example b/configs/.env.main.example index 30a46e6f..63b0aa53 100644 --- a/configs/.env.main.example +++ b/configs/.env.main.example @@ -1,39 +1,37 @@ # ============================================================================= # 生产/主分支环境 (main) 环境变量配置模板 (configs/.env.main) # 作用域:main 分支正式线上发布与生产运营 +# 根 .env 提供公共模型与检索配置;本文件覆盖生产端口、数据库和生产密钥。 # ============================================================================= ENV=prod -COMPOSE_PROJECT_NAME=agent-main # 1. 业务 HTTP 服务 HTTP_PORT=8000 APP_PUBLISH_HOST=0.0.0.0 -ADMIN_TOKEN=main-prod-admin-token-super-secure -ALLOW_INSECURE_ADMIN=0 +AUTH_REGISTRATION_ENABLED=false +AUTH_REQUIRE_APPROVAL=true # 2. 对话数据与工作区隔离目录 (禁止跨环境共享) DATA_DIR=./data/main -AUX_FILE_DSN=file:/var/lib/feishu/outbox_main.db?_journal_mode=WAL&_busy_timeout=5000 # 3. 生产环境专属数据库 (禁止与 dev/test 共享) SYSTEM_DB_USER=app -SYSTEM_DB_PASSWORD=prod_highly_secure_db_pass +SYSTEM_DB_PASSWORD= SYSTEM_DB_NAME=agent_system_main SYSTEM_DB_PUBLISH_PORT=5435 # 4. 共享知识库连接 (只读检索共享数据) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_DB_HOST=shuimu-kb-shared KB_DB_PORT=5432 # 5. Cursor / LLM Agent Runner 服务 CURSOR_RUNNER_PORT=18767 -AGENT_BACKEND=stub +PI_AGENT_RUNNER_PORT=18867 +AGENT_BACKEND=pi-agent AGENT_TIMEOUT=60m # 生产必填(不可使用 shared_kb_secret_pass / admin-secret-password-123 等默认值) -RUNNER_AUTH_TOKEN=change-me-runner-token -KB_DB_PASSWORD=change-me-kb-prod-secret -ADMIN_PASSWORD=change-me-admin-prod-secret +RUNNER_AUTH_TOKEN= +ADMIN_PASSWORD= diff --git a/configs/.env.shared.example b/configs/.env.shared.example index 518679f8..e5b79afe 100644 --- a/configs/.env.shared.example +++ b/configs/.env.shared.example @@ -1,13 +1,10 @@ # ============================================================================= # 共享基础设施环境变量配置模板 (configs/.env.shared) # 作用域:所有分支与环境共享的知识库、向量检索与底层公共网络 +# KB_DB_PASSWORD 统一在根 .env 配置,本文件只覆盖共享服务差异。 # ============================================================================= -COMPOSE_PROJECT_NAME=agent-infra - # 共享知识库 PostgreSQL (带 pgvector) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_PUBLISH_PORT=5432 -KB_DATA_DIR=./data/shared/kb diff --git a/configs/.env.test.example b/configs/.env.test.example index 8ca77b0b..7ac78432 100644 --- a/configs/.env.test.example +++ b/configs/.env.test.example @@ -1,20 +1,17 @@ # ============================================================================= # 测试环境 (test) 环境变量配置模板 (configs/.env.test) # 作用域:test 分支阶段集成、长期部署实测与运维验证 +# 根 .env 提供公共模型、检索、认证配置;本文件只覆盖 test 差异。 # ============================================================================= ENV=test -COMPOSE_PROJECT_NAME=agent-test # 1. 业务 HTTP 服务 HTTP_PORT=8081 APP_PUBLISH_HOST=0.0.0.0 -ADMIN_TOKEN=test-admin-token-secure-key -ALLOW_INSECURE_ADMIN=0 # 2. 对话数据与工作区隔离目录 (禁止跨环境共享) DATA_DIR=./data/test -AUX_FILE_DSN=file:/var/lib/feishu/outbox_test.db?_journal_mode=WAL&_busy_timeout=5000 # 3. 测试环境专属数据库 (禁止与 dev/main 共享) SYSTEM_DB_USER=app @@ -24,7 +21,6 @@ SYSTEM_DB_PUBLISH_PORT=5434 # 4. 共享知识库连接 (只读检索共享数据) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_DB_HOST=shuimu-kb-shared KB_DB_PORT=5432 @@ -32,7 +28,7 @@ KB_DB_PORT=5432 # 5. Cursor / LLM Agent Runner 服务 CURSOR_RUNNER_PORT=18766 PI_AGENT_RUNNER_PORT=18866 -AGENT_BACKEND=stub +AGENT_BACKEND=pi-agent AGENT_TIMEOUT=60m # app 与 pi-agent 容器共享(test 环境建议设置非空值) # RUNNER_AUTH_TOKEN=test-runner-shared-secret diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index afd6ab4f..fa6c6aa4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -60,7 +60,6 @@ services: MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible} MINIMAX_PROVIDER: minimax TAVILY_API_KEYS: ${TAVILY_API_KEYS:-} - TAVILY_API_KEY: ${TAVILY_API_KEY:-} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} ports: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18865}:18865" @@ -94,19 +93,38 @@ services: HTTP_PORT: "${HTTP_PORT:-8080}" HTTP_HOST: "0.0.0.0" AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent} + AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m} + MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8} DATA_DIR: /app/data # 独占业务与对话库 APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:-dev_secret_db_pass}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_dev}?sslmode=disable # 共享知识库 - POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:-shared_kb_secret_pass}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable + POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:?KB_DB_PASSWORD is required}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable CURSOR_RUNNER_URL: http://cursor-agent:18765 PI_AGENT_RUNNER_URL: http://pi-agent:18865 PI_AGENT_RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} PI_AGENT_PROVIDER: ${PI_AGENT_PROVIDER:-minimax} PI_AGENT_MODEL: ${PI_AGENT_MODEL:-MiniMax-M2.7-highspeed} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} - ALLOW_IN_MEMORY: "false" - AUX_FILE_DSN: file:/var/lib/feishu/outbox_dev.db?_journal_mode=WAL&_busy_timeout=5000 + EMBEDDING_ENDPOINT_URL: ${EMBEDDING_ENDPOINT_URL:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-bge-m3} + EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-} + RERANKER_ENDPOINT_URL: ${RERANKER_ENDPOINT_URL:-} + RERANKER_MODEL: ${RERANKER_MODEL:-bge-reranker-v2-m3-free} + RERANKER_API_KEY: ${RERANKER_API_KEY:-} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin-secret-password-123} + AUTH_REGISTRATION_ENABLED: ${AUTH_REGISTRATION_ENABLED:-true} + AUTH_REQUIRE_APPROVAL: ${AUTH_REQUIRE_APPROVAL:-false} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} + ALLOW_IN_MEMORY: ${ALLOW_IN_MEMORY:-false} + CACHE_ENABLED: ${CACHE_ENABLED:-true} + INTENT_V2_SHADOW: ${INTENT_V2_SHADOW:-false} + INTENT_V2_DECISION: ${INTENT_V2_DECISION:-false} + INTENT_V2_DECISION_TIER: ${INTENT_V2_DECISION_TIER:-none} + WORKFLOW_INTENTS_ENABLED: ${WORKFLOW_INTENTS_ENABLED:-true} + MULTI_INTENT_ENABLED: ${MULTI_INTENT_ENABLED:-true} + SEMANTIC_FALLBACK_ENABLED: ${SEMANTIC_FALLBACK_ENABLED:-false} ports: - "${APP_PUBLISH_HOST:-127.0.0.1}:${HTTP_PORT:-8080}:${HTTP_PORT:-8080}" healthcheck: diff --git a/docker-compose.main.yml b/docker-compose.main.yml index 786a6e62..553c770d 100644 --- a/docker-compose.main.yml +++ b/docker-compose.main.yml @@ -14,7 +14,7 @@ services: command: ["postgres", "-c", "shared_buffers=512MB", "-c", "max_connections=300"] environment: POSTGRES_USER: ${SYSTEM_DB_USER:-app} - POSTGRES_PASSWORD: ${SYSTEM_DB_PASSWORD:-prod_highly_secure_db_pass} + POSTGRES_PASSWORD: ${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD is required for main/production} POSTGRES_DB: ${SYSTEM_DB_NAME:-agent_system_main} ports: - "127.0.0.1:${SYSTEM_DB_PUBLISH_PORT:-5435}:5432" @@ -60,7 +60,6 @@ services: MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible} MINIMAX_PROVIDER: minimax TAVILY_API_KEYS: ${TAVILY_API_KEYS:-} - TAVILY_API_KEY: ${TAVILY_API_KEY:-} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:?RUNNER_AUTH_TOKEN is required for main/production} ports: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18867}:18867" @@ -92,19 +91,39 @@ services: HTTP_PORT: "${HTTP_PORT:-8000}" HTTP_HOST: "0.0.0.0" AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent} + AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m} + MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8} DATA_DIR: /app/data # 独占生产业务与对话库 - APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:-prod_highly_secure_db_pass}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_main}?sslmode=disable + APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD is required for main/production}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_main}?sslmode=disable # 共享知识库 - POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:-shared_kb_secret_pass}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable + POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:?KB_DB_PASSWORD is required for main/production}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable CURSOR_RUNNER_URL: http://cursor-agent:18767 PI_AGENT_RUNNER_URL: http://pi-agent:18867 PI_AGENT_RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:?RUNNER_AUTH_TOKEN is required for main/production} + PI_AGENT_PROVIDER: ${PI_AGENT_PROVIDER:-minimax} + PI_AGENT_MODEL: ${PI_AGENT_MODEL:-${MINIMAX_MODEL_ID:-MiniMax-M3}} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:?RUNNER_AUTH_TOKEN is required for main/production} KB_DB_PASSWORD: ${KB_DB_PASSWORD:?KB_DB_PASSWORD is required for main/production} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required for main/production} + AUTH_REGISTRATION_ENABLED: ${AUTH_REGISTRATION_ENABLED:-true} + AUTH_REQUIRE_APPROVAL: ${AUTH_REQUIRE_APPROVAL:-false} + EMBEDDING_ENDPOINT_URL: ${EMBEDDING_ENDPOINT_URL:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-bge-m3} + EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-} + RERANKER_ENDPOINT_URL: ${RERANKER_ENDPOINT_URL:-} + RERANKER_MODEL: ${RERANKER_MODEL:-bge-reranker-v2-m3-free} + RERANKER_API_KEY: ${RERANKER_API_KEY:-} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} ALLOW_IN_MEMORY: "false" - AUX_FILE_DSN: file:/var/lib/feishu/outbox_main.db?_journal_mode=WAL&_busy_timeout=5000 + CACHE_ENABLED: ${CACHE_ENABLED:-true} + INTENT_V2_SHADOW: ${INTENT_V2_SHADOW:-false} + INTENT_V2_DECISION: ${INTENT_V2_DECISION:-false} + INTENT_V2_DECISION_TIER: ${INTENT_V2_DECISION_TIER:-none} + WORKFLOW_INTENTS_ENABLED: ${WORKFLOW_INTENTS_ENABLED:-true} + MULTI_INTENT_ENABLED: ${MULTI_INTENT_ENABLED:-true} + SEMANTIC_FALLBACK_ENABLED: ${SEMANTIC_FALLBACK_ENABLED:-false} ports: - "${APP_PUBLISH_HOST:-0.0.0.0}:${HTTP_PORT:-8000}:${HTTP_PORT:-8000}" healthcheck: diff --git a/docker-compose.shared.yml b/docker-compose.shared.yml index 7cb19a7e..5ee68b40 100644 --- a/docker-compose.shared.yml +++ b/docker-compose.shared.yml @@ -10,7 +10,7 @@ services: container_name: shuimu-kb-shared environment: POSTGRES_USER: ${KB_DB_USER:-regkb} - POSTGRES_PASSWORD: ${KB_DB_PASSWORD:-shared_kb_secret_pass} + POSTGRES_PASSWORD: ${KB_DB_PASSWORD:?KB_DB_PASSWORD is required} POSTGRES_DB: ${KB_DB_NAME:-regulations} ports: - "127.0.0.1:${KB_PUBLISH_PORT:-5432}:5432" @@ -29,7 +29,7 @@ services: networks: shared-net: name: chat-agent-shared-net - driver: bridge + external: true volumes: shared-kb-pgdata: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 6a9604f7..d0d880e4 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -60,7 +60,6 @@ services: MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible} MINIMAX_PROVIDER: minimax TAVILY_API_KEYS: ${TAVILY_API_KEYS:-} - TAVILY_API_KEY: ${TAVILY_API_KEY:-} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} ports: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18866}:18866" @@ -92,17 +91,38 @@ services: HTTP_PORT: "${HTTP_PORT:-8081}" HTTP_HOST: "0.0.0.0" AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent} + AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m} + MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8} DATA_DIR: /app/data # 独占测试业务与对话库 APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:-test_secret_db_pass}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_test}?sslmode=disable # 共享知识库 - POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:-shared_kb_secret_pass}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable + POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:?KB_DB_PASSWORD is required}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable CURSOR_RUNNER_URL: http://cursor-agent:18766 PI_AGENT_RUNNER_URL: http://pi-agent:18866 PI_AGENT_RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} + PI_AGENT_PROVIDER: ${PI_AGENT_PROVIDER:-minimax} + PI_AGENT_MODEL: ${PI_AGENT_MODEL:-${MINIMAX_MODEL_ID:-MiniMax-M3}} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} - ALLOW_IN_MEMORY: "false" - AUX_FILE_DSN: file:/var/lib/feishu/outbox_test.db?_journal_mode=WAL&_busy_timeout=5000 + EMBEDDING_ENDPOINT_URL: ${EMBEDDING_ENDPOINT_URL:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-bge-m3} + EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-} + RERANKER_ENDPOINT_URL: ${RERANKER_ENDPOINT_URL:-} + RERANKER_MODEL: ${RERANKER_MODEL:-bge-reranker-v2-m3-free} + RERANKER_API_KEY: ${RERANKER_API_KEY:-} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin-secret-password-123} + AUTH_REGISTRATION_ENABLED: ${AUTH_REGISTRATION_ENABLED:-true} + AUTH_REQUIRE_APPROVAL: ${AUTH_REQUIRE_APPROVAL:-false} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} + ALLOW_IN_MEMORY: ${ALLOW_IN_MEMORY:-false} + CACHE_ENABLED: ${CACHE_ENABLED:-true} + INTENT_V2_SHADOW: ${INTENT_V2_SHADOW:-false} + INTENT_V2_DECISION: ${INTENT_V2_DECISION:-false} + INTENT_V2_DECISION_TIER: ${INTENT_V2_DECISION_TIER:-none} + WORKFLOW_INTENTS_ENABLED: ${WORKFLOW_INTENTS_ENABLED:-true} + MULTI_INTENT_ENABLED: ${MULTI_INTENT_ENABLED:-true} + SEMANTIC_FALLBACK_ENABLED: ${SEMANTIC_FALLBACK_ENABLED:-false} ports: - "${APP_PUBLISH_HOST:-0.0.0.0}:${HTTP_PORT:-8081}:${HTTP_PORT:-8081}" healthcheck: diff --git a/internal/config/config.go b/internal/config/config.go index 6f9d7412..c6d52240 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,27 +10,27 @@ import ( ) type Config struct { - Env string `json:"env"` - DataDir string `json:"data_dir"` - HTTPHost string `json:"http_host"` - HTTPPort int `json:"http_port"` - AgentBackend string `json:"agent_backend"` - AgentTimeout time.Duration `json:"agent_timeout"` - CursorAPIKey string `json:"cursor_api_key"` - CursorModel string `json:"cursor_model"` - CursorBin string `json:"cursor_bin"` - CursorBridgeBin string `json:"cursor_bridge_bin"` - PiAgentBin string `json:"pi_agent_bin"` - PiAgentBridgeBin string `json:"pi_agent_bridge_bin"` - PiAgentProvider string `json:"pi_agent_provider"` - PiAgentModel string `json:"pi_agent_model"` - PiAgentRunnerURL string `json:"pi_agent_runner_url"` - PiAgentRunnerAuthToken string `json:"pi_agent_runner_auth_token"` - KbDbHost string `json:"kb_db_host"` - KbDbPort int `json:"kb_db_port"` - KbDbUser string `json:"kb_db_user"` - KbDbPassword string `json:"kb_db_password"` - KbDbName string `json:"kb_db_name"` + Env string `json:"env"` + DataDir string `json:"data_dir"` + HTTPHost string `json:"http_host"` + HTTPPort int `json:"http_port"` + AgentBackend string `json:"agent_backend"` + AgentTimeout time.Duration `json:"agent_timeout"` + CursorAPIKey string `json:"cursor_api_key"` + CursorModel string `json:"cursor_model"` + CursorBin string `json:"cursor_bin"` + CursorBridgeBin string `json:"cursor_bridge_bin"` + PiAgentBin string `json:"pi_agent_bin"` + PiAgentBridgeBin string `json:"pi_agent_bridge_bin"` + PiAgentProvider string `json:"pi_agent_provider"` + PiAgentModel string `json:"pi_agent_model"` + PiAgentRunnerURL string `json:"pi_agent_runner_url"` + PiAgentRunnerAuthToken string `json:"pi_agent_runner_auth_token"` + KbDbHost string `json:"kb_db_host"` + KbDbPort int `json:"kb_db_port"` + KbDbUser string `json:"kb_db_user"` + KbDbPassword string `json:"kb_db_password"` + KbDbName string `json:"kb_db_name"` // 专属记忆与多级上下文数据库配置 ContextDbHost string `json:"context_db_host"` @@ -252,9 +252,29 @@ func Load(path string) (*Config, error) { if v := os.Getenv("AGENT_BACKEND"); v != "" { cfg.AgentBackend = v } + if v := os.Getenv("AGENT_TIMEOUT"); v != "" { + if d, err := time.ParseDuration(v); err == nil { + cfg.AgentTimeout = d + } + } + if v := os.Getenv("CURSOR_API_KEY"); v != "" { + cfg.CursorAPIKey = v + } + if v := os.Getenv("CURSOR_MODEL"); v != "" { + cfg.CursorModel = v + } + if v := os.Getenv("CURSOR_BIN"); v != "" { + cfg.CursorBin = v + } + if v := os.Getenv("CURSOR_BRIDGE_BIN"); v != "" { + cfg.CursorBridgeBin = v + } if v := os.Getenv("PI_AGENT_BIN"); v != "" { cfg.PiAgentBin = v } + if v := os.Getenv("PI_AGENT_BRIDGE_BIN"); v != "" { + cfg.PiAgentBridgeBin = v + } if v := os.Getenv("PI_AGENT_PROVIDER"); v != "" { cfg.PiAgentProvider = v } @@ -398,7 +418,7 @@ func validateProductionSecrets(cfg *Config) error { "CONTEXT_DB_PASSWORD": cfg.ContextDbPassword, "ADMIN_PASSWORD": cfg.AdminPassword, } - defaults := []string{"shared_kb_secret_pass", "dev_secret_db_pass", "admin-secret-password-123"} + defaults := []string{"shared_kb_secret_pass", "dev_secret_db_pass", "prod_highly_secure_db_pass", "admin-secret-password-123"} for envKey, val := range weak { if strings.TrimSpace(val) == "" { return fmt.Errorf("%s must be set in %s environment", envKey, cfg.Env) @@ -482,4 +502,3 @@ func (c *Config) KbDSN() string { func (c *Config) ContextDSN() string { return "postgres://" + c.ContextDbUser + ":" + c.ContextDbPassword + "@" + c.ContextDbHost + ":" + strconv.Itoa(c.ContextDbPort) + "/" + c.ContextDbName + "?sslmode=disable" } - diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b126f868..ec789ed0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -69,6 +69,35 @@ func TestConfigLoadDSNOverrides(t *testing.T) { } } +func TestConfigLoadRuntimeEnvironmentOverrides(t *testing.T) { + t.Setenv("ENV", "dev") + t.Setenv("AGENT_TIMEOUT", "47m") + t.Setenv("CURSOR_API_KEY", "cursor-test-key") + t.Setenv("CURSOR_MODEL", "cursor-test-model") + t.Setenv("CURSOR_BIN", "/test/cursor") + t.Setenv("CURSOR_BRIDGE_BIN", "/test/cursor-bridge.mjs") + t.Setenv("PI_AGENT_BRIDGE_BIN", "/test/pi-bridge.mjs") + t.Setenv("EMBEDDING_MODEL", "embedding-test-model") + t.Setenv("RERANKER_MODEL", "reranker-test-model") + + cfg, err := config.Load("/dev/null") + if err != nil { + t.Fatalf("unexpected error loading config: %v", err) + } + if cfg.AgentTimeout != 47*time.Minute { + t.Fatalf("expected environment AgentTimeout 47m, got %v", cfg.AgentTimeout) + } + if cfg.CursorAPIKey != "cursor-test-key" || cfg.CursorModel != "cursor-test-model" || cfg.CursorBin != "/test/cursor" || cfg.CursorBridgeBin != "/test/cursor-bridge.mjs" { + t.Fatalf("cursor environment overrides were not applied") + } + if cfg.PiAgentBridgeBin != "/test/pi-bridge.mjs" { + t.Fatalf("pi bridge environment override was not applied") + } + if cfg.EmbeddingModel != "embedding-test-model" || cfg.RerankerModel != "reranker-test-model" { + t.Fatalf("retrieval model environment overrides were not applied") + } +} + func TestConfigLoadCORSDefaultsForDev(t *testing.T) { cfg, err := config.Load("/dev/null") if err != nil { @@ -103,3 +132,15 @@ func TestConfigLoadRejectsWeakSecretsOutsideDev(t *testing.T) { t.Fatal("expected config load to fail for missing secrets in main env") } } + +func TestConfigLoadRejectsProductionDatabasePlaceholder(t *testing.T) { + t.Setenv("ENV", "prod") + t.Setenv("KB_DB_PASSWORD", "kb-prod-secret-xyz") + t.Setenv("CONTEXT_DB_PASSWORD", "prod_highly_secure_db_pass") + t.Setenv("ADMIN_PASSWORD", "admin-prod-secret-xyz") + + _, err := config.Load("/dev/null") + if err == nil { + t.Fatal("expected config load to reject the documented production database placeholder") + } +} diff --git a/scripts/env-service.sh b/scripts/env-service.sh index 0c5a0eeb..68571b07 100755 --- a/scripts/env-service.sh +++ b/scripts/env-service.sh @@ -90,44 +90,45 @@ run_compose() { prepare_env_file "${target}" fi - local env_arg="" + local env_args=() + # 根 .env 提供跨环境共用的模型、检索与认证配置;环境文件仅覆盖端口、 + # 数据库和其他环境差异。后声明的 env-file 优先级更高。 + if [[ -f ".env" ]]; then + env_args+=(--env-file ".env") + fi if [[ -f "${env_file}" ]]; then - env_arg="--env-file ${env_file}" + env_args+=(--env-file "${env_file}") fi case "${act}" in start) - if [[ "${target}" != "shared" ]]; then - ensure_shared_network - fi + ensure_shared_network echo "==> [${target}] 启动服务 (${compose_file})..." - docker compose -f "${compose_file}" ${env_arg} up -d + docker compose -f "${compose_file}" "${env_args[@]}" up -d echo "==> [${target}] 服务已启动" ;; stop) echo "==> [${target}] 停止服务..." - docker compose -f "${compose_file}" ${env_arg} stop + docker compose -f "${compose_file}" "${env_args[@]}" stop ;; restart) - if [[ "${target}" != "shared" ]]; then - ensure_shared_network - fi + ensure_shared_network echo "==> [${target}] 重启服务..." - docker compose -f "${compose_file}" ${env_arg} restart + docker compose -f "${compose_file}" "${env_args[@]}" restart ;; status|ps) echo "------------------------------------------------------------" echo "环境: [${target}] (编排: ${compose_file})" echo "------------------------------------------------------------" - docker compose -f "${compose_file}" ${env_arg} ps + docker compose -f "${compose_file}" "${env_args[@]}" ps echo "" ;; logs) - docker compose -f "${compose_file}" ${env_arg} logs -f --tail=100 + docker compose -f "${compose_file}" "${env_args[@]}" logs -f --tail=100 ;; down) echo "==> [${target}] 清理并下线容器..." - docker compose -f "${compose_file}" ${env_arg} down + docker compose -f "${compose_file}" "${env_args[@]}" down ;; *) echo "[错误] 未知操作: ${act}"