haixunMaster/lib/ai/keys.ts

93 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 設定頁填入`);
}
}