源码
packages/desktop/src/main/chatgpt-auth.ts
import { randomUUID } from "node:crypto";
import { fetchProviderModels, getProviderPreset } from "@zpi-dev/ai";
import {
beginChatGPTLogin,
type ChatGPTLogin,
revokeChatGPTCredential,
} from "@zpi-dev/ai/auth/openai-chatgpt";
import { mergeDiscoveredModels } from "../shared/config.ts";
import type { ErrorLog } from "./error-log.ts";
import type { SettingsStore } from "./storage.ts";
export class ChatGPTAuth {
private pending = new Map<string, { login: ChatGPTLogin; providerId?: string; completing: boolean }>();
private settings: SettingsStore;
private fetcher: typeof fetch;
private errors?: ErrorLog;
constructor(settings: SettingsStore, fetcher: typeof fetch = fetch, errors?: ErrorLog) {
this.settings = settings;
this.fetcher = fetcher;
this.errors = errors;
}
async begin(providerId: string | null) {
if (this.pending.size) throw new Error("busy: 已有 ChatGPT 登录正在进行");
const credential = providerId ? this.settings.getChatGPTCredential(providerId) : undefined;
// Reserve before opening the callback listener so simultaneous IPC requests cannot start two flows.
const loginId = randomUUID();
const reservation = {
login: undefined as unknown as ChatGPTLogin,
...(providerId ? { providerId } : {}),
completing: false,
};
this.pending.set(loginId, reservation);
try {
const login = await beginChatGPTLogin({
hostId: this.settings.getChatGPTHostId(),
credential,
fetch: this.fetcher,
});
if (this.pending.get(loginId) !== reservation) {
login.cancel();
throw new Error("provider: 登录已取消");
}
reservation.login = login;
void login.wait().catch(() => {
if (!reservation.completing) this.pending.delete(loginId);
});
return { loginId, url: login.url };
} catch (error) {
this.pending.delete(loginId);
throw error;
}
}
async complete(loginId: string) {
const attempt = this.attempt(loginId);
if (attempt.completing) throw new Error("busy: 登录正在完成");
attempt.completing = true;
try {
const credential = await attempt.login.wait();
if (this.pending.get(loginId) !== attempt) throw new Error("provider: 登录已取消");
const providerId = attempt.providerId ?? randomUUID();
let warning: string | undefined;
let discovered: Awaited<ReturnType<typeof fetchProviderModels>> | undefined;
try {
discovered = await fetchProviderModels(
{ preset: "openai-chatgpt", apiKey: credential.access },
this.fetcher,
);
warning = discovered.warning;
} catch (error) {
this.errors?.write("chatgpt.models", error, { providerId });
warning = "ChatGPT 已登录,模型列表获取失败,请点击获取模型列表重试。";
}
// 模型发现期间仍可取消登录;确认流程有效后再同步落盘,避免取消后留下已登录账号。
if (this.pending.get(loginId) !== attempt) throw new Error("provider: 登录已取消");
if (!attempt.providerId) {
const preset = getProviderPreset("openai-chatgpt");
if (!preset) throw new Error("configuration: 缺少 ChatGPT 提供商");
// Commit the provider and its credentials together; a failed login must not leave an empty row.
this.settings.saveProvider(
{
id: providerId,
preset: preset.id,
name: credential.email ? `${preset.name} · ${credential.email}` : preset.name,
baseUrl: preset.baseUrl,
models: [],
},
credential,
);
} else this.settings.saveChatGPTCredential(providerId, credential);
if (discovered) {
try {
const { apiKey: _key, ...saved } = this.settings.snapshot(providerId);
this.settings.saveProvider({
...saved,
models: mergeDiscoveredModels(saved.models, discovered.models),
});
} catch (error) {
this.errors?.write("chatgpt.models", error, { providerId });
warning = "ChatGPT 已登录,模型列表获取失败,请点击获取模型列表重试。";
}
}
return { settings: this.settings.get(), providerId, ...(warning ? { warning } : {}) };
} finally {
this.pending.delete(loginId);
}
}
submit(loginId: string, url: string) {
this.attempt(loginId).login.submitCallback(url);
}
cancel(loginId: string) {
const attempt = this.pending.get(loginId);
this.pending.delete(loginId);
attempt?.login?.cancel();
}
async disconnect(providerId: string) {
for (const [id, attempt] of this.pending) if (attempt.providerId === providerId) this.cancel(id);
const credential = this.settings.getChatGPTCredential(providerId);
// Invalidate local requests immediately, including refreshes already in flight.
const value = this.settings.disconnectChatGPT(providerId);
let warning: string | undefined;
if (credential) {
try {
await revokeChatGPTCredential(credential, this.fetcher);
} catch (error) {
this.errors?.write("chatgpt.revoke", error, { providerId });
warning = "已在本机退出,但无法确认远端撤销。可在 ChatGPT 设置中断开此应用。";
}
}
return { settings: value, ...(warning ? { warning } : {}) };
}
close() {
for (const id of this.pending.keys()) this.cancel(id);
}
private attempt(id: string) {
const value = this.pending.get(id);
if (!value?.login) throw new Error("not_found: ChatGPT 登录已过期或取消");
return value;
}
}