From 2235fc723042e5af8d0475244d3ff2ec968b24a5 Mon Sep 17 00:00:00 2001 From: agent-bot Date: Tue, 29 Sep 2026 07:25:28 +0000 Subject: [PATCH] feat(cli): 'config check' subcommand (U10a tail, KTD16/17 minimal) - server config check: load + validate + masked summary; exit 1 on issues - no side effects: creates no DB, starts nothing, writes no business dirs - Config.Check() (production secrets, port range, data dir) exported for reuse; MaskedSummary never prints credential values - default no-arg invocation unchanged (serve); help added - smoke-verified: pass exit 0, invalid port exit 1 with issue line - layer-source attribution (config explain) deferred to U10b ConfigSnapshot --- cmd/server/main.go | 53 +++++++++++++++++++++++++++++++++++++++ internal/config/config.go | 38 ++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/cmd/server/main.go b/cmd/server/main.go index c5e3684c..c66e349d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -19,6 +19,22 @@ import ( ) func main() { + // KTD16:同一程序提供运维子命令;默认(无子命令)保持 serve 行为不变。 + if len(os.Args) > 1 { + switch os.Args[1] { + case "config": + if len(os.Args) > 2 && os.Args[2] == "check" { + configCheck(os.Args[3:]) + return + } + fmt.Fprintln(os.Stderr, "用法: server config check [--env path] [--port N] [--host H] [--backend name]") + os.Exit(2) + case "-h", "--help", "help": + fmt.Println("用法: server [serve flags] 启动服务(默认)\n server config check [flags] 加载并校验配置(脱敏摘要,无副作用)") + return + } + } + envPath := flag.String("env", ".env", "环境配置文件路径") port := flag.Int("port", 0, "HTTP 服务端口 (默认读取配置文件或 8080)") host := flag.String("host", "", "HTTP 监听主机 (默认读取配置文件或 127.0.0.1)") @@ -157,3 +173,40 @@ func shutdownDrainTimeout(cfg *config.Config) time.Duration { } return at + 30*time.Second } + + +// configCheck 加载并校验配置:输出脱敏摘要与问题清单。 +// 不创建 DB、不启动服务、不写业务目录(KTD17 config check 约束)。 +func configCheck(args []string) { + fs := flag.NewFlagSet("config check", flag.ExitOnError) + envPath := fs.String("env", ".env", "环境配置文件路径") + port := fs.Int("port", 0, "HTTP 端口覆盖") + host := fs.String("host", "", "HTTP 主机覆盖") + backend := fs.String("backend", "", "智能体后端覆盖") + _ = fs.Parse(args) + + cfg, err := config.Load(*envPath) + if err != nil { + fmt.Fprintf(os.Stderr, "config check: 加载失败: %v\n", err) + os.Exit(1) + } + if *port > 0 { + cfg.HTTPPort = *port + } + if *host != "" { + cfg.HTTPHost = *host + } + if *backend != "" { + cfg.AgentBackend = *backend + } + + fmt.Println(cfg.MaskedSummary()) + if issues := cfg.Check(); len(issues) > 0 { + fmt.Fprintln(os.Stderr, "config check: 未通过") + for _, it := range issues { + fmt.Fprintf(os.Stderr, " - %s\n", it) + } + os.Exit(1) + } + fmt.Println("config check: 通过") +} diff --git a/internal/config/config.go b/internal/config/config.go index a47a6a08..9cf970ff 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -737,3 +737,41 @@ func RedactPostgresDSN(dsn string) string { func (c *Config) ContextDSN() string { return "postgres://" + c.ContextDbUser + ":" + c.ContextDbPassword + "@" + c.ContextDbHost + ":" + strconv.Itoa(c.ContextDbPort) + "/" + c.ContextDbName + "?sslmode=disable" } + + +// Check 返回配置问题列表(空 = 通过)。只读、无副作用,供 config check +// 子命令与部署预检使用;完整分层溯源(explain)随 U10b ConfigSnapshot 提供。 +func (c *Config) Check() []string { + var issues []string + if c == nil { + return []string{"config is nil"} + } + if err := validateProductionSecrets(c); err != nil { + issues = append(issues, err.Error()) + } + if c.HTTPPort <= 0 || c.HTTPPort > 65535 { + issues = append(issues, fmt.Sprintf("http port out of range: %d", c.HTTPPort)) + } + if strings.TrimSpace(c.DataDir) == "" { + issues = append(issues, "data dir is empty") + } + return issues +} + +// MaskedSummary 输出脱敏的关键配置摘要(不含任何凭证值)。 +func (c *Config) MaskedSummary() string { + mask := func(s string) string { + if strings.TrimSpace(s) == "" { + return "" + } + return "" + } + return fmt.Sprintf("env=%s data_dir=%s listen=%s:%d backend=%s timeout=%s\n"+ + "app_db=%s:%d/%s user=%s password=%s\n"+ + "kb_db=%s:%d/%s user=%s password=%s\n"+ + "runner_url=%s runner_token=%s agentx_key=%s allow_in_memory=%v", + c.Env, c.DataDir, c.HTTPHost, c.HTTPPort, c.AgentBackend, c.AgentTimeout, + c.ContextDbHost, c.ContextDbPort, c.ContextDbName, c.ContextDbUser, mask(c.ContextDbPassword), + c.KbDbHost, c.KbDbPort, c.KbDbName, c.KbDbUser, mask(c.KbDbPassword), + c.PiAgentRunnerURL, mask(c.PiAgentRunnerAuthToken), mask(c.AgentXAPIKey), c.AllowInMemory) +}