package intent import ( "fmt" "strings" ) var workflowAliasMap = map[string]string{ "产品经理": "product_manager", "智能产品经理": "product_manager", "产品工作流": "product_manager", "产品经理工作流": "product_manager", "urs": "product_manager", "需求规格": "product_manager", "测试经理": "test_manager", "智能测试经理": "test_manager", "测试工作流": "test_manager", "测试经理工作流": "test_manager", "验证方案": "test_manager", "主工作流": "main", "默认工作流": "main", "main": "main", } // extractWorkflowKey 从输入中解析工作流键 func extractWorkflowKey(input string) string { lower := strings.ToLower(input) for phrase, key := range workflowAliasMap { if strings.Contains(lower, strings.ToLower(phrase)) || strings.Contains(input, phrase) { return key } } return "" } func isWorkflowControlInput(input string) bool { if strings.Contains(input, "工作流") { return true } return extractWorkflowKey(input) != "" } func matchSwitchWorkflow(input string, snap *ContextSnapshot) *CandidateIntent { act := detectDialogueAct(input) if act == DialogueActAsk { return nil } if !isWorkflowControlInput(input) { return nil } switchMarkers := []string{"切换", "换成", "改用", "激活", "切到", "切换到", "切换至"} hasSwitch := false for _, m := range switchMarkers { if strings.Contains(input, m) { hasSwitch = true break } } if !hasSwitch { return nil } wfKey := extractWorkflowKey(input) slots := map[string]string{} if wfKey != "" { slots[SlotTargetWorkflow] = wfKey } reasoning := "用户请求切换项目激活工作流" if wfKey == "" { return &CandidateIntent{ Intent: IntentSwitchWorkflow, Confidence: 0.88, FilledSlots: slots, Reasoning: reasoning, ClarificationQuestion: "请说明要切换到哪个工作流:产品经理(product_manager)、测试经理(test_manager)还是主工作流(main)?", } } return &CandidateIntent{ Intent: IntentSwitchWorkflow, Confidence: 0.93, FilledSlots: slots, Reasoning: reasoning + " → " + wfKey, } } func matchTriggerWorkflow(input string, snap *ContextSnapshot) *CandidateIntent { if detectDialogueAct(input) == DialogueActAsk { return nil } if !isWorkflowControlInput(input) { return nil } runMarkers := []string{"运行", "启动", "执行", "跑一下", "跑一版", "开始跑"} hasRun := false for _, m := range runMarkers { if strings.Contains(input, m) { hasRun = true break } } wfKey := extractWorkflowKey(input) if !hasRun && wfKey == "" { return nil } if !hasRun && wfKey != "" { // 「生成 URS」类可能走 TRIGGER_TASK,不强行 workflow run if strings.Contains(input, "切换") { return nil } } slots := map[string]string{} if wfKey != "" { slots[SlotTargetWorkflow] = wfKey } else if snap != nil && snap.AvailableActions != nil { // 默认当前上下文工作流由 L2 解析 for _, a := range snap.AvailableActions { if strings.HasPrefix(a, "TRIGGER_WORKFLOW:") { slots[SlotTargetWorkflow] = strings.TrimPrefix(a, "TRIGGER_WORKFLOW:") break } } } if wfKey == "" && slots[SlotTargetWorkflow] == "" { return &CandidateIntent{ Intent: IntentTriggerWorkflow, Confidence: 0.86, FilledSlots: slots, Reasoning: "用户请求运行工作流但未指定目标", ClarificationQuestion: "请说明要运行哪个工作流:产品经理(product_manager)还是测试经理(test_manager)?", } } return &CandidateIntent{ Intent: IntentTriggerWorkflow, Confidence: 0.91, FilledSlots: slots, Reasoning: "用户请求运行/启动项目工作流", } } func matchQueryWorkflowStatus(input string, snap *ContextSnapshot) *CandidateIntent { if detectDialogueAct(input) != DialogueActAsk { return nil } markers := []string{ "当前工作流", "哪个工作流", "什么工作流", "激活的工作流", "激活的是哪个", "有哪些工作流", "可用工作流", "工作流列表", "工作流状态", } hasWF := strings.Contains(input, "工作流") matched := false for _, m := range markers { if strings.Contains(input, m) { matched = true break } } if !matched && !(hasWF && (strings.Contains(input, "吗") || strings.Contains(input, "?") || strings.Contains(input, "?"))) { return nil } return &CandidateIntent{ Intent: IntentQueryWorkflowStatus, Confidence: 0.92, FilledSlots: map[string]string{ SlotQueryTopic: "workflow_status", }, Reasoning: "用户查询当前或可用工作流状态", } } func matchQueryDocStatus(input string, snap *ContextSnapshot) *CandidateIntent { // “总结/概括/摘要”是正文只读操作,即使语气是命令式也不得落入写任务。 // 若同时出现明确写动作,则交由写任务意图继续裁决。 summaryMarkers := []string{"总结", "概括", "摘要", "提炼要点", "归纳"} writeMarkers := []string{"编写", "编制", "生成", "起草", "创建", "撰写", "重写", "更新", "修改", "完善", "补充", "刷新", "重做"} isSummary := false for _, marker := range summaryMarkers { if strings.Contains(input, marker) { isSummary = true break } } if isSummary { for _, marker := range writeMarkers { if strings.Contains(input, marker) { isSummary = false break } } } if isSummary { target := extractDocName(input) if target != "" || strings.Contains(input, "文档") || strings.Contains(input, "报告") { slots := map[string]string{SlotQueryTopic: "doc_summary"} if target != "" { slots[SlotTargetDoc] = target } else if snap != nil && snap.ActiveDocID != "" { slots[SlotTargetDoc] = snap.ActiveDocID } return &CandidateIntent{ Intent: IntentQueryDocStatus, Confidence: 0.97, FilledSlots: slots, Reasoning: "用户请求只读总结受控文档正文", } } } if detectDialogueAct(input) != DialogueActAsk { return nil } // 已由 DOCGOV 状态查询覆盖的认领/送审不重复 if strings.Contains(input, "认领") || strings.Contains(input, "送审") { return nil } markers := []string{ "文档状态", "审核状态", "什么状态", "当前状态", "版本状态", "血缘", "stale", "过期原因", "完成了吗", "生成完了吗", } matched := false for _, m := range markers { if strings.Contains(input, m) { matched = true break } } if !matched { return nil } slots := map[string]string{SlotQueryTopic: "doc_status"} if snap != nil && snap.ActiveDocID != "" { slots[SlotTargetDoc] = snap.ActiveDocID } return &CandidateIntent{ Intent: IntentQueryDocStatus, Confidence: 0.90, FilledSlots: slots, Reasoning: "用户查询受控文档或交付物状态/血缘(只读)", } } func matchHelpCapabilities(input string) *CandidateIntent { helpMarkers := []string{ "能做什么", "可以做什么", "有什么功能", "怎么用", "如何使用", "帮助", "使用说明", "能力清单", "支持哪些", "你会什么", } for _, m := range helpMarkers { if strings.Contains(input, m) { return &CandidateIntent{ Intent: IntentHelpCapabilities, Confidence: 0.94, FilledSlots: map[string]string{ SlotQueryTopic: "help", }, Reasoning: "用户询问系统当前可执行能力", } } } return nil } func matchUnsupportedActionCandidate(input string) *CandidateIntent { unsupportedMarkers := []string{ "下载回复", "下载下来", "复制回复", "保存为私密文档", "共享到项目", "取消共享", "删除私密文档", "批量导出", "修改正文", "直接改文档正文", } for _, m := range unsupportedMarkers { if strings.Contains(input, m) { return &CandidateIntent{ Intent: IntentUnsupportedAction, Confidence: 0.93, FilledSlots: map[string]string{ SlotQueryTopic: "unsupported", }, Reasoning: "用户请求的能力当前仅支持显式 UI 操作,不支持自然语言自动执行", } } } return nil } // applyCapabilityHint 若上下文宣告了 AvailableActions,为已识别动作附加证据(不篡改语义) func applyCapabilityHint(top *CandidateIntent, snap *ContextSnapshot) { if top == nil || snap == nil || len(snap.AvailableActions) == 0 { return } spec, ok := DefaultRegistry().GetByLegacyIntent(top.Intent) if !ok || spec.CapabilityKey == "" { return } capKey := spec.CapabilityKey if top.Intent == IntentTriggerWorkflow { if wf := top.FilledSlots[SlotTargetWorkflow]; wf != "" { capKey = "TRIGGER_WORKFLOW:" + wf } } for _, a := range snap.AvailableActions { if a == capKey || strings.HasPrefix(a, capKey) { return } } top.Reasoning += fmt.Sprintf(";注意:当前上下文 AvailableActions 未包含 %s(L2 将做权限/能力裁决)", capKey) }