Files
WeiCHEN 4c07716933 refactor(server): split handlers.go by domain
Keep slim handlers.go core; move chat/docgov/health/http/context
handlers into domain files ≤800 lines without behavior change.
2026-09-23 09:54:18 +08:00

282 lines
8.3 KiB
Go

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 {
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
return
}
if req.Message == "" {
writeSafeError(w, r, http.StatusBadRequest, "message cannot be empty")
return
}
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
}
writeSafeError(w, r, http.StatusInternalServerError, "resolve session failed: "+err.Error())
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.allowActiveStageChatAccess(r.Context(), sess.Project(), user.ID) {
s.writeWorkflowAccessDenied(w)
return
}
s.bindSessionDocumentSummaryReader(sess, user)
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 {
writeSafeError(w, r, http.StatusInternalServerError, "process chat message failed: "+err.Error())
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 {
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
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, 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, 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)
}