工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。 服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。 同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。 src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
124 lines
4.6 KiB
JavaScript
124 lines
4.6 KiB
JavaScript
// extract.js - 把 webpack bundle 拆成可读源码工程
|
||
// 用法: node extract.js [bundle路径] [输出目录]
|
||
// 默认 bundle = ../../main.6469b966.js(TankIC 根目录)
|
||
// 默认输出 = ../src
|
||
// 输出:
|
||
// src/module_<id>.js 每个代码模块(prettier 美化)
|
||
// src/resources.json 资源模块 ID → 路径
|
||
// src/index.json 模块索引(id / 文件 / 是否资源)
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const acorn = require('acorn');
|
||
const walk = require('acorn-walk');
|
||
const prettier = require('prettier');
|
||
|
||
const PROJECT = path.resolve(__dirname, '..');
|
||
const argv = process.argv.slice(2).filter((a) => !a.startsWith('--'));
|
||
const BUNDLE_PATH = path.resolve(argv[0] || path.join(PROJECT, '..', 'main.6469b966.js'));
|
||
const SRC_DIR = path.resolve(argv[1] || path.join(PROJECT, 'src'));
|
||
const RESOURCES_JSON = path.join(SRC_DIR, 'resources.json');
|
||
const INDEX_JSON = path.join(SRC_DIR, 'index.json');
|
||
|
||
function tryExtractResource(valueSrc) {
|
||
let m = valueSrc.match(/\{\s*"use strict";\s*(?:t|n|i)\.exports\s*=\s*(?:t|n|i)\.p\+"([^"]+)"\s*;?\s*\}/);
|
||
if (m) return m[1];
|
||
m = valueSrc.match(/(?:t|n|i)\.exports\s*=\s*(?:t|n|i)\.p\+"([^"]+)"\s*;?/);
|
||
if (m && valueSrc.length < 200) return m[1];
|
||
return null;
|
||
}
|
||
|
||
(async () => {
|
||
if (!fs.existsSync(BUNDLE_PATH)) {
|
||
console.error('找不到 bundle: ' + BUNDLE_PATH);
|
||
process.exit(1);
|
||
}
|
||
if (!fs.existsSync(SRC_DIR)) fs.mkdirSync(SRC_DIR, { recursive: true });
|
||
// 清理旧产物
|
||
for (const f of fs.readdirSync(SRC_DIR)) {
|
||
if (/^module_\d+\.js$/.test(f) || f === 'resources.json' || f === 'index.json') {
|
||
fs.unlinkSync(path.join(SRC_DIR, f));
|
||
}
|
||
}
|
||
|
||
console.log('[1/4] 读取 bundle: ' + BUNDLE_PATH);
|
||
const source = fs.readFileSync(BUNDLE_PATH, 'utf8');
|
||
console.log(' 大小: ' + (source.length / 1024 / 1024).toFixed(2) + ' MB');
|
||
|
||
console.log('[2/4] 解析 AST...');
|
||
const ast = acorn.parse(source, {
|
||
ecmaVersion: 'latest',
|
||
sourceType: 'script',
|
||
allowReturnOutsideFunction: true,
|
||
});
|
||
let modulesDecl = null;
|
||
walk.simple(ast, {
|
||
VariableDeclarator(node) {
|
||
if (node.id && node.id.name === '__webpack_modules__' && node.init && node.init.type === 'ObjectExpression') {
|
||
modulesDecl = node;
|
||
}
|
||
},
|
||
});
|
||
if (!modulesDecl) throw new Error('未找到 __webpack_modules__');
|
||
|
||
console.log('[3/4] 拆分模块...');
|
||
const props = modulesDecl.init.properties;
|
||
const resourceMap = {};
|
||
const index = [];
|
||
let codeCount = 0, resourceCount = 0, failCount = 0;
|
||
|
||
for (const prop of props) {
|
||
let keyRaw, keyNum;
|
||
if (prop.key.type === 'Identifier') {
|
||
keyRaw = prop.key.name;
|
||
keyNum = Number(keyRaw);
|
||
} else {
|
||
keyRaw = String(prop.key.value);
|
||
keyNum = typeof prop.key.value === 'number' ? prop.key.value : keyRaw;
|
||
}
|
||
const valueSrc = source.slice(prop.value.start, prop.value.end);
|
||
const resourcePath = tryExtractResource(valueSrc);
|
||
if (resourcePath) {
|
||
resourceMap[keyNum] = resourcePath;
|
||
resourceCount++;
|
||
index.push({ id: keyNum, type: 'resource', path: resourcePath });
|
||
continue;
|
||
}
|
||
|
||
let beautified = valueSrc;
|
||
try {
|
||
beautified = await prettier.format('(' + valueSrc + ')', {
|
||
parser: 'babel',
|
||
semi: true,
|
||
singleQuote: false,
|
||
printWidth: 110,
|
||
tabWidth: 2,
|
||
});
|
||
beautified = beautified.trim();
|
||
if (beautified.startsWith('(') && beautified.endsWith(')')) beautified = beautified.slice(1, -1).trim();
|
||
while (beautified.endsWith(';')) beautified = beautified.slice(0, -1).trimEnd();
|
||
} catch (e) {
|
||
failCount++;
|
||
}
|
||
|
||
const padded = String(keyNum).padStart(5, '0');
|
||
const file = 'module_' + padded + '.js';
|
||
const header =
|
||
'// ============================================================\n' +
|
||
'// Module ID: ' + keyRaw + '\n' +
|
||
'// ============================================================\n';
|
||
fs.writeFileSync(path.join(SRC_DIR, file), header + ';(window.__modules__ = window.__modules__ || {})[' + keyNum + '] = ' + beautified + ';\n', 'utf8');
|
||
codeCount++;
|
||
index.push({ id: keyNum, type: 'code', file });
|
||
}
|
||
|
||
console.log('[4/4] 写 resources.json / index.json...');
|
||
fs.writeFileSync(RESOURCES_JSON, JSON.stringify({ type: 'webpack-resource-map', resourceMap }, null, 0), 'utf8');
|
||
fs.writeFileSync(INDEX_JSON, JSON.stringify({ bundle: path.basename(BUNDLE_PATH), modules: index }, null, 1), 'utf8');
|
||
|
||
console.log('代码模块 ' + codeCount + ' / 资源模块 ' + resourceCount + ' / 美化失败 ' + failCount);
|
||
console.log('输出目录: ' + SRC_DIR);
|
||
})().catch((err) => {
|
||
console.error('Fatal:', err);
|
||
process.exit(1);
|
||
});
|