345 lines
13 KiB
Go
345 lines
13 KiB
Go
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,
|
||
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 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")
|
||
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,
|
||
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
|
||
}
|