package server import ( "encoding/json" "errors" "net/http" "path/filepath" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/chat" "chat-agent-system/internal/ctxmgr" "chat-agent-system/internal/projectworkflow" ) func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } sessionID := r.URL.Query().Get("session_id") if sessionID == "" { writeSafeError(w, r, http.StatusBadRequest, "missing session_id query param") return } // 传入真实 caller,触发 getOrCreateSession 归属校验;无 owner 的遗留会话仍可读。 sess, err := s.getOrCreateSession(r.Context(), user.ID, sessionID, "", "", "", "", "", "", "", "") if err != nil { if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "SESSION_FORBIDDEN", "message": "无权访问该会话", }) return } writeSafeError(w, r, http.StatusNotFound, "session or project context not found") return } if sess == nil || sess.UserCtx == nil { writeSafeError(w, r, http.StatusNotFound, "session or project context not found") 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 { writeDomainError(w, r, http.StatusBadRequest, err) return } storedMessage, attachmentOnly, reject := prepareChatTurn(req.Message, req.Attachments) if reject != "" { writeSafeError(w, r, http.StatusBadRequest, reject) return } req.Message = storedMessage req.Attachments = normalizeTurnAttachments(req.Attachments) req.WorkDir = "" sess, err := s.getOrCreateSession( r.Context(), user.ID, req.SessionID, req.ProjectName, req.CustomerName, req.DeviceType, req.IntendedUse, req.TargetMarket, req.WorkDir, req.UserName, req.UserRole, ) if err != nil { if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "SESSION_FORBIDDEN", "message": "无权访问该会话", }) return } writeDomainError(w, r, http.StatusInternalServerError, err) return } s.hydrateVisibleHistory(r.Context(), sess, user.ID) if req.ProjectID > 0 && req.ProjectID != sess.Project() { sess.SetProjectID(req.ProjectID) } if !s.enforceChatRequiresProject(w, r, user, sess.Project()) { return } if sess.Project() > 0 { if !s.enforceChatProjectAccess(w, r, user, sess.Project()) { return } } s.enrichSessionWorkflowStage(r.Context(), sess, user.ID) if sess.Project() > 0 && !s.allowFreeLaneChatInitiation(sess.Project(), user.ID) { s.writeWorkflowAccessDenied(w) return } s.bindSessionDocumentSummaryReader(sess, user) s.bindTurnAttachments(sess, req.Attachments, attachmentOnly) var resp *chat.ChatResponse err = sess.WithExecLock(func() error { var sendErr error resp, sendErr = sess.SendMessage(r.Context(), req.Message) return sendErr }) if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) 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 { writeDomainError(w, r, http.StatusBadRequest, err) 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 } } // 工作流作用域:任务键不属于当前激活工作流时拦截并引导切换(不执行)。 if blocked, guidance := s.taskKeyOutsideActiveWorkflow(r.Context(), req.ProjectID, req.TaskKey); blocked { writeJSONResponse(w, http.StatusConflict, map[string]string{ "error_code": "ERR_DOC_WRONG_WORKFLOW", "message": guidance, }) 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, }) } // 同步任务可能远超全局 WriteTimeout(120s)。先把本响应的写期限拉长, // 否则业务已执行完但客户端收不到结果、重试会表现为重复执行。 extendSyncTaskWriteDeadline(w) // 1. 如果指定了 stage,则执行成熟度阶段编排 if req.Stage != "" { stageRes, err := s.taskUseCase().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: resolveTaskBackend(s, 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, r, 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.taskUseCase().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: resolveTaskBackend(s, 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, r, 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) }