package coordinator import ( "context" "errors" "fmt" "path/filepath" "strings" "chat-agent-system/internal/app/stageflow" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/intent" "chat-agent-system/internal/projectworkflow" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowscope" "chat-agent-system/internal/workspace" ) func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) { // 跨工作流作用域:识别器已打 mismatch 标记时直接引导切换,不执行任务。 if cand != nil && cand.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" { active := "" if userCtx != nil { active = userCtx.Project.ActiveWorkflowKey } guidance := cand.ClarificationQuestion if guidance == "" { workflows := []string{} if v := cand.FilledSlots[intent.SlotMismatchedWorkflow]; v != "" { workflows = strings.Split(v, ",") } guidance = intent.WorkflowScopeGuidanceQuestion(cand.FilledSlots[intent.SlotMismatchedDoc], workflows, active) } return &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: guidance, NaturalReply: guidance, UpdatedContext: userCtx, }, nil } taskKey := cand.FilledSlots[intent.SlotTargetTask] if taskKey == "" { taskKey = "intake" } // 消费端二次校验:任务键不属于激活工作流时拦截(防御识别器漏标)。 if userCtx != nil && userCtx.Project.ActiveWorkflowKey != "" { owner := intent.TaskKeyWorkflowOwner(taskKey) if workflowscope.BlocksWrite(userCtx.Project.ActiveWorkflowKey, owner, taskKey) { guidance := intent.WorkflowScopeGuidanceQuestion(taskKey, []string{owner}, userCtx.Project.ActiveWorkflowKey) return &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: guidance, NaturalReply: guidance, UpdatedContext: userCtx, }, nil } } // 智能确认门禁:弱写动词 / 阶段不匹配 / 中等置信 → 先确认,不盲目执行。 if gated := e.confirmGate(ctx, cand, userCtx, taskKey); gated != nil { return gated, nil } nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.applyPending(nextExp) if e.tasks() == nil { return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("已收到生成【%s】的指令。", taskKey), UpdatedContext: userCtx, NextExpectation: nextExp, }, nil } var projectID, userID int64 if userCtx.Project.ID != "" { projectID = parseDecimalID(userCtx.Project.ID) } if userCtx.User.ID != "" { userID = parseDecimalID(userCtx.User.ID) } taskRes, err := e.tasks().ExecuteTask(ctx, stageflow.TaskRunRequest{ TaskKey: taskKey, ProjectName: userCtx.Project.Name, CustomerName: userCtx.Project.CustomerName, DeviceType: userCtx.Project.DeviceType, IntendedUse: userCtx.Project.IntendedUse, TargetMarket: userCtx.Project.TargetMarket, Fields: userCtx.BlackboardFacts, SessionID: userCtx.ChatSessionID, ProjectID: projectID, UserID: userID, // 会话触发与特设任务一致:质量门禁失败时给修复预算。 // 受控文档仍由 bizdev 强制开启;预算硬停在 DefaultMaxRepairRounds。 AutoRepair: true, }) if err != nil { if errors.Is(err, vocabulary.ErrDocumentBusy) { msg := "该文档正在由项目助手生成或他人编辑中,请稍后再发起更新。您可以先继续对话或处理其他文档。" if taskRes != nil && taskRes.ErrorReason != "" { msg = taskRes.ErrorReason + "。您可以先继续对话或处理其他文档。" } return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: msg, UpdatedContext: userCtx, }, nil } if errors.Is(err, vocabulary.ErrDocumentLocked) { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: "该文档已进入审核/批准锁定状态,无法自动覆写正文。如需修订请走驳回解锁或变更提案流程。", UpdatedContext: userCtx, }, nil } if taskRes != nil && strings.Contains(taskRes.ErrorReason, "会话未绑定项目") { return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: taskRes.ErrorReason, NaturalReply: taskRes.ErrorReason, UpdatedContext: userCtx, }, nil } if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) { msg := userFacingPrerequisite(err) if taskRes != nil && strings.Contains(taskRes.ErrorReason, "请先完成商务对接流程") { msg = taskRes.ErrorReason[strings.Index(taskRes.ErrorReason, "请先完成商务对接流程"):] } return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: msg, UpdatedContext: userCtx, }, nil } return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: fmt.Sprintf("任务执行失败: %v", err), UpdatedContext: userCtx, }, err } userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief outPath := filepath.Join(taskRes.WorkDir, workspace.DeliverableKey) reply, taskStatus := formatTriggerTaskReply(taskRes, outPath) res := &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: reply, AdvisorBrief: taskRes.AdvisorBrief, ArtifactPath: outPath, UpdatedContext: userCtx, NextExpectation: nextExp, TaskStatus: taskStatus, TaskKey: taskRes.TaskKey, ArtifactTitle: taskRes.ArtifactTitle, } attachTaskRunProjection(res, taskRes) return e.applyNarrative(ctx, userCtx, "handleTriggerTask", "", "", res), nil } // formatTriggerTaskReply builds sync coordinator copy for a TaskRunResult. // Pending high-risk candidates must not claim "正文已保存 / 已完成". func userFacingPrerequisite(err error) string { const hint = "请先完成商务对接流程" if err != nil && strings.Contains(err.Error(), hint) { return err.Error()[strings.Index(err.Error(), hint):] } return "请先完成商务对接流程(CDO 报价批准)后再启动研发设计控制流程" } func formatTriggerTaskReply(taskRes *stageflow.TaskRunResult, outPath string) (reply string, taskStatus string) { _ = outPath // 不把宿主机工作区路径写进用户可见回复 if taskRes == nil { return "任务执行失败。", stageflow.TaskStatusFailed } taskStatus = taskRes.TerminalStatus() if taskRes.AwaitsAgentConfirmation() || taskStatus != stageflow.TaskStatusCompleted { return taskRes.CompletionSummary(), taskStatus } var sb strings.Builder sb.WriteString(fmt.Sprintf("已为您自适应规划并完成生成【%s】!\n", taskRes.ArtifactTitle)) if taskRes.DocRecord != nil && taskRes.DocRecord.DocID != "" { sb.WriteString(fmt.Sprintf("交付物已写入受控文档:%s\n", taskRes.DocRecord.DocID)) sb.WriteString("正文已保存。\n") } else { sb.WriteString("交付物已生成于工作区,尚未写入受控文档账本。\n") } sb.WriteString(fmt.Sprintf("耗时: %v\n", taskRes.Duration)) if taskRes.AdvisorBrief != nil && len(taskRes.AdvisorBrief.BaselineAssumptions) > 0 { sb.WriteString("\n📌 预设基准假设(已融入方案,您可随时修正):\n") for _, a := range taskRes.AdvisorBrief.BaselineAssumptions { sb.WriteString(fmt.Sprintf(" • %s\n", a)) } } return sb.String(), taskStatus } func attachTaskRunProjection(res *CoordinationResult, taskRes *stageflow.TaskRunResult) { if res == nil || taskRes == nil { return } if taskRes.DocRecord != nil { res.DocID = taskRes.DocRecord.DocID } if taskRes.AwaitsAgentConfirmation() { res.PendingAgentCandidateID = taskRes.PendingAgentCandidateID res.PendingChangeReportID = taskRes.PendingChangeReportID res.PendingRiskLevel = taskRes.PendingRiskLevel } } // handleTriggerAdhocTask 处理自由任务升级确认(路由层 R1 升级建议的兑现路径): // 基于任务简报组装特设小队,走 planAutonomousAdhoc 自主规划 + TaskGov 全量治理 // (预算账本 / 执行轨迹 / 重复识别 / 归档锁定 / 质量门禁修复闭环)。 func (e *CoordinatorEngine) handleTriggerAdhocTask(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) { brief := cand.FilledSlots[intent.SlotTaskBrief] if brief == "" { brief = rawInput } title := deriveAdhocTitle(brief) taskKey := sanitizeAdhocTaskKey(title) nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.applyPending(nextExp) if e.tasks() == nil { return e.applyNarrative(ctx, userCtx, "handleTriggerAdhocTask", rawInput, "", &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: fmt.Sprintf("已收到自由任务《%s》的升级指令(当前环境未接入小队服务,简报已登记)。\n任务简报: %s", title, brief), UpdatedContext: userCtx, NextExpectation: nextExp, }), nil } var projectID, userID int64 if userCtx.Project.ID != "" { projectID = parseDecimalID(userCtx.Project.ID) } if userCtx.User.ID != "" { userID = parseDecimalID(userCtx.User.ID) } if st := stream.FromContext(ctx); st != nil { st.EmitPhase("squad_assembly", fmt.Sprintf("正在为自由任务《%s》组建特设智能体小队(动态规划 + 预算管控 + 归档锁定)...", title), "leader", 0, 0, 5) } // 任务简报材料化:作为首要输入材料注入工作区(context/materials_text/task_brief.md), // 供 organizer 专员在容器内直接读到用户原始诉求(否则首步将报"无材料可整理") briefMaterial := stageflow.MaterialInput{ Filename: "task_brief.md", Content: []byte(fmt.Sprintf("# 任务简报(用户原始诉求,权威等级:primary_evidence)\n\n%s\n", brief)), } taskRes, err := e.tasks().ExecuteTask(ctx, stageflow.TaskRunRequest{ TaskKey: taskKey, Title: title, ProjectName: userCtx.Project.Name, CustomerName: userCtx.Project.CustomerName, DeviceType: userCtx.Project.DeviceType, IntendedUse: userCtx.Project.IntendedUse, TargetMarket: userCtx.Project.TargetMarket, Fields: userCtx.BlackboardFacts, Materials: []stageflow.MaterialInput{briefMaterial}, AutoRepair: true, SessionID: userCtx.ChatSessionID, ProjectID: projectID, UserID: userID, }) if err != nil { if taskRes != nil && taskRes.WorkDir != "" { userCtx.WorkDir = taskRes.WorkDir } return &CoordinationResult{ Status: StatusRejected, Intent: cand.Intent, Branch: cand.Branch, RejectReason: err.Error(), NaturalReply: fmt.Sprintf("自由任务《%s》执行失败: %v", title, err), UpdatedContext: userCtx, }, err } userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief if taskRes.WorkDir != "" { userCtx.WorkDir = taskRes.WorkDir } outPath := filepath.Join(taskRes.WorkDir, workspace.DeliverableKey) var sb strings.Builder taskStatus := taskRes.TerminalStatus() if taskRes.AwaitsAgentConfirmation() { sb.WriteString(taskRes.CompletionSummary()) } else { sb.WriteString(fmt.Sprintf("已为您组建特设小队并完成自由任务《%s》!\n任务简报: %s\n交付物正文已保存至:%s\n耗时: %v\n", title, brief, outPath, taskRes.Duration)) if taskRes.QualityReport != nil { verdict := "⚠️ 未完全通过" if taskRes.QualityReport.Passed { verdict = "✅ 通过" } sb.WriteString(fmt.Sprintf("质量门禁: %s(评分 %.0f)\n", verdict, taskRes.QualityReport.Score)) } if gov := governanceSummary(taskRes.WorkDir); gov != "" { sb.WriteString("治理审计: " + gov + "\n") } if taskRes.AdvisorBrief != nil && len(taskRes.AdvisorBrief.BaselineAssumptions) > 0 { sb.WriteString("\n📌 预设基准假设(已融入方案,您可随时修正):\n") for _, a := range taskRes.AdvisorBrief.BaselineAssumptions { sb.WriteString(fmt.Sprintf(" • %s\n", a)) } } } res := &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: cand.Branch, NaturalReply: sb.String(), AdvisorBrief: taskRes.AdvisorBrief, ArtifactPath: outPath, UpdatedContext: userCtx, NextExpectation: nextExp, TaskStatus: taskStatus, TaskKey: taskRes.TaskKey, ArtifactTitle: taskRes.ArtifactTitle, } attachTaskRunProjection(res, taskRes) return e.applyNarrative(ctx, userCtx, "handleTriggerAdhocTask", rawInput, "", res), nil }