Files
Agent-System/internal/chat/action_executor.go
T
weichen 0ab4a8bd2d feat(server): 接线对话 workflow.run 并补 P0 验证记录
server 侧窄端口适配 projectworkflow,消除 chat→projectworkflow 边界违规;记录单测与 arch-check 证据,PI 全链路待补证。
2026-09-25 14:37:11 +00:00

289 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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())
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 "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
}
}