feat: build licensed desktop image processor

This commit is contained in:
2026-08-26 07:26:35 +08:00
parent 6ff3d71921
commit 1f31424015
32 changed files with 6515 additions and 2 deletions
+170
View File
@@ -0,0 +1,170 @@
import { app } from 'electron'
import { createHash } from 'node:crypto'
import os from 'node:os'
import { machineIdSync } from 'node-machine-id'
import type {
AppInfo,
LicenseSession,
LicenseViewState,
VersionInfo,
} from '../../src/shared/types'
import { LicenseStore, type StoredLicense } from './license-store'
import {
DesktopServerApi,
type DevicePayload,
ServerApiError,
} from './server-api'
const OFFLINE_GRACE_MS = 72 * 60 * 60 * 1000
export class LicenseManager {
private readonly store = new LicenseStore()
private readonly server = new DesktopServerApi()
getAppInfo(): AppInfo {
return {
name: '正版情趣王',
version: app.getVersion(),
platform: process.platform,
deviceName: os.hostname(),
}
}
async getState(): Promise<LicenseViewState> {
const stored = await this.store.load()
if (!stored) {
return {
status: 'unlicensed',
message: '输入激活码后即可开始使用',
offline: false,
}
}
return this.validateStored(stored)
}
async activate(activationCode: string): Promise<LicenseViewState> {
try {
const session = await this.server.activate(activationCode, this.devicePayload())
if (!session.token) {
throw new ServerApiError('INVALID_RESPONSE', '授权服务未返回设备凭据')
}
await this.persistSession(session)
return {
status: 'active',
message: '设备已激活',
offline: false,
session,
version: session.version,
}
} catch (error) {
return this.errorState(error)
}
}
async validate(): Promise<LicenseViewState> {
const stored = await this.store.load()
if (!stored) {
return {
status: 'unlicensed',
message: '当前设备尚未激活',
offline: false,
}
}
return this.validateStored(stored)
}
async clear(): Promise<LicenseViewState> {
await this.store.clear()
return {
status: 'unlicensed',
message: '本机授权凭据已清除',
offline: false,
}
}
checkVersion(): Promise<VersionInfo> {
return this.server.checkVersion(app.getVersion())
}
private async validateStored(stored: StoredLicense): Promise<LicenseViewState> {
try {
const session = await this.server.login(stored.token, this.devicePayload())
session.token = stored.token
await this.persistSession(session)
return {
status: 'active',
message: '授权有效',
offline: false,
session,
version: session.version,
}
} catch (error) {
if (error instanceof ServerApiError && error.code.startsWith('NETWORK_')
&& this.canUseOffline(stored)) {
const session: LicenseSession = {
licenseCodeMask: stored.licenseCodeMask,
deviceMask: stored.deviceMask,
expiresAt: stored.expiresAt,
maxDevices: stored.maxDevices,
}
return {
status: 'active',
message: '当前处于离线授权模式,请在 72 小时内联网校验',
offline: true,
session,
}
}
return this.errorState(error)
}
}
private async persistSession(session: LicenseSession): Promise<void> {
if (!session.token) {
throw new Error('缺少设备授权凭据')
}
await this.store.save({
token: session.token,
licenseCodeMask: session.licenseCodeMask,
deviceMask: session.deviceMask,
expiresAt: session.expiresAt,
maxDevices: session.maxDevices,
lastVerifiedAt: new Date().toISOString(),
})
}
private canUseOffline(stored: StoredLicense): boolean {
const lastVerifiedAt = new Date(stored.lastVerifiedAt).getTime()
const expiresAt = stored.expiresAt ? new Date(stored.expiresAt).getTime() : Number.POSITIVE_INFINITY
return Number.isFinite(lastVerifiedAt)
&& Date.now() - lastVerifiedAt <= OFFLINE_GRACE_MS
&& Date.now() < expiresAt
}
private errorState(error: unknown): LicenseViewState {
if (error instanceof ServerApiError) {
return {
status: error.code.startsWith('NETWORK_') ? 'network-error' : 'invalid',
message: error.message,
offline: false,
}
}
return {
status: 'invalid',
message: error instanceof Error ? error.message : '授权校验失败',
offline: false,
}
}
private devicePayload(): DevicePayload {
const deviceId = createHash('sha256')
.update(`正版情趣王:${machineIdSync()}`)
.digest('hex')
return {
deviceId,
deviceName: os.hostname(),
osName: `${os.type()} ${os.arch()}`,
osVersion: os.release(),
appVersion: app.getVersion(),
}
}
}