Merge branch 'main' into feat-plugin-npm

This commit is contained in:
手瓜一十雪
2026-02-20 21:54:13 +08:00
111 changed files with 4377 additions and 1025 deletions

View File

@@ -26,6 +26,7 @@
"json5": "^2.2.3",
"multer": "^2.0.1",
"napcat-common": "workspace:*",
"napcat-dpapi": "workspace:*",
"napcat-pty": "workspace:*",
"ws": "^8.18.3"
},

View File

@@ -0,0 +1,90 @@
import { RequestHandler } from 'express';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import json5 from 'json5';
import Ajv from 'ajv';
import { NapcatConfigSchema } from '@/napcat-core/helper/config';
// 动态获取 NapCat 配置默认值
function getDefaultNapcatConfig (): Record<string, unknown> {
const ajv = new Ajv({ useDefaults: true, coerceTypes: true });
const validate = ajv.compile(NapcatConfigSchema);
const data = {};
validate(data);
return data;
}
/**
* 获取 napcat 配置文件路径
*/
function getNapcatConfigPath (): string {
return resolve(webUiPathWrapper.configPath, './napcat.json');
}
/**
* 读取 napcat 配置
*/
function readNapcatConfig (): Record<string, unknown> {
const configPath = getNapcatConfigPath();
try {
if (existsSync(configPath)) {
const content = readFileSync(configPath, 'utf-8');
return { ...getDefaultNapcatConfig(), ...json5.parse(content) };
}
} catch (_e) {
// 读取失败,使用默认值
}
return { ...getDefaultNapcatConfig() };
}
/**
* 写入 napcat 配置
*/
function writeNapcatConfig (config: Record<string, unknown>): void {
const configPath = resolve(webUiPathWrapper.configPath, './napcat.json');
mkdirSync(webUiPathWrapper.configPath, { recursive: true });
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
}
// 获取 NapCat 配置
export const NapCatGetConfigHandler: RequestHandler = (_, res) => {
try {
const config = readNapcatConfig();
return sendSuccess(res, config);
} catch (e) {
return sendError(res, 'Config Get Error: ' + (e as Error).message);
}
};
// 设置 NapCat 配置
export const NapCatSetConfigHandler: RequestHandler = (req, res) => {
try {
const newConfig = req.body;
if (!newConfig || typeof newConfig !== 'object') {
return sendError(res, 'config is empty or invalid');
}
// 读取当前配置并合并
const currentConfig = readNapcatConfig();
const mergedConfig = { ...currentConfig, ...newConfig };
// 验证 bypass 字段
if (mergedConfig.bypass && typeof mergedConfig.bypass === 'object') {
const bypass = mergedConfig.bypass as Record<string, unknown>;
const validKeys = ['hook', 'window', 'module', 'process', 'container', 'js'];
for (const key of validKeys) {
if (key in bypass && typeof bypass[key] !== 'boolean') {
return sendError(res, `bypass.${key} must be boolean`);
}
}
}
writeNapcatConfig(mergedConfig);
return sendSuccess(res, null);
} catch (e) {
return sendError(res, 'Config Set Error: ' + (e as Error).message);
}
};

View File

