Files
Agent-System/internal/coordinator/engine_workflow.go
T
weichen d55488ae37 feat(coordinator): 用智能体叙述查询、帮助与操作回执
确定性事实仍由代码生成;PI Agent 只组织语言,失败或关闭开关时回退原模板。安全拦截与确认提问保持模板。
2026-09-28 03:25:06 +00:00

224 lines
8.6 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 coordinator
import (
"context"
"fmt"
"strings"
"chat-agent-system/internal/intent"
)
func (e *CoordinatorEngine) handleSwitchWorkflow(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
targetWF := cand.FilledSlots[intent.SlotTargetWorkflow]
active := defaultStr(userCtx.Project.ActiveWorkflowKey, "main")
if targetWF == "" {
msg := fmt.Sprintf(
"切换项目激活工作流属于治理结构操作,仅支持在项目设置界面完成。当前激活工作流为「%s」。如需了解状态,可询问「当前工作流是什么」。",
workflowDisplayName(active),
)
return e.applyNarrative(context.Background(), userCtx, "handleSwitchWorkflow", "", "", &CoordinationResult{
Status: StatusRejected,
Intent: intent.IntentSwitchWorkflow,
RejectReason: "WORKFLOW_SWITCH_UI_ONLY",
NaturalReply: msg,
UpdatedContext: userCtx,
}), nil
}
if userCtx.Project.ActiveWorkflowKey == targetWF {
return e.applyNarrative(context.Background(), userCtx, "handleSwitchWorkflow", "", "", &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentSwitchWorkflow,
NaturalReply: fmt.Sprintf("当前项目已激活工作流「%s」,无需切换。激活工作流的变更请在项目设置界面操作。", workflowDisplayName(targetWF)),
UpdatedContext: userCtx,
}), nil
}
msg := fmt.Sprintf(
"无法通过对话切换工作流:从「%s」切换至「%s」属于项目治理结构操作,请在项目设置 → 工作流面板完成。对话仅支持查询工作流状态与(在授权下)确认运行已激活的工作流。",
workflowDisplayName(active),
workflowDisplayName(targetWF),
)
return e.applyNarrative(context.Background(), userCtx, "handleSwitchWorkflow", "", "", &CoordinationResult{
Status: StatusRejected,
Intent: intent.IntentSwitchWorkflow,
RejectReason: "WORKFLOW_SWITCH_UI_ONLY",
NaturalReply: msg,
UpdatedContext: userCtx,
}), nil
}
func (e *CoordinatorEngine) handleTriggerWorkflow(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
targetWF := cand.FilledSlots[intent.SlotTargetWorkflow]
if targetWF == "" {
targetWF = defaultStr(userCtx.Project.ActiveWorkflowKey, "main")
}
capKey := "TRIGGER_WORKFLOW:" + targetWF
if !hasCapability(userCtx, capKey) && !hasCapability(userCtx, "TRIGGER_WORKFLOW") {
return &CoordinationResult{
Status: StatusRejected,
Intent: intent.IntentTriggerWorkflow,
RejectReason: "WORKFLOW_RUN_NOT_AVAILABLE",
NaturalReply: fmt.Sprintf(
"当前上下文未开放工作流「%s」的运行能力(缺少 %s)。请先确认项目阶段或在界面授权后再试。",
targetWF, capKey,
),
UpdatedContext: userCtx,
}, nil
}
card, exp := buildConfirmCard(
"工作流运行确认",
"", workflowDisplayName(targetWF), "",
fmt.Sprintf("确认运行工作流「%s」", workflowDisplayName(targetWF)),
"确认后将创建一次真实的工作流运行记录;取消则不做任何变更。",
"confirm_workflow_run", "确认运行",
map[string]string{intent.SlotTargetWorkflow: targetWF, "workflow_key": targetWF},
)
// TargetDocID unused for workflow; keep card title meaningful
card.TargetDocTitle = workflowDisplayName(targetWF)
userCtx.applyCard(card)
userCtx.applyPending(exp)
msg := fmt.Sprintf("已为工作流「%s」起草运行确认单。确认后将创建运行实例;取消则不会启动。", workflowDisplayName(targetWF))
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentTriggerWorkflow,
NaturalReply: msg,
InteractiveCard: card,
NextExpectation: exp,
UpdatedContext: userCtx,
}, nil
}
func (e *CoordinatorEngine) handleQueryWorkflowStatus(_ context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
active := defaultStr(userCtx.Project.ActiveWorkflowKey, userCtx.Project.CurrentWorkflow)
if active == "" {
active = "main"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("📋 **工作流状态**\n\n• 当前激活:%s(%s)\n• 选择版本:%d\n",
workflowDisplayName(active), active, userCtx.Project.WorkflowSelectionVersion))
if len(userCtx.AvailableActions) > 0 {
sb.WriteString("\n• 当前可用能力:\n")
for _, a := range userCtx.AvailableActions {
if strings.Contains(a, "WORKFLOW") || strings.Contains(a, "TRIGGER_TASK") {
sb.WriteString(fmt.Sprintf(" - %s\n", a))
}
}
}
return e.applyNarrative(context.Background(), userCtx, "handleQueryWorkflowStatus", "", "", &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentQueryWorkflowStatus,
NaturalReply: sb.String(),
UpdatedContext: userCtx,
}), nil
}
func (e *CoordinatorEngine) handleQueryDocStatus(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
if cand.FilledSlots[intent.SlotQueryTopic] == "doc_summary" {
return e.handleDocumentSummary(ctx, cand, userCtx)
}
docID := cand.FilledSlots[intent.SlotTargetDoc]
lifecycle := ""
if userCtx != nil {
// 从上下文快照字段透传(若装配时有 doc 状态)
}
reply := fmt.Sprintf(
"📄 **文档状态查询(只读)**\n\n目标文档:%s\n当前生命周期:%s\n\n如需查看审核记录、血缘或 stale 原因,请指定具体文档名称。",
defaultStr(docID, "当前激活文档"),
defaultStr(lifecycle, "请在工作台文档面板查看详情"),
)
return e.applyNarrative(ctx, userCtx, "handleQueryDocStatus", "", "", &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentQueryDocStatus,
NaturalReply: reply,
UpdatedContext: userCtx,
}), nil
}
func (e *CoordinatorEngine) handleHelpCapabilities(ctx context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
var sb strings.Builder
sb.WriteString("🧭 **我能帮您做什么**\n\n")
sb.WriteString("当前阶段您可以通过对话尝试:\n")
if len(userCtx.AvailableActions) == 0 {
sb.WriteString("• 查询项目进展与风险\n• 补充项目事实\n• 查询法规与权限\n")
} else {
for _, a := range userCtx.AvailableActions {
sb.WriteString(fmt.Sprintf("• %s\n", formatCapabilityLabel(a)))
}
}
sb.WriteString("\n高风险操作(文档审批、阶段推进、运行工作流)需要明确确认后才会执行;切换激活工作流请使用项目设置界面。")
return e.applyNarrative(ctx, userCtx, "handleHelpCapabilities", "", "", &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentHelpCapabilities,
NaturalReply: sb.String(),
UpdatedContext: userCtx,
}), nil
}
func (e *CoordinatorEngine) handleUnsupportedAction(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
raw := ""
if cand != nil {
raw = cand.Reasoning
}
return e.applyNarrative(ctx, userCtx, "handleUnsupportedAction", raw, "", &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentUnsupportedAction,
NaturalReply: "我理解您的诉求,但该操作目前仅支持通过界面按钮完成(如下载、共享、批量导出、直接修改受控正文等),暂不支持自然语言自动执行。",
UpdatedContext: userCtx,
}), nil
}
func hasCapability(userCtx *UserProjectContext, key string) bool {
if userCtx == nil {
return false
}
for _, a := range userCtx.AvailableActions {
if a == key {
return true
}
}
return false
}
func defaultStr(v, fallback string) string {
if v == "" {
return fallback
}
return v
}
func workflowDisplayName(key string) string {
switch key {
case "product_manager":
return "智能产品经理"
case "test_manager":
return "智能测试经理"
case "main":
return "主工作流"
default:
return key
}
}
func formatCapabilityLabel(cap string) string {
switch {
case cap == "QUERY_PROGRESS":
return "查询项目进展、风险与沟通话术"
case cap == "SUPPLEMENT_INFO":
return "补充或修正项目事实"
case cap == "VIEW_DOC":
return "查看受控文档"
case cap == "SWITCH_WORKFLOW":
return "切换项目工作流(仅界面;对话不代执行)"
case strings.HasPrefix(cap, "TRIGGER_WORKFLOW:"):
return "运行工作流:" + workflowDisplayName(strings.TrimPrefix(cap, "TRIGGER_WORKFLOW:"))
case strings.HasPrefix(cap, "TRIGGER_TASK:"):
return "生成交付物:" + strings.TrimPrefix(cap, "TRIGGER_TASK:")
case strings.HasPrefix(cap, "ADVANCE_STAGE:"):
return "推进阶段:" + strings.TrimPrefix(cap, "ADVANCE_STAGE:")
default:
return cap
}
}