mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-13 00:10:27 +00:00
Add Linux machine-info GUID management
Add end-to-end support for reading/writing Linux machine-info and computing GUIDs. Backend: - Introduce MachineInfoUtils (TS) for machine-info path lookup, ROT13 serialization, read/write/delete, backups, and MD5-based GUID computation. - Add a Python utility (guid.py) for CLI inspection, encode/decode, dump, and GUID computation. - Extend QQLogin API with new handlers: GetPlatformInfo, GetLinuxMAC, SetLinuxMAC, GetLinuxMachineId, ComputeLinuxGUID, GetLinuxMachineInfoBackups, CreateLinuxMachineInfoBackup, RestoreLinuxMachineInfoBackup, ResetLinuxDeviceID. Handlers include automatic backup behavior and error handling. Router: Register new QQLogin routes for platform info and Linux machine-info operations. Frontend: - Enhance guid_manager UI to detect platform and provide Linux-specific workflow (display machine-id, show/edit MAC, preview computed GUID via MD5, backup/restore/delete machine-info, and restart actions). - Add client-side MD5 (crypto-js) usage and new QQManager API methods to call the new backend endpoints. This change enables cross-platform GUID management (Windows and Linux), includes CLI tooling for low-level inspection, and adds frontend workflows for Linux device-id management.
This commit is contained in:
@@ -4,7 +4,8 @@ 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 } from '@/napcat-webui-backend/src/utils/guid';
|
||||
import { Registry20Utils, MachineInfoUtils } from '@/napcat-webui-backend/src/utils/guid';
|
||||
import os from 'node:os';
|
||||
|
||||
// 获取 Registry20 路径的辅助函数
|
||||
const getRegistryPath = () => {
|
||||
@@ -21,6 +22,19 @@ const getRegistryPath = () => {
|
||||
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) => {
|
||||
// 判断是否已经登录
|
||||
@@ -265,4 +279,137 @@ export const QQRestartNapCatHandler: RequestHandler = async (_, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 平台信息 & 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}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,15 @@ import {
|
||||
QQGetGUIDBackupsHandler,
|
||||
QQRestoreGUIDBackupHandler,
|
||||
QQCreateGUIDBackupHandler,
|
||||
QQGetPlatformInfoHandler,
|
||||
QQGetLinuxMACHandler,
|
||||
QQSetLinuxMACHandler,
|
||||
QQGetLinuxMachineIdHandler,
|
||||
QQComputeLinuxGUIDHandler,
|
||||
QQGetLinuxMachineInfoBackupsHandler,
|
||||
QQCreateLinuxMachineInfoBackupHandler,
|
||||
QQRestoreLinuxMachineInfoBackupHandler,
|
||||
QQResetLinuxDeviceIDHandler,
|
||||
} from '@/napcat-webui-backend/src/api/QQLogin';
|
||||
|
||||
const router: Router = Router();
|
||||
@@ -56,4 +65,26 @@ 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 };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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]);
|
||||
@@ -114,3 +115,161 @@ export class Registry20Utils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { Divider } from '@heroui/divider';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Listbox, ListboxItem } from '@heroui/listbox';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { MdContentCopy, MdDelete, MdRefresh, MdSave, MdRestorePage, MdBackup } from 'react-icons/md';
|
||||
import MD5 from 'crypto-js/md5';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
@@ -20,15 +21,59 @@ interface GUIDManagerProps {
|
||||
|
||||
const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact = false }) => {
|
||||
const dialog = useDialog();
|
||||
|
||||
// 平台检测
|
||||
const [platform, setPlatform] = useState<string>('');
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform !== '' && platform !== 'win32' && platform !== 'darwin';
|
||||
const platformDetected = platform !== '';
|
||||
|
||||
// Windows 状态
|
||||
const [currentGUID, setCurrentGUID] = useState<string>('');
|
||||
const [inputGUID, setInputGUID] = useState<string>('');
|
||||
const [backups, setBackups] = useState<string[]>([]);
|
||||
|
||||
// Linux 状态
|
||||
const [currentMAC, setCurrentMAC] = useState<string>('');
|
||||
const [inputMAC, setInputMAC] = useState<string>('');
|
||||
const [machineId, setMachineId] = useState<string>('');
|
||||
const [linuxBackups, setLinuxBackups] = useState<string[]>([]);
|
||||
|
||||
// 通用状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
|
||||
const isValidGUID = (guid: string) => /^[0-9a-fA-F]{32}$/.test(guid);
|
||||
const isValidMAC = (mac: string) => /^[0-9a-fA-F]{2}(-[0-9a-fA-F]{2}){5}$/.test(mac.trim().toLowerCase());
|
||||
|
||||
// 前端实时计算 Linux GUID = MD5(machine-id + MAC)
|
||||
const computedLinuxGUID = useMemo(() => {
|
||||
if (!isLinux) return '';
|
||||
const mac = inputMAC.trim().toLowerCase();
|
||||
if (!machineId && !mac) return '';
|
||||
return MD5(machineId + mac).toString();
|
||||
}, [isLinux, machineId, inputMAC]);
|
||||
|
||||
// 当前生效的 GUID (基于已保存的 MAC)
|
||||
const currentLinuxGUID = useMemo(() => {
|
||||
if (!isLinux || !currentMAC) return '';
|
||||
return MD5(machineId + currentMAC).toString();
|
||||
}, [isLinux, machineId, currentMAC]);
|
||||
|
||||
// 检测平台
|
||||
const fetchPlatform = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getPlatformInfo();
|
||||
setPlatform(data.platform);
|
||||
} catch {
|
||||
// 如果获取失败,默认 win32 向后兼容
|
||||
setPlatform('win32');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Windows: 获取 GUID
|
||||
const fetchGUID = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -47,6 +92,7 @@ const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact =
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Windows: 获取备份
|
||||
const fetchBackups = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getGUIDBackups();
|
||||
@@ -56,14 +102,56 @@ const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact =
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Linux: 获取 MAC + machine-id
|
||||
const fetchLinuxInfo = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [macData, midData] = await Promise.all([
|
||||
QQManager.getLinuxMAC().catch(() => ({ mac: '' })),
|
||||
QQManager.getLinuxMachineId().catch(() => ({ machineId: '' })),
|
||||
]);
|
||||
setCurrentMAC(macData.mac);
|
||||
setInputMAC(macData.mac);
|
||||
setMachineId(midData.machineId);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`获取设备信息失败: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Linux: 获取备份
|
||||
const fetchLinuxBackups = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getLinuxMachineInfoBackups();
|
||||
setLinuxBackups(data);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGUID();
|
||||
fetchBackups();
|
||||
}, [fetchGUID, fetchBackups]);
|
||||
fetchPlatform();
|
||||
}, [fetchPlatform]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!platformDetected) return;
|
||||
if (isWindows) {
|
||||
fetchGUID();
|
||||
fetchBackups();
|
||||
} else {
|
||||
fetchLinuxInfo();
|
||||
fetchLinuxBackups();
|
||||
}
|
||||
}, [platformDetected, isWindows, fetchGUID, fetchBackups, fetchLinuxInfo, fetchLinuxBackups]);
|
||||
|
||||
// ========== Windows 操作 ==========
|
||||
|
||||
const handleCopy = () => {
|
||||
if (currentGUID) {
|
||||
navigator.clipboard.writeText(currentGUID);
|
||||
const guid = isLinux ? currentLinuxGUID : currentGUID;
|
||||
if (guid) {
|
||||
navigator.clipboard.writeText(guid);
|
||||
toast.success('已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
@@ -139,6 +227,89 @@ const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact =
|
||||
});
|
||||
};
|
||||
|
||||
// ========== Linux 操作 ==========
|
||||
|
||||
const handleLinuxSaveMAC = async () => {
|
||||
const mac = inputMAC.trim().toLowerCase();
|
||||
if (!isValidMAC(mac)) {
|
||||
toast.error('MAC 格式无效,需要 xx-xx-xx-xx-xx-xx 格式');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await QQManager.setLinuxMAC(mac);
|
||||
setCurrentMAC(mac);
|
||||
toast.success('MAC 已设置,重启后生效');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`设置 MAC 失败: ${msg}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxCopyMAC = () => {
|
||||
if (currentMAC) {
|
||||
navigator.clipboard.writeText(currentMAC);
|
||||
toast.success('MAC 已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxDelete = () => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除 machine-info 后,QQ 将在下次启动时生成新的设备标识。确定要删除吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.resetLinuxDeviceID();
|
||||
setCurrentMAC('');
|
||||
setInputMAC('');
|
||||
toast.success('已删除,重启后生效');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`删除失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLinuxBackup = async () => {
|
||||
try {
|
||||
await QQManager.createLinuxMachineInfoBackup();
|
||||
toast.success('备份已创建');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`备份失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxRestore = (backupName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认恢复',
|
||||
content: `确定要从备份 "${backupName}" 恢复吗?当前的 machine-info 将被覆盖。`,
|
||||
confirmText: '恢复',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.restoreLinuxMachineInfoBackup(backupName);
|
||||
toast.success('已恢复,重启后生效');
|
||||
await fetchLinuxInfo();
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`恢复失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 重启 ==========
|
||||
|
||||
const handleRestart = () => {
|
||||
dialog.confirm({
|
||||
title: '确认重启',
|
||||
@@ -160,7 +331,7 @@ const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact =
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (loading || !platformDetected) {
|
||||
return (
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<Spinner label='加载中...' />
|
||||
@@ -168,6 +339,232 @@ const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact =
|
||||
);
|
||||
}
|
||||
|
||||
// ========== macOS 不支持 ==========
|
||||
if (isMac) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
<div className='flex flex-col items-center justify-center py-8 gap-2'>
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
macOS 平台暂不支持 GUID 管理
|
||||
</Chip>
|
||||
<div className='text-xs text-default-400'>
|
||||
该功能仅适用于 Windows 和 Linux 平台
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Linux 渲染 ==========
|
||||
if (isLinux) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
{/* 当前设备信息 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前设备 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentLinuxGUID ? (
|
||||
<Chip variant='flat' color='primary' className='font-mono text-xs max-w-full'>
|
||||
{currentLinuxGUID}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentLinuxGUID && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleCopy}
|
||||
aria-label='复制GUID'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={fetchLinuxInfo}
|
||||
aria-label='刷新'
|
||||
>
|
||||
<MdRefresh size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
GUID = MD5(machine-id + MAC),修改 MAC 即可改变 GUID
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* machine-id 显示 */}
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='text-sm font-medium text-default-700'>Machine ID</div>
|
||||
<Chip variant='flat' color='default' className='font-mono text-xs max-w-full'>
|
||||
{machineId || '未知'}
|
||||
</Chip>
|
||||
<div className='text-xs text-default-400'>
|
||||
来自 /etc/machine-id,不可修改
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 当前 MAC 显示 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前 MAC 地址</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentMAC ? (
|
||||
<Chip variant='flat' color='secondary' className='font-mono text-xs max-w-full'>
|
||||
{currentMAC}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentMAC && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleLinuxCopyMAC}
|
||||
aria-label='复制MAC'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 编辑 MAC 地址 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>设置 MAC 地址</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='bordered'
|
||||
placeholder='xx-xx-xx-xx-xx-xx'
|
||||
value={inputMAC}
|
||||
onValueChange={setInputMAC}
|
||||
isInvalid={inputMAC.length > 0 && !isValidMAC(inputMAC)}
|
||||
errorMessage={inputMAC.length > 0 && !isValidMAC(inputMAC) ? '格式: xx-xx-xx-xx-xx-xx' : undefined}
|
||||
classNames={{
|
||||
input: 'font-mono text-sm',
|
||||
}}
|
||||
maxLength={17}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 实时 GUID 预览 */}
|
||||
{inputMAC && isValidMAC(inputMAC) && (
|
||||
<div className='flex flex-col gap-1 p-2 rounded-lg bg-default-100'>
|
||||
<div className='text-xs font-medium text-default-500'>预览 GUID</div>
|
||||
<div className='font-mono text-xs text-primary break-all'>
|
||||
{computedLinuxGUID}
|
||||
</div>
|
||||
{computedLinuxGUID !== currentLinuxGUID && (
|
||||
<div className='text-xs text-warning-500'>
|
||||
与当前 GUID 不同,保存后重启生效
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
isLoading={saving}
|
||||
isDisabled={!isValidMAC(inputMAC) || inputMAC.trim().toLowerCase() === currentMAC}
|
||||
onPress={handleLinuxSaveMAC}
|
||||
startContent={<MdSave size={16} />}
|
||||
>
|
||||
保存 MAC
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='flat'
|
||||
isDisabled={!currentMAC}
|
||||
onPress={handleLinuxDelete}
|
||||
startContent={<MdDelete size={16} />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='secondary'
|
||||
variant='flat'
|
||||
isDisabled={!currentMAC}
|
||||
onPress={handleLinuxBackup}
|
||||
startContent={<MdBackup size={16} />}
|
||||
>
|
||||
手动备份
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
修改 MAC 后 GUID 将变化,需重启 NapCat 才能生效,操作前会自动备份
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备份恢复 */}
|
||||
{linuxBackups.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>
|
||||
备份列表
|
||||
<span className='text-xs text-default-400 ml-2'>(点击恢复)</span>
|
||||
</div>
|
||||
<div className='max-h-[160px] overflow-y-auto rounded-lg border border-default-200'>
|
||||
<Listbox
|
||||
aria-label='备份列表'
|
||||
selectionMode='none'
|
||||
onAction={(key) => handleLinuxRestore(key as string)}
|
||||
>
|
||||
{linuxBackups.map((name) => (
|
||||
<ListboxItem
|
||||
key={name}
|
||||
startContent={<MdRestorePage size={16} className='text-default-400' />}
|
||||
className='font-mono text-xs'
|
||||
>
|
||||
{name}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 重启 */}
|
||||
{showRestart && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
size='sm'
|
||||
color='warning'
|
||||
variant='flat'
|
||||
isLoading={restarting}
|
||||
onPress={handleRestart}
|
||||
startContent={<MdRefresh size={16} />}
|
||||
>
|
||||
重启 NapCat
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Windows 渲染 ==========
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
{/* 当前 GUID 显示 */}
|
||||
|
||||
@@ -132,5 +132,51 @@ export default class QQManager {
|
||||
const data = await serverRequest.post<ServerResponse<{ path: string; }>>('/QQLogin/CreateGUIDBackup');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 平台信息 & Linux GUID 管理
|
||||
// ============================================================
|
||||
|
||||
public static async getPlatformInfo () {
|
||||
const data = await serverRequest.post<ServerResponse<{ platform: string; }>>('/QQLogin/GetPlatformInfo');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async getLinuxMAC () {
|
||||
const data = await serverRequest.post<ServerResponse<{ mac: string; }>>('/QQLogin/GetLinuxMAC');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async setLinuxMAC (mac: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetLinuxMAC', { mac });
|
||||
}
|
||||
|
||||
public static async getLinuxMachineId () {
|
||||
const data = await serverRequest.post<ServerResponse<{ machineId: string; }>>('/QQLogin/GetLinuxMachineId');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async computeLinuxGUID (mac?: string, machineId?: string) {
|
||||
const data = await serverRequest.post<ServerResponse<{ guid: string; machineId: string; mac: string; }>>('/QQLogin/ComputeLinuxGUID', { mac, machineId });
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async getLinuxMachineInfoBackups () {
|
||||
const data = await serverRequest.post<ServerResponse<string[]>>('/QQLogin/GetLinuxMachineInfoBackups');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async createLinuxMachineInfoBackup () {
|
||||
const data = await serverRequest.post<ServerResponse<{ path: string; }>>('/QQLogin/CreateLinuxMachineInfoBackup');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async restoreLinuxMachineInfoBackup (backupName: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestoreLinuxMachineInfoBackup', { backupName });
|
||||
}
|
||||
|
||||
public static async resetLinuxDeviceID () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/ResetLinuxDeviceID');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user