package server import ( "context" "fmt" "net/http" "os" "path/filepath" "strings" "sync" "time" "chat-agent-system/internal/agent" "chat-agent-system/internal/auth" "chat-agent-system/internal/authz" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/chat" "chat-agent-system/internal/config" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/intent" "chat-agent-system/internal/notification" "chat-agent-system/internal/observability" "chat-agent-system/internal/projectinstance" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/taskrun" "chat-agent-system/internal/workflowdef" "chat-agent-system/internal/workspace" ) // Server 提供基于 HTTP RESTful 与 SSE 的商务与研发参谋接口服务 type Server struct { cfg *config.Config svc *bizdev.Service pwSvc *projectworkflow.Service rec intent.Recognizer engine *coordinator.CoordinatorEngine authSvc *auth.Service sessions map[string]*chat.Session sessionsMu sync.RWMutex taskRuns *taskrun.Manager authorizer *authz.Authorizer authzSvc *authz.Service wfDefSvc *workflowdef.Service instSvc *projectinstance.Service notifySvc *notification.Service resolver *workspace.Resolver httpServer *http.Server } // NewServer 构造函数 func NewServer(cfg *config.Config, svc *bizdev.Service) *Server { if cfg == nil { cfg, _ = config.Load("") } if cfg != nil { observability.InitLogger(cfg.Env) } if svc == nil { var err error svc, err = bizdev.NewService(cfg) if err != nil { panic("server: bizdev service init failed: " + err.Error()) } } rec := intent.NewRecognizer() dialogueBackend := agent.NewBackend(cfg.AgentBackend, cfg) coordEngine := coordinator.NewCoordinatorEngineWithDialogue(svc, dialogueBackend, cfg.DataDir) pool := svc.Pool() stores, storeErr := auth.NewStores(context.Background(), pool, cfg) if storeErr != nil { panic("server: auth store init failed: " + storeErr.Error()) } authSvc := auth.NewService(stores.Auth, stores.Sessions, cfg) pwSvc := svc.ProjectWorkflow() if pwSvc == nil { pwSvc = projectworkflow.NewService(projectworkflow.NewStore(nil), nil) } authzSvc := authz.NewService(pool) wfDefSvc := workflowdef.NewService(pool, svc.CAS()) instStore := projectinstance.NewStore(pool) var instSvc *projectinstance.Service if wfDefSvc.Available() { docChecker := projectinstance.NewDocGovChecker(svc.DocGov()) instSvc = projectinstance.NewService(instStore, wfDefSvc.Store(), pwSvc.Store(), docChecker) } taskMgr := taskrun.NewManager(pwSvc, svc.ContextManager()) if pool != nil { taskMgr.SetStore(taskrun.NewPGStore(pool)) } s := &Server{ cfg: cfg, svc: svc, pwSvc: pwSvc, rec: rec, engine: coordEngine, authSvc: authSvc, sessions: make(map[string]*chat.Session), taskRuns: taskMgr, authorizer: authz.NewAuthorizer(pool), authzSvc: authzSvc, wfDefSvc: wfDefSvc, instSvc: instSvc, notifySvc: notification.NewService(pool), resolver: workspace.NewResolver(cfg.DataDir, pool), } if instSvc != nil && instSvc.Available() { svc.SetStageAccessGate(newBizdevStageGate(instSvc, pwSvc)) coordEngine.SetInstanceService(instSvc) } s.registerWorkflowApprovalHook() s.startNotificationReminderLoop() s.initTaskRunExecutors() if svc != nil && svc.DocRevision() != nil && s.notifySvc != nil { rev := svc.DocRevision() rev.OnOutbox = func(ctx context.Context, eventType string, payload map[string]any) error { if !s.notifySvc.Available() { return nil } docID, _ := payload["doc_id"].(string) projectID, _ := payload["project_id"].(int64) risk, _ := payload["risk"].(string) var recipient int64 if s.svc.DocGov() != nil { if doc, err := s.svc.DocGov().GetDocument(ctx, docID); err == nil && doc != nil && doc.AuthorID > 0 { recipient = doc.AuthorID projectID = doc.ProjectID } } if recipient == 0 { return nil } p := notification.DocumentEventPayload{ ProjectID: projectID, DocID: docID, RiskLevel: risk, } switch eventType { case notification.EventDocAgentCandidateReady: if v, ok := payload["candidate_id"].(int64); ok { p.CandidateID = v } _, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocAgentCandidateReady, "智能体变更待确认", "文档有高风险智能体候选变更,请确认后生效。", fmt.Sprintf("doc_candidate:%s:%d:%d", docID, p.CandidateID, recipient), recipient, p) return err case notification.EventDocAgentVersionCommitted: if v, ok := payload["version_no"].(int); ok { p.VersionNo = v } _, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocAgentVersionCommitted, "智能体版本已生效", "新的智能体文档版本已提交。", fmt.Sprintf("doc_agent_version:%s:%d:%d", docID, p.VersionNo, recipient), recipient, p) return err case notification.EventDocDownstreamImpact: impacted, _ := payload["impacted_doc_id"].(string) p.ImpactedDocID = impacted _, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocDownstreamImpact, "下游文档受影响", "上游文档变更,请复核关联文档。", fmt.Sprintf("doc_impact:%s:%s:%d", docID, impacted, recipient), recipient, p) return err case notification.EventDocChangeReportDegraded: if v, ok := payload["report_id"].(int64); ok { p.ReportID = v } _, err := s.notifySvc.EmitDocumentEvent(ctx, eventType, notification.TypeDocChangeReportDegraded, "变更报告未完整评估", "变更报告降级,受控文档暂不可送审。", fmt.Sprintf("doc_report_degraded:%s:%d:%d", docID, p.ReportID, recipient), recipient, p) return err default: return nil } } } return s } // AuthService 获取认证服务 func (s *Server) AuthService() *auth.Service { return s.authSvc } // BizdevService 获取业务交付服务 func (s *Server) BizdevService() *bizdev.Service { return s.svc } // TaskRuns 获取长任务管理器(测试与运维钩子)。 func (s *Server) TaskRuns() *taskrun.Manager { return s.taskRuns } // ProjectWorkflowService 获取项目多工作流控制服务 func (s *Server) ProjectWorkflowService() *projectworkflow.Service { return s.pwSvc } // 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) mux.HandleFunc("GET /ready", s.handleReadiness) mux.HandleFunc("GET /api/v1/ready", s.handleReadiness) // 2. 项目与多工作流控制面 mux.HandleFunc("POST /api/v1/projects", s.handleCreateProject) mux.HandleFunc("GET /api/v1/projects", s.handleListProjects) mux.HandleFunc("GET /api/v1/projects/{id}", s.handleGetProjectByID) mux.HandleFunc("GET /api/v1/projects/{id}/members", s.handleProjectMembers) mux.HandleFunc("POST /api/v1/projects/{id}/members", s.handleProjectMembers) mux.HandleFunc("PUT /api/v1/projects/{id}/members", s.handleProjectMembers) mux.HandleFunc("DELETE /api/v1/projects/{id}/members", s.handleProjectMembers) mux.HandleFunc("GET /api/v1/projects/{id}/workflows", s.handleListProjectWorkflows) mux.HandleFunc("PUT /api/v1/projects/{id}/active-workflow", s.handleActivateWorkflow) mux.HandleFunc("GET /api/v1/projects/{id}/settings", s.handleGetProjectSettings) mux.HandleFunc("GET /api/v1/projects/{id}/workflow-plan/default", s.handleGetDefaultWorkflowPlan) mux.HandleFunc("PUT /api/v1/projects/{id}/workflow-plan", s.handleSaveWorkflowPlan) mux.HandleFunc("POST /api/v1/projects/{id}/activation-preview", s.handleActivationPreview) mux.HandleFunc("POST /api/v1/projects/{id}/workflow-instances/activate", s.handleActivateWorkflowInstance) mux.HandleFunc("GET /api/v1/workflow-instances/{id}", s.handleGetWorkflowInstance) mux.HandleFunc("GET /api/v1/workflow-instances/{id}/stages/{stage}/readiness", s.handleGetStageReadiness) mux.HandleFunc("POST /api/v1/workflow-instances/{id}/stages/{stage}/advance", s.handleAdvanceStage) mux.HandleFunc("GET /api/v1/workflow-instances/{id}/termination-preview", s.handleTerminationPreview) mux.HandleFunc("POST /api/v1/workflow-instances/{id}/terminate", s.handleTerminateWorkflowInstance) mux.HandleFunc("GET /api/v1/workflows", s.handleListWorkflowDefinitions) mux.HandleFunc("GET /api/v1/workflows/catalog", s.handleWorkflowCatalog) mux.HandleFunc("POST /api/v1/projects/{id}/workflows/{workflow_key}/run", s.handleRunProjectWorkflow) mux.HandleFunc("GET /api/v1/projects/{id}/workflows/{workflow_key}/runs", s.handleListProjectWorkflowRuns) mux.HandleFunc("GET /api/v1/projects/{id}/lineage", s.handleGetProjectLineage) mux.HandleFunc("GET /api/v1/projects/{id}/documents", s.handleListProjectWorkflowDocuments) mux.HandleFunc("GET /api/v1/projects/{id}/notifications", s.handleListProjectNotifications) // 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) mux.HandleFunc("GET /api/v1/chat/turns/{client_turn_id}", s.handleGetChatTurn) // 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) mux.HandleFunc("POST /api/v1/tasks/runs", s.handleCreateTaskRun) mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}", s.handleGetTaskRun) mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}/events", s.handleTaskRunEvents) mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/cancel", s.handleCancelTaskRun) mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/resume", s.handleResumeTaskRun) mux.HandleFunc("POST /api/v1/admin/workspaces/reconcile", s.handleReconcileWorkspace) mux.HandleFunc("POST /api/v1/projects/{id}/assets", s.handleUploadProjectAsset) mux.HandleFunc("GET /api/v1/projects/{id}/assets", s.handleListProjectAssets) // 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("POST /api/v1/chat/sessions", s.handleUpsertChatSession) mux.HandleFunc("GET /api/v1/chat/messages", s.handleGetChatMessages) mux.HandleFunc("GET /api/v1/chat/messages/window", s.handleGetChatWindow) mux.HandleFunc("GET /api/v1/chat/messages/day", s.handleGetChatDayMessages) mux.HandleFunc("GET /api/v1/chat/dates", s.handleListChatDates) mux.HandleFunc("GET /api/v1/chat/categories", s.handleListChatCategories) mux.HandleFunc("GET /api/v1/chat/search", s.handleChatSearch) mux.HandleFunc("GET /api/v1/chat/digests", s.handleListDailyDigests) mux.HandleFunc("POST /api/v1/chat/digests/generate", s.handleGenerateDailyDigest) mux.HandleFunc("POST /api/v1/chat/messages/import", s.handleImportChatMessages) // P3: 未读提醒 & 聊天导出 mux.HandleFunc("GET /api/v1/chat/unread", s.handleChatUnread) mux.HandleFunc("POST /api/v1/chat/read", s.handleChatRead) mux.HandleFunc("GET /api/v1/chat/export/messages", s.handleChatExportMessages) mux.HandleFunc("POST /api/v1/chat/recategorize", s.handleChatRecategorize) mux.HandleFunc("GET /api/v1/context/cases/recommend", s.handleRecommendCases) // 7.1 单次助手回复导出与快照 mux.HandleFunc("GET /api/v1/chat/messages/{message_id}/export-capability", s.handleGetExportCapability) mux.HandleFunc("GET /api/v1/chat/messages/{message_id}/export-source", s.handleGetExportSource) // 7.2 自由私密文档与项目共享参考 mux.HandleFunc("POST /api/v1/freeform-documents", s.handleCreateFreeformDoc) mux.HandleFunc("POST /api/v1/freeform-documents/from-message", s.handleCreateFreeformDocFromMessage) mux.HandleFunc("GET /api/v1/freeform-documents", s.handleListFreeformDocs) mux.HandleFunc("GET /api/v1/freeform-documents/{doc_id}", s.handleGetFreeformDoc) mux.HandleFunc("PUT /api/v1/freeform-documents/{doc_id}", s.handleUpdateFreeformDoc) mux.HandleFunc("POST /api/v1/freeform-documents/{doc_id}/share", s.handleShareFreeformDoc) mux.HandleFunc("POST /api/v1/freeform-documents/{doc_id}/unshare", s.handleUnshareFreeformDoc) mux.HandleFunc("DELETE /api/v1/freeform-documents/{doc_id}", s.handleDeleteFreeformDoc) // 8. 受控文档编制、审核、批准生命周期与认领治理 mux.HandleFunc("GET /api/v1/docgov/document", s.handleDocGovGetDocument) mux.HandleFunc("GET /api/v1/docgov/documents/{doc_id}/content", s.handleDocGovGetDocumentContent) 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) mux.HandleFunc("POST /api/v1/docgov/content", s.handleDocGovSaveContent) mux.HandleFunc("POST /api/v1/docgov/lease/acquire", s.handleDocGovAcquireLease) mux.HandleFunc("POST /api/v1/docgov/lease/heartbeat", s.handleDocGovHeartbeatLease) mux.HandleFunc("POST /api/v1/docgov/lease/release", s.handleDocGovReleaseLease) // 8.1 文档版本 / 修订 ledger s.registerDocumentRevisionRoutes(mux) // 9. 用户认证、安全改密与超级管理员后台接口 mux.HandleFunc("POST /api/v1/auth/login", s.handleAuthLogin) mux.HandleFunc("POST /api/v1/auth/register", s.handleAuthRegister) mux.HandleFunc("GET /api/v1/auth/public-config", s.handleAuthPublicConfig) 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/me/profile", s.handleMeProfile) mux.HandleFunc("PATCH /api/v1/me/profile", s.handleMeProfile) mux.HandleFunc("GET /api/v1/me/preferences", s.handleMePreferences) mux.HandleFunc("PUT /api/v1/me/preferences", s.handleMePreferences) mux.HandleFunc("GET /api/v1/auth/sessions", s.handleAuthSessions) mux.HandleFunc("GET /api/v1/admin/permissions", s.handleAdminPermissions) mux.HandleFunc("DELETE /api/v1/auth/sessions/{id}", s.handleAuthSessionByID) mux.HandleFunc("GET /api/v1/admin/roles", s.handleAdminRoles) mux.HandleFunc("POST /api/v1/admin/roles", s.handleAdminRoles) mux.HandleFunc("PATCH /api/v1/admin/roles/{id}", s.handleAdminRoleByID) mux.HandleFunc("DELETE /api/v1/admin/roles/{id}", s.handleAdminRoleByID) mux.HandleFunc("GET /api/v1/admin/workflows", s.handleAdminWorkflows) mux.HandleFunc("POST /api/v1/admin/workflows", s.handleAdminWorkflows) mux.HandleFunc("GET /api/v1/admin/workflows/{id}", s.handleAdminWorkflowByID) mux.HandleFunc("PATCH /api/v1/admin/workflows/{id}/versions/{version}", s.handleAdminWorkflowVersion) mux.HandleFunc("POST /api/v1/admin/workflows/{id}/versions/{version}/validate", s.handleAdminWorkflowValidate) mux.HandleFunc("POST /api/v1/admin/workflows/{id}/versions/{version}/publish", s.handleAdminWorkflowPublish) mux.HandleFunc("POST /api/v1/admin/workflows/{id}/versions/{version}/retire", s.handleAdminWorkflowRetire) mux.HandleFunc("GET /api/v1/admin/templates", s.handleAdminTemplates) mux.HandleFunc("POST /api/v1/admin/templates", s.handleAdminTemplates) mux.HandleFunc("GET /api/v1/admin/templates/{key}/versions", s.handleAdminTemplateVersions) 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(s.bodyLimitMiddleware(s.authMiddleware(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", "/app/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, ReadTimeout: 60 * time.Second, WriteTimeout: 120 * time.Second, IdleTimeout: 120 * 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 } // CleanStaleSessions 清理超过空闲 TTL 的内存会话 (释放内存,后续请求可通过 RestoreSessionContext 自动从 DB 恢复) func (s *Server) CleanStaleSessions(ttl time.Duration) int { s.sessionsMu.Lock() defer s.sessionsMu.Unlock() return s.cleanStaleSessionsLocked(ttl) } func (s *Server) cleanStaleSessionsLocked(ttl time.Duration) int { if ttl <= 0 { ttl = 120 * time.Minute } now := time.Now() cleaned := 0 for id, sess := range s.sessions { if sess == nil || now.Sub(sess.LastAccessedAt) > ttl { delete(s.sessions, id) cleaned++ } } return cleaned } // 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 { existing.LastAccessedAt = time.Now() return existing } s.sessionsMu.Lock() defer s.sessionsMu.Unlock() // 双重检查 if existing, exists := s.sessions[sessionID]; exists && existing != nil { existing.LastAccessedAt = time.Now() return existing } // 惰性内存防泄漏保护 (会话数超过 200 时清理空闲超 120 分钟会话) if len(s.sessions) > 200 { s.cleanStaleSessionsLocked(120 * time.Minute) } // 尝试从持久层恢复会话上下文 (服务重启后跨进程保持 G3) var restored *ctxmgr.RestoredSessionContext if s.svc != nil && s.svc.ContextManager() != nil { if r, err := s.svc.ContextManager().RestoreSessionContext(context.Background(), sessionID); err == nil && r != nil && len(r.Messages) > 0 { restored = r } } 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() sess.Actions = &chat.ActionServices{ DocGov: s.svc.DocGov(), Bizdev: s.svc, TaskRuns: s.taskRuns, } } // 若从持久层恢复了会话历史,回填消息列表、事实黑板与近期对话 (G3) if restored != nil { if restored.UserID > 0 { sess.UserID = restored.UserID } if restored.ProjectID > 0 { sess.ProjectID = restored.ProjectID } for k, v := range restored.Facts { if _, exists := userCtx.BlackboardFacts[k]; !exists { userCtx.BlackboardFacts[k] = v } } for _, rm := range restored.Messages { sess.Messages = append(sess.Messages, chat.Message{ ID: fmt.Sprintf("msg_%d", rm.ID), Role: rm.Role, Content: rm.Content, Intent: intent.IntentType(rm.Intent), Branch: intent.ResolutionBranch(rm.Branch), Timestamp: rm.CreatedAt, }) } sess.SyncRecentDialogue() if sess.UserID > 0 && sess.CtxMgr != nil { if guidance, err := sess.CtxMgr.CompilePromptGuidance(context.Background(), sess.UserID); err == nil && guidance != "" { userCtx.PersonalizedGuidance = guidance } } } sess.LastAccessedAt = time.Now() 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) { s.writeCORS(w, r) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, X-Session-Token, X-Requested-With") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }