package server import ( "context" "fmt" "net/http" "os" "path/filepath" "strings" "sync" "time" "chat-agent-system/internal/auth" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/chat" "chat-agent-system/internal/config" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/intent" ) // Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务 type Server struct { cfg *config.Config svc *bizdev.Service rec intent.Recognizer engine *coordinator.CoordinatorEngine authSvc *auth.Service sessions map[string]*chat.Session sessionsMu sync.RWMutex httpServer *http.Server } // NewServer 构造函数 func NewServer(cfg *config.Config, svc *bizdev.Service) *Server { if cfg == nil { cfg, _ = config.Load("") } if svc == nil { svc = bizdev.NewService(cfg) } rec := intent.NewRecognizer() coordEngine := coordinator.NewCoordinatorEngine(svc) authStore := auth.NewMemoryStore(cfg) authSvc := auth.NewService(authStore, cfg) s := &Server{ cfg: cfg, svc: svc, rec: rec, engine: coordEngine, authSvc: authSvc, sessions: make(map[string]*chat.Session), } return s } // AuthService 获取认证服务 func (s *Server) AuthService() *auth.Service { return s.authSvc } // Routes 构建 HTTP 路由分发器 func (s *Server) Routes() http.Handler { mux := http.NewServeMux() // 1. 健康检查与系统自检 mux.HandleFunc("GET /health", s.handleHealth) mux.HandleFunc("GET /api/v1/health", s.handleHealth) // 2. 项目状态与会话查询 mux.HandleFunc("GET /api/v1/projects", s.handleGetProject) // 3. 对话协同服务 (标准 JSON 阻塞) mux.HandleFunc("POST /api/v1/chat", s.handleChat) // 4. 对话协同服务 (核心 SSE 流式长连接) mux.HandleFunc("POST /api/v1/chat/stream", s.handleChatStream) mux.HandleFunc("GET /api/v1/chat/stream", s.handleChatStream) // 5. 独立任务 / 阶段执行 mux.HandleFunc("POST /api/v1/tasks/run", s.handleRunTask) mux.HandleFunc("POST /api/v1/tasks/run/stream", s.handleRunTaskStream) mux.HandleFunc("GET /api/v1/tasks/run/stream", s.handleRunTaskStream) // 6. 多级上下文与偏好记忆管理 mux.HandleFunc("GET /api/v1/context/user", s.handleGetUserContext) mux.HandleFunc("PUT /api/v1/context/user/preference", s.handleUpdateUserPreference) mux.HandleFunc("GET /api/v1/context/project", s.handleGetProjectContext) // 7. 历史多轮会话管理与标杆案例语义检索 mux.HandleFunc("GET /api/v1/chat/sessions", s.handleListChatSessions) mux.HandleFunc("GET /api/v1/chat/messages", s.handleGetChatMessages) mux.HandleFunc("GET /api/v1/context/cases/recommend", s.handleRecommendCases) // 8. 受控文档编制、审核、批准生命周期与认领治理 mux.HandleFunc("GET /api/v1/docgov/document", s.handleDocGovGetDocument) mux.HandleFunc("POST /api/v1/docgov/claim", s.handleDocGovClaim) mux.HandleFunc("POST /api/v1/docgov/submit-review", s.handleDocGovSubmitReview) mux.HandleFunc("POST /api/v1/docgov/review-decision", s.handleDocGovReviewDecision) mux.HandleFunc("POST /api/v1/docgov/approver-decision", s.handleDocGovApproverDecision) mux.HandleFunc("POST /api/v1/docgov/proposal/decide", s.handleDocGovDecideProposal) mux.HandleFunc("GET /api/v1/docgov/comments", s.handleDocGovListComments) mux.HandleFunc("POST /api/v1/docgov/comments", s.handleDocGovAddComment) mux.HandleFunc("GET /api/v1/docgov/audit-logs", s.handleDocGovListAuditLogs) // 9. 用户认证、安全改密与超级管理员后台接口 mux.HandleFunc("POST /api/v1/auth/login", s.handleAuthLogin) mux.HandleFunc("POST /api/v1/auth/register", s.handleAuthRegister) mux.HandleFunc("POST /api/v1/auth/logout", s.handleAuthLogout) mux.HandleFunc("POST /api/v1/auth/change-password", s.handleAuthChangePassword) mux.HandleFunc("GET /api/v1/auth/me", s.handleAuthMe) mux.HandleFunc("GET /api/v1/admin/users", s.handleAdminUsers) mux.HandleFunc("POST /api/v1/admin/users", s.handleAdminUsers) mux.HandleFunc("PUT /api/v1/admin/users/{id}/role", s.handleAdminUpdateRole) mux.HandleFunc("PUT /api/v1/admin/users/{id}/status", s.handleAdminUpdateStatus) mux.HandleFunc("GET /api/v1/admin/config", s.handleAdminConfig) mux.HandleFunc("PUT /api/v1/admin/config", s.handleAdminConfig) // 10. 现代 SPA 前端页面与静态资产托管 mux.HandleFunc("GET /", s.serveSPA) return s.corsMiddleware(mux) } // serveSPA 托管 web/dist 静态产物并提供前端路由自动回退 func (s *Server) serveSPA(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/") { http.NotFound(w, r) return } candidates := []string{ "web/dist", "../web/dist", "../../web/dist", filepath.Join(s.cfg.DataDir, "web/dist"), filepath.Join(s.cfg.DataDir, "../web/dist"), } var distDir string for _, dir := range candidates { if fi, err := os.Stat(filepath.Join(dir, "index.html")); err == nil && !fi.IsDir() { distDir = dir break } } if distDir == "" { http.NotFound(w, r) return } reqPath := filepath.Clean(r.URL.Path) if reqPath == "/" { http.ServeFile(w, r, filepath.Join(distDir, "index.html")) return } targetFile := filepath.Join(distDir, reqPath) if fi, err := os.Stat(targetFile); err == nil && !fi.IsDir() { http.ServeFile(w, r, targetFile) return } // SPA Fallback 默认回退 index.html http.ServeFile(w, r, filepath.Join(distDir, "index.html")) } // Start 启动 HTTP 守护进程 func (s *Server) Start() error { addr := fmt.Sprintf("%s:%d", s.cfg.HTTPHost, s.cfg.HTTPPort) s.httpServer = &http.Server{ Addr: addr, Handler: s.Routes(), ReadHeaderTimeout: 10 * time.Second, } return s.httpServer.ListenAndServe() } // Shutdown 优雅停机 func (s *Server) Shutdown(ctx context.Context) error { if s.httpServer != nil { return s.httpServer.Shutdown(ctx) } return nil } // getOrCreateSession 获取或动态构建会话 func (s *Server) getOrCreateSession( sessionID string, projectName string, customerName string, deviceType string, intendedUse string, targetMarket string, workDir string, userName string, userRole string, ) *chat.Session { if sessionID == "" { sessionID = fmt.Sprintf("sess_%d", time.Now().UnixNano()) } s.sessionsMu.RLock() existing, exists := s.sessions[sessionID] s.sessionsMu.RUnlock() if exists && existing != nil { return existing } s.sessionsMu.Lock() defer s.sessionsMu.Unlock() // 双重检查 if existing, exists := s.sessions[sessionID]; exists && existing != nil { return existing } if userName == "" { userName = "用户" } if userRole == "" { userRole = "engineer" } u := coordinator.UserInfo{ ID: "u_" + sessionID, Name: userName, Role: userRole, ProjectRole: "composer", } var userCtx *coordinator.UserProjectContext if workDir != "" { assembler := coordinator.NewContextAssembler() userCtx, _ = assembler.AssembleFromWorkspace(workDir, u) } if userCtx == nil { if projectName == "" { projectName = "医疗器械研发项目" } if customerName == "" { customerName = "水木东方客户" } userCtx = &coordinator.UserProjectContext{ SnapshotID: fmt.Sprintf("ctx_%d", time.Now().UnixNano()), AssembledAt: time.Now(), User: u, WorkDir: workDir, Project: coordinator.ProjectInfo{ Name: projectName, CustomerName: customerName, DeviceType: deviceType, IntendedUse: intendedUse, TargetMarket: targetMarket, CurrentWorkflow: "intake", CurrentStage: bizdev.StageV01_Triage, }, BlackboardFacts: make(map[string]string), AvailableActions: coordinator.ComputeAvailableActions(bizdev.StageV01_Triage, u.ProjectRole), } if targetMarket != "" { userCtx.BlackboardFacts["target_market"] = targetMarket } if deviceType != "" { userCtx.BlackboardFacts["device_type"] = deviceType } } sess := chat.NewSession(sessionID, userCtx, s.rec, s.engine) if s.svc != nil { sess.CtxMgr = s.svc.ContextManager() } s.sessions[sessionID] = sess return sess } func (s *Server) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }