Files
hermes-agent/apps/desktop/electron/freemodel2api-ipc.ts
T
m4 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
feat: consolidate desktop and provider updates
2026-09-27 17:37:19 +08:00

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 }
})
}