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:
200
electron/preload/device_service.ts
Normal file
200
electron/preload/device_service.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 设备信息服务 — TypeScript 实现
|
||||
*
|
||||
* 与 device_service.py 逻辑一致,保留原 Python 方法不变。
|
||||
* 提供带缓存的机器码、用户 IP、MAC 地址查询。
|
||||
*
|
||||
* 适用环境:Node.js / Electron
|
||||
*/
|
||||
|
||||
import * as os from "node:os";
|
||||
import * as dgram from "node:dgram";
|
||||
import { DeviceFingerprint } from "./device";
|
||||
import type { QueryMethod } from "./device";
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
|
||||
/** 对应 Python: _is_valid_ipv4(value) -> bool */
|
||||
function isValidIPv4(value: string): boolean {
|
||||
const parts = value.split(".");
|
||||
if (parts.length !== 4) return false;
|
||||
for (const part of parts) {
|
||||
const num = parseInt(part, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255) return false;
|
||||
if (part.length > 1 && part.startsWith("0")) return false;
|
||||
}
|
||||
// 排除环回地址和 0.0.0.0
|
||||
if (value.startsWith("127.") || value === "0.0.0.0") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 对应 Python: _resolve_user_ip() -> Optional[str] */
|
||||
function resolveUserIp(): string | null {
|
||||
// 优先通过 UDP "伪连接" 获取本机出站 IP
|
||||
try {
|
||||
const sock = dgram.createSocket("udp4");
|
||||
// 同步方式不可行;使用 UDP connect + getsockname 的异步模式
|
||||
// 但在 Node.js 中这本质是同步的 (connect 不发送数据)
|
||||
sock.connect(80, "8.8.8.8");
|
||||
const address = sock.address();
|
||||
sock.close();
|
||||
if (address && typeof address.address === "string") {
|
||||
const ip = address.address;
|
||||
if (isValidIPv4(ip)) return ip;
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
// 回退: 遍历网络接口查找第一个有效的非环回 IPv4
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const ifaceList of Object.values(interfaces)) {
|
||||
if (!ifaceList) continue;
|
||||
for (const iface of ifaceList) {
|
||||
if (iface.family === "IPv4") {
|
||||
const ip = iface.address;
|
||||
if (isValidIPv4(ip)) return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 对应 Python: _resolve_user_mac_address() -> Optional[str] */
|
||||
function resolveUserMacAddress(): string | null {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const ifaceList of Object.values(interfaces)) {
|
||||
if (!ifaceList) continue;
|
||||
for (const iface of ifaceList) {
|
||||
// 跳过环回接口
|
||||
if (iface.internal) continue;
|
||||
if (iface.mac && iface.mac !== "00:00:00:00:00:00") {
|
||||
// 对应 Python: 检查多播位
|
||||
const parts = iface.mac.split(":");
|
||||
if (parts.length === 6) {
|
||||
const firstByte = parseInt(parts[0], 16);
|
||||
// uuid.getnode 的多播位检查: (node >> 40) & 1
|
||||
if ((firstByte & 1) === 0) {
|
||||
return iface.mac.toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 未解析标记
|
||||
// ============================================================
|
||||
|
||||
const UNRESOLVED = Symbol("unresolved");
|
||||
|
||||
// ============================================================
|
||||
// DeviceService 类
|
||||
// ============================================================
|
||||
|
||||
export class DeviceService {
|
||||
private fingerprint = new DeviceFingerprint();
|
||||
|
||||
private cachedMachineCode: string | null | typeof UNRESOLVED = UNRESOLVED;
|
||||
private cachedUserIp: string | null | typeof UNRESOLVED = UNRESOLVED;
|
||||
private cachedUserMacAddress: string | null | typeof UNRESOLVED = UNRESOLVED;
|
||||
|
||||
private warmupStarted = false;
|
||||
|
||||
/**
|
||||
* 获取机器码(带缓存)。
|
||||
* 对应 Python: get_machine_code() -> Optional[str]
|
||||
*/
|
||||
getMachineCode(method: QueryMethod = "auto"): string | null {
|
||||
if (this.cachedMachineCode !== UNRESOLVED) {
|
||||
return this.cachedMachineCode;
|
||||
}
|
||||
|
||||
const machineCode = this.fingerprint.getMachineCode(method);
|
||||
if (this.cachedMachineCode === UNRESOLVED) {
|
||||
this.cachedMachineCode = machineCode;
|
||||
}
|
||||
return machineCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户本机 IP(带缓存)。
|
||||
* 对应 Python: get_user_ip() -> Optional[str]
|
||||
*/
|
||||
getUserIp(): string | null {
|
||||
if (this.cachedUserIp !== UNRESOLVED) {
|
||||
return this.cachedUserIp;
|
||||
}
|
||||
|
||||
const value = resolveUserIp();
|
||||
if (this.cachedUserIp === UNRESOLVED && value) {
|
||||
this.cachedUserIp = value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户 MAC 地址(带缓存)。
|
||||
* 对应 Python: get_user_mac_address() -> Optional[str]
|
||||
*/
|
||||
getUserMacAddress(): string | null {
|
||||
if (this.cachedUserMacAddress !== UNRESOLVED) {
|
||||
return this.cachedUserMacAddress;
|
||||
}
|
||||
|
||||
const value = resolveUserMacAddress();
|
||||
if (this.cachedUserMacAddress === UNRESOLVED) {
|
||||
this.cachedUserMacAddress = value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步预热机器码(后台线程)。
|
||||
* 对应 Python: warm_up_machine_code() -> None
|
||||
*/
|
||||
warmUpMachineCode(): void {
|
||||
if (this.cachedMachineCode !== UNRESOLVED) return;
|
||||
if (this.warmupStarted) return;
|
||||
this.warmupStarted = true;
|
||||
|
||||
// 使用 setImmediate 模拟后台线程,不阻塞当前事件循环
|
||||
setImmediate(() => {
|
||||
const machineCode = this.fingerprint.getMachineCode();
|
||||
if (this.cachedMachineCode === UNRESOLVED) {
|
||||
this.cachedMachineCode = machineCode;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 便捷函数(单例)
|
||||
// ============================================================
|
||||
|
||||
const defaultService = new DeviceService();
|
||||
|
||||
/** 获取机器码(单例,带缓存) */
|
||||
export function getMachineCode(method: QueryMethod = "auto"): string | null {
|
||||
return defaultService.getMachineCode(method);
|
||||
}
|
||||
|
||||
/** 获取用户本机 IP(单例,带缓存) */
|
||||
export function getUserIp(): string | null {
|
||||
return defaultService.getUserIp();
|
||||
}
|
||||
|
||||
/** 获取用户 MAC 地址(单例,带缓存) */
|
||||
export function getUserMacAddress(): string | null {
|
||||
return defaultService.getUserMacAddress();
|
||||
}
|
||||
|
||||
/** 异步预热机器码 */
|
||||
export function warmUpMachineCode(): void {
|
||||
defaultService.warmUpMachineCode();
|
||||
}
|
||||
Reference in New Issue
Block a user