## 架构重构
- useAsyncData 集成 AbortController:cleanup 时真正中断 HTTP 请求,
不再仅丢弃响应(解决 StrictMode 双重请求问题)
- API 模块统一 signal?: AbortSignal 参数,透传 axios config
- use-api.ts 成为数据 Hook 集中管理中心,新增 useAccountInfo、
useBillingBalance、useBillingLedger
- 页面组件改为纯渲染:const {data,loading,error,refetch} = useXxx()
## 新功能
- Mac 硬件指纹采集(electron/preload/device.ts):
ioreg(UUID) + sysctl(CPU) + system_profiler(HDD三级回退)
- 账单页面 BillingInfo(余额卡片 + 账单表格 + 分页)
- PageDispatcher 注册 /billing 页面(FloatingPanel 960×680)
- 共享工具函数 centToYuan / formatDate(src/utils/display.ts)
## Bug 修复
- Token 刷新容错:网络错误时保留 refresh_token,等网络恢复后重试;
仅 RefreshError(token 过期)时清除(auth-token.ts)
- AppProvider 自动登录时 nullable 字段默认值(email→''、admin_permissions→[])
- AccountInfo 重构消除 ~40 行手写状态管理代码
## 代码质量
- AccountInfo 消除 centToYuan/formatDate 重复定义
- BillingInfo 标题层级精简(移除冗余页面/Card标题)
- antd Table align 字面量类型修正('left' as const)
- billing.ts signal 参数位置修复(params→config)
- modelFirstFetchDone 模块级变量移除(StrictMode 不安全)
373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
/**
|
||
* 设备指纹采集 — TypeScript 实现
|
||
*
|
||
* 与 device.py 逻辑一致,保留原 Python 方法不变。
|
||
* 通过采集 UUID / CPU / HDD 三项硬件 ID,拼接后做 SHA256 得到 device_id。
|
||
*
|
||
* 适用环境:
|
||
* - Node.js / Electron(Windows):通过 child_process 执行 wmic / powershell 采集硬件 ID
|
||
* - Node.js / Electron(macOS) :通过 ioreg / sysctl / system_profiler 采集硬件 ID
|
||
* - 浏览器:仅提供 SHA256 哈希工具函数,硬件采集需由上层注入
|
||
*/
|
||
|
||
import * as crypto from "node:crypto";
|
||
import { spawnSync } from "node:child_process";
|
||
import { platform } from "node:os";
|
||
|
||
// ============================================================
|
||
// 类型定义
|
||
// ============================================================
|
||
|
||
/** 可采集的硬件组件类型 */
|
||
export type Component = "uuid" | "cpu" | "hdd";
|
||
|
||
/** 查询方式 */
|
||
export type QueryMethod = "auto" | "ps" | "powershell" | "wmic";
|
||
|
||
/** 内部解析后的方法 */
|
||
type ResolvedMethod = "auto" | "powershell" | "wmic";
|
||
|
||
// ============================================================
|
||
// 查询配置(对应 Python _COMPONENT_QUERIES)
|
||
// ============================================================
|
||
|
||
interface ComponentQuery {
|
||
powershell: string;
|
||
wmicArgs: string[];
|
||
headerTokens: string[];
|
||
}
|
||
|
||
const COMPONENT_QUERIES: Record<Component, ComponentQuery> = {
|
||
uuid: {
|
||
powershell:
|
||
"Get-CimInstance -Class Win32_ComputerSystemProduct | " +
|
||
"Select-Object -ExpandProperty UUID",
|
||
wmicArgs: ["wmic", "csproduct", "get", "uuid"],
|
||
headerTokens: ["uuid"],
|
||
},
|
||
cpu: {
|
||
powershell:
|
||
"Get-CimInstance -Class Win32_Processor | " +
|
||
"Select-Object -ExpandProperty ProcessorId",
|
||
wmicArgs: ["wmic", "cpu", "get", "processorid"],
|
||
headerTokens: ["processorid"],
|
||
},
|
||
hdd: {
|
||
powershell:
|
||
"Get-CimInstance -Class Win32_DiskDrive | " +
|
||
"Select-Object -First 1 -ExpandProperty SerialNumber",
|
||
wmicArgs: ["wmic", "diskdrive", "get", "serialnumber"],
|
||
headerTokens: ["serialnumber"],
|
||
},
|
||
};
|
||
|
||
// ============================================================
|
||
// Mac 查询配置
|
||
// ============================================================
|
||
|
||
interface MacComponentQuery {
|
||
command: string[];
|
||
/** 从命令 stdout 中提取目标值 */
|
||
extract: (output: string) => string;
|
||
}
|
||
|
||
const MAC_COMPONENT_QUERIES: Record<Component, MacComponentQuery> = {
|
||
uuid: {
|
||
command: ['ioreg', '-d2', '-c', 'IOPlatformExpertDevice'],
|
||
extract: (output: string): string => {
|
||
const m = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
||
return m?.[1] ?? '';
|
||
},
|
||
},
|
||
cpu: {
|
||
command: ['sysctl', '-n', 'hw.model'],
|
||
extract: (output: string): string => output.trim(),
|
||
},
|
||
hdd: {
|
||
command: ['system_profiler', 'SPNVMeDataType'],
|
||
extract: (output: string): string => {
|
||
const m = output.match(/Serial Number:\s*(.+)/i);
|
||
return m?.[1]?.trim() ?? '';
|
||
},
|
||
},
|
||
};
|
||
|
||
// ============================================================
|
||
// 工具函数
|
||
// ============================================================
|
||
|
||
/** 对应 Python: _normalize_identifier(value) -> str */
|
||
function normalizeIdentifier(value: string): string {
|
||
if (!value) return "";
|
||
return value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
||
}
|
||
|
||
/** 对应 Python: _first_value_from_output(raw_output, header_tokens) -> str */
|
||
function firstValueFromOutput(
|
||
rawOutput: string,
|
||
headerTokens: readonly string[],
|
||
): string {
|
||
if (!rawOutput) return "";
|
||
const headers = new Set(headerTokens.map((t) => t.toLowerCase()));
|
||
for (const line of rawOutput.split(/\r?\n/)) {
|
||
const candidate = line.trim();
|
||
if (!candidate) continue;
|
||
if (headers.has(candidate.toLowerCase())) continue;
|
||
return candidate;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/** 对应 Python: _resolve_method(method) -> _ResolvedMethod */
|
||
function resolveMethod(method: QueryMethod): ResolvedMethod {
|
||
if (method === "ps") return "powershell";
|
||
return method;
|
||
}
|
||
|
||
/** 计算 SHA256 哈希(hex 大写),对应 Python hashlib.sha256(...).hexdigest().upper() */
|
||
function sha256HexUpper(input: string): string {
|
||
return crypto.createHash("sha256").update(input, "utf-8").digest("hex").toUpperCase();
|
||
}
|
||
|
||
// ============================================================
|
||
// 环境检测
|
||
// ============================================================
|
||
|
||
function isWindows(): boolean {
|
||
return platform() === "win32";
|
||
}
|
||
|
||
function isMacOS(): boolean {
|
||
return platform() === "darwin";
|
||
}
|
||
|
||
function isWmicAvailable(): boolean {
|
||
if (!isWindows()) return false;
|
||
const result = spawnSync("where", ["wmic"], {
|
||
timeout: 3000,
|
||
stdio: "ignore",
|
||
windowsHide: true,
|
||
});
|
||
return result.status === 0;
|
||
}
|
||
|
||
function isPowershellAvailable(): boolean {
|
||
if (!isWindows()) return false;
|
||
const result = spawnSync("where", ["powershell"], {
|
||
timeout: 3000,
|
||
stdio: "ignore",
|
||
windowsHide: true,
|
||
});
|
||
return result.status === 0;
|
||
}
|
||
|
||
// ============================================================
|
||
// 进程执行
|
||
// ============================================================
|
||
|
||
/**
|
||
* 执行命令并返回 stdout(去除首尾空白)。
|
||
* 对应 Python: _run_process(args, timeout=5.0) -> str
|
||
*/
|
||
function runProcess(args: string[], timeoutSec: number = 5.0): string {
|
||
const command = args[0];
|
||
const cmdArgs = args.slice(1);
|
||
try {
|
||
const result = spawnSync(command, cmdArgs, {
|
||
timeout: Math.round(timeoutSec * 1000),
|
||
encoding: "utf-8",
|
||
stdio: ["ignore", "pipe", "pipe"],
|
||
windowsHide: true, // 对应 Python STARTF_USESHOWWINDOW
|
||
});
|
||
if (result.status !== 0) return "";
|
||
return (result.stdout ?? "").trim();
|
||
} catch {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/** 对应 Python: _run_powershell(command) -> str */
|
||
function runPowershell(command: string): string {
|
||
if (!isPowershellAvailable()) return "";
|
||
const args = ["powershell", "-NoProfile", "-NonInteractive", "-Command", command];
|
||
return runProcess(args);
|
||
}
|
||
|
||
/** 对应 Python: _run_wmic(args) -> str */
|
||
function runWmic(args: readonly string[]): string {
|
||
if (!isWmicAvailable()) return "";
|
||
return runProcess([...args]);
|
||
}
|
||
|
||
/** 执行 Mac 命令并返回 stdout(去除首尾空白) */
|
||
function runMacCommand(args: string[], timeoutSec: number = 5.0): string {
|
||
if (!isMacOS()) return "";
|
||
return runProcess(args, timeoutSec);
|
||
}
|
||
|
||
/**
|
||
* Mac HDD 标识采集 — 三级回退保证覆盖率
|
||
* ① NVMe 磁盘序列号(Apple Silicon / 新款 Intel)
|
||
* ② SATA 磁盘序列号(老款 Intel Mac)
|
||
* ③ 系统序列号(最终兜底,仍唯一)
|
||
*/
|
||
function getMacHddValue(): string {
|
||
// ① 尝试 NVMe
|
||
let output = runMacCommand(['system_profiler', 'SPNVMeDataType']);
|
||
let m = output.match(/Serial Number:\s*(.+)/i);
|
||
if (m?.[1]?.trim()) return m[1].trim();
|
||
|
||
// ② 回退:SATA
|
||
output = runMacCommand(['system_profiler', 'SPSerialATADataType']);
|
||
m = output.match(/Serial Number:\s*(.+)/i);
|
||
if (m?.[1]?.trim()) return m[1].trim();
|
||
|
||
// ③ 最终兜底:系统序列号
|
||
output = runMacCommand(['system_profiler', 'SPHardwareDataType']);
|
||
m = output.match(/Serial Number[^:]*:\s*(.+)/i);
|
||
return m?.[1]?.trim() ?? '';
|
||
}
|
||
|
||
// ============================================================
|
||
// DeviceFingerprint 类
|
||
// ============================================================
|
||
|
||
export class DeviceFingerprint {
|
||
// private wmicAvailableCache: boolean | null = null;
|
||
// private powershellAvailableCache: boolean | null = null;
|
||
|
||
// ---------- 组件 ID 查询 ----------
|
||
|
||
/**
|
||
* 查询单个硬件组件 ID。
|
||
* 对应 Python: get_component_id(component, method="auto") -> str
|
||
*/
|
||
getComponentId(component: Component, method: QueryMethod = "auto"): string {
|
||
// Mac 路径:ioreg / sysctl / system_profiler
|
||
if (isMacOS()) {
|
||
if (component === 'hdd') {
|
||
return normalizeIdentifier(getMacHddValue());
|
||
}
|
||
const macQuery = MAC_COMPONENT_QUERIES[component];
|
||
if (!macQuery) return '';
|
||
const output = runMacCommand(macQuery.command);
|
||
return normalizeIdentifier(macQuery.extract(output));
|
||
}
|
||
|
||
// Windows 路径:wmic / powershell
|
||
const query = COMPONENT_QUERIES[component];
|
||
if (!query) return "";
|
||
|
||
const resolvedMethod = resolveMethod(method);
|
||
|
||
const readViaPowershell = (): string => {
|
||
const rawOutput = runPowershell(query.powershell);
|
||
return firstValueFromOutput(rawOutput, query.headerTokens);
|
||
};
|
||
|
||
const readViaWmic = (): string => {
|
||
const rawOutput = runWmic(query.wmicArgs);
|
||
return firstValueFromOutput(rawOutput, query.headerTokens);
|
||
};
|
||
|
||
let rawValue = "";
|
||
if (resolvedMethod === "powershell") {
|
||
rawValue = readViaPowershell();
|
||
} else if (resolvedMethod === "wmic") {
|
||
rawValue = readViaWmic();
|
||
if (!rawValue) {
|
||
rawValue = readViaPowershell();
|
||
}
|
||
} else {
|
||
// auto: wmic 优先,回退 powershell
|
||
rawValue = readViaWmic() || readViaPowershell();
|
||
}
|
||
|
||
return normalizeIdentifier(rawValue);
|
||
}
|
||
|
||
/**
|
||
* 查询单个硬件组件(别名)。
|
||
* 对应 Python: get_component(component_type, method="auto") -> str
|
||
*/
|
||
getComponent(component: Component, method: QueryMethod = "auto"): string {
|
||
return this.getComponentId(component, method);
|
||
}
|
||
|
||
// ---------- 机器指纹 ----------
|
||
|
||
/**
|
||
* 生成设备指纹 SHA256。
|
||
* 采集 UUID + HDD + CPU,拼接后做 SHA256 哈希。
|
||
* 对应 Python: fingerprint_sha256(method="auto") -> str | None
|
||
*/
|
||
fingerprintSha256(method: QueryMethod = "auto"): string | null {
|
||
// Mac 路径:ioreg / sysctl / system_profiler
|
||
if (isMacOS()) {
|
||
const uuidId = this.getComponentId("uuid", method);
|
||
const diskId = this.getComponentId("hdd", method);
|
||
const cpuId = this.getComponentId("cpu", method);
|
||
|
||
if (!uuidId && !diskId && !cpuId) {
|
||
return null;
|
||
}
|
||
|
||
const combined = `${uuidId}|${diskId}|${cpuId}`;
|
||
const normalized = normalizeIdentifier(combined);
|
||
return sha256HexUpper(normalized);
|
||
}
|
||
|
||
// Windows 路径:wmic / powershell
|
||
const resolvedMethod = resolveMethod(method);
|
||
const wmicOk = isWmicAvailable();
|
||
const psOk = isPowershellAvailable();
|
||
|
||
if (resolvedMethod === "powershell") {
|
||
if (!psOk) return null;
|
||
} else {
|
||
if (!wmicOk && !psOk) return null;
|
||
}
|
||
|
||
const uuidId = this.getComponentId("uuid", method);
|
||
const diskId = this.getComponentId("hdd", method);
|
||
const cpuId = this.getComponentId("cpu", method);
|
||
|
||
if (!uuidId && !diskId && !cpuId) {
|
||
return null;
|
||
}
|
||
|
||
const combined = `${uuidId}|${diskId}|${cpuId}`;
|
||
const normalized = normalizeIdentifier(combined);
|
||
return sha256HexUpper(normalized);
|
||
}
|
||
|
||
/**
|
||
* 获取机器码(指纹的别名)。
|
||
* 对应 Python: get_machine_code(method="auto") -> str | None
|
||
*/
|
||
getMachineCode(method: QueryMethod = "auto"): string | null {
|
||
return this.fingerprintSha256(method);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 便捷函数(无需实例化)
|
||
// ============================================================
|
||
|
||
const defaultFingerprint = new DeviceFingerprint();
|
||
|
||
/** 获取机器码(单例便捷调用) */
|
||
export function getMachineCode(method: QueryMethod = "auto"): string | null {
|
||
return defaultFingerprint.getMachineCode(method);
|
||
}
|
||
|
||
/** 获取设备指纹 SHA256(单例便捷调用) */
|
||
export function fingerprintSha256(method: QueryMethod = "auto"): string | null {
|
||
return defaultFingerprint.fingerprintSha256(method);
|
||
}
|
||
|
||
/** 查询单个硬件组件 ID(单例便捷调用) */
|
||
export function getComponentId(component: Component, method: QueryMethod = "auto"): string {
|
||
return defaultFingerprint.getComponentId(component, method);
|
||
}
|