haixunMaster/lib/ai/keys.ts

93 lines
2.9 KiB
TypeScript
Raw Normal View History

2026-06-21 12:50:31 +00:00
export type ProviderId = "xai" | "openai" | "anthropic" | "google" | "opencode-go";
export type ProviderApiKeys = Partial<Record<ProviderId, string>>;
const ENV_MAP: Record<ProviderId, string> = {
xai: "XAI_API_KEY",
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
"opencode-go": "OPENCODE_GO_API_KEY",
};
export const PROVIDER_KEY_LABELS: Record<
ProviderId,
{ label: string; hint: string; docsUrl?: string }
> = {
"opencode-go": {
label: "OpenCode Go",
hint: "從 opencode.ai/auth 訂閱 Go 方案後取得",
docsUrl: "https://opencode.ai/docs/go/",
},
xai: { label: "Grok (xAI)", hint: "xAI Console API key" },
openai: { label: "OpenAI", hint: "platform.openai.com API key" },
anthropic: { label: "Anthropic", hint: "console.anthropic.com API key" },
google: { label: "Google Gemini", hint: "Google AI Studio API key" },
};
export function parseProviderApiKeys(raw: string | null | undefined): ProviderApiKeys {
if (!raw) return {};
try {
return JSON.parse(raw) as ProviderApiKeys;
} catch {
return {};
}
}
export function serializeProviderApiKeys(keys: ProviderApiKeys): string {
return JSON.stringify(keys);
}
export function maskApiKey(key: string | undefined | null): string | null {
if (!key) return null;
if (key.length <= 4) return "••••";
return `••••${key.slice(-4)}`;
}
export function isMaskedKey(value: string): boolean {
return value.startsWith("••••");
}
/** 只讀使用者自己存在 DB 的 key不 fallback .env避免新帳號看到舊設定。 */
export function resolveApiKey(provider: ProviderId, keys: ProviderApiKeys): string | undefined {
return keys[provider]?.trim() || undefined;
}
export function getApiKeyStatus(keys: ProviderApiKeys): Record<ProviderId, boolean> {
return (Object.keys(ENV_MAP) as ProviderId[]).reduce(
(acc, id) => {
acc[id] = !!keys[id]?.trim();
return acc;
},
{} as Record<ProviderId, boolean>
);
}
export function mergeProviderApiKeys(
existing: ProviderApiKeys,
incoming: ProviderApiKeys
): ProviderApiKeys {
const merged = { ...existing };
for (const [provider, value] of Object.entries(incoming) as [ProviderId, string][]) {
const trimmed = value?.trim();
if (!trimmed || isMaskedKey(trimmed)) continue;
merged[provider] = trimmed;
}
return merged;
}
export function getMaskedProviderApiKeys(keys: ProviderApiKeys): ProviderApiKeys {
return (Object.keys(ENV_MAP) as ProviderId[]).reduce((acc, id) => {
const masked = maskApiKey(keys[id]);
if (masked) acc[id] = masked;
return acc;
}, {} as ProviderApiKeys);
}
export function assertProviderApiKey(provider: ProviderId, keys: ProviderApiKeys): void {
const key = resolveApiKey(provider, keys);
if (!key) {
const label = PROVIDER_KEY_LABELS[provider].label;
throw new Error(`尚未設定 ${label} API key請到 AI 設定頁填入`);
}
}