286 lines
8.5 KiB
Go
286 lines
8.5 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
|
|
}
|
|
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
|
|
}
|
|
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.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 {
|
|
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)
|
|
}
|