192 lines
9.2 KiB
TypeScript
192 lines
9.2 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
getAssetGatewayConfig,
|
||
listLlmProviderKeys,
|
||
updateAssetGatewayConfig,
|
||
updateAssetPluginConfig,
|
||
} from '../../api/client';
|
||
import type { AssetGatewayConfig, AssetPluginConfig, LlmProviderKeyRow } from '../../types';
|
||
|
||
type PluginDraft = {
|
||
enabled: boolean;
|
||
provider: string;
|
||
llmProviderKeyId: string;
|
||
llmModel: string;
|
||
purposes: Record<string, boolean>;
|
||
};
|
||
|
||
function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
|
||
return {
|
||
enabled: plugin.enabled,
|
||
provider: plugin.provider ?? '',
|
||
llmProviderKeyId: plugin.llmProviderKeyId ?? '',
|
||
llmModel: plugin.llmModel ?? '',
|
||
purposes: { ...(plugin.purposes ?? {}) },
|
||
};
|
||
}
|
||
|
||
const pluginVisuals: Record<string, { icon: string; accent: string }> = {
|
||
'asset-search': { icon: '⌕', accent: 'blue' },
|
||
'asset-generate': { icon: '✦', accent: 'violet' },
|
||
'asset-transform': { icon: '✧', accent: 'amber' },
|
||
'asset-analyze': { icon: '◉', accent: 'green' },
|
||
};
|
||
|
||
export function AssetGatewayPage() {
|
||
const [config, setConfig] = useState<AssetGatewayConfig | null>(null);
|
||
const [keys, setKeys] = useState<LlmProviderKeyRow[]>([]);
|
||
const [drafts, setDrafts] = useState<Record<string, PluginDraft>>({});
|
||
const [loading, setLoading] = useState(true);
|
||
const [busy, setBusy] = useState<string | null>(null);
|
||
const [message, setMessage] = useState<string | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const activeKeys = useMemo(() => keys.filter((key) => key.status === 'active'), [keys]);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const [nextConfig, nextKeys] = await Promise.all([getAssetGatewayConfig(), listLlmProviderKeys()]);
|
||
setConfig(nextConfig);
|
||
setKeys(nextKeys);
|
||
setDrafts(Object.fromEntries(nextConfig.plugins.map((plugin) => [plugin.pluginId, draftFromPlugin(plugin)])));
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '加载资产能力配置失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => { void load(); }, [load]);
|
||
|
||
const updateDraft = (pluginId: string, patch: Partial<PluginDraft>) => {
|
||
setDrafts((current) => ({ ...current, [pluginId]: { ...current[pluginId], ...patch } }));
|
||
};
|
||
|
||
const updatePurpose = (pluginId: string, purposeId: string, enabled: boolean) => {
|
||
setDrafts((current) => ({
|
||
...current,
|
||
[pluginId]: {
|
||
...current[pluginId],
|
||
purposes: { ...(current[pluginId]?.purposes ?? {}), [purposeId]: enabled },
|
||
},
|
||
}));
|
||
};
|
||
|
||
const saveGlobal = async (enabled: boolean) => {
|
||
setBusy('global'); setMessage(null); setError(null);
|
||
try {
|
||
const next = await updateAssetGatewayConfig({ enabled });
|
||
setConfig(next);
|
||
setMessage(`资产能力总开关已${enabled ? '开启' : '关闭'}。`);
|
||
} catch (err) { setError(err instanceof Error ? err.message : '保存总开关失败'); }
|
||
finally { setBusy(null); }
|
||
};
|
||
|
||
const savePlugin = async (plugin: AssetPluginConfig) => {
|
||
const draft = drafts[plugin.pluginId];
|
||
if (!draft) return;
|
||
setBusy(plugin.pluginId); setMessage(null); setError(null);
|
||
try {
|
||
const result = await updateAssetPluginConfig(plugin.pluginId, {
|
||
enabled: draft.enabled,
|
||
provider: draft.provider || null,
|
||
llmProviderKeyId: draft.llmProviderKeyId || null,
|
||
llmModel: draft.llmModel || null,
|
||
purposes: draft.purposes,
|
||
});
|
||
setConfig(result.config);
|
||
setDrafts(Object.fromEntries(result.config.plugins.map((item) => [item.pluginId, draftFromPlugin(item)])));
|
||
setMessage(`${plugin.label}配置已保存。`);
|
||
} catch (err) { setError(err instanceof Error ? err.message : '保存插件配置失败'); }
|
||
finally { setBusy(null); }
|
||
};
|
||
|
||
if (loading) return <div className="admin-page"><p className="muted">加载资产能力配置…</p></div>;
|
||
if (!config) return <div className="admin-page"><p className="banner banner-error">{error ?? '无法加载配置'}</p></div>;
|
||
|
||
return (
|
||
<div className="admin-page">
|
||
<div className="admin-page-head">
|
||
<h2>资产能力</h2>
|
||
<p className="muted">可插拔图片、素材与处理能力。默认关闭,不会影响现有聊天或页面生成流程。</p>
|
||
</div>
|
||
{error && <p className="banner banner-error">{error}</p>}
|
||
{message && <p className="banner banner-success">{message}</p>}
|
||
<section className="asset-gateway-hero">
|
||
<div>
|
||
<div className="asset-kicker">OPTIONAL CAPABILITY</div>
|
||
<h3>资产能力 Gateway</h3>
|
||
<p>关闭时所有调用自动降级为现有流程;配置保留,现有聊天和页面生成不受影响。</p>
|
||
</div>
|
||
<label className="asset-toggle">
|
||
<input type="checkbox" checked={config.enabled} disabled={busy === 'global'} onChange={(event) => void saveGlobal(event.target.checked)} />
|
||
<span>{config.enabled ? '已启用' : '已关闭'}</span>
|
||
</label>
|
||
</section>
|
||
<div className="asset-plugin-grid asset-plugin-grid--balanced">
|
||
{config.plugins.map((plugin) => {
|
||
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
||
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
|
||
const visual = pluginVisuals[plugin.pluginId] ?? { icon: '✦', accent: 'blue' };
|
||
return <section className={`asset-plugin-card asset-plugin-card--${visual.accent}`} key={plugin.pluginId}>
|
||
<div className="asset-plugin-card-head">
|
||
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
|
||
<div><h3>{plugin.label}</h3><p>{plugin.description}</p></div>
|
||
<label className="asset-mini-toggle" title={`启用${plugin.label}`}>
|
||
<input type="checkbox" checked={draft.enabled} onChange={(event) => updateDraft(plugin.pluginId, { enabled: event.target.checked })} />
|
||
<span>{draft.enabled ? '开启' : '关闭'}</span>
|
||
</label>
|
||
</div>
|
||
<div className="asset-plugin-fields">
|
||
<label>运行 Provider<select value={draft.provider} onChange={(event) => {
|
||
const provider = event.target.value;
|
||
updateDraft(plugin.pluginId, {
|
||
provider,
|
||
...(provider === 'image_make' ? { llmProviderKeyId: '', llmModel: '' } : {}),
|
||
});
|
||
}} disabled={!draft.enabled}>
|
||
<option value="">选择 Provider</option>
|
||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||
</select></label>
|
||
{plugin.pluginId === 'asset-generate' && draft.provider === 'image_make' ? (
|
||
<div className="asset-image-make-note">
|
||
image_make 独立管理本地模型,不读取统一模型中心密钥。
|
||
</div>
|
||
) : plugin.supportsLlm ? <>
|
||
<label>专属 LLM<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
|
||
<option value="">不使用 LLM</option>
|
||
{activeKeys.map((key) => <option key={key.id} value={key.id}>{key.name}</option>)}
|
||
</select></label>
|
||
<label>模型<select value={draft.llmModel} onChange={(event) => updateDraft(plugin.pluginId, { llmModel: event.target.value })} disabled={!draft.enabled || !selectedKey}>
|
||
<option value="">选择模型</option>
|
||
{selectedKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||
</select></label>
|
||
</> : <div className="asset-no-llm">确定性处理 · 不调用 LLM</div>}
|
||
{plugin.pluginId === 'asset-generate' && plugin.purposeCatalog?.length ? (
|
||
<fieldset className="asset-purpose-fieldset" disabled={!draft.enabled || draft.provider !== 'image_make'}>
|
||
<legend>image_make 生效范围</legend>
|
||
<div className="asset-purpose-grid">
|
||
{plugin.purposeCatalog.map((purpose) => (
|
||
<label key={purpose.id}>
|
||
<input
|
||
type="checkbox"
|
||
checked={Boolean(draft.purposes[purpose.id])}
|
||
onChange={(event) => updatePurpose(plugin.pluginId, purpose.id, event.target.checked)}
|
||
/>
|
||
<span>{purpose.label}<small>{purpose.presetId}</small></span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
<p>信息流缩略图仍由 MindSpace 从封面源图派生,不会重复调用生图模型。</p>
|
||
</fieldset>
|
||
) : null}
|
||
</div>
|
||
<div className="asset-plugin-footer"><span className={`asset-status ${draft.enabled ? 'is-on' : ''}`}>{draft.enabled ? '待保存为启用' : '默认安全关闭'}</span><button type="button" className="send-btn" disabled={busy === plugin.pluginId} onClick={() => void savePlugin(plugin)}>{busy === plugin.pluginId ? '保存中…' : '保存配置'}</button></div>
|
||
</section>;
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|