Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
410 lines
12 KiB
Go
410 lines
12 KiB
Go
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
|
||
}
|
||
}
|