package chat import ( "context" "fmt" "strconv" "strings" "time" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/docgov" "chat-agent-system/internal/taskrun" ) // WorkflowStartRequest is the narrow DTO for starting a workflow run from dialogue. type WorkflowStartRequest struct { RunID string ProjectID int64 WorkflowKey string StartedBy int64 Status string } // WorkflowStartResult is the narrow receipt after CreateWorkflowRun. type WorkflowStartResult struct { RunID string } // WorkflowRunner creates workflow run records (composition adapts projectworkflow.Service). type WorkflowRunner interface { CreateWorkflowRun(ctx context.Context, req WorkflowStartRequest) (*WorkflowStartResult, error) } // ActionServices 对话层执行 PendingAction 所需的后端服务。 // 具体 chatflow / intentflow / taskrun 实现由组合根经窄端口注入。 // // 生产主路径(server 会话装配)已接线:Workflow = AdaptChatWorkflow(s.pwSvc)。 // BindChatSession(internal/app)若启用亦应适配 rt.PW。 type ActionServices struct { DocGov *docgov.GovernanceEngine TaskRuns TaskStarter IntentPlans IntentPlans Workflow WorkflowRunner Reopens ReopenService Environment string } // ActionExecResult 动作执行结果摘要。 type ActionExecResult struct { DocState string RunID string Message string } func parseUserID(userID string) int64 { id, _ := strconv.ParseInt(strings.TrimSpace(userID), 10, 64) return id } func actorName(userCtx *coordinator.UserProjectContext) string { if userCtx == nil { return "用户" } if userCtx.User.Name != "" { return userCtx.User.Name } return "用户" } // EnsureDocResolver wires ListDocResolver from DocGov when the engine has none yet. func (s *Session) EnsureDocResolver() { if s == nil || s.Coordinator == nil || s.Actions == nil || s.Actions.DocGov == nil { return } if s.Coordinator.HasDocResolver() { return } gov := s.Actions.DocGov s.Coordinator.SetDocResolver(&coordinator.ListDocResolver{ List: func(ctx context.Context, projectID int64) ([]coordinator.DocListItem, error) { rows, err := gov.ListWorkflowDocumentsByProject(ctx, projectID) if err != nil { return nil, err } out := make([]coordinator.DocListItem, 0, len(rows)) for _, r := range rows { out = append(out, coordinator.DocListItem{ DocID: r.DocID, Title: r.Title, State: r.LifecycleState, TaskKey: r.TaskKey, }) } return out, nil }, }) } func mapCommentType(raw string) vocabulary.CommentType { switch strings.ToLower(strings.TrimSpace(raw)) { case "issue", "problem", "comment": return vocabulary.CommentGeneral case "approval", "approve": return vocabulary.CommentSuggestion case "reject", "rejection": return vocabulary.CommentRejection default: return vocabulary.CommentSuggestion } } func mapUserRole(raw string) vocabulary.UserRole { switch strings.ToLower(strings.TrimSpace(raw)) { case "reviewer": return vocabulary.RoleReviewer case "approver": return vocabulary.RoleApprover case "system": return vocabulary.RoleSystem default: return vocabulary.RoleAuthor } } func (s *Session) executeAction(ctx context.Context, action *coordinator.PendingAction, userCtx *coordinator.UserProjectContext) (*ActionExecResult, error) { if action == nil || s.Actions == nil { return nil, fmt.Errorf("action services not configured") } uid := s.UserID if uid <= 0 { uid = parseUserID(userCtx.User.ID) } name := actorName(userCtx) switch action.Kind { case "docgov.claim": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } doc, err := s.Actions.DocGov.RecordDocumentViewed(ctx, action.DocID, uid, vocabulary.RoleAuthor, name) if err != nil { return nil, err } if userCtx != nil { label := action.DocID if doc != nil && doc.Title != "" { label = doc.Title } found := false for _, d := range userCtx.UserDocs.Claimed { if d == label || d == action.DocID { found = true break } } if !found { userCtx.UserDocs.Claimed = append(userCtx.UserDocs.Claimed, label) } } return &ActionExecResult{DocState: string(doc.CurrentState), Message: "认领完成"}, nil case "docgov.submit_review": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } doc, err := s.Actions.DocGov.GetDocument(ctx, action.DocID) if err != nil || doc == nil { return nil, fmt.Errorf("文档不存在或无法读取") } if doc.CurrentState != vocabulary.StateDrafting { return nil, fmt.Errorf("仅编制中(drafting)文档可送审,当前状态:%s", doc.CurrentState) } if doc.AuthorID > 0 && doc.AuthorID != uid { return nil, fmt.Errorf("仅编制责任人可送审") } updated, err := s.Actions.DocGov.SubmitForReview(ctx, action.DocID, uid, vocabulary.RoleAuthor, name) if err != nil { return nil, err } return &ActionExecResult{DocState: string(updated.CurrentState), Message: "送审完成"}, nil case "docgov.review_approve": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } doc, err := s.Actions.DocGov.ReviewApprove(ctx, action.DocID, uid, vocabulary.RoleReviewer, name, action.Comment) if err != nil { return nil, err } return &ActionExecResult{DocState: string(doc.CurrentState), Message: "审核通过"}, nil case "docgov.approver_approve": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } doc, err := s.Actions.DocGov.ApproverApprove(ctx, action.DocID, uid, vocabulary.RoleApprover, name, action.Comment) if err != nil { return nil, err } return &ActionExecResult{DocState: string(doc.CurrentState), Message: "终审批准"}, nil case "docgov.reject": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } reason := action.Reason if reason == "" { reason = "用户确认驳回" } doc, err := s.Actions.DocGov.ReviewReject(ctx, action.DocID, uid, vocabulary.RoleReviewer, name, reason) if err != nil { rollback := action.Rollback if rollback == "" { rollback = vocabulary.StateDrafting } doc, err = s.Actions.DocGov.ApproverReject(ctx, action.DocID, uid, vocabulary.RoleApprover, name, reason, rollback) if err != nil { return nil, err } } return &ActionExecResult{DocState: string(doc.CurrentState), Message: "驳回完成"}, nil case "workflow.run": if s.Actions.Workflow == nil { return nil, fmt.Errorf("workflow service not available") } key := strings.TrimSpace(action.WorkflowKey) if key == "" && userCtx != nil { key = userCtx.Project.ActiveWorkflowKey } if key == "" { key = "main" } runID := fmt.Sprintf("wf_run_%d", time.Now().UnixNano()) created, err := s.Actions.Workflow.CreateWorkflowRun(ctx, WorkflowStartRequest{ RunID: runID, ProjectID: s.ProjectID, WorkflowKey: key, StartedBy: uid, Status: "running", }) if err != nil { return nil, err } return &ActionExecResult{RunID: created.RunID, Message: "工作流已启动"}, nil case "docgov.comment_add": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } role := mapUserRole(action.Role) content := action.Comment if content == "" { return nil, fmt.Errorf("批注内容不能为空") } cType := mapCommentType(action.Reason) _, err := s.Actions.DocGov.AddComment(ctx, action.DocID, uid, name, role, action.Section, cType, content) if err != nil { return nil, err } doc, _ := s.Actions.DocGov.GetDocument(ctx, action.DocID) state := "" if doc != nil { state = string(doc.CurrentState) } return &ActionExecResult{DocState: state, Message: "批注已添加"}, nil case "docgov.comment_resolve": if s.Actions.DocGov == nil { return nil, fmt.Errorf("docgov engine not available") } role := mapUserRole(action.Role) comms, err := s.Actions.DocGov.ListComments(ctx, action.DocID) if err != nil { return nil, err } var target *docgov.DocumentComment for _, c := range comms { if c == nil || c.Status != "open" { continue } if action.Section != "" && c.TargetSection != action.Section { continue } target = c break } if target == nil { return nil, fmt.Errorf("未找到可解决的未关闭批注") } _, err = s.Actions.DocGov.ResolveComment(ctx, target.ID, uid, role, true) if err != nil { return nil, err } return &ActionExecResult{Message: fmt.Sprintf("已解决批注 #%d", target.ID)}, nil case "docgov.reopen_request": if s.Actions.Reopens == nil { return nil, fmt.Errorf("reopen service not available") } taskKey := action.TaskKey if taskKey == "" { taskKey = action.DocID } view, err := s.Actions.Reopens.RequestReopenByDoc(ctx, s.ProjectID, taskKey, action.Reason, uid) if err != nil { return nil, err } return &ActionExecResult{Message: fmt.Sprintf("重开申请已提交(#%d,状态:%s)", view.ID, view.Status)}, nil case "docgov.reopen_approve": if s.Actions.Reopens == nil { return nil, fmt.Errorf("reopen service not available") } if action.RequestID <= 0 { return nil, fmt.Errorf("缺少重开申请编号") } view, err := s.Actions.Reopens.ApproveReopenByID(ctx, s.ProjectID, action.RequestID, uid) if err != nil { return nil, err } return &ActionExecResult{Message: fmt.Sprintf("已批准重开申请 #%d", view.ID)}, nil case "taskrun.resume", "taskrun.restart": if s.Actions.TaskRuns == nil { return nil, fmt.Errorf("task run services not available") } taskKey := action.TaskKey if taskKey == "" { taskKey = "intake" } runID := fmt.Sprintf("run_%d", time.Now().UnixNano()) resumeDir := "" if action.Kind == "taskrun.resume" { resumeDir = action.WorkDir } taskReq := TaskStartRequest{ TaskKey: taskKey, ProjectName: userCtx.Project.Name, UserID: uid, ProjectID: s.ProjectID, SessionID: s.ID, } record := &taskrun.RunRecord{ RunID: runID, SessionID: s.ID, ProjectID: s.ProjectID, UserID: uid, TaskKey: taskKey, Status: "running", } created, err := s.Actions.TaskRuns.CreateAndStart(ctx, runID, record, taskReq, resumeDir) if err != nil { return nil, err } return &ActionExecResult{RunID: created.RunID, Message: "任务已启动"}, nil default: return nil, fmt.Errorf("unsupported pending action: %s", action.Kind) } } func renderActionReply(action *coordinator.PendingAction, result *ActionExecResult, execErr error) string { docName := action.DocID if docName == "" { docName = "当前任务" } if execErr != nil { return fmt.Sprintf("操作未能完成:%s", execErr.Error()) } if result == nil { return "操作已完成。" } switch action.Kind { case "docgov.claim": return fmt.Sprintf("已确认认领《%s》(当前状态:%s)。您可以开始修订正文或提交审核。", docName, result.DocState) case "docgov.submit_review": return fmt.Sprintf("已确认送审!《%s》已进入审核锁定(当前状态:%s)。", docName, result.DocState) case "docgov.review_approve": return fmt.Sprintf("已确认审核通过!《%s》已流转至批准环节(当前状态:%s)。", docName, result.DocState) case "docgov.approver_approve": return fmt.Sprintf("已确认终审批准!《%s》已被法定受控锁定并正式归档生效(当前状态:%s)。", docName, result.DocState) case "docgov.reject": return fmt.Sprintf("已确认执行驳回操作!《%s》已退回(当前状态:%s),驳回意见已同步下发。", docName, result.DocState) case "workflow.run": return fmt.Sprintf("工作流已启动(run_id=%s)。您可在工作流面板查看进度。", result.RunID) case "docgov.comment_add": return fmt.Sprintf("已添加批注到《%s》。", docName) case "docgov.comment_resolve": if result.Message != "" { return result.Message + "。" } return fmt.Sprintf("已解决《%s》的批注。", docName) case "docgov.reopen_request": if result.Message != "" { return result.Message + "。" } return fmt.Sprintf("已提交《%s》的重开申请。", docName) case "docgov.reopen_approve": if result.Message != "" { return result.Message + "。" } return "已批准重开申请。" case "taskrun.resume": return fmt.Sprintf("续跑任务已启动(run_id=%s)。您可在任务面板查看进度。", result.RunID) case "taskrun.restart": return fmt.Sprintf("任务已重新启动(run_id=%s)。您可在任务面板查看进度。", result.RunID) default: return result.Message } }