'use client' import { useEffect, useMemo, useRef, useState } from 'react' import { Plus, RefreshCw, Save, Search } from 'lucide-react' import type { AiModelAdminState, AiModelCatalogEntry, AiModelProvider, AiModelStage } from '@/lib/ai-model-types' const stages: Array<{ id: AiModelStage; label: string; shortLabel: string }> = [ { id: 'analysis', label: '资料分析', shortLabel: '分析' }, { id: 'copy', label: '卖点文案', shortLabel: '文案' }, { id: 'image', label: '图片生成', shortLabel: '生图' }, ] const fieldClass = 'min-h-11 w-full min-w-0 rounded-lg border border-[#cfd8e3] bg-white px-3 py-2 text-sm text-[#101828] placeholder:text-[#66758b] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] disabled:bg-[#f8fafc] disabled:text-[#66758b]' const buttonClass = 'inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-lg border border-[#cfd8e3] bg-white px-3 text-sm font-medium text-[#344054] transition-colors hover:bg-[#f8fafc] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50' const primaryClass = `${buttonClass} !border-[#0050cb] !bg-[#0050cb] !text-white hover:!bg-[#003da3]` const checkboxClass = 'h-4 w-4 shrink-0 accent-[#0050cb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2 disabled:cursor-not-allowed' const capabilityLabels = { text: '对话', vision: '视觉理解', image: '生图', other: '其他', unknown: '待核验' } as const interface ProviderForm { id: string | null; revision: number; name: string; baseUrl: string; apiKey: string } async function request(path = '', init: RequestInit = {}): Promise { const response = await fetch(`/api/admin/ai-models${path}`, { ...init, cache: 'no-store', headers: { 'Content-Type': 'application/json', ...init.headers } }) const body = await response.json().catch(() => ({})) if (!response.ok) throw new Error(typeof body.error === 'string' ? body.error : body.error?.message || '操作未完成,请重试') return body as AiModelAdminState } function usable(provider: AiModelProvider, model: AiModelCatalogEntry, stage: AiModelStage) { return provider.enabled && provider.hasApiKey && model.present && model.enabled && model.compatibility === 'confirmed' && model.stages.includes(stage) && model.capabilities.allowedStages.includes(stage) } export default function AiModelManagementPanel({ onStateChange }: { onStateChange?: (state: { dirty: boolean; busy: boolean }) => void }) { const [state, setState] = useState(null) const [draft, setDraft] = useState(null) const [loading, setLoading] = useState(true) const [busy, setBusy] = useState('') const [error, setError] = useState('') const [notice, setNotice] = useState('') const [formError, setFormError] = useState('') const [form, setForm] = useState(null) const [search, setSearch] = useState('') const [capabilityFilter, setCapabilityFilter] = useState('all') const [statusFilter, setStatusFilter] = useState('all') const [page, setPage] = useState(1) const busyRef = useRef(false) const dirty = !!state && !!draft && JSON.stringify(state) !== JSON.stringify(draft) function accept(next: AiModelAdminState) { setState(next); setDraft(next) } useEffect(() => { let cancelled = false request().then((next) => { if (!cancelled) accept(next) }).catch((cause) => { if (!cancelled) setError(cause.message) }).finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, []) useEffect(() => { onStateChange?.({ dirty: dirty || !!form, busy: !!busy }) }, [dirty, form, busy, onStateChange]) useEffect(() => () => { onStateChange?.({ dirty: false, busy: false }) }, [onStateChange]) useEffect(() => { if (!dirty && !form) return const prevent = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = '' } window.addEventListener('beforeunload', prevent) return () => window.removeEventListener('beforeunload', prevent) }, [dirty, form]) async function run(identity: string, action: () => Promise, success: string, closeForm = false) { if (busyRef.current) return busyRef.current = true; setBusy(identity); setError(''); setFormError(''); setNotice('') try { accept(await action()) setNotice(success) if (closeForm) setForm(null) } catch (cause) { const message = cause instanceof Error ? cause.message : '操作未完成,请重试' if (identity === 'provider-save') setFormError(message) else setError(message) } finally { busyRef.current = false; setBusy('') } } function updateModel(providerId: string, key: string, change: Partial) { setDraft((current) => { if (!current) return current const defaults = { ...current.defaults } const providers = current.providers.map((provider) => provider.id !== providerId ? provider : { ...provider, models: provider.models.map((model) => { if (model.key !== key) return model const updated = { ...model, ...change } if (!updated.stages.some((stage) => updated.capabilities.allowedStages.includes(stage))) updated.enabled = false for (const stage of stages) if (defaults[stage.id] === key && !usable(provider, updated, stage.id)) defaults[stage.id] = null return updated }), }) return { ...current, providers, defaults } }) setError(''); setNotice('') } function saveProvider(event: React.FormEvent) { event.preventDefault() if (!form) return const body = { name: form.name.trim(), baseUrl: form.baseUrl.trim(), ...(form.apiKey.trim() ? { apiKey: form.apiKey.trim() } : {}), ...(form.id ? { revision: form.revision } : {}) } void run('provider-save', () => request(form.id ? `/providers/${encodeURIComponent(form.id)}` : '/providers', { method: form.id ? 'PATCH' : 'POST', body: JSON.stringify(body) }), form.id ? '上游已保存' : '上游已添加,请同步模型目录', true) } const rows = useMemo(() => (draft?.providers || []).flatMap((provider) => provider.models.map((model) => ({ provider, model }))), [draft]) const visibleRows = rows.filter(({ provider, model }) => { const query = search.trim().toLowerCase() if (query && !`${model.id} ${provider.name}`.toLowerCase().includes(query)) return false if (capabilityFilter !== 'all' && model.capabilities.kind !== capabilityFilter) return false if (statusFilter === 'enabled' && !model.enabled) return false if (statusFilter === 'disabled' && model.enabled) return false if (statusFilter === 'missing' && model.present) return false if (statusFilter === 'unconfirmed' && model.compatibility !== 'unconfirmed') return false return true }) const providerBlocked = !!busy || dirty || !!form const pageCount = Math.max(1, Math.ceil(visibleRows.length / 20)) const currentPage = Math.min(page, pageCount) const pageRows = visibleRows.slice((currentPage - 1) * 20, currentPage * 20) if (loading) return
if (!draft) return

{error || 'AI 模型配置未能加载'}

return

AI 模型接入

{draft.legacyAvailable && }
{!draft.encryptionConfigured &&

服务端尚未配置密钥加密,暂不能新增或替换 API Key。

} {(error || notice) &&

{error || notice}

}

上游连接

{!draft.providers.length && !form &&

{draft.legacyAvailable ? '迁入当前连接,或新增上游后同步模型目录。' : '新增上游后同步模型目录。'}

}
{draft.providers.map((provider) =>
{provider.name}{provider.enabled ? '已启用' : '已停用'}

{provider.baseUrl}

{provider.hasApiKey ? (provider.credentialSource === 'environment' ? '使用服务端密钥' : '密钥已安全保存') : '未配置密钥'} · {provider.models.filter((model) => model.present).length} 个目录模型{provider.syncedAt ? ` · 同步于 ${new Date(provider.syncedAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}` : ' · 尚未同步'}

)}
{form &&
{formError &&

{formError}

}
}

用户可选模型

{dirty && 有未保存修改}
阶段默认模型 {stages.map((stage) => { const options = rows.filter(({ provider, model }) => usable(provider, model, stage.id)) const selected = draft.defaults[stage.id] const unavailable = !!selected && !options.some(({ model }) => model.key === selected) const selectedRow = rows.find(({ model }) => model.key === selected) return })}
{!!rows.length && <>

用户可自行选择已开放的模型;不支持的用途不可勾选。

{!visibleRows.length &&

没有匹配模型

} {pageRows.map(({ provider, model }) =>

{model.id}

{capabilityLabels[model.capabilities.kind]}{model.capabilities.kind !== 'unknown' && !model.capabilities.allowedStages.length ? ' · 待适配' : ''} · {provider.name}{!provider.enabled ? ' · 上游已停用' : ''}{!model.present ? ' · 已离开上游目录' : ''}

{model.capabilities.reason}
{model.id} 的用户可选用途{stages.map((stage) => { const supported = model.capabilities.allowedStages.includes(stage.id) const checked = model.stages.includes(stage.id) const reason = model.capabilities.kind === 'unknown' ? '能力待核验,暂不能开放此用途' : stage.id === 'image' && (model.capabilities.kind === 'text' || model.capabilities.kind === 'vision') ? '不支持图片输出' : '当前详情页未适配此用途' return })}
)}
}
}