"use client"; import { useEffect, useMemo, useState } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { KeyRound, Plus, Save, Trash2 } from "lucide-react"; import { LocalAiSectionShell, LocalAiSectionSummary, } from "@/app/(admin)/settings/_components/local-ai-section-shell"; import { formatBeijingTime } from "@/lib/utils"; import { defaultExternalImageApiConfig, EXTERNAL_IMAGE_API_CONFIG_KEY, maskExternalImageApiKey, normalizeExternalImageApiConfig, type ExternalImageApiConfig, } from "@/lib/external-image-api/config"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; const fetcher = async (url: string) => { const response = await fetch(url); if (!response.ok) { const payload = await response.json().catch(() => ({})); throw new Error(payload?.error || payload?.message || "请求失败"); } return response.json(); }; type AdminExternalImageApiLog = { id: string; apiKeyId: string | null; apiKeyName: string | null; ownerUserEmail: string | null; modelDisplayName: string | null; modelRuntimeId: string | null; prompt: string | null; negativePrompt: string | null; imageCount: number; aspectRatio: string | null; imageSize: string | null; status: string; generatedImageUrls: string[] | null; errorMessage: string | null; durationMs: number | null; requesterIp: string | null; createdAt: string; completedAt: string | null; }; function buildKeyDraft() { return { id: `external-key-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, name: "", apiKey: "", enabled: true, notes: "", }; } export function ExternalImageApiSettingsCard({ expanded, onToggle, sectionId = "local-ai-section-external-image-api", }: { expanded: boolean; onToggle: () => void; sectionId?: string; }) { const { data: configData, mutate: mutateConfig } = useSWR<{ setting?: { value?: unknown }; }>(`/api/admin/settings?key=${EXTERNAL_IMAGE_API_CONFIG_KEY}`, fetcher); const { data: logsData, mutate: mutateLogs } = useSWR<{ success: boolean; data: { logs: AdminExternalImageApiLog[] }; }>("/api/admin/external-image-api/logs?limit=50", fetcher); const [config, setConfig] = useState( defaultExternalImageApiConfig, ); const [isSaving, setIsSaving] = useState(false); useEffect(() => { setConfig( normalizeExternalImageApiConfig(configData?.setting?.value), ); }, [configData]); const enabledKeyCount = useMemo( () => config.apiKeys.filter((item) => item.enabled).length, [config.apiKeys], ); const handleAddKey = () => { setConfig((current) => ({ ...current, apiKeys: [...current.apiKeys, buildKeyDraft()], })); }; const handleRemoveKey = (id: string) => { setConfig((current) => ({ ...current, apiKeys: current.apiKeys.filter((item) => item.id !== id), })); }; const handleSave = async () => { setIsSaving(true); try { const response = await fetch("/api/admin/settings", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ key: EXTERNAL_IMAGE_API_CONFIG_KEY, value: normalizeExternalImageApiConfig(config), }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error( String(payload?.message || payload?.error || "保存失败"), ); } toast.success("第三方生图 API 配置已保存"); await Promise.all([mutateConfig(), mutateLogs()]); } catch (error) { console.error("[External Image API Settings] Save failed:", error); toast.error(error instanceof Error ? error.message : "保存失败"); } finally { setIsSaving(false); } }; return ( 对外开放当前系统里已启用的生图模型。第三方先调 /api/external/image/models 获取模型名称,再调用 /api/external/image/generate 生图。结果会继续走我们的储存桶并落到数据库。 } expanded={expanded} id={sectionId} onToggle={onToggle} summary={ 0 ? "enabled" : "warning", }, { label: `启用中 ${enabledKeyCount} 个`, tone: enabledKeyCount > 0 ? "enabled" : "neutral", }, ]} /> } title={ 第三方生图 API } >
关闭后第三方 API Key 会全部失效。
setConfig((current) => ({ ...current, enabled: checked })) } /> {config.enabled ? "已开启" : "已关闭"}
setConfig((current) => ({ ...current, defaultOwnerEmail: event.target.value, })) } placeholder="external-image-api@system.local" />
外部 API 生成图片会统一挂到这个用户下,方便我们保留生成记录、媒体资产和后续分析数据。
当前共 {config.apiKeys.length} 个 Key,启用中 {enabledKeyCount} 个。 外部调用支持 Authorization: Bearer <apiKey> 和 x-api-key 两种方式。
{config.apiKeys.map((item, index) => (
Key #{index + 1}
setConfig((current) => ({ ...current, apiKeys: current.apiKeys.map((record) => record.id === item.id ? { ...record, name: event.target.value } : record, ), })) } placeholder="渠道 A / 合作方 B" />
setConfig((current) => ({ ...current, apiKeys: current.apiKeys.map((record) => record.id === item.id ? { ...record, id: event.target.value } : record, ), })) } placeholder="partner-a" />
setConfig((current) => ({ ...current, apiKeys: current.apiKeys.map((record) => record.id === item.id ? { ...record, apiKey: event.target.value } : record, ), })) } placeholder="请输入调用第三方生图 API 的密钥" />
当前显示:{maskExternalImageApiKey(item.apiKey)}