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 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 () => { // No /test endpoint yet — render a hint instead. toast(`已配置 ${item.has_key ? '✓' : '✗'} 密钥(在线测试请用对应功能页面)`) } 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 && ( )} {isAdult && (