diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 7b0e35a6..d6925893 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -4,8 +4,12 @@ import ( "encoding/json" "net/http" "path/filepath" + "strconv" + "strings" "chat-agent-system/internal/bizdev" + "chat-agent-system/internal/ctxmgr" + "chat-agent-system/internal/docgov" ) // HealthResponse 健康检查响应 @@ -29,20 +33,32 @@ type ChatRequest struct { 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"` + 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"` } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { @@ -117,15 +133,25 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { // 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, + 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 { http.Error(w, "execute stage failed: "+err.Error(), http.StatusInternalServerError) @@ -140,15 +166,25 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { 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, - Backend: req.Backend, - AutoRepair: req.AutoRepair, + TaskKey: req.TaskKey, + 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 { http.Error(w, "execute task failed: "+err.Error(), http.StatusInternalServerError) @@ -168,3 +204,390 @@ func writeJSONResponse(w http.ResponseWriter, status int, data any) { 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) { + 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.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) { + 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, req.UserID, docgov.UserRole(req.UserRole), req.UserName) + 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) { + 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, req.UserID, docgov.RoleAuthor, req.UserName) + 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) { + 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, req.UserID, docgov.RoleReviewer, req.UserName, req.Comment) + } else if action == "reject" { + reason := req.Reason + if reason == "" { + reason = req.Comment + } + doc, err = s.svc.DocGov().ReviewReject(r.Context(), req.DocID, req.UserID, docgov.RoleReviewer, req.UserName, 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) { + 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, req.UserID, docgov.RoleApprover, req.UserName, 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, req.UserID, docgov.RoleApprover, req.UserName, 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) { + 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, req.AuthorID, 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 { + http.Error(w, "add comment failed: "+err.Error(), http.StatusBadRequest) + 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) +} diff --git a/internal/server/server.go b/internal/server/server.go index 02fda00c..1a4245f0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -70,6 +70,22 @@ func (s *Server) Routes() http.Handler { 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/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) + return s.corsMiddleware(mux) } diff --git a/internal/server/sse.go b/internal/server/sse.go index a5cdd32f..0afd0ed7 100644 --- a/internal/server/sse.go +++ b/internal/server/sse.go @@ -25,6 +25,8 @@ type StreamChatRequest struct { 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"` } func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) { @@ -165,15 +167,25 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) { if req.Stage != "" { _, err := s.svc.ExecuteStage(ctxWithS, 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, + 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 { streamer.EmitError(err) @@ -188,15 +200,25 @@ func (s *Server) handleRunTaskStream(w http.ResponseWriter, r *http.Request) { } _, err := s.svc.ExecuteTask(ctxWithS, bizdev.TaskRunRequest{ - TaskKey: req.TaskKey, - ProjectName: req.ProjectName, - CustomerName: req.CustomerName, - DeviceType: req.DeviceType, - IntendedUse: req.IntendedUse, - TargetMarket: req.TargetMarket, - Fields: req.Fields, - Backend: req.Backend, - AutoRepair: req.AutoRepair, + TaskKey: req.TaskKey, + 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 { streamer.EmitError(err)