Files
weichen b5a782bb38 feat(chat): dialogue triggers for comments/reopen/pending-supplements with mistouch guards
Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
2026-09-25 20:14:10 +00:00

410 lines
12 KiB
Go
Raw Permalink 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
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
}
}