feat(protocol): 采集运行时 modelId→codec 注册并用源码反查模块归属

This commit is contained in:
TermiNexus committed 2026-10-21 01:32:03 +08:00
1 parent 76e00f7aa2
commit fcd071485d
3 files changed
+286

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
# resolve_model_binding.py - 用「运行时 modelId → codec 实例」dump 反查 codec 的模块归属
#
# 为什么需要这个工具:
# regdump.json 的 ModelReg.w7n_1 只给出「类短名 + codec 槽位名」。类短名在 126 个模块里
# 大量复用(Fs / hs / It 这类两字母名到处都是),而 regdump 不含模块信息,
# 所以靠「短名 + 槽位」做指纹经常多解或误绑(例:658797348514407988 被绑到 ItemModelCC,
# 真实 codec 是另一个模块里的 Fs)→ pt 项静默串位,最后读出 modelId=8 这种不存在的值。
#
# 数据来源(由镜像站自动采集,不需要手工粘控制台):
# 3dtank.com/play/static/js/unpacked/module_61860.js 的 [Hornet 调试] 补丁
# 在 ModelReg.n7o(modelId, codec) 注册时记录 codec 的 u7h/t7h/s7h 源码文本,
# 节流后 POST 到 serve.js 的 /__dump__/regdump_model.json
# → 落盘到 3DTank/names/regdump_model.json
#
# 反查方法:
# u7h(codec 的「建槽」函数)文本在每个模块里都是唯一的:把它的空白全部去掉,
# 在同样去掉空白的模块源码里搜;命中后回看最近的一个 `X(类名).u7h = function`,
# 该赋值左侧的类名就是 codec 类,文件名给出模块 id。
# u7h 缺失(继承父类)时退回 t7h。
#
# 用法: python tools/resolve_model_binding.py [--names names] [--src src]
# 输出: names/model_binding.json
import json
import os
import re
import sys
Root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def Arg(Name, Def):
if '--' + Name in sys.argv:
I = sys.argv.index('--' + Name)
if I + 1 < len(sys.argv):
return sys.argv[I + 1]
return Def
NamesDir = os.path.abspath(Arg('names', os.path.join(Root, 'names')))
SrcDir = os.path.abspath(Arg('src', os.path.join(Root, 'src')))
DumpPath = os.path.join(NamesDir, 'regdump_model.json')
OutPath = os.path.join(NamesDir, 'model_binding.json')
# u7h/t7h 的赋值点:Se(ds).u7h = function (...) / x(lt).t7h = function (...)
AssignRe = re.compile(r'\((\w+)\)\.(u7h|t7h)\s*=\s*function')
ModuleRe = re.compile(r'module_(\d+)\.js$')
def Normalize(Text):
"""去掉全部空白,同时保留「归一化下标 → 原文下标」的映射。"""
Chars = []
Idx = []
for I, C in enumerate(Text):
if C.isspace():
continue
Chars.append(C)
Idx.append(I)
return ''.join(Chars), Idx
def LoadModules():
Out = []
for Name in sorted(os.listdir(SrcDir)):
if not ModuleRe.search(Name):
continue
Path = os.path.join(SrcDir, Name)
with open(Path, 'r', encoding='utf-8', errors='replace') as F:
Text = F.read()
Norm, Idx = Normalize(Text)
Assigned = [(M.start(), M.group(1), M.group(2)) for M in AssignRe.finditer(Text)]
Out.append({'Module': ModuleRe.search(Name).group(0)[:-3], 'Text': Text,
'Norm': Norm, 'Idx': Idx, 'Assigned': Assigned})
return Out
def OwnerAt(Mod, Offset, Want):
"""在原文 Offset 处,找出包含它的那一次 `<类>.u7h/t7h = function` 赋值。"""
Best = None
for Start, Cls, Kind in Mod['Assigned']:
if Start <= Offset and (Best is None or Start > Best[0]):
Best = (Start, Cls, Kind)
if Best is None:
return None
# 命中的函数体必须就是这次赋值(同一 class 可能先后有 u7h 和 t7h 两个赋值)
return Best[1] if Best[2] == Want else None
def Main():
if not os.path.exists(DumpPath):
print('缺少运行期 dump:' + DumpPath)
print('请先用镜像站进一次游戏(镜像 module_61860.js 已打采集补丁,会自动 POST)。')
return 1
with open(DumpPath, 'r', encoding='utf-8') as F:
Dump = json.load(F)
Entries = Dump.get('entries') or []
print('运行期 dump:%d 条 modelId 注册(采集于 %s)' % (len(Entries), Dump.get('at', '?')))
Modules = LoadModules()
print('已载入 %d 个模块源码' % len(Modules))
# codec_fields 里已有的 codec 键(module_XXXX::类名),用于判断「绑定成功但字段表缺失」
with open(os.path.join(NamesDir, 'codec_fields.json'), 'r', encoding='utf-8') as F:
Codecs = json.load(F).get('Codecs') or {}
Resolved = []
Unresolved = []
Ambiguous = []
for E in Entries:
ModelId = str(E.get('modelId'))
Hit = None
HitCount = 0
for Kind in ('u7h', 't7h'):
Sig = E.get(Kind)
if not Sig:
continue
NormSig = ''.join(Sig.split())
if len(NormSig) < 40:
continue
Hits = []
for Mod in Modules:
Pos = Mod['Norm'].find(NormSig)
if Pos < 0:
continue
Owner = OwnerAt(Mod, Mod['Idx'][Pos], Kind)
if Owner:
Hits.append((Mod['Module'], Owner))
if len(Hits) == 1:
Hit = Hits[0]
HitCount = 1
break
if len(Hits) > 1:
Ambiguous.append({'ModelId': ModelId, 'How': Kind, 'Candidates': Hits})
HitCount = len(Hits)
break
if Hit:
Module, Codec = Hit
Key = Module + '::' + Codec
Resolved.append({
'ModelId': ModelId,
'Module': Module,
'Codec': Codec,
'Key': Key,
'InCodecFields': Key in Codecs,
'Ctor': E.get('ctor'),
'Keys': E.get('keys'),
})
elif HitCount == 0:
Unresolved.append({'ModelId': ModelId, 'Ctor': E.get('ctor'), 'Keys': E.get('keys')})
MissingCodec = [R for R in Resolved if not R['InCodecFields']]
Out = {
'Note': '运行时 modelId → codec 的权威绑定:u7h/t7h 源码文本反查模块 + 类名;'
'InCodecFields=false 表示绑定已确定但 codec_fields 还没抽出该 codec 的字段表。',
'Source': 'names/regdump_model.json(镜像站 module_61860.n7o 采集)',
'Count': len(Entries),
'Resolved': len(Resolved),
'MissingCodec': len(MissingCodec),
'AmbiguousCount': len(Ambiguous),
'UnresolvedCount': len(Unresolved),
'Entries': Resolved,
'MissingCodecList': MissingCodec,
'Ambiguous': Ambiguous,
'Unresolved': Unresolved,
}
with open(OutPath, 'w', encoding='utf-8') as F:
json.dump(Out, F, ensure_ascii=False, indent=1)
print('已写入 ' + OutPath)
print(' 反查成功 %d / %d;其中字段表缺失 %d;多解 %d;未解 %d' %
(len(Resolved), len(Entries), len(MissingCodec), len(Ambiguous), len(Unresolved)))
if MissingCodec:
print(' 字段表缺失的 codec(前 15):')
for R in MissingCodec[:15]:
print(' %s %s ctor=%s' % (R['ModelId'], R['Key'], R['Ctor']))
if Ambiguous:
print(' 多解(前 5):')
for A in Ambiguous[:5]:
print(' %s ' % A['ModelId'] + ' / '.join(M + '::' + C for M, C in A['Candidates']))
if Unresolved:
print(' 未解(前 10):')
for U in Unresolved[:10]:
print(' %s ctor=%s keys=%s' % (U['ModelId'], U.get('Ctor'), U.get('Keys')))
return 0
if __name__ == '__main__':
sys.exit(Main())
@@ -1663,6 +1663,81 @@
}),
(J(mr).n7o = function (t, n) {
this.a7p_1.g3(t, n);
// [Hornet 调试] modelId → codec 注册采样:把 codec 的类名/槽位/读写函数源码攒起来,
// 节流后 POST 到本地镜像服务器的 /__dump__/regdump_model.json(serve.js 已实现落盘)。
// 用途:离线用 u7h/t7h 源码文本反查 codec 所在模块与类,解决「短类名全局复用」的归属歧义。
// 全程 try/catch,不影响注册本身的语义。
try {
var H = typeof window !== "undefined" ? window : globalThis;
var D = H.__HornetModelRegDump__;
if (!D) {
D = H.__HornetModelRegDump__ = {
entries: {},
order: [],
sent: 0,
timer: 0,
push: function (Id, Codec) {
if (null == Id || null == Codec) return;
var K = String(Id);
if (this.entries[K]) return;
var Src = function (F) {
try {
return F ? String(F) : null;
} catch (E) {
return null;
}
};
var Own = [];
var Proto = [];
var Ctor = "";
try {
Own = Object.keys(Codec).slice(0, 64);
} catch (E) {}
try {
Proto = Object.keys(Codec.prototype || {}).slice(0, 64);
} catch (E) {}
try {
Ctor = String(Codec.constructor || "");
} catch (E) {}
this.entries[K] = {
modelId: K,
ctor: (Codec.constructor && Codec.constructor.name) || typeof Codec,
ctorSrc: Ctor,
keys: Own,
protoKeys: Proto,
u7h: Src(Codec.u7h),
t7h: Src(Codec.t7h),
s7h: Src(Codec.s7h),
};
this.order.push(K);
if (!this.timer) {
var Me = this;
this.timer = setTimeout(function () {
Me.timer = 0;
Me.flush();
}, 1500);
}
},
flush: function () {
if (typeof fetch !== "function") return;
var Self = this;
var Body = JSON.stringify({
at: new Date().toISOString(),
href: typeof location !== "undefined" ? location.href : "",
count: this.order.length,
entries: this.order.map(function (K) {
return Self.entries[K];
}),
});
this.sent++;
try {
fetch("/__dump__/regdump_model.json", { method: "POST", body: Body });
} catch (E) {}
},
};
}
D.push(t, n);
} catch (E) {}
}),
(J(mr).o7o = function (t) {
var n = this.a7p_1.z2(t);
+25
View File
@@ -228,6 +228,31 @@ def Main():
Codecs = CodecFields.get("Codecs", {})
# ---------- 0. 运行时权威绑定(若已采集)----------
# codec_fields 里的 ModelId/ModelIds 是「短类名 + codec 槽位名」指纹 + 源码登记点推出来的;
# 短类名在 126 个模块里大量复用,误绑会让 pt 项静默串位(最后读出 modelId=8 这种不存在值)。
# names/model_binding.json 来自客户端运行时 ModelReg.n7o 注册采样 + u7h 源码反查模块,
# 一旦存在就以它为准:只保留它给出的 modelId(其余 codec 在运行时没注册,ByModelId 里也不该有)。
BindingPath = NAMES / "model_binding.json"
if BindingPath.exists():
Binding = json.loads(BindingPath.read_text("utf-8")).get("Entries") or []
BindingOverride = {}
for Row in Binding:
BindingOverride.setdefault(Row["Key"], []).append(str(Row["ModelId"]))
Dropped = 0
for Key, C in Codecs.items():
Ids = BindingOverride.get(Key)
if Ids:
C["ModelId"] = Ids[0]
C["ModelIds"] = Ids
else:
if C.get("ModelId") or C.get("ModelIds"):
Dropped += 1
C["ModelId"] = None
C["ModelIds"] = []
print(f"[绑定] 采用运行时权威绑定:{len(Binding)} 条 modelId / {len(BindingOverride)} 个 codec,"
f"丢弃 {Dropped} 个无运行时注册的旧绑定", file=sys.stderr)
# ---------- 1. 给每个 codec 分配唯一类名 ----------
CodecKeyToName = {}
NameOwner = {}