- 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 对齐)
280 lines
9.1 KiB
TypeScript
280 lines
9.1 KiB
TypeScript
/**
|
||
* 设备指纹采集 — 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);
|
||
}
|