export type ProviderId = "xai" | "openai" | "anthropic" | "google" | "opencode-go"; export type ProviderApiKeys = Partial>; const ENV_MAP: Record = { 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 { return (Object.keys(ENV_MAP) as ProviderId[]).reduce( (acc, id) => { acc[id] = !!keys[id]?.trim(); return acc; }, {} as Record ); } 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 設定頁填入`); } }