package workflowscope import ( "strings" "unicode" "unicode/utf8" ) const ( // QualityExact 标题 / 任务键 / 文档 ID 原文精确命中。 QualityExact = 1.0 // QualityNormalized 归一化后精确命中。 QualityNormalized = 0.85 // CrossMargin 他流要胜出,分数必须高出激活流最佳分这么多。 CrossMargin = 0.15 // MinCrossQuality 模糊包含的质量上限低于此值,因此不能让他流胜出。 MinCrossQuality = 0.85 ) // Action 区分只读放行与写动作拦截。 type Action string const ( ActionRead Action = "read" ActionWrite Action = "write" ) // Kind 是裁决结果。 type Kind string const ( KindNone Kind = "" KindInScope Kind = "in_scope" KindCrossAllow Kind = "cross_allow" KindCrossBlock Kind = "cross_block" ) // Candidate 是一条文档命中。Owner 为空表示归属未知,视作可留在池内。 type Candidate struct { ID string Label string Owner string Quality float64 } // Decision 携带胜出项。KindNone 表示没有足够把握的命中。 type Decision struct { Kind Kind Winner Candidate Score float64 } // Decide 在激活工作流约束下选出文档。 // active 为空:不过滤,按质量取最高并记为 InScope。 // strict:先取激活流(含归属未知),没有才在质量达到精确档的他流上 CrossBlock。 // weighted:score = quality × 关系权重;他流仅在分差与精确质量同时满足时胜出。 func Decide(candidates []Candidate, active string, action Action) Decision { usable := make([]Candidate, 0, len(candidates)) for _, c := range candidates { if c.Quality <= 0 { continue } usable = append(usable, c) } if len(usable) == 0 { return Decision{} } if strings.TrimSpace(active) == "" { return bestOf(usable, active, func(c Candidate) float64 { return c.Quality }, KindInScope) } if CurrentMode() != ModeWeighted { return decideStrict(usable, active) } return decideWeighted(usable, active, action) } func decideStrict(candidates []Candidate, active string) Decision { var inScope []Candidate var cross []Candidate for _, c := range candidates { if c.Owner == "" || c.Owner == active { inScope = append(inScope, c) continue } cross = append(cross, c) } if len(inScope) > 0 { return bestOf(inScope, active, func(c Candidate) float64 { return c.Quality }, KindInScope) } exact := make([]Candidate, 0, len(cross)) for _, c := range cross { if c.Quality >= MinCrossQuality { exact = append(exact, c) } } if len(exact) == 0 { return Decision{} } return bestOf(exact, active, func(c Candidate) float64 { return c.Quality }, KindCrossBlock) } func decideWeighted(candidates []Candidate, active string, action Action) Decision { scoreOf := func(c Candidate) float64 { return c.Quality * RelationWeight(active, c.Owner) } var bestIn, bestCross Candidate inScore, crossScore := -1.0, -1.0 for _, c := range candidates { s := scoreOf(c) if c.Owner == "" || c.Owner == active { if s > inScore { inScore = s bestIn = c } continue } if s > crossScore || (s == crossScore && c.Quality > bestCross.Quality) { crossScore = s bestCross = c } } if crossScore < 0 { return Decision{Kind: KindInScope, Winner: bestIn, Score: inScore} } baseline := 0.0 if inScore >= 0 { baseline = inScore } crossWins := bestCross.Quality >= MinCrossQuality && crossScore >= baseline+CrossMargin if !crossWins { if inScore >= 0 { return Decision{Kind: KindInScope, Winner: bestIn, Score: inScore} } return Decision{} } kind := KindCrossAllow if action == ActionWrite { kind = KindCrossBlock } return Decision{Kind: kind, Winner: bestCross, Score: crossScore} } func bestOf(candidates []Candidate, active string, scoreOf func(Candidate) float64, kind Kind) Decision { best := candidates[0] bestScore := scoreOf(best) for _, c := range candidates[1:] { s := scoreOf(c) if s > bestScore { best = c bestScore = s } } if kind == KindInScope && active != "" && best.Owner != "" && best.Owner != active { kind = KindCrossBlock } return Decision{Kind: kind, Winner: best, Score: bestScore} } // BlocksWrite 报告一个已确定归属的任务键在写路径上是否应拦截。 // 归属未知或未激活时不拦截。 func BlocksWrite(active, owner, id string) bool { if strings.TrimSpace(active) == "" || strings.TrimSpace(owner) == "" || owner == active { return false } decision := Decide([]Candidate{{ ID: id, Label: id, Owner: owner, Quality: QualityExact, }}, active, ActionWrite) return decision.Kind == KindCrossBlock } // BestQuality 取 query 对多个字段的最高匹配质量。 func BestQuality(query string, fields ...string) float64 { best := 0.0 for _, field := range fields { if q := ContainmentQuality(query, field); q > best { best = q } } return best } // ContainmentQuality 三档质量:原文精确 1、归一化精确 0.85、双向包含 0.5×长度比。 func ContainmentQuality(query, field string) float64 { query = strings.TrimSpace(query) field = strings.TrimSpace(field) if query == "" || field == "" { return 0 } if query == field { return QualityExact } nq := NormalizeKey(query) nf := NormalizeKey(field) if nq == "" || nf == "" { return 0 } if nq == nf { return QualityNormalized } if strings.Contains(nf, nq) || strings.Contains(nq, nf) { shorter := utf8.RuneCountInString(nq) longer := utf8.RuneCountInString(nf) if shorter > longer { shorter, longer = longer, shorter } if longer == 0 { return 0 } return 0.5 * float64(shorter) / float64(longer) } return 0 } // NormalizeKey 去掉空白、标点和无区分度的虚词,供模糊比较。 func NormalizeKey(value string) string { value = strings.ToLower(strings.TrimSpace(value)) value = strings.NewReplacer("收集", "", "采集", "", "正文", "", "文档", "", "《", "", "》", "").Replace(value) return strings.Map(func(r rune) rune { if unicode.IsSpace(r) || unicode.IsPunct(r) { return -1 } return r }, value) }