@@ -4,6 +4,36 @@ import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { WebUiConfig } from '@/napcat-webui-backend/index';
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import { Registry20Utils, MachineInfoUtils } from '@/napcat-webui-backend/src/utils/guid';
import os from 'node:os';
// 获取 Registry20 路径的辅助函数
const getRegistryPath = () => {
// 优先从 WebUiDataRuntime 获取早期设置的 dataPath
let dataPath = WebUiDataRuntime.getQQDataPath();
if (!dataPath) {
// 回退: 从 OneBotContext 获取
const oneBotContext = WebUiDataRuntime.getOneBotContext();
dataPath = oneBotContext?.core?.dataPath;
}
if (!dataPath) {
throw new Error('QQ data path not available yet');
}
return Registry20Utils.getRegistryPath(dataPath);
};
// 获取 machine-info 路径的辅助函数 (Linux)
const getMachineInfoPath = () => {
let dataPath = WebUiDataRuntime.getQQDataPath();
if (!dataPath) {
const oneBotContext = WebUiDataRuntime.getOneBotContext();
dataPath = oneBotContext?.core?.dataPath;
}
if (!dataPath) {
throw new Error('QQ data path not available yet');
}
return MachineInfoUtils.getMachineInfoPath(dataPath);
};
// 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
@@ -147,3 +177,239 @@ export const QQPasswordLoginHandler: RequestHandler = async (req, res) => {
}
return sendSuccess(res, null);
};
// 重置设备信息
export const QQResetDeviceIDHandler: RequestHandler = async (_, res) => {
try {
const registryPath = getRegistryPath();
// 自动备份
try {
await Registry20Utils.backup(registryPath);
} catch (e) {
// 忽略备份错误(例如文件不存在)
}
await Registry20Utils.delete(registryPath);
return sendSuccess(res, { message: 'Device ID reset successfully (Registry20 deleted)' });
} catch (e) {
return sendError(res, `Failed to reset Device ID: ${(e as Error).message}`);
}
};
// 获取设备 GUID
export const QQGetDeviceGUIDHandler: RequestHandler = async (_, res) => {
try {
const registryPath = getRegistryPath();
const guid = await Registry20Utils.readGuid(registryPath);
return sendSuccess(res, { guid });
} catch (e) {
// 可能是文件不存在,或者非 Windows 平台,或者解密失败
return sendError(res, `Failed to get GUID: ${(e as Error).message}`);
}
};
// 设置设备 GUID
export const QQSetDeviceGUIDHandler: RequestHandler = async (req, res) => {
const { guid } = req.body;
if (!guid || typeof guid !== 'string' || guid.length !== 32) {
return sendError(res, 'Invalid GUID format, must be 32 hex characters');
}
try {
const registryPath = getRegistryPath();
// 自动备份
try {
await Registry20Utils.backup(registryPath);
} catch { }
await Registry20Utils.writeGuid(registryPath, guid);
return sendSuccess(res, { message: 'GUID set successfully' });
} catch (e) {
return sendError(res, `Failed to set GUID: ${(e as Error).message}`);
}
};
// 获取备份列表
export const QQGetGUIDBackupsHandler: RequestHandler = async (_, res) => {
try {
const registryPath = getRegistryPath();
const backups = Registry20Utils.getBackups(registryPath);
return sendSuccess(res, backups);
} catch (e) {
return sendError(res, `Failed to get backups: ${(e as Error).message}`);
}
};
// 恢复备份
export const QQRestoreGUIDBackupHandler: RequestHandler = async (req, res) => {
const { backupName } = req.body;
if (!backupName) {
return sendError(res, 'Backup name is required');
}
try {
const registryPath = getRegistryPath();
await Registry20Utils.restore(registryPath, backupName);
return sendSuccess(res, { message: 'Restored successfully' });
} catch (e) {
return sendError(res, `Failed to restore: ${(e as Error).message}`);
}
};
// 创建备份
export const QQCreateGUIDBackupHandler: RequestHandler = async (_, res) => {
try {
const registryPath = getRegistryPath();
const backupPath = await Registry20Utils.backup(registryPath);
return sendSuccess(res, { message: 'Backup created', path: backupPath });
} catch (e) {
return sendError(res, `Failed to backup: ${(e as Error).message}`);
}
};
// 重启NapCat
export const QQRestartNapCatHandler: RequestHandler = async (_, res) => {
try {
const result = await WebUiDataRuntime.requestRestartProcess();
if (result.result) {
return sendSuccess(res, { message: result.message || 'Restart initiated' });
} else {
return sendError(res, result.message || 'Restart failed');
}
} catch (e) {
return sendError(res, `Restart error: ${(e as Error).message}`);
}
};
// ============================================================
// 平台信息 & Linux GUID 管理
// ============================================================
// 获取平台信息
export const QQGetPlatformInfoHandler: RequestHandler = async (_, res) => {
return sendSuccess(res, { platform: os.platform() });
};
// 获取 Linux MAC 地址 (从 machine-info 文件读取)
export const QQGetLinuxMACHandler: RequestHandler = async (_, res) => {
try {
const machineInfoPath = getMachineInfoPath();
const mac = MachineInfoUtils.readMac(machineInfoPath);
return sendSuccess(res, { mac });
} catch (e) {
return sendError(res, `Failed to get MAC: ${(e as Error).message}`);
}
};
// 设置 Linux MAC 地址 (写入 machine-info 文件)
export const QQSetLinuxMACHandler: RequestHandler = async (req, res) => {
const { mac } = req.body;
if (!mac || typeof mac !== 'string') {
return sendError(res, 'MAC address is required');
}
try {
const machineInfoPath = getMachineInfoPath();
// 自动备份
try {
MachineInfoUtils.backup(machineInfoPath);
} catch { }
MachineInfoUtils.writeMac(machineInfoPath, mac);
return sendSuccess(res, { message: 'MAC set successfully' });
} catch (e) {
return sendError(res, `Failed to set MAC: ${(e as Error).message}`);
}
};
// 获取 Linux machine-id
export const QQGetLinuxMachineIdHandler: RequestHandler = async (_, res) => {
try {
const machineId = MachineInfoUtils.readMachineId();
return sendSuccess(res, { machineId });
} catch (e) {
return sendError(res, `Failed to read machine-id: ${(e as Error).message}`);
}
};
// 计算 Linux GUID (用于前端实时预览)
export const QQComputeLinuxGUIDHandler: RequestHandler = async (req, res) => {
const { mac, machineId } = req.body;
try {
// 如果没传 machineId从 /etc/machine-id 读取
let mid = machineId;
if (!mid || typeof mid !== 'string') {
try {
mid = MachineInfoUtils.readMachineId();
} catch {
mid = '';
}
}
// 如果没传 mac从 machine-info 文件读取
let macStr = mac;
if (!macStr || typeof macStr !== 'string') {
try {
const machineInfoPath = getMachineInfoPath();
macStr = MachineInfoUtils.readMac(machineInfoPath);
} catch {
macStr = '';
}
}
const guid = MachineInfoUtils.computeGuid(mid, macStr);
return sendSuccess(res, { guid, machineId: mid, mac: macStr });
} catch (e) {
return sendError(res, `Failed to compute GUID: ${(e as Error).message}`);
}
};
// 获取 Linux machine-info 备份列表
export const QQGetLinuxMachineInfoBackupsHandler: RequestHandler = async (_, res) => {
try {
const machineInfoPath = getMachineInfoPath();
const backups = MachineInfoUtils.getBackups(machineInfoPath);
return sendSuccess(res, backups);
} catch (e) {
return sendError(res, `Failed to get backups: ${(e as Error).message}`);
}
};
// 创建 Linux machine-info 备份
export const QQCreateLinuxMachineInfoBackupHandler: RequestHandler = async (_, res) => {
try {
const machineInfoPath = getMachineInfoPath();
const backupPath = MachineInfoUtils.backup(machineInfoPath);
return sendSuccess(res, { message: 'Backup created', path: backupPath });
} catch (e) {
return sendError(res, `Failed to backup: ${(e as Error).message}`);
}
};
// 恢复 Linux machine-info 备份
export const QQRestoreLinuxMachineInfoBackupHandler: RequestHandler = async (req, res) => {
const { backupName } = req.body;
if (!backupName) {
return sendError(res, 'Backup name is required');
}
try {
const machineInfoPath = getMachineInfoPath();
MachineInfoUtils.restore(machineInfoPath, backupName);
return sendSuccess(res, { message: 'Restored successfully' });
} catch (e) {
return sendError(res, `Failed to restore: ${(e as Error).message}`);
}
};
// 重置 Linux 设备信息 (删除 machine-info)
export const QQResetLinuxDeviceIDHandler: RequestHandler = async (_, res) => {
try {
const machineInfoPath = getMachineInfoPath();
// 自动备份
try {
MachineInfoUtils.backup(machineInfoPath);
} catch { }
MachineInfoUtils.delete(machineInfoPath);
return sendSuccess(res, { message: 'Device ID reset successfully (machine-info deleted)' });
} catch (e) {
return sendError(res, `Failed to reset Device ID: ${(e as Error).message}`);
}
};

