便宜档生成切到 cheap-worker 新路后,result_out 只产 trace.cost、缺 9d trace 七项,致后端 D11 ReadinessScorer 三维恒中性、退化成 45/55 双峰近恒定分,D9 反同质失效。
U1 cheap_studio 富化:build_trace_source + _furthest_stage,additive 捕获 verdictFull/driverType/models/stage。U2 result_out._build_trace:七项(repairs=max(0,attempts-1))+ sevenGateVerdict{pass,guards} + gatespec{driver} + cost + similarity,camelCase 镜像 _extract_trace、None 容错。U3 D9 dedup vendored 进 cheap-worker/dedup.py(registry 指自己 results/)、worker_service 接线非阻断。
Java 跨语言切片测加逐维反假绿门(反射调真 ReadinessScorer:三维脱离中性 + 合成分 100 vs 退化 ≤55 拉开 ≥35)。顺带修预存 M3a 红测:worker 异常兜底 failureReason 改传真枚举 llm_error。
验证:cheap-worker 全 16 测绿 + Java 切片 5/5 绿 BUILD SUCCESS。范围仅 cheap-worker/ + 2 fixture + 1 Java 测 + plan,零 live 触碰。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
6.3 KiB
Python
123 lines
6.3 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""dedup.py —— D9 反同质化(归一 + 精确撞重签名 + FS 登记查重)。便宜档 M3b U3。
|
||
|
||
【来源 = vendored 复制 wg1/gen-worker/worker/agent_loop/dedup.py 的纯逻辑】
|
||
norm_text / compute_sig / _load_registry / _append_registry / check_similarity 全 stdlib、无外部依赖,
|
||
逐字搬运(wg1 与 tier2/gen-worker 都不在 cheap-worker 的 import 路径上、tier2 也无 dedup,故用 vendor 复制、
|
||
不跨树 import,见 KTD2)。**唯一功能改动**:DEDUP_REGISTRY 指 cheap-worker 自己的 results/(天然路径隔离,
|
||
绝不与 wg1 串台);另把日志前缀 [wg1-worker] 改 [cheap-worker](可追溯,不改逻辑)。
|
||
|
||
【职责】给生成结果算「相似度」字段(dupHit/titleNorm/sig/dupWith),v0 只做【精确撞重 + 归一完全相等】
|
||
告警,**绝不阻断生成**(九门已是硬地板挡坏游戏;D9 首版只观测,避免误杀)。撞重只 log.warn +
|
||
similarity.dupHit=true,照常回调、照常入 feed。
|
||
|
||
【容错】登记表损坏/读写失败 → try-catch 吞 → similarity 段降级(dupHit=None),不阻断生成。
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
import re
|
||
import unicodedata
|
||
from pathlib import Path
|
||
|
||
# 查重登记表(FS 侧 append-only jsonl)。
|
||
# 【唯一功能改动】指 cheap-worker/results/_dedup_registry.jsonl(本文件在 cheap-worker/ 根,parent 即 cheap-worker/);
|
||
# 天然路径隔离、跨任务持久,绝不写 wg1(别用 wg1 的 parents[1])。
|
||
DEDUP_REGISTRY = Path(__file__).resolve().parent / "results" / "_dedup_registry.jsonl"
|
||
|
||
|
||
def norm_text(text):
|
||
"""查重归一化(移植 run_batch.py:96 _norm_text,纯函数≈复制):NFKC + casefold + 去空白与常见中英文标点。"""
|
||
s = unicodedata.normalize("NFKC", text or "").casefold()
|
||
return re.sub(
|
||
r"[\s,.;:!?'\"()\[\]{}<>~`@#$%^&*+=/\\|,。·、!?:;…“”‘’()《》【】—-]+", "", s
|
||
)
|
||
|
||
|
||
def compute_sig(title_norm, theme_norm, struct_key=""):
|
||
"""精确撞重键(移植 ledger.py:50 mint_design_id 思路):sha256(titleNorm+themeNorm+关键结构) hex64。
|
||
|
||
完全相同的 title/theme/结构 → 同 sig(撞重判定的精确键)。纯拼接 + UTF-8 编码,可复现。
|
||
|
||
:param title_norm: 归一后标题(norm_text 产)
|
||
:param theme_norm: 归一后主题(norm_text 产)
|
||
:param struct_key: 关键结构串(engine 路可空;填参路可放 templateId 等结构标识)
|
||
:return: 64 位小写 hex
|
||
"""
|
||
raw = "%s|%s|%s" % (title_norm, theme_norm, struct_key)
|
||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||
|
||
|
||
def _load_registry():
|
||
"""读 FS 登记表 → [{traceId, sig, titleNorm, themeNorm}](坏行跳过,文件缺失返回空)。
|
||
|
||
容错:单条坏行(崩溃残行/手改脏数据)跳过不抛;整文件读失败由调用方 try-catch 兜(返回空表 = 视为无历史)。
|
||
"""
|
||
if not DEDUP_REGISTRY.is_file():
|
||
return []
|
||
rows = []
|
||
for line in DEDUP_REGISTRY.read_text(encoding="utf-8").splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
rows.append(json.loads(line))
|
||
except Exception:
|
||
# 坏行跳过(borrow ledger.py resume_state 的"崩溃残行容忍"范式),不阻断查重
|
||
continue
|
||
return rows
|
||
|
||
|
||
def _append_registry(entry):
|
||
"""登记本款 sig 到 FS 表(append-only,落盘即 flush;目录不存在则建)。"""
|
||
DEDUP_REGISTRY.parent.mkdir(parents=True, exist_ok=True)
|
||
with DEDUP_REGISTRY.open("a", encoding="utf-8") as fh:
|
||
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||
fh.flush()
|
||
|
||
|
||
def check_similarity(title, theme, trace_id, struct_key="", register=True):
|
||
"""D9 v0 查重主入口:归一 → 算签名 → 比对 FS 历史 → 命中告警 + 登记本款。
|
||
|
||
v0 范围:完全相同(归一后 sig 相等)= 撞重;不做模糊近似阈值。
|
||
|
||
:param title: 游戏标题(engine 路 = brief 派生的 gameConfig.title)
|
||
:param theme: 游戏主题(engine 路占位 generic)
|
||
:param trace_id: 本款 traceId(命中时登记到 dupWith 供溯源)
|
||
:param struct_key: 关键结构串(参与 sig,可空)
|
||
:param register: 是否把本款登记进 FS 表(默认 True;查重不命中才登记,命中只告警不登记防表膨胀)
|
||
:return: similarity dict {dupHit, titleNorm, sig, dupWith};读写异常时 dupHit=None(降级,不阻断)
|
||
"""
|
||
title_norm = norm_text(title)
|
||
theme_norm = norm_text(theme)
|
||
sig = compute_sig(title_norm, theme_norm, struct_key)
|
||
try:
|
||
rows = _load_registry()
|
||
dup_with = None
|
||
for r in rows:
|
||
if r.get("traceId") == trace_id:
|
||
continue # 排除自身(重试/重投同 traceId 不与自己撞)
|
||
# 精确撞重:sig 完全相等(同 title/theme/结构)即命中;归一完全相等亦体现在 sig 相等
|
||
if sig and r.get("sig") == sig:
|
||
dup_with = r.get("traceId") or r.get("sig")
|
||
break
|
||
# 归一标题完全相等(即便 struct_key 不同也告警,对齐 _dedup_gate 的 title 维度撞重)
|
||
if title_norm and r.get("titleNorm") == title_norm:
|
||
dup_with = r.get("traceId") or r.get("sig")
|
||
break
|
||
dup_hit = dup_with is not None
|
||
if dup_hit:
|
||
# v0 不阻断铁律:撞重只告警 + 标 dupHit,照常回调入 feed;命中不再登记(防同款刷表)
|
||
print(f"[cheap-worker] [D9] 撞重告警 traceId={trace_id} sig={sig[:12]} dupWith={dup_with}(仅告警不阻断)",
|
||
flush=True)
|
||
elif register:
|
||
# 不撞 → 登记本款 sig(供后续款查重)
|
||
_append_registry({"traceId": trace_id, "sig": sig,
|
||
"titleNorm": title_norm, "themeNorm": theme_norm})
|
||
return {"dupHit": dup_hit, "titleNorm": title_norm, "sig": sig, "dupWith": dup_with}
|
||
except Exception as e:
|
||
# 登记表损坏/读写失败 → 降级(dupHit=None 表"未查重"),不阻断生成
|
||
print(f"[cheap-worker] [D9] 查重降级(登记表读写异常,不阻断)traceId={trace_id}: {type(e).__name__}: {e}",
|
||
flush=True)
|
||
return {"dupHit": None, "titleNorm": title_norm, "sig": sig, "dupWith": None}
|