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:
279
electron/preload/device.ts
Normal file
279
electron/preload/device.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* 设备指纹采集 — TypeScript 实现
|
||||
*
|
||||
* 与 device.py 逻辑一致,保留原 Python 方法不变。
|
||||
* 通过采集 UUID / CPU / HDD 三项硬件 ID,拼接后做 SHA256 得到 device_id。
|
||||
*
|
||||
* 适用环境:
|
||||
* - Node.js / Electron(Windows):通过 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);
|
||||
}
|
||||
Reference in New Issue
Block a user