package intent import ( "fmt" "regexp" "sort" "strings" "unicode/utf8" "chat-agent-system/internal/maturity" "chat-agent-system/internal/squad" ) // isProgressInquiry 为真时,带任务名词的疑问句应让行 QUERY_PROGRESS,而不是 TRIGGER_TASK。 func isProgressInquiry(input string) bool { if !isInterrogative(input) { return false } markers := []string{"进度", "进展", "怎么样了", "状态如何", "写完", "完成了吗", "完成没有", "好了吗", "好了没有"} hit := false for _, marker := range markers { if strings.Contains(input, marker) { hit = true break } } if !hit { return false } for _, verb := range []string{"开始", "重新", "再来", "重跑", "再生成", "重新生成"} { if strings.Contains(input, verb) { return false } } return true } // isInterrogative 判断输入是否疑问句:句尾问号/「吗」「呢」,或句中出现 // 「怎么/如何/什么/多少/哪/是否/大概/多久/为什么」等疑问词。 // 用于把「查询/咨询」类输入从祈使句「指令」类意图(阶段推进等)中排除。 func isInterrogative(input string) bool { trimmed := strings.TrimSpace(input) if trimmed == "" { return false } lastRune, _ := utf8.DecodeLastRuneInString(trimmed) if lastRune == '?' || lastRune == '?' || lastRune == '吗' || lastRune == '呢' { return true } questionWords := []string{"怎么", "如何", "什么", "多少", "哪些", "哪一", "是否", "大概", "多久", "为什么", "是不是", "有没有", "何时", "什么时候", "什么样的"} for _, w := range questionWords { if strings.Contains(input, w) { return true } } return false } func matchAdvanceStage(input string, currentStage maturity.MaturityStage) *CandidateIntent { // 疑问句不构成推进指令:「…做过类似的…吗?报价大概怎么定的?」属于案例/知识查询, // 曾被 "做.*报价" 过宽匹配误判为 ADVANCE_STAGE(见 2026-09-25 意图误判修复)。 // 疑问标记:句尾问号/语气词「吗」「呢」,或「怎么/什么/多少/哪/是否/大概」等疑问词。 if isInterrogative(input) { return nil } patterns := []string{ "进入下一阶段", "推进到下一阶段", "推进阶段", "进入方案", "进入论证", "方案论证", "可行性论证", "开始方案", "进入报价", "开始报价", "商业锁定", "进入v0.1", "推进到v0.1", "进入v0.5", "推进到v0.5", "进入v1.0", "推进到v1.0", "可以进入", "没有问题.*进入", "没问题.*推进", "通过.*下一阶段", "开始推进", "做.*摸底", "开始.*摸底", "先做.*摸底", "进行.*摸底", "做v0.1", "先做v0.1", "做.*论证", "做.*报价", "开始论证", } for _, p := range patterns { if matched, _ := regexp.MatchString("(?i)"+p, input); matched { var target maturity.MaturityStage if strings.Contains(input, "报价") || strings.Contains(input, "v1.0") || strings.Contains(input, "V1.0") || strings.Contains(input, "商业锁定") { target = maturity.StageV10_Quoting } else if strings.Contains(input, "方案") || strings.Contains(input, "论证") || strings.Contains(input, "可行性") || strings.Contains(input, "v0.5") || strings.Contains(input, "V0.5") { target = maturity.StageV05_Feasibility } else if strings.Contains(input, "摸底") || strings.Contains(input, "初筛") || strings.Contains(input, "v0.1") || strings.Contains(input, "V0.1") { target = maturity.StageV01_Triage } else { if currentStage == maturity.StageV01_Triage { target = maturity.StageV05_Feasibility } else if currentStage == maturity.StageV05_Feasibility { target = maturity.StageV10_Quoting } else { target = maturity.StageV10_Quoting } } reason := fmt.Sprintf("匹配推进指令 '%s',目标成熟度阶段为 %s", p, target) return &CandidateIntent{ Intent: IntentAdvanceStage, Confidence: 0.92, FilledSlots: map[string]string{ SlotTargetStage: string(target), }, Reasoning: reason, } } } return nil } func matchSupplementInfo(input string) *CandidateIntent { patterns := []string{ "补充", "更新", "客户说", "客户反馈", "客户确定", "修改为", "改为", "增加", "目标市场", "市场是", "参数是", "用途是", "设备是", "预算是", "时间是", "资料", "附件", } matchedAny := false for _, p := range patterns { if strings.Contains(input, p) { matchedAny = true break } } if !matchedAny { return nil } slots := extractFactSlots(input) if len(slots) == 0 { slots = make(map[string]string) } slots[SlotSupplementDoc] = input slots[SlotSupplementAction] = inferSupplementSubtype(input, slots) return &CandidateIntent{ Intent: IntentSupplementInfo, Confidence: 0.88, FilledSlots: slots, Reasoning: "用户输入中包含参数变更或新材料事实补充", } } func matchQueryProgress(input string) *CandidateIntent { if matchTriggerTask(input, nil) != nil { return nil } if isWorkflowControlInput(input) || isRunControlInput(input) { return nil } patterns := []string{ "测试方案", "验证方案", "测试", "验证", "型检", "型式检验", "难点", "特别注意", "注意事项", "比较难", "进展", "进度", "走到哪", "到哪一步", "状态", "情况", "评估到哪", "风险", "坑", "注意什么", "话术", "怎么说", "怎么跟客户说", "微信", "草稿", "探头功耗", "功耗", "供电", "怎么设计", "设计方案", "临床要不要", "怎么做", "查一下", "看下", "成熟度", "汇报一下", "项目怎么样", } for _, p := range patterns { if strings.Contains(input, p) { topic := "all" if strings.Contains(input, "测试") || strings.Contains(input, "验证") || strings.Contains(input, "型检") || strings.Contains(input, "难点") || strings.Contains(input, "特别注意") || strings.Contains(input, "注意事项") || strings.Contains(input, "比较难") { topic = "testing_analysis" } else if strings.Contains(input, "风险") || strings.Contains(input, "坑") { topic = "risk" } else if strings.Contains(input, "话术") || strings.Contains(input, "微信") || strings.Contains(input, "怎么说") { topic = "talk_script" } else if strings.Contains(input, "进展") || strings.Contains(input, "进度") || strings.Contains(input, "步") { topic = "progress" } else if strings.Contains(input, "功耗") || strings.Contains(input, "供电") || strings.Contains(input, "设计") { topic = "technical_design" } confidence := 0.90 if topic == "testing_analysis" { confidence = 0.95 } return &CandidateIntent{ Intent: IntentQueryProgress, Confidence: confidence, FilledSlots: map[string]string{ SlotQueryTopic: topic, }, Reasoning: fmt.Sprintf("用户在询问项目状态、技术测试方案或对外沟通话术 (主题: %s)", topic), } } } return nil } func hasTaskWriteVerb(input string) bool { for _, verb := range []string{"生成", "做", "跑", "出", "编写", "编制", "起草", "制定", "拟定"} { if strings.Contains(input, verb) { return true } } // 「定」「拟」只在祈使前缀后算写动词。「帮我决定」里的「定」不算。 return hasBareWriteImperative(input, "定") || hasBareWriteImperative(input, "拟") } func hasBareWriteImperative(input, verb string) bool { for _, marker := range []string{"帮我" + verb, "请" + verb, "给我" + verb, "帮忙" + verb} { idx := strings.Index(input, marker) if idx < 0 { continue } rest := input[idx+len(marker):] if verb == "定" && strings.HasPrefix(rest, "决") { continue } return true } return false } // hasWeakTaskWriteVerb 弱写动词:倾向整理/梳理,默认需确认后再启动正式任务。 func hasWeakTaskWriteVerb(input string) bool { for _, verb := range []string{"整理", "梳理", "汇总", "归纳", "收集"} { if strings.Contains(input, verb) { return true } } return false } func isRequirementsTopic(input string) bool { if strings.Contains(input, "需求") || strings.Contains(input, "摸底") { return true } return strings.Contains(input, "用户") && (hasTaskWriteVerb(input) || hasWeakTaskWriteVerb(input)) } // hasURSStrongMarker URS / 用户需求规格书级强标记(非「初始/摸底」时才算)。 func hasURSStrongMarker(input string) bool { if hasIntakeSemanticMarker(input) { return false } return strings.Contains(input, "URS") || strings.Contains(input, "用户需求规格") || strings.Contains(input, "用户需求表") } // stageAwareRequirementsTask 按当前阶段消解需求类任务键,与前端 resolveRequirementsTaskKey 对齐: // V0.1 / 空阶段且无 URS 强标记 → intake;含 URS 强标记 → user_requirements。 func stageAwareRequirementsTask(input string, userCtx *ContextSnapshot) string { if hasIntakeSemanticMarker(input) { return "intake" } stage := "" if userCtx != nil { stage = userCtx.CurrentStage } if hasURSStrongMarker(input) { return "user_requirements" } // 含糊「需求」在初期默认摸底表,避免误触 V0.5 门禁失败。 if stage == "" || stage == string(maturity.StageV01_Triage) { return "intake" } if strings.Contains(input, "用户需求") || strings.Contains(input, "需求表") { return "user_requirements" } return "intake" } func requirementsTriggerPhrase(taskKey string) string { switch taskKey { case "user_requirements": return "用户需求表" default: return "用户初始需求收集表" } } func matchTriggerTask(input string, userCtx *ContextSnapshot) *CandidateIntent { // 疑问句里的「编写进度怎么样了」是在问状态,不是再启动一份交付物。 // 显式启动词(开始/重新/重跑)仍走任务触发。 if isProgressInquiry(input) { return nil } type phrase struct { text string key string } taskMap := map[string]string{ "需求收集表": "intake", "需求表": "intake", "调研报告": "research", "调研": "research", "法规评估": "feasibility_reg", "注册评估": "feasibility_reg", "初步设计": "prelim_design", "设计方案": "prelim_design", "报价单": "cdo_quote", "报价": "cdo_quote", } phrases := make([]phrase, 0, len(taskMap)+60) for kw, tKey := range taskMap { phrases = append(phrases, phrase{text: kw, key: tKey}) } for _, item := range squad.RDevTriggerPhrases() { phrases = append(phrases, phrase{text: item.Text, key: item.TaskKey}) } sort.Slice(phrases, func(i, j int) bool { li := utf8.RuneCountInString(phrases[i].text) lj := utf8.RuneCountInString(phrases[j].text) if li != lj { return li > lj } return phrases[i].text < phrases[j].text }) strong := hasTaskWriteVerb(input) weak := hasWeakTaskWriteVerb(input) if !strong && !weak { return nil } // 需求类话题优先阶段感知消解,避免研发短别名「用户需求」截走商务流初期表述。 if isRequirementsTopic(input) { taskKey := stageAwareRequirementsTask(input, userCtx) phrase := requirementsTriggerPhrase(taskKey) if cand := scopedTriggerTaskCandidate(input, phrase, taskKey, userCtx); cand != nil { return cand } cand := triggerTaskCandidate(phrase, taskKey) if !strong && weak { cand.Confidence = 0.78 cand.FilledSlots[SlotNeedsConfirmation] = "true" cand.Reasoning = fmt.Sprintf("弱写动词触发需求整理,阶段感知消解为 %s,需用户确认后执行", taskKey) } return cand } if !strong { // 弱写动词仅覆盖需求类宾语;其他话题仍走对话兜底。 return nil } if workflowScopeUsesWeights() { flat := make([]struct{ text, key string }, 0, len(phrases)) for _, item := range phrases { flat = append(flat, struct{ text, key string }{text: item.text, key: item.key}) } return weightedTriggerTask(input, flat, userCtx) } for _, item := range phrases { if strings.Contains(input, item.text) { if cand := scopedTriggerTaskCandidate(input, item.text, item.key, userCtx); cand != nil { return cand } return triggerTaskCandidate(item.text, item.key) } } return nil } // scopedTriggerTaskCandidate 若目标任务键不属于当前激活工作流,返回跨流拦截候选; // 无激活工作流(存量单流 / planning 态)或归属未知时返回 nil,维持原路由。 func scopedTriggerTaskCandidate(input, phrase, taskKey string, userCtx *ContextSnapshot) *CandidateIntent { active := "" if userCtx != nil { active = userCtx.ActiveWorkflowKey } owners, ok := docScopeForPhrase(phrase) if !ok { if owner := taskKeyWorkflowOwner(taskKey); owner != "" { owners = []string{owner} ok = true } } if !ok || active == "" || containsWorkflow(owners, active) { return nil } return workflowScopeCandidate(phrase, owners, active) } func workflowScopeCandidate(phrase string, owners []string, activeWorkflow string) *CandidateIntent { slots := make(map[string]string) markWorkflowScopeMismatch(slots, phrase, owners) return &CandidateIntent{ Intent: IntentTriggerTask, Confidence: 0.86, Branch: BranchNewIntentPreempt, FilledSlots: slots, Reasoning: fmt.Sprintf("用户要求生成交付任务 %s,但该文档属于非激活工作流,前置剪枝", phrase), ClarificationQuestion: workflowScopeGuidanceQuestion(phrase, owners, activeWorkflow), } } func containsWorkflow(workflows []string, key string) bool { for _, wf := range workflows { if wf == key { return true } } return false } func hasIntakeSemanticMarker(input string) bool { return strings.Contains(input, "首轮") || strings.Contains(input, "初始") || strings.Contains(input, "需求摸底") || strings.Contains(input, "摸底") } func triggerTaskCandidate(phrase, taskKey string) *CandidateIntent { return &CandidateIntent{ Intent: IntentTriggerTask, Confidence: 0.86, FilledSlots: map[string]string{ SlotTargetTask: taskKey, }, Reasoning: fmt.Sprintf("用户要求显式生成交付任务: %s (%s)", phrase, taskKey), } } func matchImpactIntent(input string) *CandidateIntent { propose := strings.Contains(input, "修订提案") || strings.Contains(input, "生成修订") || strings.Contains(input, "提议修改文档") analyze := strings.Contains(input, "分析影响") || strings.Contains(input, "影响分析") || strings.Contains(input, "波及哪些") || strings.Contains(input, "影响哪些") || strings.Contains(input, "改动影响") || strings.Contains(input, "变更影响") if !propose && !analyze { return nil } intent := IntentImpactAnalyze if propose { intent = IntentRevisePropose } return &CandidateIntent{ Intent: intent, Confidence: 0.9, FilledSlots: map[string]string{}, } }