feat: 前后端联调 — 任务创建 + 账户信息 + 预估花费 (0.0.18)

- task: device_id 改为必填参数,API 层 Omit 封装 + getDeviceId() 自动注入
- pricing.ts: Python 后端 calculate_cost 完整移植,六种定价模式 + 6 个 Bug 修复
  (浮点冗余/维度匹配不一致/null 守卫/matrix 数据提取/unit_scale 等)
- ModelInputForm: 三层兜底预估消费展示 + React Hooks 顺序修复
- AccountInfo: VIP 等级列表匹配,套餐详情展示(说明/周期/席位/到期)
- 模型列表加密缓存 (safe-storage, stale-while-revalidate)
- VIP API 模块 (激活码/等级/订单/模拟支付)
- device_id 统一使用 UUID v4 持久化 (与 login/register 对齐)
This commit is contained in:
2026-06-10 19:38:26 +08:00
parent 848fdeca3d
commit 95019316b2
19 changed files with 1681 additions and 292 deletions

279
electron/preload/device.ts Normal file
View File

@@ -0,0 +1,279 @@
/**
* 设备指纹采集 — TypeScript 实现
*
* 与 device.py 逻辑一致,保留原 Python 方法不变。
* 通过采集 UUID / CPU / HDD 三项硬件 ID拼接后做 SHA256 得到 device_id。
*
* 适用环境:
* - Node.js / ElectronWindows通过 child_process 执行 wmic / powershell 采集硬件 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"],
},
};
// ============================================================
// 工具函数
// ============================================================
/** 对应 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 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]);
}
// ============================================================
// 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 {
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 {
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);
}