// structure_space_init.js - 把空间初始化模板从「字节」升级成「JSON」 // // 为什么需要它:names/space-init-*.json 里绝大多数记录存的是原始 hex(抓包字节), // 想改一个字段(比如用户名字)就得做字节替换。semantic.js 有了字段级解码能力后, // 这里把那些 hex 记录整段当「记录链」解一遍(旧模板的 Hex 是「该帧体从本条记录负载起剩下的全部字节」, // 所以一段 hex 里可能含多条记录 —— 见 tools/extract_space_init.js:94 与 mine_downlink.js 头注), // 能完整解出、且解出再编码逐字节一致的,就换成结构化记录;否则**原样保留 hex 并把原因写进 Note**。 // // 安全阀:只有当「结构化后的模板经 space.js 的组帧路径产出与升级前完全相同的字节」时才提交, // 所以这个脚本不可能改变任何一帧的字节(解不动的一律不动)。 // // 用法: node tools/structure_space_init.js 'use strict'; const fs = require('fs'); const path = require('path'); const Link = require('./server/link'); const Space = require('./server/space'); const Semantic = require('./server/semantic').Load(path.join(__dirname, '..', 'names')); const NamesDir = path.join(__dirname, '..', 'names'); const Files = fs.readdirSync(NamesDir).filter((F) => /^space-init-.*\.json$/.test(F)).sort(); // 按模板里存的形态拼帧体(含 nullmap):Hex 原样透传 / Layout+Value 走容器 / Fields 走语义层。 // objectId 缺省回退帧级 SpaceId —— 这正是升级前模板组帧的规则,所以这就是「升级前的字节」。 function ReferenceBody(F) { const Parts = [Buffer.from(F.NullmapHex || '00', 'hex')]; for (const R of F.Records || []) { const Id = Buffer.alloc(8); Id.writeBigInt64BE(BigInt(R.ObjectId === undefined || R.ObjectId === null ? F.SpaceId : R.ObjectId), 0); const Method = Buffer.alloc(8); Method.writeBigInt64BE(BigInt(R.MethodId), 0); let Payload; if (R.Hex !== undefined) Payload = Buffer.from(R.Hex || '', 'hex'); else if (R.Layout !== undefined) Payload = Semantic.Container.Write(R.Layout, R.Value); else if (R.Fields !== undefined) Payload = Semantic.EncodeRecord(R.MethodId, R); else return null; // 形态不认识 → 交给调用方判为不可升级 Parts.push(Id, Method, Payload); } return Buffer.concat(Parts); } function ToFrame(Body) { const N = Link.NullmapSize(Body, 0); return Link.EncodeFrame(Body.slice(N), Body.slice(0, N)); } // 解出来的记录 → 模板里的形态(① 容器 {Layout,Value} ② 普通字段 {Fields});每条都带自己的 ObjectId function ToTemplate(R) { const Out = { ObjectId: R.ObjectId, MethodId: R.MethodId }; if (R.Kind === 'container') { Out.Layout = R.Layout; Out.Value = R.Value; } else { Out.Fields = {}; for (const F of R.Fields || []) Out.Fields[F.Name] = F.Value; } return Out; } // 保留 hex 的原因分类(不要笼统写「解不动」——分类本身是施工图) function Classify(Decoded) { for (const R of Decoded.Records || []) { if (R.Short) return '短命令形态(nullmap bit=1 的 1 字节命令号):命令号 → (objectId, methodId) 的查表数据还没挖'; } const Notes = (Decoded.Records || []).filter((R) => R.Note).map((R) => R.Note); const Note = Notes[0] || Decoded.Note || '未知'; if (/容器三布局/.test(Note)) return '容器布局吃不到记录尾:' + Note; if (/没有字段表|不在 downlink_table/.test(Note)) return '没有字段表:' + Note; if (/解不动/.test(Note)) return '字段/记录解不动:' + Note; return Note; } const Total = { Spaces: 0, Frames: 0, EmptyFrames: 0, Records: 0, RecordsStructured: 0, RecordsHex: 0, HexBytes: 0, Upgraded: 0, HexReplaced: 0, Kept: 0 }; const Reasons = new Map(); const Details = []; for (const File of Files) { const Path2 = path.join(NamesDir, File); const T = JSON.parse(fs.readFileSync(Path2, 'utf8')); const Rec = { File, SpaceId: T.SpaceId, Frames: (T.Frames || []).length, EmptyFrames: 0, Records: 0, Structured: 0, Hex: 0, HexBytes: 0, Upgraded: 0, HexReplaced: 0, Kept: 0 }; let Changed = false; const ReasonsHere = new Map(); for (const F of T.Frames || []) { const Records = F.Records || []; Rec.Records += Records.length; if (!Records.length) { Rec.EmptyFrames++; continue; } for (const R of Records) { if (R.Hex !== undefined) { Rec.Hex++; Rec.HexBytes += (R.Hex || '').length / 2; } else Rec.Structured++; } // 只处理「含未解 hex 记录」的帧:容器记录(Layout 已存在)与已结构化的帧保持不动(也保证可重复运行) if (!Records.some((R) => R.Hex !== undefined)) continue; const Ref = ReferenceBody(F); let Why = ''; let Decoded = null; if (!Ref) Why = '记录形态既不认识 Hex/Layout/Fields 中的任何一种,不敢动'; else { try { Decoded = Semantic.DecodeFrameBody(Ref); } catch (Err) { Why = '解码抛错:' + Err.message; } } if (!Why && Decoded.Bad) Why = Classify(Decoded); if (!Why && !Decoded.Records.length) Why = '帧体里没解出记录'; // 安全阀:结构化后的帧必须与升级前逐字节相同,否则一律不提交 if (!Why) { const NewRecords = Decoded.Records.map(ToTemplate); let Built = null; try { Built = Space.BuildInitFrames({ Frames: [{ SpaceId: F.SpaceId, NullmapHex: F.NullmapHex, Records: NewRecords }] }, Semantic.Container, Semantic)[0].Frame; } catch (Err) { Why = '重新组帧失败:' + Err.message; } if (!Why && !Built.equals(ToFrame(Ref))) Why = '组帧字节与升级前不同(' + Built.length + ' vs ' + ToFrame(Ref).length + ' 字节)'; if (!Why) { F.Records = NewRecords; Changed = true; Rec.Upgraded += NewRecords.length; Rec.HexReplaced += Records.filter((R) => R.Hex !== undefined).length; } } if (Why) { Rec.Kept++; const Kind = Why.split(':')[0]; Reasons.set(Kind, (Reasons.get(Kind) || 0) + 1); ReasonsHere.set(Kind, (ReasonsHere.get(Kind) || 0) + 1); // 解不动就如实保留原始 hex,并把原因写进 Note(原来的 Note 会被这条更具体的原因取代) for (const R of F.Records) if (R.Hex !== undefined) R.Note = Why; Details.push(' ' + File + ' ' + F.File + ' ' + F.Records.map((R) => '#' + R.MethodId).join(',') + ' ' + Why); } } if (Changed) fs.writeFileSync(Path2, JSON.stringify(T, null, 1), 'utf8'); Total.Spaces++; Total.Frames += Rec.Frames; Total.EmptyFrames += Rec.EmptyFrames; Total.Records += Rec.Records; Total.RecordsStructured += Rec.Structured; Total.RecordsHex += Rec.Hex; Total.HexBytes += Rec.HexBytes; Total.Upgraded += Rec.Upgraded; Total.HexReplaced += Rec.HexReplaced; Total.Kept += Rec.Kept; Rec.StructuredAfter = Rec.Structured + Rec.Upgraded; Rec.HexAfter = Rec.Hex - Rec.HexReplaced; Details.push(' ' + File.padEnd(34) + '空间 ' + String(Rec.SpaceId).padEnd(14) + '帧 ' + String(Rec.Frames).padStart(3) + ' 记录 ' + String(Rec.Records).padStart(3) + ' 升级前:结构化 ' + String(Rec.Structured).padStart(3) + ' / hex ' + String(Rec.Hex).padStart(3) + '(' + Rec.HexBytes + 'B)' + ' → 升级后:结构化 ' + String(Rec.StructuredAfter).padStart(3) + ' / hex ' + String(Rec.HexAfter).padStart(3) + ' 本次 ' + Rec.HexReplaced + ' 条 hex → ' + Rec.Upgraded + ' 条结构化' + (Changed ? '(已写回)' : '(无变化)') + (Rec.EmptyFrames ? ' ⚠ 空记录帧 ' + Rec.EmptyFrames : '') + (Rec.Kept ? ' 保留 hex 原因:' + [...ReasonsHere].map(([K, V]) => K + '×' + V).join('、') : '')); } console.log('空间初始化模板结构化(升级前统计 → 升级后)'); console.log(''); for (const L of Details) console.log(L); console.log(''); console.log('合计:空间 ' + Total.Spaces + ' 帧 ' + Total.Frames + '(其中空记录帧 ' + Total.EmptyFrames + ') 记录 ' + Total.Records); console.log(' 升级前:结构化 ' + Total.RecordsStructured + ' 条 / 原始 hex ' + Total.RecordsHex + ' 条(' + Total.HexBytes + ' 字节)'); console.log(' 本次升级 ' + Total.HexReplaced + ' 条 hex 记录 → ' + Total.Upgraded + ' 条结构化记录;保留 hex ' + Total.Kept + ' 帧'); console.log(' 升级后:结构化 ' + (Total.RecordsStructured + Total.Upgraded) + ' 条 / 原始 hex ' + (Total.RecordsHex - Total.HexReplaced) + ' 条'); if (Reasons.size) { console.log(' 保留 hex 的原因分类(按帧计):'); for (const [K, V] of [...Reasons].sort((A, B) => B[1] - A[1])) console.log(' ' + String(V).padStart(5) + ' ' + K); }