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:
2026-06-11 20:08:44 +08:00
parent 95019316b2
commit 4237dcbaf6
24 changed files with 808 additions and 172 deletions

View File

@@ -6,6 +6,7 @@
*
* 适用环境:
* - Node.js / ElectronWindows通过 child_process 执行 wmic / powershell 采集硬件 ID
* - Node.js / ElectronmacOS :通过 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();