View File

@@ -16,6 +16,7 @@ const LoginRuntime: LoginRuntimeType = {
},
QQLoginError: '',
QQVersion: 'unknown',
QQDataPath: '',
OneBotContext: null,
onQQLoginStatusChange: async (status: boolean) => {
LoginRuntime.QQLoginStatus = status;
@@ -167,6 +168,14 @@ export const WebUiDataRuntime = {
return LoginRuntime.QQVersion;
},
setQQDataPath (dataPath: string) {
LoginRuntime.QQDataPath = dataPath;
},
getQQDataPath (): string {
return LoginRuntime.QQDataPath;
},
setWebUiConfigQuickFunction (func: LoginRuntimeType['WebUiConfigQuickFunction']): void {
LoginRuntime.WebUiConfigQuickFunction = func;
},

View File

@@ -0,0 +1,12 @@
import { Router } from 'express';
import { NapCatGetConfigHandler, NapCatSetConfigHandler } from '@/napcat-webui-backend/src/api/NapCatConfig';
const router: Router = Router();
// router:获取 NapCat 配置
router.get('/GetConfig', NapCatGetConfigHandler);
// router:设置 NapCat 配置
router.post('/SetConfig', NapCatSetConfigHandler);
export { router as NapCatConfigRouter };

View File

@@ -11,6 +11,22 @@ import {
setAutoLoginAccountHandler,
QQRefreshQRcodeHandler,
QQPasswordLoginHandler,
QQResetDeviceIDHandler,
QQRestartNapCatHandler,
QQGetDeviceGUIDHandler,
QQSetDeviceGUIDHandler,
QQGetGUIDBackupsHandler,
QQRestoreGUIDBackupHandler,
QQCreateGUIDBackupHandler,
QQGetPlatformInfoHandler,
QQGetLinuxMACHandler,
QQSetLinuxMACHandler,
QQGetLinuxMachineIdHandler,
QQComputeLinuxGUIDHandler,
QQGetLinuxMachineInfoBackupsHandler,
QQCreateLinuxMachineInfoBackupHandler,
QQRestoreLinuxMachineInfoBackupHandler,
QQResetLinuxDeviceIDHandler,
} from '@/napcat-webui-backend/src/api/QQLogin';
const router: Router = Router();
@@ -34,5 +50,41 @@ router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
// router:密码登录
router.post('/PasswordLogin', QQPasswordLoginHandler);
// router:重置设备信息
router.post('/ResetDeviceID', QQResetDeviceIDHandler);
// router:重启NapCat
router.post('/RestartNapCat', QQRestartNapCatHandler);
// router:获取设备GUID
router.post('/GetDeviceGUID', QQGetDeviceGUIDHandler);
// router:设置设备GUID
router.post('/SetDeviceGUID', QQSetDeviceGUIDHandler);
// router:获取GUID备份列表
router.post('/GetGUIDBackups', QQGetGUIDBackupsHandler);
// router:恢复GUID备份
router.post('/RestoreGUIDBackup', QQRestoreGUIDBackupHandler);
// router:创建GUID备份
router.post('/CreateGUIDBackup', QQCreateGUIDBackupHandler);
// ============================================================
// 平台信息 & Linux GUID 管理
// ============================================================
// router:获取平台信息
router.post('/GetPlatformInfo', QQGetPlatformInfoHandler);
// router:获取Linux MAC地址
router.post('/GetLinuxMAC', QQGetLinuxMACHandler);
// router:设置Linux MAC地址
router.post('/SetLinuxMAC', QQSetLinuxMACHandler);
// router:获取Linux machine-id
router.post('/GetLinuxMachineId', QQGetLinuxMachineIdHandler);
// router:计算Linux GUID
router.post('/ComputeLinuxGUID', QQComputeLinuxGUIDHandler);
// router:获取Linux machine-info备份列表
router.post('/GetLinuxMachineInfoBackups', QQGetLinuxMachineInfoBackupsHandler);
// router:创建Linux machine-info备份
router.post('/CreateLinuxMachineInfoBackup', QQCreateLinuxMachineInfoBackupHandler);
// router:恢复Linux machine-info备份
router.post('/RestoreLinuxMachineInfoBackup', QQRestoreLinuxMachineInfoBackupHandler);
// router:重置Linux设备信息
router.post('/ResetLinuxDeviceID', QQResetLinuxDeviceIDHandler);
export { router as QQLoginRouter };

View File

@@ -19,6 +19,7 @@ import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
import { ProcessRouter } from './Process';
import { PluginRouter } from './Plugin';
import { MirrorRouter } from './Mirror';
import { NapCatConfigRouter } from './NapCatConfig';
const router: Router = Router();
@@ -53,5 +54,7 @@ router.use('/Process', ProcessRouter);
router.use('/Plugin', PluginRouter);
// router:镜像管理相关路由
router.use('/Mirror', MirrorRouter);
// router:NapCat配置相关路由
router.use('/NapCatConfig', NapCatConfigRouter);
export { router as ALLRouter };

View File

@@ -49,6 +49,7 @@ export interface LoginRuntimeType {
QQLoginInfo: SelfInfo;
QQLoginError: string;
QQVersion: string;
QQDataPath: string;
onQQLoginStatusChange: (status: boolean) => Promise<void>;
onWebUiTokenChange: (token: string) => Promise<void>;
onRefreshQRCode: () => Promise<void>;

View File

@@ -0,0 +1,275 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
import { protectData, unprotectData } from 'napcat-dpapi';
const GUID_HEADER = Buffer.from([0x00, 0x00, 0x00, 0x14]);
const XOR_KEY = 0x10;
/**
* Unprotects data using Windows DPAPI via napcat-dpapi.
*/
function dpapiUnprotect (filePath: string): Buffer {
const encrypted = fs.readFileSync(filePath);
return Buffer.from(unprotectData(encrypted, null, 'CurrentUser'));
}
/**
* Protects data using Windows DPAPI and writes to file.
*/
function dpapiProtectAndWrite (filePath: string, data: Buffer): void {
const encrypted = protectData(data, null, 'CurrentUser');
fs.writeFileSync(filePath, Buffer.from(encrypted));
}
export class Registry20Utils {
static getRegistryPath (dataPath: string): string {
return path.join(dataPath, 'nt_qq', 'global', 'nt_data', 'msf', 'Registry20');
}
static readGuid (registryPath: string): string {
if (!fs.existsSync(registryPath)) {
throw new Error('Registry20 file not found');
}
if (os.platform() !== 'win32') {
throw new Error('Registry20 decryption is only supported on Windows');
}
const decrypted = dpapiUnprotect(registryPath);
if (decrypted.length < 20) {
throw new Error(`Decrypted data too short (got ${decrypted.length} bytes, need 20)`);
}
// Decode payload: header(4) + obfuscated_guid(16)
const payload = decrypted.subarray(4, 20);
const guidBuf = Buffer.alloc(16);
for (let i = 0; i < 16; i++) {
const payloadByte = payload[i] ?? 0;
guidBuf[i] = (~(payloadByte ^ XOR_KEY)) & 0xFF;
}
return guidBuf.toString('hex');
}
static writeGuid (registryPath: string, guidHex: string): void {
if (guidHex.length !== 32) {
throw new Error('Invalid GUID length, must be 32 hex chars');
}
if (os.platform() !== 'win32') {
throw new Error('Registry20 encryption is only supported on Windows');
}
const guidBytes = Buffer.from(guidHex, 'hex');
const payload = Buffer.alloc(16);
for (let i = 0; i < 16; i++) {
const guidByte = guidBytes[i] ?? 0;
payload[i] = XOR_KEY ^ (~guidByte & 0xFF);
}
const data = Buffer.concat([GUID_HEADER, payload]);
// Create directory if not exists
const dir = path.dirname(registryPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
dpapiProtectAndWrite(registryPath, data);
}
static getBackups (registryPath: string): string[] {
const dir = path.dirname(registryPath);
const baseName = path.basename(registryPath);
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.startsWith(`${baseName}.bak.`))
.sort()
.reverse();
}
static backup (registryPath: string): string {
if (!fs.existsSync(registryPath)) {
throw new Error('Registry20 does not exist');
}
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
const backupPath = `${registryPath}.bak.${timestamp}`;
fs.copyFileSync(registryPath, backupPath);
return backupPath;
}
static restore (registryPath: string, backupFileName: string): void {
const dir = path.dirname(registryPath);
const backupPath = path.join(dir, backupFileName);
if (!fs.existsSync(backupPath)) {
throw new Error('Backup file not found');
}
fs.copyFileSync(backupPath, registryPath);
}
static delete (registryPath: string): void {
if (fs.existsSync(registryPath)) {
fs.unlinkSync(registryPath);
}
}
}
// ============================================================
// Linux machine-info 工具类
// ============================================================
/**
* ROT13 编解码 (自逆运算)
* 字母偏移13位数字和符号不变
*/
function rot13 (s: string): string {
return s.replace(/[a-zA-Z]/g, (c) => {
const base = c <= 'Z' ? 65 : 97;
return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
});
}
/**
* Linux 平台 machine-info 文件工具类
*
* 文件格式 (逆向自 machine_guid_util.cc):
* [4字节 BE uint32 长度 N] [N字节 ROT13 编码的 MAC 字符串]
* - MAC 格式: xx-xx-xx-xx-xx-xx (17 字符)
* - ROT13: 字母偏移13位, 数字和 '-' 不变
*
* GUID 生成算法:
* GUID = MD5( /etc/machine-id + MAC地址 )
*/
export class MachineInfoUtils {
/**
* 获取 machine-info 文件路径
*/
static getMachineInfoPath (dataPath: string): string {
return path.join(dataPath, 'nt_qq', 'global', 'nt_data', 'msf', 'machine-info');
}
/**
* 从 machine-info 文件读取 MAC 地址
*/
static readMac (machineInfoPath: string): string {
if (!fs.existsSync(machineInfoPath)) {
throw new Error('machine-info file not found');
}
const data = fs.readFileSync(machineInfoPath);
if (data.length < 4) {
throw new Error(`machine-info data too short: ${data.length} < 4 bytes`);
}
const length = data.readUInt32BE(0);
if (length >= 18) {
throw new Error(`MAC string length abnormal: ${length} >= 18`);
}
if (data.length < 4 + length) {
throw new Error(`machine-info data incomplete: need ${4 + length} bytes, got ${data.length}`);
}
const rot13Str = data.subarray(4, 4 + length).toString('ascii');
return rot13(rot13Str);
}
/**
* 将 MAC 地址写入 machine-info 文件
*/
static writeMac (machineInfoPath: string, mac: string): void {
mac = mac.trim().toLowerCase();
// 验证 MAC 格式: xx-xx-xx-xx-xx-xx
if (!/^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/.test(mac)) {
throw new Error('Invalid MAC format, must be xx-xx-xx-xx-xx-xx');
}
const encoded = rot13(mac);
const length = encoded.length;
const buf = Buffer.alloc(4 + length);
buf.writeUInt32BE(length, 0);
buf.write(encoded, 4, 'ascii');
// 确保目录存在
const dir = path.dirname(machineInfoPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(machineInfoPath, buf);
}
/**
* 读取 /etc/machine-id
*/
static readMachineId (): string {
const machineIdPath = '/etc/machine-id';
if (!fs.existsSync(machineIdPath)) {
throw new Error('/etc/machine-id not found');
}
return fs.readFileSync(machineIdPath, 'utf-8').trim();
}
/**
* 计算 Linux GUID = MD5(machine-id + MAC)
*/
static computeGuid (machineId: string, mac: string): string {
const md5 = crypto.createHash('md5');
md5.update(machineId, 'ascii');
md5.update(mac, 'ascii');
return md5.digest('hex');
}
/**
* 获取备份列表
*/
static getBackups (machineInfoPath: string): string[] {
const dir = path.dirname(machineInfoPath);
const baseName = path.basename(machineInfoPath);
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.startsWith(`${baseName}.bak.`))
.sort()
.reverse();
}
/**
* 创建备份
*/
static backup (machineInfoPath: string): string {
if (!fs.existsSync(machineInfoPath)) {
throw new Error('machine-info file does not exist');
}
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
const backupPath = `${machineInfoPath}.bak.${timestamp}`;
fs.copyFileSync(machineInfoPath, backupPath);
return backupPath;
}
/**
* 恢复备份
*/
static restore (machineInfoPath: string, backupFileName: string): void {
const dir = path.dirname(machineInfoPath);
const backupPath = path.join(dir, backupFileName);
if (!fs.existsSync(backupPath)) {
throw new Error('Backup file not found');
}
fs.copyFileSync(backupPath, machineInfoPath);
}
/**
* 删除 machine-info
*/
static delete (machineInfoPath: string): void {
if (fs.existsSync(machineInfoPath)) {
fs.unlinkSync(machineInfoPath);
}
}
}