/** * 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) => 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 { 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(fn: (s: Session) => Promise): Promise { 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 } }) }