Files
ele-HeiXiu/electron/preload/device.ts
YoungestSongMo 4237dcbaf6 feat: 数据获取架构重构 + Mac指纹采集 + Token刷新容错 + 账单页面 (0.0.19)
## 架构重构
  - 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 不安全)
2026-06-11 20:08:44 +08:00

373 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 设备指纹采集 — TypeScript 实现
*
* 与 device.py 逻辑一致,保留原 Python 方法不变。
* 通过采集 UUID / CPU / HDD 三项硬件 ID拼接后做 SHA256 得到 device_id。
*
* 适用环境:
* - Node.js / ElectronWindows通过 child_process 执行 wmic / powershell 采集硬件 ID
* - Node.js / ElectronmacOS :通过 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);
}