165 lines
6.3 KiB
JavaScript
165 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* dsh-purge 兼容性自检 / compatibility self-check.
|
|
*
|
|
* 从一个 DSH 构建里只取出「补丁目标文件」,复制到临时目录,再跑完整补丁引擎,
|
|
* 逐个补丁报告 applied / already / skipped。真实安装目录只读,不会被改写。
|
|
*
|
|
* Only the files the patch table targets are copied to a temp tree; the real
|
|
* installation is read-only. Every patch is then applied to that copy.
|
|
*
|
|
* 用法 / Usage:
|
|
* node scripts/compat-check.mjs --asar "C:\...\resources\app.asar"
|
|
* node scripts/compat-check.mjs --app "C:\...\resources\app"
|
|
* node scripts/compat-check.mjs --asar ... --surface web --json out.json
|
|
*
|
|
* 退出码 / Exit code: 0 = 没有待打补丁;1 = 仍有 pending;2 = 参数错误。
|
|
*/
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import * as core from "../lib/core.js";
|
|
|
|
function usage(msg) {
|
|
if (msg) console.error(msg);
|
|
console.error('usage: node scripts/compat-check.mjs --asar <app.asar> | --app <appDir> [--surface web|desktop] [--json out.json]');
|
|
process.exit(2);
|
|
}
|
|
|
|
const argv = process.argv.slice(2);
|
|
let asar = "";
|
|
let appDir = "";
|
|
let surface = "";
|
|
let jsonOut = "";
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const a = argv[i];
|
|
const next = () => argv[++i];
|
|
if (a === "--asar") asar = next();
|
|
else if (a === "--app") appDir = next();
|
|
else if (a === "--surface") surface = next();
|
|
else if (a === "--json") jsonOut = next();
|
|
else if (a === "-h" || a === "--help") usage();
|
|
else usage(`unknown argument: ${a}`);
|
|
}
|
|
if (!asar && !appDir) usage("need --asar or --app");
|
|
if (asar && appDir) usage("pass only one of --asar / --app");
|
|
|
|
if (!surface) {
|
|
surface = /deepseek[\s._-]*harness|dsh desktop/i.test(asar || appDir) ? "desktop" : "web";
|
|
}
|
|
process.env.DSH_SURFACE = surface;
|
|
|
|
/** 读 app.asar 头,返回 { entries, dataStart }。 */
|
|
function readAsarIndex(file) {
|
|
const fd = fs.openSync(file, "r");
|
|
const head = Buffer.alloc(16);
|
|
fs.readSync(fd, head, 0, 16, 0);
|
|
const headerSize = head.readUInt32LE(12);
|
|
const raw = Buffer.alloc(headerSize);
|
|
fs.readSync(fd, raw, 0, headerSize, 16);
|
|
let text = raw.toString("utf8");
|
|
const end = text.lastIndexOf("}");
|
|
if (end >= 0) text = text.slice(0, end + 1);
|
|
const index = JSON.parse(text);
|
|
const entries = new Map();
|
|
(function walk(node, prefix) {
|
|
for (const [name, entry] of Object.entries(node.files || {})) {
|
|
const p = prefix ? prefix + "/" + name : name;
|
|
if (entry.files) walk(entry, p);
|
|
else entries.set(p, entry);
|
|
}
|
|
})(index, "");
|
|
return { fd, entries, dataStart: 16 + Buffer.byteLength(text, "utf8") };
|
|
}
|
|
|
|
const realAppDir = appDir
|
|
? path.resolve(appDir)
|
|
: path.dirname(path.resolve(asar));
|
|
const realAiBase = path.join(realAppDir, "dsh", "node_modules", "@deepseek-ai");
|
|
if (appDir && !fs.existsSync(realAiBase)) {
|
|
console.error(`not a DSH build (missing dsh/node_modules/@deepseek-ai): ${realAppDir}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dsh-purge-compat-"));
|
|
const tmpHome = path.join(tmpRoot, "home");
|
|
fs.mkdirSync(tmpHome, { recursive: true });
|
|
process.env.DSH_HOME = tmpHome;
|
|
|
|
const tmpAiBase = path.join(tmpRoot, "dsh", "node_modules", "@deepseek-ai");
|
|
|
|
const asarIndex = asar ? readAsarIndex(path.resolve(asar)) : null;
|
|
let copied = 0;
|
|
const notFound = [];
|
|
|
|
if (asarIndex) {
|
|
// 归档没解开:先用归档索引在临时目录铺一层 0 字节骨架,
|
|
// 让补丁表的 firstExisting/allExisting 按真实布局解析出路径,再填真实内容。
|
|
for (const rel of asarIndex.entries.keys()) {
|
|
const dest = path.join(tmpRoot, rel.replace(/\//g, path.sep));
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
fs.writeFileSync(dest, Buffer.alloc(0));
|
|
}
|
|
const skeletonFiles = core.targetFiles(tmpAiBase, tmpHome);
|
|
const wanted = new Set();
|
|
for (const value of Object.values(skeletonFiles)) {
|
|
for (const fp of Array.isArray(value) ? value : [value]) {
|
|
if (typeof fp !== "string" || !fp) continue;
|
|
const rel = path.relative(tmpRoot, fp).split(path.sep).join("/");
|
|
if (rel.startsWith("..")) continue;
|
|
if (asarIndex.entries.has(rel)) wanted.add(rel);
|
|
}
|
|
}
|
|
for (const rel of wanted) {
|
|
const entry = asarIndex.entries.get(rel);
|
|
const size = Number(entry.size);
|
|
const buf = Buffer.alloc(size);
|
|
if (size > 0) fs.readSync(asarIndex.fd, buf, 0, size, asarIndex.dataStart + Number(entry.offset));
|
|
fs.writeFileSync(path.join(tmpRoot, rel.replace(/\//g, path.sep)), buf);
|
|
copied += 1;
|
|
}
|
|
fs.closeSync(asarIndex.fd);
|
|
} else {
|
|
const files = core.targetFiles(realAiBase, tmpHome);
|
|
const wanted = new Set();
|
|
for (const value of Object.values(files)) {
|
|
for (const fp of Array.isArray(value) ? value : [value]) {
|
|
if (typeof fp === "string" && fp.startsWith(realAppDir + path.sep)) wanted.add(fp);
|
|
}
|
|
}
|
|
for (const abs of wanted) {
|
|
const rel = abs.slice(realAppDir.length + 1);
|
|
const src = path.join(realAppDir, rel);
|
|
if (!fs.existsSync(src)) {
|
|
notFound.push(rel);
|
|
continue;
|
|
}
|
|
const dest = path.join(tmpRoot, rel);
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
fs.copyFileSync(src, dest);
|
|
copied += 1;
|
|
}
|
|
}
|
|
|
|
const report = await core.applyPatches(tmpAiBase, core.ALL_PATCHES, {});
|
|
const byId = new Map(core.ALL_PATCHES.map((p) => [p.id, p]));
|
|
const bad = report.filter((r) => !core.isPatchResultSoftOk(r, byId.get(r.patch_id)));
|
|
|
|
console.log(`target: ${realAppDir} surface: ${surface}`);
|
|
console.log(`copied ${copied} patch-target files${notFound.length ? `, ${notFound.length} not in this build` : ""}`);
|
|
for (const r of report.filter((r) => r.status !== "applied")) {
|
|
console.log(` ${r.status.padEnd(13)} #${String(r.patch_id).padEnd(2)} ${r.name}`);
|
|
}
|
|
const tally = report.reduce((m, r) => ((m[r.status] = (m[r.status] || 0) + 1), m), {});
|
|
console.log(`patches: ${core.ALL_PATCHES.length} ${JSON.stringify(tally)}`);
|
|
if (jsonOut) fs.writeFileSync(path.resolve(jsonOut), JSON.stringify({ report, notFound }, null, 2));
|
|
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
|
|
const pending = report.filter((r) => r.status === "pending" || r.status === "pattern_not_found");
|
|
if (pending.length) {
|
|
console.log(`\nFAIL: ${pending.length} patch(es) still pending on this build.`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`\nOK: every applicable patch is settled (${bad.length} unexpected).`);
|