Files
Agent-System/cmd/server/main.go
T
agent-bot 2235fc7230
CI / go-vet (pull_request) Successful in 13s
CI / short-tests-report (pull_request) Failing after 36s
CI / backup-restore-drill (pull_request) Successful in 29s
CI / web (pull_request) Successful in 32s
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
2026-09-29 07:25:28 +00:00

213 lines
7.9 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 main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"chat-agent-system/internal/app"
"chat-agent-system/internal/app/eventsconsumer"
"chat-agent-system/internal/config"
"chat-agent-system/internal/localpg"
"chat-agent-system/internal/server"
"chat-agent-system/internal/workflow"
)
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)")
backend := flag.String("backend", "", "智能体后端调度覆盖 (stub, pi-agent, pi-native)")
flag.Parse()
cfg, err := config.Load(*envPath)
if err != nil {
fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err)
os.Exit(1)
}
if *port > 0 {
cfg.HTTPPort = *port
}
if *host != "" {
cfg.HTTPHost = *host
}
if *backend != "" {
cfg.AgentBackend = *backend
}
rootCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
pg, err := localpg.Ensure(rootCtx, cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "localpg: %v\n", err)
os.Exit(1)
}
if pg != nil {
defer func() { _ = pg.Stop() }()
fmt.Printf("localpg managed PGDATA=%s port=%d\n", pg.PGData, pg.Port)
}
rt, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "业务服务初始化失败: %v\n", err)
os.Exit(1)
}
// Inject executor/quality-gate key validators without giving workflowdef a production import of workflow.
if rt.WFDef != nil {
rt.WFDef.SetRegistryValidators(workflow.ValidateExecutorKey, workflow.ValidateQualityGateKey)
}
if err := rt.Start(rootCtx); err != nil {
fmt.Fprintf(os.Stderr, "后台 worker 启动失败: %v\n", err)
os.Exit(1)
}
var eventsH *eventsconsumer.Handle
if eventsconsumer.Enabled() && rt.Pool != nil {
eventsH, err = eventsconsumer.Start(rootCtx, rt.Pool, cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "eventsconsumer: %v\n", err)
os.Exit(1)
}
fmt.Printf("events in-process owner=%s mode=cluster-table\n", eventsH.Owner)
}
srv := server.NewFromRuntime(rt)
if eventsH != nil {
srv.SetEventsAlive(func() bool { return eventsH.Alive(30 * time.Second) })
}
addr := fmt.Sprintf("%s:%d", cfg.HTTPHost, cfg.HTTPPort)
fmt.Println("==================================================================")
fmt.Println(" 🤖 医疗器械商务与研发智能参谋系统 - HTTP / SSE API 守护服务 ")
fmt.Println("==================================================================")
fmt.Printf("服务环境: %s | 智能体后端: %s\n", cfg.Env, cfg.AgentBackend)
fmt.Printf("监听地址: http://%s\n", addr)
fmt.Println("核心服务路由:")
fmt.Println(" • [GET] /api/v1/health - 服务健康检查与基础设施状态")
fmt.Println(" • [GET] /api/v1/projects - 查询项目当前上下文与黑板事实")
fmt.Println(" • [POST] /api/v1/chat - 标准阻塞式对话协同接口")
fmt.Println(" • [POST] /api/v1/chat/stream - 核心 SSE 流式长连接对话协议 (分层按需)")
fmt.Println(" • [GET] /api/v1/chat/stream - SSE 流式长连接对话调试接口 (EventSource)")
fmt.Println(" • [POST] /api/v1/tasks/run/stream - 独立任务与阶段执行 SSE 监控流 (精简进度与拦截)")
fmt.Println(" • [GET] /api/v1/context/user - 获取单用户私有上下文与习惯偏好画像")
fmt.Println(" • [PUT] /api/v1/context/user/preference - 整理或更新用户个性化产出风格")
fmt.Println(" • [GET] /api/v1/context/project - 获取项目多方协同黑板事实与里程碑决策流")
fmt.Println(" • [GET] /api/v1/chat/sessions - 分页查询用户/项目历史会话列表")
fmt.Println(" • [GET] /api/v1/chat/messages - 分页拉取指定会话多轮历史消息记录")
fmt.Println(" • [GET] /api/v1/context/cases/recommend - 往期受控标杆案例与历史报价语义自推荐")
fmt.Println(" • [GET] /api/v1/docgov/document - 查询受控文档治理状态看板")
fmt.Println(" • [POST] /api/v1/docgov/claim - 查阅并认领受控文档 (正式稿方可认领)")
fmt.Println(" • [POST] /api/v1/docgov/proposal - 对已认领文档发起变更提案")
fmt.Println(" • [POST] /api/v1/docgov/proposal/decide - 编制负责人拍板同意/拒绝变更提案")
fmt.Println(" • [POST] /api/v1/docgov/submit-review - 编制完成提交审核 (文档正文锁定)")
fmt.Println(" • [POST] /api/v1/docgov/review-decision - 审核通过或驳回 (驳回必填原因且解锁)")
fmt.Println(" • [POST] /api/v1/docgov/approver-decision - 最终批准或驳回")
fmt.Println(" • [GET/POST] /api/v1/docgov/comments - 批注与修改建议管理")
fmt.Println(" • [GET] /api/v1/docgov/audit-logs - 全生命周期审计轨迹流")
fmt.Println("==================================================================")
go func() {
if err := srv.Start(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "服务启动失败: %v\n", err)
os.Exit(1)
}
}()
<-rootCtx.Done()
fmt.Println("\n正在优雅停止服务...")
if eventsH != nil {
eventsH.Stop()
}
// T20-B7:drain 窗口与 AGENT_TIMEOUT 量级对齐(默认 min(AgentTimeout, 35m)+30s 缓冲),
// 消除「5s Shutdown vs 30m 长任务」错配;可由 SHUTDOWN_DRAIN_TIMEOUT 显式覆盖。
drain := shutdownDrainTimeout(cfg)
ctx, cancel := context.WithTimeout(context.Background(), drain)
defer cancel()
fmt.Printf("等待 in-flight 请求/长任务排空(drain 上限 %s)...\n", drain)
if err := srv.Shutdown(ctx); err != nil {
fmt.Fprintf(os.Stderr, "服务停止异常: %v\n", err)
} else {
fmt.Println("服务已安全退出。")
}
}
// shutdownDrainTimeout 解析 T20-B7 drain 窗口:
// - SHUTDOWN_DRAIN_TIMEOUT 显式设置则优先(校验 >0);
// - 否则默认 min(AGENT_TIMEOUT, 35m) + 30s 缓冲(compose stop_grace_period 需 ≥ 此值)。
func shutdownDrainTimeout(cfg *config.Config) time.Duration {
if v := os.Getenv("SHUTDOWN_DRAIN_TIMEOUT"); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
return d
}
}
at := 30 * time.Minute
if cfg != nil && cfg.AgentTimeout > 0 {
at = cfg.AgentTimeout
}
cap := 35 * time.Minute
if at > cap {
at = cap
}
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: 通过")
}