// Package gates 提供终轮门禁与进度提醒的组合机制(自产品下沉)。 // 产品依赖——工件存取、质量评分、验证器模型、提示词、进度快照——全部端口化; // 判定逻辑(正则族)、续跑上限与节流策略由本包持有。 // 端口为 nil 时语义与「未启用该检查」一致,不改变既有行为。 package gates import ( "encoding/json" "fmt" "path/filepath" "regexp" "strings" ) // 续跑与节流上限。与产品现网取值一致;消费方引用本包常量,不再各自硬编码。 const ( // MaxDeliverableGuards 是「声称交付但无工件」的纠正轮次上限。 MaxDeliverableGuards = 2 // MaxVerifyContinuations 是终轮检查的续跑上限(与内核 Stop 的 3 次上限对齐)。 MaxVerifyContinuations = 3 // ReminderToolGap 是连续这么多个工具轮没有更新计划或待办就提醒一次。 ReminderToolGap = 3 // MaxChecklistReminders 是每个 run 的进度提醒次数上限。 MaxChecklistReminders = 3 ) // ArtifactLookup 按 ID 取工件:名称、内容与是否存在。 type ArtifactLookup func(id string) (name string, content []byte, ok bool) // QualityScorer 对文本类工件评分;nil 表示跳过评分(视为通过)。 // why 在未通过时给出可读原因。 type QualityScorer func(name string, content []byte) (passed bool, why string) // ArtifactRef 是一次工件引用(库内最小形态;宿主负责投影自有类型)。 type ArtifactRef struct { ID string Name string Kind string } var ( // ArtifactIDPattern 是工件编号的文本形态(门禁与宿主的引用提取共用)。 ArtifactIDPattern = regexp.MustCompile(`art_[0-9a-f]{12}`) fileClaimRe = regexp.MustCompile(`(?s)(?:已生成|已保存|已导出|已写入).{0,120}?\.(?:docx|xlsx|pptx|pdf|png|jpe?g|csv|md|txt|json)\b`) softClaimRe = regexp.MustCompile(`(?s)(?:文件|文档|工件).{0,40}?已(?:正常)?(?:生成|登记|保存|导出|写入)`) forgedCallRe = regexp.MustCompile(`\[(?:调用工具|工具结果|产出工件) `) ) // ClaimGate 判断终轮正文是否在没有对应工件的情况下声称交付。 // 引用本轮已产出、或确实存在的工件不算谎称;伪造调用标记始终拦截。 // lookup 为 nil 时,任何引用未产出工件的 art_ 编号都视为不存在(拦截)。 func ClaimGate(body string, produced []ArtifactRef, lookup ArtifactLookup) (bool, string) { if strings.TrimSpace(body) == "" { return false, "" } if forgedCallRe.MatchString(body) { return true, "回复正文里出现了工具调用标记,但本轮没有真实的工具调用" } producedIDs := map[string]bool{} for _, ref := range produced { if ref.ID != "" { producedIDs[ref.ID] = true } } knownExisting := false for _, id := range ArtifactIDPattern.FindAllString(body, -1) { if producedIDs[id] { continue } if lookup != nil { if _, _, ok := lookup(id); ok { knownExisting = true continue } } return true, "回复引用了不存在的工件" } if len(producedIDs) > 0 || knownExisting { return false, "" } if fileClaimRe.MatchString(body) || softClaimRe.MatchString(body) { return true, "回复声称已经生成或登记了文件,但这一轮没有产生对应的工件" } return false, "" } // CheckArtifacts 确认登记的工件存在且非空。 // md、txt、docx 还要过 scorer;其他类型只检查存在与非空。 func CheckArtifacts(refs []ArtifactRef, lookup ArtifactLookup, scorer QualityScorer) []string { var reasons []string for _, ref := range refs { if ref.ID == "" { reasons = append(reasons, "工件引用缺少 ID") continue } name := ref.Name if name == "" { name = ref.ID } var ( content []byte ok bool artName string ) if lookup != nil { artName, content, ok = lookup(ref.ID) } if !ok { reasons = append(reasons, name+" 不存在") continue } if artName != "" { name = artName } if len(content) == 0 { reasons = append(reasons, name+" 内容为空") continue } switch strings.ToLower(filepath.Ext(name)) { case ".md", ".txt", ".docx": if scorer == nil { continue } passed, why := scorer(name, content) if !passed { if why == "" { why = "评分未通过" } reasons = append(reasons, name+":"+why) } } } return reasons } // Verdict 是独立验证器的裁决。 type Verdict struct { Passed bool `json:"passed"` Reason string `json:"reason"` NextAction string `json:"nextAction"` } // ParseVerdict 解析验证器输出:剥围栏、取最外层 JSON、必须有 passed 键。 func ParseVerdict(raw string) (Verdict, error) { s := stripFence(raw) start := strings.Index(s, "{") end := strings.LastIndex(s, "}") if start < 0 || end < start { return Verdict{}, fmt.Errorf("验证器返回无法解析") } s = s[start : end+1] var probe map[string]json.RawMessage if err := json.Unmarshal([]byte(s), &probe); err != nil { return Verdict{}, err } if _, ok := probe["passed"]; !ok { return Verdict{}, fmt.Errorf("验证器返回无法解析") } var verdict Verdict if err := json.Unmarshal([]byte(s), &verdict); err != nil { return Verdict{}, err } return verdict, nil } func stripFence(s string) string { s = strings.TrimSpace(s) if !strings.HasPrefix(s, "```") { return s } s = strings.TrimPrefix(s, "```") if i := strings.IndexByte(s, '\n'); i >= 0 { s = s[i+1:] } s = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(s), "```")) return s }