Files
ele-HeiXiu/electron/preload/device_service.ts
YoungestSongMo 95019316b2 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 对齐)
2026-06-10 19:38:26 +08:00

201 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 设备信息服务 — 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();
}