fb13457f6f
CI / Supply-chain scan (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
/**
|
|
* freemodel2api-ipc.ts — FreeModel2api(sub2api)会话的 IPC 边界。
|
|
*
|
|
* main 进程持有内存态 session(access token + 过期时间),不落盘;
|
|
* 过期或 401 时用 safeStorage 里存的密码静默重登一次。
|
|
* 密码本身永远不经过 renderer —— login 之后 renderer 只能拿到
|
|
* email/balance/key 列表。
|
|
*/
|
|
|
|
import {
|
|
Sub2ApiAuthError,
|
|
loadFreeModel2ApiCredentials,
|
|
loadRememberedLogin,
|
|
normalizeSub2ApiBaseUrl,
|
|
persistFreeModel2ApiCredentials,
|
|
persistRememberedLogin,
|
|
sub2ApiFetchKeys,
|
|
sub2ApiFetchProfile,
|
|
sub2ApiKeyBalance,
|
|
sub2ApiLogin,
|
|
type FetchLike,
|
|
type FreeModel2ApiStoreIo
|
|
} from './freemodel2api'
|
|
|
|
export interface FreeModel2ApiIpcDeps {
|
|
ipcMain: { handle: (channel: string, fn: (event: any, payload?: any) => Promise<any>) => void }
|
|
io: FreeModel2ApiStoreIo
|
|
fetcher: FetchLike
|
|
}
|
|
|
|
interface Session {
|
|
baseUrl: string
|
|
accessToken: string
|
|
expiresAt: number
|
|
}
|
|
|
|
export function registerFreeModel2ApiIpc(deps: FreeModel2ApiIpcDeps): void {
|
|
const { ipcMain, io, fetcher } = deps
|
|
let session: Session | null = null
|
|
|
|
async function freshLogin(baseUrl: string, email: string, password: string) {
|
|
const result = await sub2ApiLogin(baseUrl, email, password, fetcher)
|
|
session = { baseUrl: normalizeSub2ApiBaseUrl(baseUrl), accessToken: result.accessToken, expiresAt: result.expiresAt }
|
|
return result
|
|
}
|
|
|
|
async function ensureSession(): Promise<Session> {
|
|
if (session && session.expiresAt > Date.now()) {
|
|
return session
|
|
}
|
|
const creds = loadFreeModel2ApiCredentials(io)
|
|
if (!creds) {
|
|
throw new Sub2ApiAuthError('FreeModel2api is not configured')
|
|
}
|
|
await freshLogin(creds.baseUrl, creds.email, creds.password)
|
|
return session!
|
|
}
|
|
|
|
async function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T> {
|
|
const current = await ensureSession()
|
|
try {
|
|
return await fn(current)
|
|
} catch (err) {
|
|
if (!(err instanceof Sub2ApiAuthError)) throw err
|
|
// token 可能已被服务端吊销:用已存密码重登一次再试
|
|
const creds = loadFreeModel2ApiCredentials(io)
|
|
if (!creds) throw err
|
|
await freshLogin(creds.baseUrl, creds.email, creds.password)
|
|
return fn(session!)
|
|
}
|
|
}
|
|
|
|
ipcMain.handle('hermes:freemodel2api:login', async (_event, payload) => {
|
|
const baseUrl = normalizeSub2ApiBaseUrl(String(payload?.baseUrl ?? ''))
|
|
const email = String(payload?.email ?? '').trim()
|
|
const password = String(payload?.password ?? '')
|
|
if (!email || !password) {
|
|
throw new Error('Email and password are required')
|
|
}
|
|
const result = await freshLogin(baseUrl, email, password)
|
|
persistFreeModel2ApiCredentials({ baseUrl, email, password }, io)
|
|
return {
|
|
ok: true,
|
|
email: result.email,
|
|
balance: result.balance,
|
|
username: result.username,
|
|
frozenBalance: result.frozenBalance
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('hermes:freemodel2api:status', async () => {
|
|
const creds = loadFreeModel2ApiCredentials(io)
|
|
if (!creds) {
|
|
return { configured: false }
|
|
}
|
|
const profile = await withSession(s => sub2ApiFetchProfile(s.baseUrl, s.accessToken, fetcher))
|
|
return {
|
|
configured: true,
|
|
baseUrl: creds.baseUrl,
|
|
email: profile.email || creds.email,
|
|
balance: profile.balance,
|
|
username: profile.username,
|
|
frozenBalance: profile.frozenBalance
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('hermes:freemodel2api:keys', async () => {
|
|
const keys = await withSession(s => sub2ApiFetchKeys(s.baseUrl, s.accessToken, fetcher))
|
|
return { keys }
|
|
})
|
|
|
|
ipcMain.handle('hermes:freemodel2api:logout', async () => {
|
|
session = null
|
|
persistFreeModel2ApiCredentials(null, io)
|
|
return { ok: true }
|
|
})
|
|
|
|
ipcMain.handle('hermes:freemodel2api:remembered:load', async () => {
|
|
return loadRememberedLogin(io)
|
|
})
|
|
|
|
// 未登录的余额读数:凭 renderer 传来的 key 直查,不走会话。失败只报 null,不打扰人。
|
|
ipcMain.handle('hermes:freemodel2api:key-balance', async (_event, payload) => {
|
|
const baseUrl = String(payload?.baseUrl ?? '')
|
|
const apiKey = String(payload?.apiKey ?? '')
|
|
if (!baseUrl || !apiKey) {
|
|
return { balance: null }
|
|
}
|
|
try {
|
|
return { balance: await sub2ApiKeyBalance(baseUrl, apiKey, fetcher) }
|
|
} catch {
|
|
return { balance: null }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('hermes:freemodel2api:remembered:save', async (_event, payload) => {
|
|
if (!payload) {
|
|
persistRememberedLogin(null, io)
|
|
return { ok: true }
|
|
}
|
|
persistRememberedLogin(
|
|
{
|
|
baseUrl: normalizeSub2ApiBaseUrl(String(payload.baseUrl ?? '')),
|
|
email: String(payload.email ?? '').trim(),
|
|
password: String(payload.password ?? '')
|
|
},
|
|
io
|
|
)
|
|
return { ok: true }
|
|
})
|
|
}
|