Files
Agent-System/internal/config/config.go
T
weichen 738bbe1579 feat(agent): add pi-native backend and drop cursor-cli
Keep the container pi-agent backend as the default while the in-process runtime is available beside it, and remove the unused cursor bridge.
2026-09-21 14:52:53 +00:00

719 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
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"`
AgentXAPIBase string `json:"agentx_api_base"`
AgentXAPIKey string `json:"agentx_api_key"`
AgentXModel string `json:"agentx_model"`
AgentXTimeout time.Duration `json:"agentx_timeout"`
WorkspaceTTL time.Duration `json:"workspace_ttl"`
WorkspaceQuotaBytes int64 `json:"workspace_quota_bytes"`
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"`
ContextDbPort int `json:"context_db_port"`
ContextDbUser string `json:"context_db_user"`
ContextDbPassword string `json:"context_db_password"`
ContextDbName string `json:"context_db_name"`
// 向量化 Embedding 服务配置
EmbeddingEndpointURL string `json:"embedding_endpoint_url"`
EmbeddingProvider string `json:"embedding_provider"`
EmbeddingModel string `json:"embedding_model"`
EmbeddingAPIKey string `json:"embedding_api_key"`
// 语义重排 Reranker 服务配置
RerankerEndpointURL string `json:"reranker_endpoint_url"`
RerankerProvider string `json:"reranker_provider"`
RerankerModel string `json:"reranker_model"`
RerankerAPIKey string `json:"reranker_api_key"`
// MinerU 文档解析(精准 API);未配置 API Key 时整条链路降级为本地提取
MineruEndpointURL string `json:"mineru_endpoint_url"`
MineruAPIKey string `json:"mineru_api_key"`
MineruModelVersion string `json:"mineru_model_version"`
MineruLanguage string `json:"mineru_language"`
MineruTimeout time.Duration `json:"mineru_timeout"`
// 材料注入策略:小文档全文,大文档 RAG
MaterialFulltextMaxBytes int `json:"material_fulltext_max_bytes"`
MaterialRAGTopK int `json:"material_rag_top_k"`
// 系统管理员与准入认证配置
AdminUsername string `json:"admin_username"`
AdminPassword string `json:"admin_password"`
AuthRegistrationEnabled bool `json:"auth_registration_enabled"`
AuthRequireApproval bool `json:"auth_require_approval"`
// Agent 全局并发上限(保护 pi-agent / GPU / API)
MaxConcurrentAgents int `json:"max_concurrent_agents"`
// AllowInMemory 显式允许数据库不可用时降级内存模式(仅限本地 Demo)
AllowInMemory bool `json:"allow_in_memory"`
// CORSAllowedOrigins 允许的浏览器 Origin 列表;空列表时不回显 Access-Control-Allow-Origin
CORSAllowedOrigins []string `json:"cors_allowed_origins"`
// Final Artifact Vault (FAV) — defaults stay off until separate approval.
VaultStateRoot string `json:"vault_state_root"`
VaultStorageIdentity string `json:"vault_storage_identity"`
VaultMasterKey string `json:"vault_master_key"`
FAVDegradedRead bool `json:"fav_degraded_read"` // FAV_DEGRADED_READ; default false
FAVPublisherEnabled bool `json:"fav_publisher_enabled"` // FAV_PUBLISHER_ENABLED; default false
}
func Load(path string) (*Config, error) {
cfg := &Config{
Env: "dev",
DataDir: "./data/dev",
HTTPHost: "127.0.0.1",
HTTPPort: 8080,
AgentBackend: "pi-agent",
AgentTimeout: 30 * time.Minute,
AgentXAPIBase: "https://api.minimaxi.com/v1",
AgentXModel: "MiniMax-M2.7-highspeed",
AgentXTimeout: 30 * time.Minute,
WorkspaceTTL: 7 * 24 * time.Hour,
WorkspaceQuotaBytes: 20 << 30,
PiAgentBin: "/opt/homebrew/bin/pi",
PiAgentBridgeBin: "tools/pi-bridge/index.mjs",
PiAgentProvider: "minimax",
PiAgentModel: "MiniMax-M2.7-highspeed",
KbDbHost: "127.0.0.1",
KbDbPort: 5432,
KbDbUser: "regkb",
KbDbPassword: "", // 禁止静默默认口令;须由根 .env / 进程环境提供
KbDbName: "regulations",
ContextDbHost: "127.0.0.1",
ContextDbPort: 5433,
ContextDbUser: "app",
ContextDbPassword: "dev_secret_db_pass",
ContextDbName: "agent_system_dev",
EmbeddingEndpointURL: "https://www.dmxapi.cn/v1/embeddings",
EmbeddingProvider: "openai_compatible",
EmbeddingModel: "bge-m3",
EmbeddingAPIKey: "",
RerankerEndpointURL: "https://www.dmxapi.cn/v1/rerank",
RerankerProvider: "openai_compatible",
RerankerModel: "bge-reranker-v2-m3-free",
RerankerAPIKey: "",
MineruEndpointURL: "https://mineru.net",
MineruAPIKey: "",
MineruModelVersion: "vlm",
MineruLanguage: "ch",
MineruTimeout: 90 * time.Second,
MaterialFulltextMaxBytes: 60 * 1024,
MaterialRAGTopK: 8,
AdminUsername: "admin",
AdminPassword: "admin-secret-password-123",
AuthRegistrationEnabled: true,
AuthRequireApproval: false,
MaxConcurrentAgents: 8,
}
if path == "" {
// 与 Compose 一致:根 .env(公共密钥)+ configs/.env.{ENV}(环境差异 overlay)
_ = applyEnvFile(cfg, ".env")
envName := cfg.Env
if v := os.Getenv("ENV"); v != "" {
envName = v
}
if envName == "" {
envName = "dev"
}
_ = applyEnvFile(cfg, filepath.Join("configs", ".env."+envName))
} else {
_ = applyEnvFile(cfg, path)
}
// 允许系统环境变量覆盖
if v := os.Getenv("ENV"); v != "" {
cfg.Env = v
}
if v := os.Getenv("DATA_DIR"); v != "" {
cfg.DataDir = v
}
if v := os.Getenv("HTTP_HOST"); v != "" {
cfg.HTTPHost = v
} else if v := os.Getenv("APP_PUBLISH_HOST"); v != "" {
cfg.HTTPHost = v
}
if v := os.Getenv("HTTP_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.HTTPPort = p
}
}
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("AGENTX_API_BASE"); v != "" {
cfg.AgentXAPIBase = v
} else if v := os.Getenv("MINIMAX_ENDPOINT_URL"); v != "" {
cfg.AgentXAPIBase = v
}
if v := os.Getenv("AGENTX_API_KEY"); v != "" {
cfg.AgentXAPIKey = v
} else if v := os.Getenv("MINIMAX_API_KEY"); v != "" {
cfg.AgentXAPIKey = v
}
if v := os.Getenv("AGENTX_MODEL"); v != "" {
cfg.AgentXModel = v
}
if v := os.Getenv("AGENTX_TIMEOUT"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
cfg.AgentXTimeout = d
}
}
if v := os.Getenv("WORKSPACE_TTL"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
cfg.WorkspaceTTL = d
}
}
if v := os.Getenv("WORKSPACE_QUOTA_BYTES"); v != "" {
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 {
cfg.WorkspaceQuotaBytes = n
}
}
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
}
if v := os.Getenv("PI_AGENT_MODEL"); v != "" {
cfg.PiAgentModel = v
}
if v := os.Getenv("KB_DB_HOST"); v != "" {
cfg.KbDbHost = v
}
if v := os.Getenv("KB_DB_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.KbDbPort = p
}
}
if v := os.Getenv("KB_DB_USER"); v != "" {
cfg.KbDbUser = v
}
if v := os.Getenv("KB_DB_PASSWORD"); v != "" {
cfg.KbDbPassword = v
}
if v := os.Getenv("KB_DB_NAME"); v != "" {
cfg.KbDbName = v
}
if v := os.Getenv("CONTEXT_DB_HOST"); v != "" {
cfg.ContextDbHost = v
}
if v := os.Getenv("CONTEXT_DB_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.ContextDbPort = p
}
}
if v := os.Getenv("CONTEXT_DB_USER"); v != "" {
cfg.ContextDbUser = v
}
if v := os.Getenv("CONTEXT_DB_PASSWORD"); v != "" {
cfg.ContextDbPassword = v
}
if v := os.Getenv("CONTEXT_DB_NAME"); v != "" {
cfg.ContextDbName = v
}
if v := os.Getenv("EMBEDDING_ENDPOINT_URL"); v != "" {
cfg.EmbeddingEndpointURL = v
}
if v := os.Getenv("EMBEDDING_API_KEY"); v != "" {
cfg.EmbeddingAPIKey = v
}
if v := os.Getenv("EMBEDDING_MODEL"); v != "" {
cfg.EmbeddingModel = v
}
if v := os.Getenv("RERANKER_ENDPOINT_URL"); v != "" {
cfg.RerankerEndpointURL = v
}
if v := os.Getenv("RERANKER_API_KEY"); v != "" {
cfg.RerankerAPIKey = v
}
if v := os.Getenv("RERANKER_MODEL"); v != "" {
cfg.RerankerModel = v
}
if v := os.Getenv("MINERU_ENDPOINT_URL"); v != "" {
cfg.MineruEndpointURL = v
}
if v := os.Getenv("MINERU_API_KEY"); v != "" {
cfg.MineruAPIKey = v
}
if v := os.Getenv("MINERU_MODEL_VERSION"); v != "" {
cfg.MineruModelVersion = v
}
if v := os.Getenv("MINERU_LANGUAGE"); v != "" {
cfg.MineruLanguage = v
}
if v := os.Getenv("MINERU_TIMEOUT"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
cfg.MineruTimeout = d
}
}
if v := os.Getenv("MATERIAL_FULLTEXT_MAX_BYTES"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
cfg.MaterialFulltextMaxBytes = n
}
}
if v := os.Getenv("MATERIAL_RAG_TOP_K"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
cfg.MaterialRAGTopK = n
}
}
if v := os.Getenv("ADMIN_USERNAME"); v != "" {
cfg.AdminUsername = v
}
if v := os.Getenv("ADMIN_PASSWORD"); v != "" {
cfg.AdminPassword = v
}
if v := os.Getenv("AUTH_REGISTRATION_ENABLED"); v != "" {
cfg.AuthRegistrationEnabled = v == "true" || v == "1"
}
if v := os.Getenv("AUTH_REQUIRE_APPROVAL"); v != "" {
cfg.AuthRequireApproval = v == "true" || v == "1"
}
if v := os.Getenv("MAX_CONCURRENT_AGENTS"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
cfg.MaxConcurrentAgents = n
}
}
if v := os.Getenv("PI_AGENT_RUNNER_URL"); v != "" {
cfg.PiAgentRunnerURL = v
}
if v := os.Getenv("PI_AGENT_RUNNER_AUTH_TOKEN"); v != "" {
cfg.PiAgentRunnerAuthToken = v
}
if v := os.Getenv("RUNNER_AUTH_TOKEN"); v != "" {
cfg.PiAgentRunnerAuthToken = v
}
if v := os.Getenv("ALLOW_IN_MEMORY"); v != "" {
cfg.AllowInMemory = v == "true" || v == "1"
}
if v := os.Getenv("CORS_ALLOWED_ORIGINS"); v != "" {
cfg.CORSAllowedOrigins = parseCSVOrigins(v)
}
if v := os.Getenv("VAULT_STATE_ROOT"); v != "" {
cfg.VaultStateRoot = v
}
if v := os.Getenv("VAULT_STORAGE_IDENTITY"); v != "" {
cfg.VaultStorageIdentity = v
}
if v := os.Getenv("VAULT_MASTER_KEY"); v != "" {
cfg.VaultMasterKey = v
}
if v := os.Getenv("FAV_DEGRADED_READ"); v != "" {
cfg.FAVDegradedRead = v == "true" || v == "1"
}
if v := os.Getenv("FAV_PUBLISHER_ENABLED"); v != "" {
cfg.FAVPublisherEnabled = v == "true" || v == "1"
}
if v := os.Getenv("HTTP_ADDR"); v != "" && strings.HasPrefix(v, ":") {
if p, err := strconv.Atoi(strings.TrimPrefix(v, ":")); err == nil {
cfg.HTTPPort = p
cfg.HTTPHost = "0.0.0.0"
}
}
applyDSNOverrides(cfg)
applyCORSDefaults(cfg)
NormalizeKBHostForRuntime(cfg)
if err := validateProductionSecrets(cfg); err != nil {
return nil, err
}
if err := ValidateIndependentVault(cfg.VaultStateRoot, cfg.DataDir); err != nil {
return nil, err
}
return cfg, nil
}
// applyEnvFile overlays KEY=VALUE pairs from path onto cfg. Missing file is a no-op.
func applyEnvFile(cfg *Config, path string) error {
if cfg == nil || strings.TrimSpace(path) == "" {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
if commentIdx := strings.Index(val, " #"); commentIdx != -1 {
val = strings.TrimSpace(val[:commentIdx])
}
switch key {
case "ENV":
cfg.Env = val
case "DATA_DIR":
cfg.DataDir = val
case "APP_PUBLISH_HOST", "HTTP_HOST":
cfg.HTTPHost = val
case "HTTP_PORT":
if p, err := strconv.Atoi(val); err == nil {
cfg.HTTPPort = p
}
case "AGENT_BACKEND":
cfg.AgentBackend = val
case "AGENT_TIMEOUT":
if d, err := time.ParseDuration(val); err == nil {
cfg.AgentTimeout = d
}
case "AGENTX_API_BASE", "MINIMAX_ENDPOINT_URL":
cfg.AgentXAPIBase = val
case "AGENTX_API_KEY", "MINIMAX_API_KEY":
cfg.AgentXAPIKey = val
case "AGENTX_MODEL":
cfg.AgentXModel = val
case "AGENTX_TIMEOUT":
if d, err := time.ParseDuration(val); err == nil {
cfg.AgentXTimeout = d
}
case "WORKSPACE_TTL":
if d, err := time.ParseDuration(val); err == nil {
cfg.WorkspaceTTL = d
}
case "WORKSPACE_QUOTA_BYTES":
if n, err := strconv.ParseInt(val, 10, 64); err == nil && n > 0 {
cfg.WorkspaceQuotaBytes = n
}
case "PI_AGENT_BIN":
cfg.PiAgentBin = val
case "PI_AGENT_BRIDGE_BIN":
cfg.PiAgentBridgeBin = val
case "PI_AGENT_PROVIDER":
cfg.PiAgentProvider = val
case "PI_AGENT_MODEL":
cfg.PiAgentModel = val
case "PI_AGENT_RUNNER_URL":
cfg.PiAgentRunnerURL = val
case "KB_DB_HOST":
cfg.KbDbHost = val
case "KB_DB_PORT":
if p, err := strconv.Atoi(val); err == nil {
cfg.KbDbPort = p
}
case "KB_DB_USER":
cfg.KbDbUser = val
case "KB_DB_PASSWORD":
cfg.KbDbPassword = val
case "KB_DB_NAME":
cfg.KbDbName = val
case "CONTEXT_DB_HOST":
cfg.ContextDbHost = val
case "CONTEXT_DB_PORT":
if p, err := strconv.Atoi(val); err == nil {
cfg.ContextDbPort = p
}
case "CONTEXT_DB_USER":
cfg.ContextDbUser = val
case "CONTEXT_DB_PASSWORD":
cfg.ContextDbPassword = val
case "CONTEXT_DB_NAME":
cfg.ContextDbName = val
case "EMBEDDING_ENDPOINT_URL":
cfg.EmbeddingEndpointURL = val
case "EMBEDDING_PROVIDER":
cfg.EmbeddingProvider = val
case "EMBEDDING_MODEL":
cfg.EmbeddingModel = val
case "EMBEDDING_API_KEY":
cfg.EmbeddingAPIKey = val
case "RERANKER_ENDPOINT_URL":
cfg.RerankerEndpointURL = val
case "RERANKER_PROVIDER":
cfg.RerankerProvider = val
case "RERANKER_MODEL":
cfg.RerankerModel = val
case "RERANKER_API_KEY":
cfg.RerankerAPIKey = val
case "MINERU_ENDPOINT_URL":
cfg.MineruEndpointURL = val
case "MINERU_API_KEY":
cfg.MineruAPIKey = val
case "MINERU_MODEL_VERSION":
cfg.MineruModelVersion = val
case "MINERU_LANGUAGE":
cfg.MineruLanguage = val
case "MINERU_TIMEOUT":
if d, err := time.ParseDuration(val); err == nil {
cfg.MineruTimeout = d
}
case "MATERIAL_FULLTEXT_MAX_BYTES":
if n, err := strconv.Atoi(val); err == nil && n > 0 {
cfg.MaterialFulltextMaxBytes = n
}
case "MATERIAL_RAG_TOP_K":
if n, err := strconv.Atoi(val); err == nil && n > 0 {
cfg.MaterialRAGTopK = n
}
case "ADMIN_USERNAME":
cfg.AdminUsername = val
case "ADMIN_PASSWORD":
cfg.AdminPassword = val
case "AUTH_REGISTRATION_ENABLED":
cfg.AuthRegistrationEnabled = val == "true" || val == "1"
case "AUTH_REQUIRE_APPROVAL":
cfg.AuthRequireApproval = val == "true" || val == "1"
case "MAX_CONCURRENT_AGENTS":
if n, err := strconv.Atoi(val); err == nil && n > 0 {
cfg.MaxConcurrentAgents = n
}
case "ALLOW_IN_MEMORY":
cfg.AllowInMemory = val == "true" || val == "1"
case "CORS_ALLOWED_ORIGINS":
cfg.CORSAllowedOrigins = parseCSVOrigins(val)
case "VAULT_STATE_ROOT":
cfg.VaultStateRoot = val
case "VAULT_STORAGE_IDENTITY":
cfg.VaultStorageIdentity = val
case "VAULT_MASTER_KEY":
cfg.VaultMasterKey = val
case "FAV_DEGRADED_READ":
cfg.FAVDegradedRead = val == "true" || val == "1"
case "FAV_PUBLISHER_ENABLED":
cfg.FAVPublisherEnabled = val == "true" || val == "1"
}
}
return scanner.Err()
}
// NormalizeKBHostForRuntime rewrites Compose DNS names to localhost when running
// on the host (no /.dockerenv). Inside containers the shared network name is kept.
func NormalizeKBHostForRuntime(cfg *Config) {
if cfg == nil {
return
}
if _, err := os.Stat("/.dockerenv"); err == nil {
return
}
switch strings.TrimSpace(cfg.KbDbHost) {
case "shuimu-kb-shared", "shuimu-kb":
cfg.KbDbHost = "127.0.0.1"
}
}
// RequireKbPassword returns an error when KB_DB_PASSWORD is empty (common after
// layered Load if root .env is missing). Callers that need online KB should check this.
func (c *Config) RequireKbPassword() error {
if c == nil || strings.TrimSpace(c.KbDbPassword) == "" {
return fmt.Errorf("config: KB_DB_PASSWORD is empty; provide it in root .env (layered with configs/.env.{ENV}) or process environment")
}
return nil
}
// ValidateIndependentVault refuses vault roots that collide with runtime data/{env}.
// Empty root is allowed (FAV stays unconfigured / disabled).
func ValidateIndependentVault(root, dataDir string) error {
if strings.TrimSpace(root) == "" {
return nil
}
cr := filepath.Clean(root)
for _, env := range []string{"dev", "test", "main"} {
if strings.HasSuffix(cr, filepath.Join("data", env)) {
return fmt.Errorf("config: VAULT_STATE_ROOT must not be runtime data root %s", root)
}
}
if dataDir != "" {
cd := filepath.Clean(dataDir)
if cr == cd {
return fmt.Errorf("config: VAULT_STATE_ROOT must be independent from DATA_DIR")
}
}
return nil
}
func parseCSVOrigins(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func applyCORSDefaults(cfg *Config) {
if cfg == nil || len(cfg.CORSAllowedOrigins) > 0 {
return
}
if cfg.Env == "dev" || cfg.Env == "test" {
cfg.CORSAllowedOrigins = []string{
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:8080",
"http://127.0.0.1:8080",
"http://localhost:8081",
"http://127.0.0.1:8081",
}
}
}
func validateProductionSecrets(cfg *Config) error {
if cfg == nil || cfg.Env == "" || cfg.Env == "dev" || cfg.Env == "test" {
return nil
}
weak := map[string]string{
"KB_DB_PASSWORD": cfg.KbDbPassword,
"CONTEXT_DB_PASSWORD": cfg.ContextDbPassword,
"ADMIN_PASSWORD": cfg.AdminPassword,
}
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)
}
for _, d := range defaults {
if val == d {
return fmt.Errorf("%s uses insecure default value in %s environment", envKey, cfg.Env)
}
}
}
if cfg.AllowInMemory {
return fmt.Errorf("ALLOW_IN_MEMORY must be false in %s environment", cfg.Env)
}
if strings.TrimSpace(cfg.PiAgentRunnerURL) != "" && strings.TrimSpace(cfg.PiAgentRunnerAuthToken) == "" {
return fmt.Errorf("PI_AGENT_RUNNER_AUTH_TOKEN (or RUNNER_AUTH_TOKEN) must be set when PI_AGENT_RUNNER_URL is configured in %s environment", cfg.Env)
}
return nil
}
func applyDSNOverrides(cfg *Config) {
if dsn := os.Getenv("APP_DB_DSN"); dsn != "" {
if host, port, user, password, name, ok := parsePostgresDSN(dsn); ok {
cfg.ContextDbHost = host
cfg.ContextDbPort = port
cfg.ContextDbUser = user
cfg.ContextDbPassword = password
cfg.ContextDbName = name
}
}
if dsn := os.Getenv("POSTGRES_SHARED_KB_DSN"); dsn != "" {
if host, port, user, password, name, ok := parsePostgresDSN(dsn); ok {
cfg.KbDbHost = host
cfg.KbDbPort = port
cfg.KbDbUser = user
cfg.KbDbPassword = password
cfg.KbDbName = name
}
}
}
func parsePostgresDSN(dsn string) (host string, port int, user, password, dbName string, ok bool) {
if !strings.HasPrefix(dsn, "postgres://") {
return "", 0, "", "", "", false
}
rest := strings.TrimPrefix(dsn, "postgres://")
at := strings.LastIndex(rest, "@")
if at <= 0 {
return "", 0, "", "", "", false
}
creds := rest[:at]
hostPart := rest[at+1:]
slash := strings.Index(hostPart, "/")
if slash <= 0 {
return "", 0, "", "", "", false
}
hostPort := hostPart[:slash]
dbName = strings.SplitN(hostPart[slash+1:], "?", 2)[0]
userPass := strings.SplitN(creds, ":", 2)
if len(userPass) != 2 {
return "", 0, "", "", "", false
}
user = userPass[0]
password = userPass[1]
hostPortParts := strings.SplitN(hostPort, ":", 2)
host = hostPortParts[0]
port = 5432
if len(hostPortParts) == 2 {
if p, err := strconv.Atoi(hostPortParts[1]); err == nil {
port = p
}
}
return host, port, user, password, dbName, true
}
// KbDSN 返回 PostgreSQL 连接字符串。口令为空时仍返回可解析 DSN,但不应被静默当真口令使用。
func (c *Config) KbDSN() string {
return "postgres://" + c.KbDbUser + ":" + c.KbDbPassword + "@" + c.KbDbHost + ":" + strconv.Itoa(c.KbDbPort) + "/" + c.KbDbName + "?sslmode=disable"
}
// RedactPostgresDSN masks the password segment of a postgres:// DSN for logs.
func RedactPostgresDSN(dsn string) string {
if !strings.HasPrefix(dsn, "postgres://") {
return dsn
}
rest := strings.TrimPrefix(dsn, "postgres://")
at := strings.LastIndex(rest, "@")
if at <= 0 {
return "postgres://***"
}
creds := rest[:at]
hostPart := rest[at+1:]
userPass := strings.SplitN(creds, ":", 2)
if len(userPass) != 2 {
return "postgres://***@" + hostPart
}
return "postgres://" + userPass[0] + ":***@" + hostPart
}
// ContextDSN 返回专属记忆与多级上下文 PostgreSQL 连接字符串
func (c *Config) ContextDSN() string {
return "postgres://" + c.ContextDbUser + ":" + c.ContextDbPassword + "@" + c.ContextDbHost + ":" + strconv.Itoa(c.ContextDbPort) + "/" + c.ContextDbName + "?sslmode=disable"
}