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 不安全)
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* 适用环境:
|
||||
* - Node.js / Electron(Windows):通过 child_process 执行 wmic / powershell 采集硬件 ID
|
||||
* - Node.js / Electron(macOS) :通过 ioreg / sysctl / system_profiler 采集硬件 ID
|
||||
* - 浏览器:仅提供 SHA256 哈希工具函数,硬件采集需由上层注入
|
||||
*/
|
||||
|
||||
@@ -60,6 +61,37 @@ const COMPONENT_QUERIES: Record<Component, ComponentQuery> = {
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 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() ?? '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
@@ -105,6 +137,10 @@ function isWindows(): boolean {
|
||||
return platform() === "win32";
|
||||
}
|
||||
|
||||
function isMacOS(): boolean {
|
||||
return platform() === "darwin";
|
||||
}
|
||||
|
||||
function isWmicAvailable(): boolean {
|
||||
if (!isWindows()) return false;
|
||||
const result = spawnSync("where", ["wmic"], {
|
||||
@@ -163,6 +199,35 @@ function runWmic(args: readonly string[]): string {
|
||||
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 类
|
||||
// ============================================================
|
||||
@@ -178,6 +243,18 @@ export class DeviceFingerprint {
|
||||
* 对应 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 "";
|
||||
|
||||
@@ -225,6 +302,22 @@ export class DeviceFingerprint {
|
||||
* 对应 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();
|
||||
|
||||
Reference in New Issue
Block a user