- 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 对齐)
201 lines
6.4 KiB
TypeScript
201 lines
6.4 KiB
TypeScript
/**
|
||
* 设备信息服务 — 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();
|
||
}
|