import { FormEvent, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Eye, KeyRound, Save, Trash2 } from 'lucide-react' import { apiConfigsAPI, type APIConfig } from '../api/api_configs' import { confirmAction } from '../components/confirmAction' // APIConfigsPage manages third-party API keys (TMDb / Bangumi / TheTVDB / // Fanart / OpenAI / Douban). Plaintext keys are never returned by the // backend — only a "abc1****wxyz" mask. The actual secret is encrypted // in SQLite with AES-GCM keyed off the JWT secret. export function APIConfigsPage() { const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const refresh = () => apiConfigsAPI .list() .then(setItems) .finally(() => setLoading(false)) useEffect(() => { refresh().catch(() => undefined) }, []) return (

外部 API 配置

管理 TMDb / Bangumi / TheTVDB / Fanart / OpenAI / Douban / Adult 的密钥与源。 后端使用 AES-GCM 加密存储,数据库泄漏时密钥仍然安全。

{loading &&

加载中…

}
{items.map((item) => ( ))}
) } function ProviderCard({ item, onUpdated }: { item: APIConfig; onUpdated: () => void }) { const [apiKey, setAPIKey] = useState('') const [baseURL, setBaseURL] = useState(item.base_url ?? '') const [extra, setExtra] = useState(item.extra ?? '') const [enabled, setEnabled] = useState(item.enabled) const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) const isAdult = item.provider === 'adult' const submit = async (e: FormEvent) => { e.preventDefault() setSaving(true) try { const patch: Record = { base_url: baseURL, enabled } if (isAdult) patch.extra = extra if (apiKey.trim()) patch.api_key = apiKey.trim() await apiConfigsAPI.update(item.provider, patch) toast.success(`${item.provider} 已保存`) setAPIKey('') onUpdated() } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '保存失败' toast.error(msg) } finally { setSaving(false) } } const testKey = async () => { setTesting(true) try { const res = await apiConfigsAPI.test(item.provider) if (res.result === 'success') { toast.success(`${item.provider} 连接/验证测试成功`) } else { toast.error(`${item.provider} 测试失败: ${res.error || res.result}`) } } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '连接测试失败' toast.error(msg) } finally { setTesting(false) } } const removeKey = async () => { if (!(await confirmAction({ title: '清除 API Key', message: `确定清除 ${item.provider} 的 API Key?`, confirmText: '清除' }))) return await apiConfigsAPI.remove(item.provider) toast.success('已清除') onUpdated() } return (

{item.provider}

{item.description && (

{item.description}

)}

状态: {apiConfigConfigured(item) ? 已配置 : 未配置} {item.has_key && ( {item.masked_key} )} {isAdult && apiConfigSourceCount(item) > 0 && ( {apiConfigSourceCount(item)} 个源 )}

{isAdult && (