Recognize “刚刚上传” as @latest_upload and summarize the project’s newest CAS attachment (with clear parse pending/failed/no-asset replies) instead of searching only controlled workflow docs.
342 lines
9.9 KiB
Go
342 lines
9.9 KiB
Go
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 || hasDocViewVerb(input) {
|
||
if cand := readDocScopeCandidate(input, snap); cand != nil {
|
||
return cand
|
||
}
|
||
}
|
||
if isSummary {
|
||
if isLatestUploadReference(input) {
|
||
return &CandidateIntent{
|
||
Intent: IntentQueryDocStatus,
|
||
Confidence: 0.97,
|
||
FilledSlots: map[string]string{
|
||
SlotQueryTopic: "doc_summary",
|
||
SlotTargetDoc: TargetDocLatestUpload,
|
||
},
|
||
Reasoning: "用户请求只读总结刚刚上传的附件正文",
|
||
}
|
||
}
|
||
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: "用户查询受控文档或交付物状态/血缘(只读)",
|
||
}
|
||
}
|
||
|
||
// isLatestUploadReference 识别「刚刚/最近上传」类时间指代(只读总结目标)。
|
||
func isLatestUploadReference(input string) bool {
|
||
input = strings.TrimSpace(input)
|
||
if input == "" {
|
||
return false
|
||
}
|
||
hasUploadNoun := strings.Contains(input, "文档") ||
|
||
strings.Contains(input, "文件") ||
|
||
strings.Contains(input, "附件") ||
|
||
strings.Contains(input, "材料")
|
||
if !hasUploadNoun {
|
||
return false
|
||
}
|
||
markers := []string{
|
||
"刚刚上传", "刚上传", "最近上传", "刚才上传",
|
||
"我上传的", "上传的这个", "上传的那个", "上传的文档", "上传的文件", "上传的附件",
|
||
}
|
||
for _, m := range markers {
|
||
if strings.Contains(input, m) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
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)
|
||
}
|