Files
Agent-System/internal/chat/action_executor.go
T
weichen ce694baef1 feat(coordinator): P0 消除对话主链路假成功
claim/submit 经 DocResolver+确认卡后 PendingAction 真写 docgov;proposal 默认分支改澄清;workflow.switch 诚实拒绝(UI专属);workflow.run 确认后走 CreateWorkflowRun(ActionServices.Workflow 由合入时接线)。
2026-09-25 13:19:36 +00:00

274 lines
8.6 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/projectworkflow"
"chat-agent-system/internal/taskrun"
)
// WorkflowRunner creates workflow run records (narrow port; composition injects projectworkflow.Service).
type WorkflowRunner interface {
CreateWorkflowRun(ctx context.Context, run *projectworkflow.WorkflowRun) (*projectworkflow.WorkflowRun, error)
}
// ActionServices 对话层执行 PendingAction 所需的后端服务。
// 具体 chatflow / intentflow / taskrun 实现由组合根经窄端口注入。
type ActionServices struct {
DocGov *docgov.GovernanceEngine
TaskRuns TaskStarter
IntentPlans IntentPlans
Workflow WorkflowRunner
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 (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())
startedBy := uid
created, err := s.Actions.Workflow.CreateWorkflowRun(ctx, &projectworkflow.WorkflowRun{
RunID: runID,
ProjectID: s.ProjectID,
WorkflowKey: key,
StartedBy: &startedBy,
Status: "running",
})
if err != nil {
return nil, err
}
return &ActionExecResult{RunID: created.RunID, Message: "工作流已启动"}, 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 "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
}
}