package intent import ( "fmt" "regexp" "strings" ) // 风险等级。透传给宿主做授权与确认判断,本模块不解释。 const ( RiskLow = "low" RiskMedium = "medium" RiskHigh = "high" ) // ConfidenceSpec 各字段为零值时使用包默认值。 const ( DefaultBase = 0.7 DefaultPatternBonus = 0.2 DefaultThreshold = 0.6 // DefaultSemanticThreshold 语义重排默认过阈值。 DefaultSemanticThreshold = 0.8 ) // MatchSpec 声明式匹配规则。 // 计分:score = base + (任一 pattern 命中 ? pattern_bonus : 0),上限 1.0。 // 命中条件:excludes_any 全不命中 && includes_all 全命中 &&(includes_any 至少一词命中 // 或任一 pattern 命中;仅配置 includes_all 时以全命中为准)。 // case_sensitive 只作用于 includes/excludes 词表;pattern 自行用 (?i) 表达大小写。 // boundary 为真时 includes/excludes 词表走词界匹配(命中两侧不得与词条边缘 // 同类:汉字对汉字、词字符对词字符),用于压住中文子串误命中(I3);默认 false 保持旧行为。 type MatchSpec struct { IncludesAny []string `json:"includes_any,omitempty"` IncludesAll []string `json:"includes_all,omitempty"` ExcludesAny []string `json:"excludes_any,omitempty"` Patterns []string `json:"patterns,omitempty"` CaseSensitive bool `json:"case_sensitive,omitempty"` Boundary bool `json:"boundary,omitempty"` } // SlotSpec 槽位抽取规则。group 未填默认取 1(第一个捕获组)。 // label/question 供宿主做 HITL 提问与目录展示;enum/format/normalize/validator // 构成确定性校验链:抽取 → 归一化 → 校验,校验失败进 Candidate.InvalidSlots。 type SlotSpec struct { Key string `json:"key"` Required bool `json:"required,omitempty"` Pattern string `json:"pattern"` Group int `json:"group,omitempty"` // Label 槽位的人类可读名(目录/HITL 展示用)。 Label string `json:"label,omitempty"` // Question 建议的澄清提问(HITL 生成用),如"要查询哪个运行?请提供 run 编号"。 Question string `json:"question,omitempty"` // Enum 合法值枚举(归一化后校验)。 Enum []string `json:"enum,omitempty"` // Format 整段匹配正则(校验,区别于 Pattern 的抽取用途)。 Format string `json:"format,omitempty"` // Normalize 归一化步骤,按序应用:trim | lower | upper。 Normalize []string `json:"normalize,omitempty"` // Validator 槽位校验器名。优先解析 Options.SlotValidators,查不到再回落 // 已弃用的全局表(RegisterSlotValidator)。 Validator string `json:"validator,omitempty"` } // ConfidenceSpec 置信度参数。Base 初始分;PatternBonus 模式命中加成; // Threshold 为该 spec 自身的过阈线,score ≥ threshold 才进入候选。 type ConfidenceSpec struct { Base float64 `json:"base,omitempty"` PatternBonus float64 `json:"pattern_bonus,omitempty"` Threshold float64 `json:"threshold,omitempty"` } // IntentSpec 意图定义,Go 注册与 JSON overlay 共用同一结构。 // Handler 与 Metadata 由宿主解释(路由通道、动作键等),本模块只透传。 type IntentSpec struct { ID string `json:"id"` Domain string `json:"domain"` Version int `json:"version"` Description string `json:"description,omitempty"` Priority int `json:"priority,omitempty"` Disabled bool `json:"disabled,omitempty"` Matcher MatchSpec `json:"matcher"` Slots []SlotSpec `json:"slots,omitempty"` Confidence ConfidenceSpec `json:"confidence,omitempty"` Risk string `json:"risk,omitempty"` RequiresConfirm bool `json:"requires_confirm,omitempty"` Handler string `json:"handler,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` // Action 为显式动作声明(I1 WP2 四键统一的首选来源): // 声明后优先于 Handler 与 Metadata["blueprint"] 被 ActionRef 采用。 // kind 必须是 command/workflow/blueprint/handler 之一。 Action *ActionRef `json:"action,omitempty"` // Examples 示例句(I2 语义端口):歧义时语义打分器按它们路由 // (semantic-router 借鉴;向量模型由宿主注入,库不做 I/O)。 Examples []string `json:"examples,omitempty"` } var ( domainRe = regexp.MustCompile(`^[a-z][a-z0-9-]{1,31}$`) segmentRe = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`) slotKeyRe = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`) ) // limits 在 Options 中声明,防止配置膨胀拖垮识别与构建。 type limits struct { maxSpecs int maxPatternsPerSpec int maxSlotsPerSpec int } // validateSpec 校验单条 spec 的结构合法性,错误可定位到字段。limits 为零值时跳过数量上限。 // scoped 为 Options.SlotValidators;查不到名字时回落已弃用的全局表。 func validateSpec(spec IntentSpec, lim limits, scoped map[string]SlotValidator) error { if spec.Domain == "" { return newError(InvalidSpec, "domain 为空", "field", "domain") } if !domainRe.MatchString(spec.Domain) { return newError(InvalidSpec, "domain 格式不合法(^[a-z][a-z0-9-]{1,31}$)", "field", "domain", "domain", spec.Domain) } if spec.ID == "" { return newError(InvalidSpec, "id 为空", "field", "id") } // id 形如 .,mnemonic 可继续分级(demo.run.status_query)。 segments := strings.Split(spec.ID, ".") if len(segments) < 2 { return newError(InvalidSpec, "id 至少包含 domain 与记忆名两段", "field", "id", "id", spec.ID) } if segments[0] != spec.Domain { return newError(InvalidSpec, "id 首段必须等于 domain", "field", "id", "id", spec.ID, "domain", spec.Domain) } for _, seg := range segments[1:] { if !segmentRe.MatchString(seg) { return newError(InvalidSpec, "id 段格式不合法(^[a-z][a-z0-9_-]{0,63}$)", "field", "id", "id", spec.ID) } } if spec.Version < 1 { return newError(InvalidSpec, "version 必须 ≥ 1", "field", "version", "version", spec.Version) } switch spec.Risk { case "", RiskLow, RiskMedium, RiskHigh: default: return newError(InvalidSpec, "risk 只能是 low/medium/high 或留空", "field", "risk", "risk", spec.Risk) } if spec.Action != nil { switch spec.Action.Kind { case ActionCommand, ActionWorkflow, ActionBlueprint, ActionHandler: default: return newError(InvalidSpec, "action.kind 必须是 command/workflow/blueprint/handler", "field", "action.kind", "action.kind", spec.Action.Kind) } if strings.TrimSpace(spec.Action.ID) == "" { return newError(InvalidSpec, "action.id 为空", "field", "action.id", "id", spec.ID) } } m := spec.Matcher for _, terms := range []struct { name string value []string }{{"includes_any", m.IncludesAny}, {"includes_all", m.IncludesAll}, {"excludes_any", m.ExcludesAny}} { for _, t := range terms.value { if strings.TrimSpace(t) == "" { return newError(InvalidSpec, "matcher 词条为空", "field", "matcher."+terms.name) } } } if len(m.IncludesAny) == 0 && len(m.IncludesAll) == 0 && len(m.Patterns) == 0 { return newError(InvalidSpec, "matcher 缺少正向条件(includes_any / includes_all / patterns 至少一项)", "field", "matcher", "id", spec.ID) } if lim.maxPatternsPerSpec > 0 && len(m.Patterns) > lim.maxPatternsPerSpec { return newError(InvalidSpec, "patterns 数量超过上限", "field", "matcher.patterns", "count", len(m.Patterns), "max", lim.maxPatternsPerSpec, "id", spec.ID) } for i, p := range m.Patterns { if _, err := regexp.Compile(p); err != nil { return newError(InvalidSpec, "pattern 编译失败", "field", fmt.Sprintf("matcher.patterns[%d]", i), "pattern", p, "id", spec.ID, "error", err.Error()) } } if lim.maxSlotsPerSpec > 0 && len(spec.Slots) > lim.maxSlotsPerSpec { return newError(InvalidSpec, "slots 数量超过上限", "field", "slots", "count", len(spec.Slots), "max", lim.maxSlotsPerSpec, "id", spec.ID) } for i, s := range spec.Slots { if !slotKeyRe.MatchString(s.Key) { return newError(InvalidSpec, "slot key 格式不合法(^[a-z][a-z0-9_]{0,63}$)", "field", fmt.Sprintf("slots[%d].key", i), "key", s.Key) } re, err := regexp.Compile(s.Pattern) if err != nil { return newError(InvalidSpec, "slot pattern 编译失败", "field", fmt.Sprintf("slots[%d].pattern", i), "key", s.Key, "error", err.Error()) } // group 约定:未填默认取 1(第一个捕获组);需整段匹配时在 pattern 里写捕获组即可。 g := effectiveGroup(s.Group) if g < 1 || g > re.NumSubexp() { return newError(InvalidSpec, "slot group 超出捕获组范围", "field", fmt.Sprintf("slots[%d].group", i), "key", s.Key, "group", s.Group, "groups", re.NumSubexp()) } for _, e := range s.Enum { if strings.TrimSpace(e) == "" { return newError(InvalidSpec, "slot enum 含空值", "field", fmt.Sprintf("slots[%d].enum", i), "key", s.Key) } } if s.Format != "" { if _, err := regexp.Compile(s.Format); err != nil { return newError(InvalidSpec, "slot format 编译失败", "field", fmt.Sprintf("slots[%d].format", i), "key", s.Key, "error", err.Error()) } } for _, op := range s.Normalize { if _, ok := builtinNormalizers[op]; !ok { return newError(InvalidSpec, "slot normalize 步骤不合法(trim|lower|upper)", "field", fmt.Sprintf("slots[%d].normalize", i), "key", s.Key, "op", op) } } if s.Validator != "" { if _, ok := resolveSlotValidator(scoped, s.Validator); !ok { return newError(InvalidSpec, "slot validator 未注册", "field", fmt.Sprintf("slots[%d].validator", i), "key", s.Key, "validator", s.Validator, "hint", "在 Options.SlotValidators 登记,或调用已弃用的 intent.RegisterSlotValidator") } } } c := spec.Confidence if c.Base < 0 || c.Base > 1 { return newError(InvalidSpec, "confidence.base 须在 (0,1],0/留空取默认", "field", "confidence.base", "id", spec.ID) } if c.PatternBonus < 0 || c.PatternBonus >= 1 { return newError(InvalidSpec, "confidence.pattern_bonus 须在 [0,1),留空取默认", "field", "confidence.pattern_bonus", "id", spec.ID) } if c.Threshold < 0 || c.Threshold > 1 { return newError(InvalidSpec, "confidence.threshold 须在 (0,1],0/留空取默认", "field", "confidence.threshold", "id", spec.ID) } for k := range spec.Metadata { if strings.TrimSpace(k) == "" { return newError(InvalidSpec, "metadata 键为空", "field", "metadata", "id", spec.ID) } } return nil } // effectiveGroup 槽位 group 的缺省规则:未填(0)默认取 1。 func effectiveGroup(g int) int { if g == 0 { return 1 } return g }