package server import ( "context" "encoding/json" "errors" "net/http" "path/filepath" "strconv" "strings" "time" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/docgov" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/squad" "chat-agent-system/internal/taskrun" ) // HealthResponse 健康检查响应 type HealthResponse struct { Status string `json:"status"` Env string `json:"env"` AgentBackend string `json:"agent_backend"` Version string `json:"version"` SharedKBPort int `json:"shared_kb_port"` DatabaseReady bool `json:"database_ready"` SchemaVersion int64 `json:"schema_version,omitempty"` SchemaReady bool `json:"schema_ready,omitempty"` TaskStoreReady bool `json:"task_store_ready,omitempty"` } // ChatRequest 对话请求 type ChatRequest struct { SessionID string `json:"session_id"` Message string `json:"message"` ProjectName string `json:"project_name,omitempty"` CustomerName string `json:"customer_name,omitempty"` DeviceType string `json:"device_type,omitempty"` IntendedUse string `json:"intended_use,omitempty"` TargetMarket string `json:"target_market,omitempty"` WorkDir string `json:"work_dir,omitempty"` UserName string `json:"user_name,omitempty"` UserRole string `json:"user_role,omitempty"` UserID int64 `json:"user_id,omitempty"` ProjectID int64 `json:"project_id,omitempty"` } // TaskRunRequestDTO 独立任务运行请求 type TaskRunRequestDTO struct { TaskKey string `json:"task_key"` Stage string `json:"stage,omitempty"` // 若指定 stage,则执行成熟度阶段跃迁 ProjectName string `json:"project_name"` CustomerName string `json:"customer_name"` DeviceType string `json:"device_type"` IntendedUse string `json:"intended_use"` TargetMarket string `json:"target_market"` Fields map[string]string `json:"fields,omitempty"` Backend string `json:"backend,omitempty"` AutoRepair bool `json:"auto_repair"` UserID int64 `json:"user_id,omitempty"` ProjectID int64 `json:"project_id,omitempty"` DocID string `json:"doc_id,omitempty"` AuthorID int64 `json:"author_id,omitempty"` AuthorName string `json:"author_name,omitempty"` ReviewerID int64 `json:"reviewer_id,omitempty"` ReviewerName string `json:"reviewer_name,omitempty"` ApproverID int64 `json:"approver_id,omitempty"` ApproverName string `json:"approver_name,omitempty"` ForceDirectEdit bool `json:"force_direct_edit,omitempty"` CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"` WorkflowKey string `json:"workflow_key,omitempty"` SessionID string `json:"session_id,omitempty"` ClientTurnID string `json:"client_turn_id,omitempty"` // Materials 用户上传的参考文档材料,由前端 Base64 编码后传入;后端 json 反序列化自动解码 []byte Materials []bizdev.MaterialInput `json:"materials,omitempty"` } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { resp, code := s.buildHealthResponse(false) writeJSONResponse(w, code, resp) } func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) { resp, code := s.buildHealthResponse(true) writeJSONResponse(w, code, resp) } func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) { dbReady := s.svc != nil && s.svc.DatabaseReady() schemaVersion := int64(0) if s.svc != nil { if dbReady { schemaVersion = s.svc.LiveSchemaVersion(context.Background()) } else { schemaVersion = s.svc.SchemaVersion() } } schemaReady := schemaVersion > 0 || (s.cfg != nil && s.cfg.AllowInMemory) taskStoreReady := s.svc != nil && taskrun.TablesReady(context.Background(), s.svc.Pool()) status := "ok" code := http.StatusOK if s.cfg != nil && s.cfg.ContextDbHost != "" && !s.cfg.AllowInMemory { if !dbReady { status = "degraded" code = http.StatusServiceUnavailable } else if strict && (!schemaReady || !taskStoreReady) { status = "not_ready" code = http.StatusServiceUnavailable } } return HealthResponse{ Status: status, Env: s.cfg.Env, AgentBackend: s.cfg.AgentBackend, Version: "v1.0", SharedKBPort: s.cfg.KbDbPort, DatabaseReady: dbReady, SchemaVersion: schemaVersion, SchemaReady: schemaReady, TaskStoreReady: taskStoreReady, }, code } func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) { sessionID := r.URL.Query().Get("session_id") if sessionID == "" { http.Error(w, "missing session_id query param", http.StatusBadRequest) return } sess := s.getOrCreateSession(sessionID, "", "", "", "", "", "", "", "") if sess == nil || sess.UserCtx == nil { http.Error(w, "session or project context not found", http.StatusNotFound) return } writeJSONResponse(w, http.StatusOK, sess.UserCtx) } func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { user, ok := s.requirePrincipal(w, r) if !ok { return } var req ChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if req.Message == "" { http.Error(w, "message cannot be empty", http.StatusBadRequest) return } req.WorkDir = "" sess := s.getOrCreateSession( req.SessionID, req.ProjectName, req.CustomerName, req.DeviceType, req.IntendedUse, req.TargetMarket, req.WorkDir, req.UserName, req.UserRole, ) sess.UserID = user.ID if req.ProjectID > 0 { sess.ProjectID = req.ProjectID } if sess.ProjectID > 0 { if !s.enforceChatProjectAccess(w, r, user, sess.ProjectID) { return } } s.enrichSessionWorkflowStage(r.Context(), sess, user.ID) if sess.ProjectID > 0 && !s.allowActiveStageChatAccess(r.Context(), sess.ProjectID, user.ID) { s.writeWorkflowAccessDenied(w) return } s.bindSessionDocumentSummaryReader(sess, user) resp, err := sess.SendMessage(r.Context(), req.Message) if err != nil { http.Error(w, "process chat message failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, resp) } func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { user, ok := s.requirePrincipal(w, r) if !ok { return } var req TaskRunRequestDTO if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } req.UserID = user.ID if req.ProjectID > 0 { if !s.isSystemAdminUser(r.Context(), user) { if err := s.authorizer.RequireProjectAccess(r.Context(), req.ProjectID, user.ID); err != nil { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权访问该项目", }) return } } } // 0. 如果关联了项目,在 projectworkflow 服务中记录运行实例 var runRecord *projectworkflow.WorkflowRun if s.pwSvc != nil && req.ProjectID > 0 { wfKey := req.WorkflowKey if wfKey == "" { if p, _ := s.pwSvc.GetProject(r.Context(), req.ProjectID, req.UserID); p != nil { wfKey = p.ActiveWorkflowKey } } if wfKey == "" { wfKey = "main" } manifest := map[string]interface{}{ "fields": req.Fields, "doc_id": req.DocID, } runRecord, _ = s.pwSvc.CreateWorkflowRun(r.Context(), &projectworkflow.WorkflowRun{ ProjectID: req.ProjectID, WorkflowKey: wfKey, Status: "running", StartedBy: &req.UserID, StageKey: req.Stage, TaskKey: req.TaskKey, InputManifest: manifest, }) } // 1. 如果指定了 stage,则执行成熟度阶段编排 if req.Stage != "" { stageRes, err := s.svc.ExecuteStage(r.Context(), bizdev.StageRunRequest{ Stage: bizdev.MaturityStage(req.Stage), ProjectName: req.ProjectName, CustomerName: req.CustomerName, DeviceType: req.DeviceType, IntendedUse: req.IntendedUse, TargetMarket: req.TargetMarket, Fields: req.Fields, Backend: req.Backend, AutoRepair: req.AutoRepair, UserID: req.UserID, ProjectID: req.ProjectID, DocID: req.DocID, AuthorID: req.AuthorID, AuthorName: req.AuthorName, ReviewerID: req.ReviewerID, ReviewerName: req.ReviewerName, ApproverID: req.ApproverID, ApproverName: req.ApproverName, ForceDirectEdit: req.ForceDirectEdit, }) if err != nil { if runRecord != nil { _ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error()) } writeTaskError(w, err, stageRes) return } if runRecord != nil && stageRes != nil && stageRes.PrimaryArtifact != nil { status := resolveTerminalStatus(stageRes.PrimaryArtifact) outDocID := "" if stageRes.PrimaryArtifact.DocRecord != nil { outDocID = stageRes.PrimaryArtifact.DocRecord.DocID } outSummary := stageRes.PrimaryArtifact.ArtifactTitle if outSummary == "" { outSummary = stageRes.StageTitle } _ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, status, outDocID, outSummary, "") } writeJSONResponse(w, http.StatusOK, stageRes) return } // 2. 执行单任务 if req.TaskKey == "" { req.TaskKey = "intake" } taskRes, err := s.svc.ExecuteTask(r.Context(), bizdev.TaskRunRequest{ TaskKey: req.TaskKey, ProjectName: req.ProjectName, CustomerName: req.CustomerName, DeviceType: req.DeviceType, IntendedUse: req.IntendedUse, TargetMarket: req.TargetMarket, Fields: req.Fields, Materials: req.Materials, Backend: req.Backend, AutoRepair: req.AutoRepair, UserID: req.UserID, ProjectID: req.ProjectID, DocID: req.DocID, AuthorID: req.AuthorID, AuthorName: req.AuthorName, ReviewerID: req.ReviewerID, ReviewerName: req.ReviewerName, ApproverID: req.ApproverID, ApproverName: req.ApproverName, ForceDirectEdit: req.ForceDirectEdit, CustomSteps: req.CustomSteps, }) if err != nil { if runRecord != nil { _ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, "failed", "", "", err.Error()) } writeTaskError(w, err, taskRes) return } if runRecord != nil && taskRes != nil { status := resolveTerminalStatus(taskRes) outDocID := "" if taskRes.DocRecord != nil { outDocID = taskRes.DocRecord.DocID } _ = s.pwSvc.UpdateWorkflowRun(r.Context(), runRecord.RunID, status, outDocID, taskRes.ArtifactTitle, "") } // 隐藏不必要的中间大体积字节 if taskRes.WorkDir != "" { taskRes.WorkDir = filepath.Clean(taskRes.WorkDir) } writeJSONResponse(w, http.StatusOK, taskRes) } func writeTaskError(w http.ResponseWriter, err error, payload any) { status := http.StatusInternalServerError if errors.Is(err, docgov.ErrDocumentBusy) { status = http.StatusConflict } else if errors.Is(err, docgov.ErrDocumentLocked) || errors.Is(err, docgov.ErrForceEditNotAllowed) { status = http.StatusConflict } else if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) { status = http.StatusBadRequest } if payload != nil { writeJSONResponse(w, status, payload) return } http.Error(w, err.Error(), status) } func writeDocGovError(w http.ResponseWriter, err error) { var busy *docgov.DocumentBusyError if errors.As(err, &busy) { writeJSONResponse(w, http.StatusConflict, map[string]any{ "error": "document_busy", "detail": busy, }) return } if errors.Is(err, docgov.ErrDocumentBusy) { http.Error(w, err.Error(), http.StatusConflict) return } if errors.Is(err, docgov.ErrDocumentLocked) { http.Error(w, err.Error(), http.StatusConflict) return } if errors.Is(err, docgov.ErrDocumentNotFound) { http.Error(w, err.Error(), http.StatusNotFound) return } if errors.Is(err, docgov.ErrLeaseNotHeld) || errors.Is(err, docgov.ErrLeaseExpired) { http.Error(w, err.Error(), http.StatusConflict) return } http.Error(w, err.Error(), http.StatusBadRequest) } func writeJSONResponse(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(data) } // ----------------------------------------------------------------------------- // 多级上下文与偏好记忆 RESTful 控制器 // ----------------------------------------------------------------------------- // handleGetUserContext 获取单用户私有上下文与个性化偏好 func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) { userIDStr := r.URL.Query().Get("user_id") projectIDStr := r.URL.Query().Get("project_id") sessionID := r.URL.Query().Get("session_id") userID, _ := strconv.ParseInt(userIDStr, 10, 64) if userID <= 0 { userID = 1 } projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } uCtx, err := s.svc.ContextManager().GetUserContext(r.Context(), projectID, userID, sessionID) if err != nil { http.Error(w, "get user context failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, uCtx) } // UpdateUserPreferenceRequest 更新用户工作偏好请求 type UpdateUserPreferenceRequest struct { UserID int64 `json:"user_id"` CommunicationStyle ctxmgr.CommunicationStyle `json:"communication_style"` FocusAreas []string `json:"focus_areas"` OutputFormatPref string `json:"output_format_pref"` CustomInstructions string `json:"custom_instructions"` } // handleUpdateUserPreference 定期整理或前端保存用户习惯偏好 func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Request) { var req UpdateUserPreferenceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if req.UserID <= 0 { http.Error(w, "user_id must be positive", http.StatusBadRequest) return } if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } pref := &ctxmgr.UserPreference{ UserID: req.UserID, CommunicationStyle: req.CommunicationStyle, FocusAreas: req.FocusAreas, OutputFormatPref: req.OutputFormatPref, CustomInstructions: req.CustomInstructions, } if err := s.svc.ContextManager().UpdateUserPreference(r.Context(), pref); err != nil { http.Error(w, "save user preference failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, map[string]any{"status": "ok", "user_id": req.UserID}) } // handleGetProjectContext 获取项目级多方协同事实黑板与里程碑决策流 func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } projectIDStr := r.URL.Query().Get("project_id") projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) if projectID <= 0 { http.Error(w, "missing or invalid project_id query param", http.StatusBadRequest) return } if !s.allowAnyStageAccess(r.Context(), projectID, user.ID) { s.writeWorkflowAccessDenied(w) return } if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } pCtx, err := s.svc.ContextManager().GetProjectContext(r.Context(), projectID) if err != nil { http.Error(w, "get project context failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, pCtx) } // ----------------------------------------------------------------------------- // 受控文档编制、审核、批准与认领生命周期 RESTful 控制器 // ----------------------------------------------------------------------------- // handleDocGovGetDocument 查询指定受控文档详情与治理状态 func (s *Server) handleDocGovGetDocument(w http.ResponseWriter, r *http.Request) { docID := r.URL.Query().Get("doc_id") if docID == "" { http.Error(w, "missing doc_id query param", http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } doc, err := s.svc.DocGov().GetDocument(r.Context(), docID) if err != nil { http.Error(w, "get document failed: "+err.Error(), http.StatusNotFound) return } writeJSONResponse(w, http.StatusOK, doc) } // DocGovClaimRequest 认领文档请求 type DocGovClaimRequest struct { DocID string `json:"doc_id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` UserRole string `json:"user_role"` // author, reviewer, approver } // handleDocGovClaim 查阅并认领受控文档 (非模板状态查看方可认领) func (s *Server) handleDocGovClaim(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req DocGovClaimRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if req.DocID == "" { http.Error(w, "doc_id cannot be empty", http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } doc, err := s.svc.DocGov().RecordDocumentViewed(r.Context(), req.DocID, user.ID, docgov.UserRole(req.UserRole), user.Name) if err != nil { http.Error(w, "claim document failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, doc) } // DocGovSubmitReviewRequest 送审请求 type DocGovSubmitReviewRequest struct { DocID string `json:"doc_id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` } // handleDocGovSubmitReview 编制人将文档送审 (drafting -> reviewing, 正文受控锁定) func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req DocGovSubmitReviewRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } doc, err := s.svc.DocGov().SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name) if err != nil { http.Error(w, "submit review failed: "+err.Error(), http.StatusBadRequest) return } writeJSONResponse(w, http.StatusOK, doc) } // DocGovDecisionRequest 审核/批准决议请求 type DocGovDecisionRequest struct { DocID string `json:"doc_id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` Action string `json:"action"` // approve 或 reject Comment string `json:"comment,omitempty"` Reason string `json:"reason,omitempty"` // 驳回时必填 RollbackTo string `json:"rollback_to,omitempty"` // 批准人驳回时可选: drafting 或 reviewing } // handleDocGovReviewDecision 审核人决议 (通过流转至 approving,驳回必须填理由并回退 drafting 解锁) func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req DocGovDecisionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } action := strings.ToLower(strings.TrimSpace(req.Action)) var doc *docgov.DocumentRecord var err error if action == "approve" { doc, err = s.svc.DocGov().ReviewApprove(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, req.Comment) } else if action == "reject" { reason := req.Reason if reason == "" { reason = req.Comment } doc, err = s.svc.DocGov().ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason) } else { http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest) return } if err != nil { http.Error(w, "review decision failed: "+err.Error(), http.StatusBadRequest) return } writeJSONResponse(w, http.StatusOK, doc) } // handleDocGovApproverDecision 批准人决议 (通过流转至 approved 终态归档,驳回必填理由并回退) func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req DocGovDecisionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } action := strings.ToLower(strings.TrimSpace(req.Action)) var doc *docgov.DocumentRecord var err error if action == "approve" { doc, err = s.svc.DocGov().ApproverApprove(r.Context(), req.DocID, user.ID, docgov.RoleApprover, user.Name, req.Comment) } else if action == "reject" { reason := req.Reason if reason == "" { reason = req.Comment } rollbackState := docgov.DocumentState(req.RollbackTo) doc, err = s.svc.DocGov().ApproverReject(r.Context(), req.DocID, user.ID, docgov.RoleApprover, user.Name, reason, rollbackState) } else { http.Error(w, "invalid action: must be approve or reject", http.StatusBadRequest) return } if err != nil { http.Error(w, "approver decision failed: "+err.Error(), http.StatusBadRequest) return } writeJSONResponse(w, http.StatusOK, doc) } // DecideProposalRequest 拍板变更提案请求 type DecideProposalRequest struct { ProposalID string `json:"proposal_id"` AuthorID int64 `json:"author_id"` Approve bool `json:"approve"` Reason string `json:"reason"` } // handleDocGovDecideProposal 编制负责人拍板决定是否采纳变更提案 func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req DecideProposalRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } proposal, err := s.svc.DocGov().DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason) if err != nil { http.Error(w, "decide proposal failed: "+err.Error(), http.StatusBadRequest) return } writeJSONResponse(w, http.StatusOK, proposal) } // handleDocGovListComments 查询批注与修改建议清单 func (s *Server) handleDocGovListComments(w http.ResponseWriter, r *http.Request) { docID := r.URL.Query().Get("doc_id") if docID == "" { http.Error(w, "missing doc_id query param", http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } comments, err := s.svc.DocGov().ListComments(r.Context(), docID) if err != nil { http.Error(w, "list comments failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, comments) } // AddCommentRequestDTO 新增批注请求 type AddCommentRequestDTO struct { DocID string `json:"doc_id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` UserRole string `json:"user_role"` TargetSection string `json:"target_section,omitempty"` CommentType string `json:"comment_type,omitempty"` Content string `json:"content"` } // handleDocGovAddComment 审核人/批准人/编制人新增章节批注或修改建议 func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request) { var req AddCommentRequestDTO if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } cType := docgov.CommentType(req.CommentType) if cType == "" { cType = docgov.CommentGeneral } comment, err := s.svc.DocGov().AddComment( r.Context(), req.DocID, req.UserID, req.UserName, docgov.UserRole(req.UserRole), req.TargetSection, cType, req.Content, ) if err != nil { writeDocGovError(w, err) return } writeJSONResponse(w, http.StatusOK, comment) } // handleDocGovListAuditLogs 查询文档全生命周期审计轨迹流 func (s *Server) handleDocGovListAuditLogs(w http.ResponseWriter, r *http.Request) { docID := r.URL.Query().Get("doc_id") if docID == "" { http.Error(w, "missing doc_id query param", http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } logs, err := s.svc.DocGov().ListAuditLogs(r.Context(), docID) if err != nil { http.Error(w, "list audit logs failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, logs) } // ----------------------------------------------------------------------------- // 历史多轮会话管理与标杆案例语义检索 RESTful 控制器 // ----------------------------------------------------------------------------- // handleListChatSessions 分页查询用户/项目关联的历史会话列表 func (s *Server) handleListChatSessions(w http.ResponseWriter, r *http.Request) { if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } userIDStr := r.URL.Query().Get("user_id") userID, _ := strconv.ParseInt(userIDStr, 10, 64) projectIDStr := r.URL.Query().Get("project_id") projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) limitStr := r.URL.Query().Get("limit") limit, _ := strconv.Atoi(limitStr) if limit <= 0 { limit = 20 } offsetStr := r.URL.Query().Get("offset") offset, _ := strconv.Atoi(offsetStr) if offset < 0 { offset = 0 } sessions, err := s.svc.ContextManager().ListChatSessions(r.Context(), ctxmgr.SessionFilter{ UserID: userID, ProjectID: projectID, Limit: limit, Offset: offset, }) if err != nil { http.Error(w, "list chat sessions failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, sessions) } type upsertChatSessionRequest struct { SessionID string `json:"session_id"` Title string `json:"title"` UserID int64 `json:"user_id"` ProjectID int64 `json:"project_id"` } // handleUpsertChatSession 注册或更新会话元数据(立项/成员加入时同步)。 func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request) { if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } var req upsertChatSessionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } if req.SessionID == "" { http.Error(w, "missing session_id", http.StatusBadRequest) return } summary, err := s.svc.ContextManager().EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title) if err != nil { http.Error(w, "upsert chat session failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, summary) } // handleGetChatMessages 分页获取指定会话的历史多轮消息记录 func (s *Server) handleGetChatMessages(w http.ResponseWriter, r *http.Request) { if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } sessionID := r.URL.Query().Get("session_id") if sessionID == "" { http.Error(w, "missing session_id query param", http.StatusBadRequest) return } limitStr := r.URL.Query().Get("limit") limit, _ := strconv.Atoi(limitStr) if limit <= 0 { limit = 50 } offsetStr := r.URL.Query().Get("offset") offset, _ := strconv.Atoi(offsetStr) if offset < 0 { offset = 0 } user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } msgs, err := s.svc.ContextManager().GetChatHistory(r.Context(), sessionID, limit, offset) if err != nil { http.Error(w, "get chat messages failed: "+err.Error(), http.StatusInternalServerError) return } if projectID, pidErr := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); pidErr == nil && projectID > 0 { msgs = s.filterChatMessagesByStageAccess(r.Context(), projectID, user.ID, msgs) } writeJSONResponse(w, http.StatusOK, msgs) } // handleRecommendCases 基于语义向量检索往期相似标杆案例 func (s *Server) handleRecommendCases(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusServiceUnavailable) return } projectID, _ := strconv.ParseInt(r.URL.Query().Get("project_id"), 10, 64) query := r.URL.Query().Get("query") if query == "" { http.Error(w, "missing query param", http.StatusBadRequest) return } taskKey := r.URL.Query().Get("task_key") excludeDoc := r.URL.Query().Get("exclude_doc") limitStr := r.URL.Query().Get("limit") limit, _ := strconv.Atoi(limitStr) if limit <= 0 { limit = 3 } minScoreStr := r.URL.Query().Get("min_score") minScore, _ := strconv.ParseFloat(minScoreStr, 64) if minScore <= 0 { minScore = 0.50 } cases, err := s.svc.ContextManager().SearchSimilarCases(r.Context(), ctxmgr.CaseSearchOptions{ Query: query, TaskKey: taskKey, ExcludeDoc: excludeDoc, Limit: limit, MinScore: minScore, }) if err != nil { http.Error(w, "search similar cases failed: "+err.Error(), http.StatusInternalServerError) return } if projectID > 0 && len(cases) > 0 { filtered := make([]ctxmgr.CaseRecommendationItem, 0, len(cases)) for _, c := range cases { if s.allowWorkflowDocTaskAccess(r.Context(), projectID, user.ID, c.TaskKey) { filtered = append(filtered, c) } } cases = filtered } writeJSONResponse(w, http.StatusOK, cases) } // ----------------------------------------------------------------------------- // 文档正文保存与编辑租约 // ----------------------------------------------------------------------------- type SaveContentRequestDTO struct { DocID string `json:"doc_id"` MarkdownBody string `json:"markdown_body"` ExpectedSHA256 string `json:"expected_sha256,omitempty"` } func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request) { user, ok := s.requirePrincipal(w, r) if !ok { return } var req SaveContentRequestDTO if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } role := docgov.RoleAuthor res, err := s.svc.DocGov().SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{ DocID: req.DocID, ActorID: user.ID, ActorRole: role, ActorName: user.Name, MarkdownBody: req.MarkdownBody, ExpectedSHA256: req.ExpectedSHA256, }) if err != nil { writeDocGovError(w, err) return } writeJSONResponse(w, http.StatusOK, res) } type LeaseRequestDTO struct { DocID string `json:"doc_id"` HolderType string `json:"holder_type"` HolderID string `json:"holder_id"` TTLSeconds int `json:"ttl_seconds,omitempty"` } func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request) { if _, ok := s.requirePrincipal(w, r); !ok { return } var req LeaseRequestDTO if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } workDir, werr := s.resolver.ResolveDocWorkDir(r.Context(), req.DocID) if werr != nil { workDir = "" } ttl := time.Duration(req.TTLSeconds) * time.Second doc, err := s.svc.DocGov().AcquireLease(r.Context(), docgov.LeaseAcquireRequest{ DocID: req.DocID, HolderType: docgov.LeaseHolderType(req.HolderType), HolderID: req.HolderID, WorkDir: workDir, TTL: ttl, }) if err != nil { writeDocGovError(w, err) return } writeJSONResponse(w, http.StatusOK, doc) } func (s *Server) handleDocGovHeartbeatLease(w http.ResponseWriter, r *http.Request) { if _, ok := s.requirePrincipal(w, r); !ok { return } var req LeaseRequestDTO if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } ttl := time.Duration(req.TTLSeconds) * time.Second doc, err := s.svc.DocGov().HeartbeatLease(r.Context(), req.DocID, req.HolderType, req.HolderID, ttl) if err != nil { writeDocGovError(w, err) return } writeJSONResponse(w, http.StatusOK, doc) } func (s *Server) handleDocGovReleaseLease(w http.ResponseWriter, r *http.Request) { if _, ok := s.requirePrincipal(w, r); !ok { return } var req LeaseRequestDTO if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if s.svc == nil || s.svc.DocGov() == nil { http.Error(w, "docgov engine not initialized", http.StatusServiceUnavailable) return } doc, err := s.svc.DocGov().ReleaseLease(r.Context(), req.DocID, req.HolderType, req.HolderID) if err != nil { writeDocGovError(w, err) return } writeJSONResponse(w, http.StatusOK, doc) }