Files
Agent-System/internal/coordinator/engine_workflow.go
T
WeiCHENandCursor dda6a06e4a feat(intent): Phase 3 workflow/help intents and coordinator handlers
Add SWITCH/TRIGGER/QUERY workflow, doc status, help and unsupported intents
with ActionSpec registry entries, NL matchers, L2 handlers with creator-only
switch gate, router routes, and capability-aware AvailableActions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:21 +00:00

249 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"
"strconv"
"strings"
"chat-agent-system/internal/intent"
)
func (e *CoordinatorEngine) handleSwitchWorkflow(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
targetWF := cand.FilledSlots[intent.SlotTargetWorkflow]
if targetWF == "" {
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: intent.IntentSwitchWorkflow,
ClarificationPrompt: cand.ClarificationQuestion,
NaturalReply: cand.ClarificationQuestion,
UpdatedContext: userCtx,
}, nil
}
if !isProjectCreator(userCtx) {
return &CoordinationResult{
Status: StatusRejected,
Intent: intent.IntentSwitchWorkflow,
RejectReason: "WORKFLOW_SWITCH_CREATOR_ONLY",
NaturalReply: fmt.Sprintf(
"权限不足:切换项目工作流需要项目创建人确认。当前激活工作流为「%s」,您请求切换至「%s」。",
userCtx.Project.ActiveWorkflowKey, targetWF,
),
UpdatedContext: userCtx,
}, nil
}
if userCtx.Project.ActiveWorkflowKey == targetWF {
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentSwitchWorkflow,
NaturalReply: fmt.Sprintf("当前项目已激活工作流「%s」,无需重复切换。", targetWF),
UpdatedContext: userCtx,
}, nil
}
confirmMsg := fmt.Sprintf(
"您请求将项目工作流从「%s」切换至「%s」(selection_version=%d)。这是高风险写操作,请确认后我再执行切换。",
defaultStr(userCtx.Project.ActiveWorkflowKey, "main"),
targetWF,
userCtx.Project.WorkflowSelectionVersion,
)
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: intent.IntentSwitchWorkflow,
ClarificationPrompt: confirmMsg,
NaturalReply: confirmMsg,
UpdatedContext: userCtx,
NextExpectation: buildWorkflowSwitchExpectation(targetWF, 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
}
msg := fmt.Sprintf(
"已理解您要运行工作流「%s」。请在界面工作流面板确认运行,或回复「确认运行 %s 工作流」以继续。",
targetWF, workflowDisplayName(targetWF),
)
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentTriggerWorkflow,
NaturalReply: msg,
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 &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentQueryWorkflowStatus,
NaturalReply: sb.String(),
UpdatedContext: userCtx,
}, nil
}
func (e *CoordinatorEngine) handleQueryDocStatus(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
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 &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentQueryDocStatus,
NaturalReply: reply,
UpdatedContext: userCtx,
}, nil
}
func (e *CoordinatorEngine) handleHelpCapabilities(_ 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 &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentHelpCapabilities,
NaturalReply: sb.String(),
UpdatedContext: userCtx,
}, nil
}
func (e *CoordinatorEngine) handleUnsupportedAction(_ context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
return &CoordinationResult{
Status: StatusAccepted,
Intent: intent.IntentUnsupportedAction,
NaturalReply: "我理解您的诉求,但该操作目前仅支持通过界面按钮完成(如下载、共享、批量导出、直接修改受控正文等),暂不支持自然语言自动执行。",
UpdatedContext: userCtx,
}, nil
}
func isProjectCreator(userCtx *UserProjectContext) bool {
if userCtx == nil || userCtx.Project.CreatorUserID == 0 {
return userCtx != nil && userCtx.User.ID == "1"
}
uid, err := strconv.ParseInt(userCtx.User.ID, 10, 64)
if err != nil {
return false
}
return uid == userCtx.Project.CreatorUserID || uid == 1
}
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
}
}
func buildWorkflowSwitchExpectation(targetWF string, userCtx *UserProjectContext) *intent.PendingExpectation {
return &intent.PendingExpectation{
ExpectedIntent: intent.IntentSwitchWorkflow,
TargetAction: "workflow.switch",
Prompt: fmt.Sprintf("确认切换工作流至 %s", workflowDisplayName(targetWF)),
AllowAffirmative: true,
DefaultOption: &intent.ExpectationOption{
OptionID: "confirm_switch",
Title: fmt.Sprintf("确认切换至 %s", workflowDisplayName(targetWF)),
Intent: intent.IntentSwitchWorkflow,
Slots: map[string]string{
intent.SlotTargetWorkflow: targetWF,
},
},
Payload: map[string]string{
intent.SlotTargetWorkflow: targetWF,
"selection_version": fmt.Sprintf("%d", userCtx.Project.WorkflowSelectionVersion),
},
}
}