/** * 离线迁移脚本:把 dev Local Storage leveldb 里所有 * http://localhost:5173\x00\x01 前缀的 key * 替换成 * app://netcatty\x00\x01 * * 运行时机:绿色便携版打包完、给用户之前执行一次。 * 之后 Electron 启动时直接就能读正确 origin 的数据。 * * 用法:node scripts/migratePortableLocalStorage.cjs * 例:node scripts/migratePortableLocalStorage.cjs release-green\win-unpacked\data * * 需要:level 包(npm i --no-save level) */ const fs = require("node:fs"); const path = require("node:path"); const { Level } = require("level"); const DEV_ORIGIN = "http://localhost:5173"; const PKG_ORIGIN = "app://netcatty"; const KEY_SEP = Buffer.from([0x00, 0x01]); // Chromium Local Storage leveldb key 格式: // _\x00\x01 // 注意:origin 前有个下划线前缀! const DEV_ORIGIN_FULL = "_" + DEV_ORIGIN; const PKG_ORIGIN_FULL = "_" + PKG_ORIGIN; function findSep(keyBuf, from) { for (let i = from; i < keyBuf.length - 1; i++) { if (keyBuf[i] === 0x00 && keyBuf[i + 1] === 0x01) return i; } return -1; } async function main() { const dataDir = process.argv[2]; if (!dataDir) { console.error("usage: node migratePortableLocalStorage.cjs "); process.exit(2); } const lsDir = path.join(path.resolve(dataDir), "Local Storage", "leveldb"); if (!fs.existsSync(lsDir)) { console.error("leveldb dir not found:", lsDir); process.exit(2); } // 先看看 origin 统计 const db = new Level(lsDir, { keyEncoding: "buffer", valueEncoding: "buffer", createIfMissing: false }); const counts = new Map(); // origin -> count const ops = []; let scanned = 0, migrated = 0; for await (const [keyBuf, valBuf] of db.iterator()) { scanned++; const sepIdx = findSep(keyBuf, 0); if (sepIdx < 0) continue; const origin = keyBuf.subarray(0, sepIdx).toString("utf-8"); counts.set(origin, (counts.get(origin) || 0) + 1); if (origin === DEV_ORIGIN_FULL) { // 新 key:_app://netcatty + \x00\x01 + actualKey const newKey = Buffer.concat([Buffer.from(PKG_ORIGIN_FULL, "utf-8"), KEY_SEP, keyBuf.subarray(sepIdx + 2)]); ops.push({ type: "put", key: newKey, value: valBuf }); ops.push({ type: "del", key: keyBuf }); migrated++; } } await db.close(); console.log(`扫描 ${scanned} 个 key,origin 分布:`); for (const [o, c] of counts) console.log(` ${o || "(无 origin)"} → ${c}`); if (migrated === 0) { console.log("\n没有 http://localhost:5173 前缀的 key 需要迁移。跳过。"); return; } // 备份当前 leveldb const bak = lsDir + ".bak_" + new Date().toISOString().replace(/[:.]/g, "-"); fs.cpSync(lsDir, bak, { recursive: true }); console.log(`\n已备份 leveldb → ${bak}`); // 执行迁移 const db2 = new Level(lsDir, { keyEncoding: "buffer", valueEncoding: "buffer", createIfMissing: false }); await db2.batch(ops); await db2.close(); console.log(`迁移完成:${migrated} 个 key 从 ${DEV_ORIGIN} → ${PKG_ORIGIN}`); // 验证 const db3 = new Level(lsDir, { keyEncoding: "buffer", valueEncoding: "buffer", createIfMissing: false }); const after = new Map(); for await (const [k] of db3.iterator()) { const sep = findSep(k, 0); if (sep >= 0) { const o = k.subarray(0, sep).toString("utf-8"); after.set(o, (after.get(o) || 0) + 1); } } await db3.close(); console.log("\n迁移后 origin 分布:"); for (const [o, c] of after) console.log(` ${o || "(无)"} → ${c}`); } main().catch(e => { console.error("FATAL:", e.message); process.exit(1); });