package intent import ( "context" "fmt" "io/fs" "math" "regexp" "sort" "strings" "sync" "sync/atomic" "logging-system/logsys" ) // Options 注册表与识别器的共享选项。零值字段在构建时填入默认值; // GuardDomains / ProtectedDomains 决定快照的阶段归属与 overlay 保护范围。 type Options struct { // ProtectedDomains 保护域:overlay 覆盖或禁用这些域内的 spec 会整批失败(fail-closed)。 ProtectedDomains []string // GuardDomains 守卫域:这些域内的 spec 在 Stage 0 演练,命中即短路返回 GuardHit。 GuardDomains []string // TopN 结果候选数上限(默认 5)。 TopN int // AmbiguityMargin 前两名分差小于该值判 Ambiguous(默认 0.1)。 AmbiguityMargin float64 // MaxInputLen 输入字节长度上限(默认 4096),超过返回 InvalidArgument。 MaxInputLen int // MaxSpecs 快照内 spec 总数上限,含禁用(默认 2048)。 MaxSpecs int // MaxPatternsPerSpec 单 spec 正则数量上限(默认 32)。 MaxPatternsPerSpec int // MaxSlotsPerSpec 单 spec 槽位数量上限(默认 16)。 MaxSlotsPerSpec int // Log 可选日志器;nil 时使用 Nop。识别日志不记录原始输入。 Log *logsys.Logger // Semantic 语义相似度端口(I2):nil 时语义层不启用。 Semantic SemanticScorer // SemanticThreshold 语义重排的过阈值(默认 0.8)。 SemanticThreshold float64 // Generator 模型命令生成端口(I2):nil 时 fallback 不生成。 Generator CommandGenerator // SlotValidators 作用域槽位校验器。构建与校验时先查此表,查不到再回落 // 已弃用的全局表(RegisterSlotValidator)。不同 Registry 的同名校验器互不影响。 SlotValidators map[string]SlotValidator // NFKC 为真时,词条与输入先做兼容等价归一(全角 ASCII、组合字符),再按 // case_sensitive 决定是否小写。NFKC 不做繁简转换。 NFKC bool } // Options 字段默认值。 const ( DefaultTopN = 5 DefaultAmbiguityMargin = 0.1 DefaultMaxInputLen = 4096 DefaultMaxSpecs = 2048 DefaultMaxPatterns = 32 DefaultMaxSlots = 16 ) func (o Options) normalized() Options { n := o if n.TopN <= 0 { n.TopN = DefaultTopN } if n.AmbiguityMargin <= 0 { n.AmbiguityMargin = DefaultAmbiguityMargin } if n.SemanticThreshold <= 0 { n.SemanticThreshold = DefaultSemanticThreshold } if n.MaxInputLen <= 0 { n.MaxInputLen = DefaultMaxInputLen } if n.MaxSpecs <= 0 { n.MaxSpecs = DefaultMaxSpecs } if n.MaxPatternsPerSpec <= 0 { n.MaxPatternsPerSpec = DefaultMaxPatterns } if n.MaxSlotsPerSpec <= 0 { n.MaxSlotsPerSpec = DefaultMaxSlots } if n.Log == nil { n.Log = logsys.Nop() } if len(n.SlotValidators) > 0 { cp := make(map[string]SlotValidator, len(n.SlotValidators)) for k, v := range n.SlotValidators { cp[k] = v } n.SlotValidators = cp } return n } func (o Options) protectedSet() map[string]bool { return domainSet(o.ProtectedDomains) } func (o Options) guardSet() map[string]bool { return domainSet(o.GuardDomains) } func domainSet(domains []string) map[string]bool { m := make(map[string]bool, len(domains)) for _, d := range domains { m[d] = true } return m } func (o Options) limits() limits { return limits{maxSpecs: o.MaxSpecs, maxPatternsPerSpec: o.MaxPatternsPerSpec, maxSlotsPerSpec: o.MaxSlotsPerSpec} } // ---- 编译产物 ---- // 槽位的编译产物与抽取/校验逻辑见 slots.go。 type compiledSpec struct { spec IntentSpec termsAny []string // 已按 case_sensitive 折叠 termsAll []string termsEx []string patterns []*regexp.Regexp slots []compiledSlot guard bool index int // 合并序,确定性次序裁决用 base float64 bonus float64 threshold float64 } // matchOutcome 单个 spec 对一次输入的评估结果。 type matchOutcome struct { score float64 passed bool hitTerms []string hitPat []string reject string // 非 passed 时的排除原因 } // evaluate 按文档化的计分规则评估。input 为原文,folded 为小写折叠后文本。 func (cs *compiledSpec) evaluate(input, folded string) matchOutcome { hay := input if !cs.spec.Matcher.CaseSensitive { hay = folded } for _, t := range cs.termsEx { if cs.contains(hay, t) { return matchOutcome{reject: "excluded_term:" + t} } } for _, t := range cs.termsAll { if !cs.contains(hay, t) { return matchOutcome{reject: "missing_includes_all:" + t} } } var hitTerms, hitPat []string positive := false for _, t := range cs.termsAny { if cs.contains(hay, t) { positive = true hitTerms = append(hitTerms, t) } } for i, p := range cs.patterns { if p.MatchString(input) { positive = true hitPat = append(hitPat, cs.spec.Matcher.Patterns[i]) } } if !positive && (len(cs.termsAny) > 0 || len(cs.patterns) > 0) { return matchOutcome{reject: "no_positive_match"} } score := cs.base if len(hitPat) > 0 { score += cs.bonus } if score > 1 { score = 1 } // 固定精度舍入:避免浮点噪声进入结果与 margin 比较。 return matchOutcome{score: math.Round(score*10000) / 10000, passed: score >= cs.threshold, hitTerms: hitTerms, hitPat: hitPat} } // contains 按 spec 的 boundary 配置选择子串或词界匹配(I3)。 func (cs *compiledSpec) contains(hay, term string) bool { if cs.spec.Matcher.Boundary { return boundedContains(hay, term) } return strings.Contains(hay, term) } func compileSpec(spec IntentSpec, guard bool, index int, scoped map[string]SlotValidator, nfkc bool) (*compiledSpec, error) { m := spec.Matcher fold := func(terms []string) []string { out := make([]string, len(terms)) for i, t := range terms { out[i] = foldMatchText(t, nfkc, m.CaseSensitive) } return out } cs := &compiledSpec{ spec: spec, termsAny: fold(m.IncludesAny), termsAll: fold(m.IncludesAll), termsEx: fold(m.ExcludesAny), patterns: make([]*regexp.Regexp, len(m.Patterns)), guard: guard, index: index, base: spec.Confidence.Base, bonus: spec.Confidence.PatternBonus, threshold: spec.Confidence.Threshold, } if cs.base == 0 { cs.base = DefaultBase } if cs.bonus == 0 { cs.bonus = DefaultPatternBonus } if cs.threshold == 0 { cs.threshold = DefaultThreshold } for i, p := range m.Patterns { re, err := regexp.Compile(p) if err != nil { return nil, newError(InvalidSpec, "pattern 编译失败", "field", fmt.Sprintf("matcher.patterns[%d]", i), "id", spec.ID, "error", err.Error()) } cs.patterns[i] = re } for _, s := range spec.Slots { cs2, err := compileSlot(s, scoped) if err != nil { return nil, err.(*Error).with("id", spec.ID) } cs.slots = append(cs.slots, cs2) } return cs, nil } // ---- 快照 ---- // Snapshot 是合并产物的不可变快照:全部 matcher 已编译,识别全程无锁读。 // 同一快照对同样输入给出同样结果;revision 单调递增。 type Snapshot struct { rev int64 margin float64 topN int maxInput int specs []*compiledSpec // 仅启用 spec,按合并序 byID map[string]int guardIdx []int disabled []string // 已禁用 id(overlay disable 与 spec.disabled 两个入口) sources map[string]string total int // 含禁用 nfkc bool } // Revision 返回快照版本号,随每次成功 Reload 递增。 func (s *Snapshot) Revision() int64 { return s.rev } // SpecIDs 返回启用中的 spec id,按合并序(确定性次序裁决的注册序)。 func (s *Snapshot) SpecIDs() []string { ids := make([]string, len(s.specs)) for i, cs := range s.specs { ids[i] = cs.spec.ID } return ids } // DisabledIDs 返回被禁用的 spec id,字典序。 func (s *Snapshot) DisabledIDs() []string { out := make([]string, len(s.disabled)) copy(out, s.disabled) sort.Strings(out) return out } // Source 返回 spec 的来源描述("builtin" 或 "#intents[i]")。 func (s *Snapshot) Source(id string) (string, bool) { src, ok := s.sources[id] return src, ok } // SpecByID 返回指定 id 的启用 spec 定义副本。切片与 Metadata 深拷贝, // 调用方改返回值不会改到快照。不存在或已禁用时第二个返回值为 false。 func (s *Snapshot) SpecByID(id string) (IntentSpec, bool) { if idx, ok := s.byID[id]; ok { return cloneIntentSpec(s.specs[idx].spec), true } return IntentSpec{}, false } // Conflict 诊断信息:两个启用 spec 共享同一 includes_any 词条。 // 共享不一定是错误(计分与确定性次序会裁决),用于配置评审提示。 type Conflict struct { Kind string A string B string Term string } // Conflicts 报告非守卫 spec 之间共享的 includes_any 词条,确定性排序。 func (s *Snapshot) Conflicts() []Conflict { termOwners := make(map[string][]string) for _, cs := range s.specs { if cs.guard { continue } for _, t := range cs.spec.Matcher.IncludesAny { termOwners[t] = append(termOwners[t], cs.spec.ID) } } terms := make([]string, 0, len(termOwners)) for t, owners := range termOwners { if len(owners) > 1 { terms = append(terms, t) } } sort.Strings(terms) var out []Conflict for _, t := range terms { owners := append([]string(nil), termOwners[t]...) sort.Strings(owners) for i := 0; i < len(owners); i++ { for j := i + 1; j < len(owners); j++ { out = append(out, Conflict{Kind: "shared_term", A: owners[i], B: owners[j], Term: t}) } } } return out } // specHit 规则阶段的内部命中记录。 type specHit struct { cs *compiledSpec score float64 terms []string pats []string } func lessHit(a, b specHit) bool { if a.score != b.score { return a.score > b.score } if a.cs.spec.Priority != b.cs.spec.Priority { return a.cs.spec.Priority > b.cs.spec.Priority } return a.cs.index < b.cs.index } func sortHits(hits []specHit) { sort.SliceStable(hits, func(i, j int) bool { return lessHit(hits[i], hits[j]) }) } // evalGuard 评估守卫域 spec,返回过阈命中(已排序)。 func (s *Snapshot) evalGuard(input, folded string) []specHit { var hits []specHit for _, idx := range s.guardIdx { cs := s.specs[idx] if o := cs.evaluate(input, folded); o.passed { hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat}) } } sortHits(hits) return hits } // evalIDs 按给定 id 列表评估(挂起预期阶段用),返回过阈命中(已排序)。 func (s *Snapshot) evalIDs(ids []string, input, folded string) []specHit { var hits []specHit for _, id := range ids { idx, ok := s.byID[id] if !ok { continue } cs := s.specs[idx] if o := cs.evaluate(input, folded); o.passed { hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat}) } } sortHits(hits) return hits } // evalRules 对全部非守卫 spec 做线性扫描,返回过阈命中(已排序)。 // 规模上限是 Options.MaxSpecs(默认 DefaultMaxSpecs = 2048)。倒排索引留到 // 超过该上限仍不够用时再做;构建期已经拒绝超过上限的快照。 func (s *Snapshot) evalRules(input, folded string) []specHit { var hits []specHit for _, cs := range s.specs { if cs.guard { continue } if o := cs.evaluate(input, folded); o.passed { hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat}) } } sortHits(hits) return hits } func (h specHit) reason() string { var parts []string if len(h.terms) > 0 { parts = append(parts, "terms="+strings.Join(h.terms, ",")) } if len(h.pats) > 0 { parts = append(parts, "patterns="+strings.Join(h.pats, ",")) } return strings.Join(parts, " ") } func (h specHit) candidate(input string, withSlots bool) Candidate { cs := h.cs c := Candidate{ ID: cs.spec.ID, Domain: cs.spec.Domain, Version: cs.spec.Version, Confidence: h.score, Priority: cs.spec.Priority, Risk: cs.spec.Risk, RequiresConfirm: cs.spec.RequiresConfirm, Handler: cs.spec.Handler, Reason: h.reason(), } if c.Risk == "" { c.Risk = RiskLow } if cs.spec.Action != nil { ref := *cs.spec.Action c.Action = &ref } if len(cs.spec.Metadata) > 0 { c.Metadata = make(map[string]string, len(cs.spec.Metadata)) for k, v := range cs.spec.Metadata { c.Metadata[k] = v } } if withSlots { c.Slots, c.MissingRequiredSlots, c.InvalidSlots = cs.extractSlots(input) } return c } // fillTopSlots 给裁决后的 Top 补抽槽位。Ambiguous 路径建候选时不抽槽位, // 分类器选定 Top 之后才知道该抽哪一条。 func (s *Snapshot) fillTopSlots(c *Candidate, input string) { idx, ok := s.byID[c.ID] if !ok { return } slots, missing, invalid := s.specs[idx].extractSlots(input) c.Slots = slots c.MissingRequiredSlots = missing c.InvalidSlots = invalid } func candidatesOf(hits []specHit, input string, topSlots bool) []Candidate { out := make([]Candidate, 0, len(hits)) for i, h := range hits { out = append(out, h.candidate(input, topSlots && i == 0)) } return out } // ---- 构建与合并 ---- // RegistryBuilder 收集 Go 内置基线 spec 并构建 Registry。 type RegistryBuilder struct { opts Options builtin []IntentSpec seen map[string]bool } // NewRegistryBuilder 创建构建器。opts 零值字段填默认。 func NewRegistryBuilder(opts Options) *RegistryBuilder { return &RegistryBuilder{opts: opts.normalized(), seen: make(map[string]bool)} } // Register 登记内置基线 spec,立即校验;重复 id 报 InvalidSpec。 func (b *RegistryBuilder) Register(specs ...IntentSpec) error { lim := b.opts.limits() for _, spec := range specs { if b.seen[spec.ID] { return newError(InvalidSpec, "内置 spec id 重复", "id", spec.ID) } if err := validateSpec(spec, lim, b.opts.SlotValidators); err != nil { return err } b.seen[spec.ID] = true b.builtin = append(b.builtin, spec) } return nil } // Build 仅用内置基线构建(rev 1)。此后 Reload 会报错(无配置来源)。 func (b *RegistryBuilder) Build() (*Registry, error) { return b.buildWithSource(nil, nil) } // BuildFS 用内置基线 + JSON overlay 来源构建,并记住来源供 Reload 重读。 // roots 按给定顺序应用;目录 root 内按文件名字典序加载 *.json。 func (b *RegistryBuilder) BuildFS(ctx context.Context, fsys fs.FS, roots ...string) (*Registry, error) { if fsys == nil { return nil, newError(InvalidArgument, "fsys 为 nil") } if len(roots) == 0 { return nil, newError(InvalidArgument, "至少提供一个配置 root") } return b.buildWithSource(fsys, roots) } func (b *RegistryBuilder) buildWithSource(fsys fs.FS, roots []string) (*Registry, error) { var entries []overlayEntry if fsys != nil { var err error entries, err = loadOverlays(fsys, roots) if err != nil { return nil, err } } snap, err := buildSnapshot(b.opts, b.builtin, entries, 1) if err != nil { return nil, err } r := &Registry{ opts: b.opts, builtin: append([]IntentSpec(nil), b.builtin...), fsys: fsys, roots: append([]string(nil), roots...), hasSource: fsys != nil, log: b.opts.Log, } r.snap.Store(snap) return r, nil } // Registry 持有当前快照与配置来源,支持原子 Reload。 type Registry struct { opts Options builtin []IntentSpec fsys fs.FS roots []string hasSource bool mu sync.Mutex // 串行化 Reload snap atomic.Pointer[Snapshot] log *logsys.Logger } // Snapshot 返回当前生效快照。永不为 nil。 func (r *Registry) Snapshot() *Snapshot { return r.snap.Load() } // Options 返回生效选项(已填默认值)。 func (r *Registry) Options() Options { return r.opts } // Reload 重读配置来源并构建新快照;成功后原子替换并返回新快照。 // 任一步失败保留旧快照,revision 不变,返回带来源与原因的错误。 func (r *Registry) Reload(ctx context.Context) (*Snapshot, error) { r.mu.Lock() defer r.mu.Unlock() old := r.snap.Load() if !r.hasSource { err := newError(LoadFailed, "没有登记配置来源;用 BuildFS 构建后才能 Reload", "revision", old.rev) r.log.Module("intent").Error("intent config rejected", err) return nil, err } entries, err := loadOverlays(r.fsys, r.roots) if err != nil { r.log.Module("intent").Error("intent config rejected", err) return nil, err } ns, err := buildSnapshot(r.opts, r.builtin, entries, old.rev+1) if err != nil { r.log.Module("intent").Error("intent config rejected", err) return nil, err } r.snap.Store(ns) r.log.Module("intent").Info("intent snapshot reloaded", "revision", ns.rev, "specs", len(ns.specs), "disabled", len(ns.disabled)) return ns, nil } // buildSnapshot 执行双源合并与编译。任一错误整批失败,不产生半套快照。 func buildSnapshot(opts Options, baseline []IntentSpec, entries []overlayEntry, rev int64) (*Snapshot, error) { opts = opts.normalized() protected := opts.protectedSet() guards := opts.guardSet() lim := opts.limits() ordered := append([]IntentSpec(nil), baseline...) pos := make(map[string]int, len(ordered)) sources := make(map[string]string, len(ordered)) for i, spec := range ordered { pos[spec.ID] = i sources[spec.ID] = "builtin" } var disabledList []string for _, e := range entries { if e.disable { idx, ok := pos[e.id] if !ok { return nil, newError(InvalidSpec, "disable 目标不存在", "id", e.id, "source", e.source) } if protected[ordered[idx].Domain] { return nil, newError(SpecProtected, "overlay 试图禁用保护域 spec", "id", e.id, "domain", ordered[idx].Domain, "source", e.source) } ordered = append(ordered[:idx], ordered[idx+1:]...) for id, p := range pos { if p > idx { pos[id] = p - 1 } } delete(pos, e.id) disabledList = append(disabledList, e.id) sources[e.id] = e.source + " (disabled)" continue } spec := e.spec if err := validateSpec(spec, lim, opts.SlotValidators); err != nil { return nil, err.(*Error).with("source", e.source) } if idx, ok := pos[spec.ID]; ok { old := ordered[idx] if protected[old.Domain] { return nil, newError(SpecProtected, "overlay 试图覆盖保护域 spec", "id", spec.ID, "domain", old.Domain, "source", e.source) } if spec.Version <= old.Version { return nil, newError(InvalidSpec, "覆盖必须递增 version", "id", spec.ID, "old_version", old.Version, "new_version", spec.Version, "source", e.source) } ordered[idx] = spec } else { ordered = append(ordered, spec) pos[spec.ID] = len(ordered) - 1 } sources[spec.ID] = e.source } total := len(ordered) + len(disabledList) if total > lim.maxSpecs { return nil, newError(InvalidSpec, "spec 总数超过上限", "total", total, "max", lim.maxSpecs) } snap := &Snapshot{ rev: rev, margin: opts.AmbiguityMargin, topN: opts.TopN, maxInput: opts.MaxInputLen, byID: make(map[string]int, len(ordered)), disabled: disabledList, sources: sources, total: total, nfkc: opts.NFKC, } nextIndex := 0 for _, spec := range ordered { if spec.Disabled { snap.disabled = append(snap.disabled, spec.ID) continue } cs, err := compileSpec(spec, guards[spec.Domain], nextIndex, opts.SlotValidators, opts.NFKC) if err != nil { return nil, err } snap.specs = append(snap.specs, cs) snap.byID[spec.ID] = len(snap.specs) - 1 if cs.guard { snap.guardIdx = append(snap.guardIdx, len(snap.specs)-1) } nextIndex++ } return snap, nil }