Merge branch 'codex/asset-gateway-controls'
This commit is contained in:
@@ -91,6 +91,7 @@ export function createAdminApp(services) {
|
||||
const {
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
pool,
|
||||
ready,
|
||||
wechatAdmin,
|
||||
@@ -325,6 +326,33 @@ export function createAdminApp(services) {
|
||||
res.json({ config: result });
|
||||
});
|
||||
|
||||
adminApi.get('/asset-gateway/config', requireAdmin, async (_req, res) => {
|
||||
if (!assetGatewayConfigService?.getConfig) {
|
||||
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
||||
}
|
||||
res.json(await assetGatewayConfigService.getConfig());
|
||||
});
|
||||
|
||||
adminApi.put('/asset-gateway/config', requireAdmin, async (req, res) => {
|
||||
if (!assetGatewayConfigService?.updateGlobalConfig) {
|
||||
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
||||
}
|
||||
res.json(await assetGatewayConfigService.updateGlobalConfig(req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
}));
|
||||
});
|
||||
|
||||
adminApi.put('/asset-gateway/plugins/:pluginId', requireAdmin, async (req, res) => {
|
||||
if (!assetGatewayConfigService?.updatePluginConfig) {
|
||||
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
||||
}
|
||||
const result = await assetGatewayConfigService.updatePluginConfig(req.params.pluginId, req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
});
|
||||
if (!result.ok) return res.status(400).json({ message: result.message });
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
|
||||
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
||||
res.json(await memoryV2ConfigService.getAdminConfig());
|
||||
|
||||
@@ -29,6 +29,8 @@ export async function bootstrapAdminServices() {
|
||||
updateMindSpaceConfig,
|
||||
} = await importMemind('mindspace-config.mjs');
|
||||
const { createMemoryV2AdminConfigService } = await importMemind('memory-v2-admin-config.mjs');
|
||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
||||
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
||||
const { createAdminSystemTestService } = await importMemind('admin-system-tests.mjs');
|
||||
const { createOpsApi } = await importMemind('admin-routes.mjs');
|
||||
@@ -86,6 +88,8 @@ export async function bootstrapAdminServices() {
|
||||
apiTarget,
|
||||
apiSecret,
|
||||
});
|
||||
await ensureAssetGatewaySchema(pool);
|
||||
const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService });
|
||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
@@ -133,6 +137,7 @@ export async function bootstrapAdminServices() {
|
||||
pool,
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
|
||||
@@ -98,6 +98,7 @@ ready
|
||||
pool,
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
@@ -120,6 +121,7 @@ ready
|
||||
pool,
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SystemTestsPage } from './admin/pages/SystemTestsPage';
|
||||
import { UserDetailPage } from './admin/pages/UserDetailPage';
|
||||
import { UsersPage } from './admin/pages/UsersPage';
|
||||
import { WechatPage } from './admin/pages/WechatPage';
|
||||
import { AssetGatewayPage } from './admin/pages/AssetGatewayPage';
|
||||
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
|
||||
import { defaultHomePath } from './lib/routes';
|
||||
import { OpsLayout } from './ops/components/OpsLayout';
|
||||
@@ -122,6 +123,7 @@ function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void }
|
||||
<Route path="memory-v2" element={<MemoryV2Page />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
<Route path="blocked-words" element={<BlockedWordsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
@@ -168,6 +170,7 @@ function loginRedirectPath(pathname: string, role: string | undefined) {
|
||||
|| pathname.startsWith('/memory-v2')
|
||||
|| pathname.startsWith('/providers')
|
||||
|| pathname.startsWith('/wechat')
|
||||
|| pathname.startsWith('/asset-gateway')
|
||||
) {
|
||||
return role === 'admin' || role === undefined ? pathname : '/ops';
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
{ to: '/skills', label: '技能' },
|
||||
{ to: '/policies', label: '策略' },
|
||||
{ to: '/providers', label: '统一模型中心' },
|
||||
{ to: '/asset-gateway', label: '资产能力' },
|
||||
{ to: '/blocked-words', label: '违禁词管理' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
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;
|
||||
};
|
||||
|
||||
function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft {
|
||||
return {
|
||||
enabled: plugin.enabled,
|
||||
provider: plugin.provider ?? '',
|
||||
llmProviderKeyId: plugin.llmProviderKeyId ?? '',
|
||||
llmModel: plugin.llmModel ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
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 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,
|
||||
});
|
||||
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) => updateDraft(plugin.pluginId, { provider: event.target.value })} disabled={!draft.enabled}>
|
||||
<option value="">选择 Provider</option>
|
||||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||
</select></label>
|
||||
{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>}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const QUICK_LINKS = [
|
||||
{ to: '/users', label: '用户管理', desc: '创建账号、启用禁用' },
|
||||
{ to: '/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
||||
{ to: '/providers', label: '统一模型中心', desc: 'Provider、执行器模型与启动控制' },
|
||||
{ to: '/asset-gateway', label: '资产能力', desc: '可插拔素材插件、Provider 与专属 LLM 选择' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' },
|
||||
{ to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' },
|
||||
{ to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' },
|
||||
|
||||
+283
-183
@@ -124,6 +124,17 @@ const BACKENDS: Array<{
|
||||
];
|
||||
|
||||
const MODEL_BACKENDS: ModelBackendKey[] = ['mem0', 'letta', 'langgraph'];
|
||||
const BACKEND_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
|
||||
const BACKEND_ICONS: Record<BackendKey, string> = {
|
||||
pgvector: 'P',
|
||||
qdrant: 'Q',
|
||||
weaviate: 'W',
|
||||
mem0: 'M',
|
||||
letta: 'L',
|
||||
neo4j: 'N',
|
||||
redisStreams: 'R',
|
||||
langgraph: 'G',
|
||||
};
|
||||
const MODEL_API_TYPES: Array<{ value: MemoryV2ModelApiType; label: string }> = [
|
||||
{ value: 'chat', label: 'Chat API' },
|
||||
{ value: 'response', label: 'Response API' },
|
||||
@@ -231,6 +242,7 @@ export function MemoryV2Page() {
|
||||
const [busyScope, setBusyScope] = useState<'global' | 'chatIntentRouter' | BackendKey | 'reload' | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [expandedBackends, setExpandedBackends] = useState<Record<string, boolean>>({});
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -288,6 +300,9 @@ export function MemoryV2Page() {
|
||||
};
|
||||
|
||||
const updateBackendFlag = (backend: BackendKey) => (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target.checked) {
|
||||
setExpandedBackends((current) => ({ ...current, [backend]: false }));
|
||||
}
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[backend]: { ...current[backend], enabled: event.target.checked },
|
||||
@@ -398,9 +413,13 @@ export function MemoryV2Page() {
|
||||
const currentRouterSection = current.chatIntentRouter;
|
||||
const routerProviderKey = findProviderKey(providerKeys, String(routerSection.modelProviderKeyId ?? ''));
|
||||
const routerAvailableModels = modelOptionsForProvider(routerProviderKey, globalModelSettings);
|
||||
const enabledBackendCount = BACKENDS.filter((backend) => Boolean(current[backend.key]?.enabled)).length;
|
||||
const updatedAtText = payload?.updatedAt
|
||||
? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false })
|
||||
: '未保存过数据库配置';
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page memory-v2-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>Memory V2 配置</h2>
|
||||
<p className="muted">这里是 Memory V2 的统一后台控制面。`memind_adm` 负责改配置,主站会从共享数据库读取并自动切换。</p>
|
||||
@@ -409,20 +428,47 @@ export function MemoryV2Page() {
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
|
||||
<div className="admin-form">
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">MEMORY V2</div>
|
||||
<h3>统一记忆控制面</h3>
|
||||
<p>
|
||||
全局 {draft.global.enabled ? '已启用' : '未启用'}
|
||||
{' · '}
|
||||
默认 backend <strong className="mono">{String(draft.global.backend ?? 'legacy')}</strong>
|
||||
{' · '}
|
||||
已启用 backend {enabledBackendCount}/{BACKENDS.length}
|
||||
{' · '}
|
||||
更新时间 {updatedAtText}
|
||||
</p>
|
||||
</div>
|
||||
<div className="model-center-hero-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void load()}
|
||||
disabled={busyScope !== null && busyScope !== 'reload'}
|
||||
>
|
||||
{busyScope === 'reload' ? '加载中…' : '重新加载'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3 className="model-center-section-title">全局与路由</h3>
|
||||
<div className="asset-plugin-grid memory-v2-top-grid">
|
||||
<section className="asset-plugin-card asset-plugin-card--violet model-center-card memory-v2-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">M</div>
|
||||
<div>
|
||||
<h2>全局开关</h2>
|
||||
<p className="muted">更新时间:{payload?.updatedAt ? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false }) : '未保存过数据库配置'}</p>
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="send-btn" disabled={loading || busyScope !== null} onClick={() => void saveGlobalConfig()}>
|
||||
{busyScope === 'global' ? '保存中...' : '保存全局'}
|
||||
</button>
|
||||
<h3>全局开关</h3>
|
||||
<p>控制 Memory V2 总开关、Profile / Event Log / Vector 与 fail-open 行为。</p>
|
||||
</div>
|
||||
<span className={`asset-status ${draft.global.enabled ? 'is-on' : ''}`}>
|
||||
{draft.global.enabled ? '已启用' : '未启用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
|
||||
<div className="memory-v2-toggle-grid">
|
||||
<label className="inline-check">
|
||||
<input type="checkbox" checked={Boolean(draft.global.enabled)} onChange={updateGlobalFlag('enabled')} />
|
||||
启用 Memory V2
|
||||
@@ -443,8 +489,11 @@ export function MemoryV2Page() {
|
||||
<input type="checkbox" checked={Boolean(draft.global.failOpen)} onChange={updateGlobalFlag('failOpen')} />
|
||||
Memory 故障时 fail-open
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
<span>默认 Backend</span>
|
||||
默认 Backend
|
||||
<select value={String(draft.global.backend ?? 'legacy')} onChange={updateGlobalText('backend')}>
|
||||
<option value="legacy">legacy</option>
|
||||
{BACKENDS.map((backend) => (
|
||||
@@ -453,39 +502,41 @@ export function MemoryV2Page() {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!loading && (
|
||||
<p className="muted">
|
||||
<p className="model-center-card-note">
|
||||
当前数据库配置默认 backend:<strong>{String(current.global.backend ?? 'legacy')}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${draft.global.enabled ? 'is-on' : ''}`}>
|
||||
{draft.global.enabled ? 'Memory V2 运行中' : 'Memory V2 关闭'}
|
||||
</span>
|
||||
<button type="button" className="send-btn" disabled={loading || busyScope !== null} onClick={() => void saveGlobalConfig()}>
|
||||
{busyScope === 'global' ? '保存中...' : '保存全局'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<section className="asset-plugin-card asset-plugin-card--blue model-center-card memory-v2-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">R</div>
|
||||
<div>
|
||||
<h2>前置 LLM 意图路由</h2>
|
||||
<p className="muted">关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM,并可读取 Memory V2 的轻量上下文。</p>
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.enabled)}
|
||||
onChange={updateRouterFlag('enabled')}
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={loading || busyScope !== null}
|
||||
onClick={() => void saveRouterConfig()}
|
||||
>
|
||||
{busyScope === 'chatIntentRouter' ? '保存中...' : '保存路由'}
|
||||
</button>
|
||||
<h3>前置 LLM 意图路由</h3>
|
||||
<p>关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM。</p>
|
||||
</div>
|
||||
<label className="asset-mini-toggle" aria-label="启用前置 LLM 意图路由">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.enabled)}
|
||||
onChange={updateRouterFlag('enabled')}
|
||||
/>
|
||||
<span>{routerSection.enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
<label>
|
||||
<span>模型来源</span>
|
||||
<select
|
||||
@@ -523,69 +574,96 @@ export function MemoryV2Page() {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>最低置信度</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={String(routerSection.minConfidence ?? '0.65')}
|
||||
onChange={updateRouterValue('minConfidence')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>记忆条数</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={String(routerSection.memoryResolveLimit ?? '8')}
|
||||
onChange={updateRouterValue('memoryResolveLimit')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>超时 Ms</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="30000"
|
||||
value={String(routerSection.timeoutMs ?? '1500')}
|
||||
onChange={updateRouterValue('timeoutMs')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>低置信 / 失败回退</span>
|
||||
<select
|
||||
value={String(routerSection.fallbackRoute ?? 'agent_orchestration')}
|
||||
onChange={updateRouterValue('fallbackRoute')}
|
||||
>
|
||||
<option value="agent_orchestration">Agent 编排</option>
|
||||
<option value="direct_chat">Direct Chat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.memoryResolveEnabled)}
|
||||
onChange={updateRouterFlag('memoryResolveEnabled')}
|
||||
/>
|
||||
路由前读取 Memory V2
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">
|
||||
|
||||
<details className="memory-v2-advanced-details">
|
||||
<summary>高级参数</summary>
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
<label>
|
||||
<span>最低置信度</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={String(routerSection.minConfidence ?? '0.65')}
|
||||
onChange={updateRouterValue('minConfidence')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>记忆条数</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
value={String(routerSection.memoryResolveLimit ?? '8')}
|
||||
onChange={updateRouterValue('memoryResolveLimit')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>超时 Ms</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="30000"
|
||||
value={String(routerSection.timeoutMs ?? '1500')}
|
||||
onChange={updateRouterValue('timeoutMs')}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>低置信 / 失败回退</span>
|
||||
<select
|
||||
value={String(routerSection.fallbackRoute ?? 'agent_orchestration')}
|
||||
onChange={updateRouterValue('fallbackRoute')}
|
||||
>
|
||||
<option value="agent_orchestration">Agent 编排</option>
|
||||
<option value="direct_chat">Direct Chat</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(routerSection.memoryResolveEnabled)}
|
||||
onChange={updateRouterFlag('memoryResolveEnabled')}
|
||||
/>
|
||||
路由前读取 Memory V2
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<p className="model-center-card-note">
|
||||
当前数据库配置:<strong>{currentRouterSection.enabled ? '已启用' : '未启用'}</strong>
|
||||
{' · '}
|
||||
生效模型链路:<strong>{describeEffectiveModelSource(routerSection, providerKeys, globalModelSettings)}</strong>
|
||||
</p>
|
||||
{!providerKeys.length && (
|
||||
<p className="muted">
|
||||
<p className="model-center-card-note">
|
||||
统一模型中心当前没有可用的激活 key;若启用路由,请先配置 Provider key 与模型列表。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{BACKENDS.map((backend) => {
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${routerSection.enabled ? 'is-on' : ''}`}>
|
||||
{routerSection.enabled ? '路由已启用' : '路由已关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={loading || busyScope !== null}
|
||||
onClick={() => void saveRouterConfig()}
|
||||
>
|
||||
{busyScope === 'chatIntentRouter' ? '保存中...' : '保存路由'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">Backend 配置</h3>
|
||||
<p className="model-center-card-note memory-v2-backend-intro">
|
||||
单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。未启用的 backend 默认折叠,点击标题可展开。
|
||||
</p>
|
||||
<div className="asset-plugin-grid memory-v2-backend-grid">
|
||||
{BACKENDS.map((backend, index) => {
|
||||
const section = draft[backend.key];
|
||||
const currentSection = current[backend.key];
|
||||
const providerKey = isModelBackend(backend.key)
|
||||
@@ -594,23 +672,123 @@ export function MemoryV2Page() {
|
||||
const availableModels = isModelBackend(backend.key)
|
||||
? modelOptionsForProvider(providerKey, globalModelSettings)
|
||||
: [];
|
||||
const accent = BACKEND_ACCENTS[index % BACKEND_ACCENTS.length];
|
||||
const isBackendOpen = Boolean(section.enabled) || Boolean(expandedBackends[backend.key]);
|
||||
|
||||
return (
|
||||
<section key={backend.key} className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<div>
|
||||
<h2>{backend.label}</h2>
|
||||
<p className="muted">单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。</p>
|
||||
<details
|
||||
key={backend.key}
|
||||
className={`asset-plugin-card asset-plugin-card--${accent} model-center-card memory-v2-card memory-v2-backend-details`}
|
||||
open={isBackendOpen}
|
||||
onToggle={(event) => {
|
||||
const nextOpen = event.currentTarget.open;
|
||||
setExpandedBackends((current) => ({
|
||||
...current,
|
||||
[backend.key]: nextOpen,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<summary className="memory-v2-backend-summary">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{BACKEND_ICONS[backend.key]}</div>
|
||||
<div className="memory-v2-backend-summary-main">
|
||||
<h3>{backend.label}</h3>
|
||||
<span className={`asset-status ${section.enabled ? 'is-on' : ''}`}>
|
||||
{section.enabled ? '已启用' : '未启用 · 点击展开'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-actions">
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(section.enabled)}
|
||||
onChange={updateBackendFlag(backend.key)}
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
<label
|
||||
className="asset-mini-toggle"
|
||||
aria-label={`启用 ${backend.label}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(section.enabled)}
|
||||
onChange={updateBackendFlag(backend.key)}
|
||||
/>
|
||||
<span>{section.enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</summary>
|
||||
|
||||
<div className="memory-v2-backend-body">
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
{backend.fields.map((field) => {
|
||||
const configured = Boolean(currentSection?.[`${field.key}Configured`]);
|
||||
const masked = String(currentSection?.[`${field.key}Masked`] ?? '');
|
||||
return (
|
||||
<label key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
value={String(section[field.key] ?? '')}
|
||||
placeholder={field.secret && configured ? `已配置:${masked || '******'};留空保持不变` : ''}
|
||||
onChange={updateBackendValue(backend.key, field.key)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isModelBackend(backend.key) && (
|
||||
<>
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
<label>
|
||||
<span>模型来源</span>
|
||||
<select
|
||||
value={String(section.modelProviderKeyId ?? '')}
|
||||
onChange={updateModelProvider(backend.key)}
|
||||
>
|
||||
<option value="">跟随统一模型中心默认</option>
|
||||
{providerKeys.map((item) => (
|
||||
<option key={item.id} value={item.id}>{providerLabel(item)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型</span>
|
||||
<select
|
||||
value={String(section.model ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'model')}
|
||||
>
|
||||
<option value="">
|
||||
{providerKey ? '使用当前 Provider 默认模型' : '跟随统一模型中心默认模型'}
|
||||
</option>
|
||||
{availableModels.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型类型</span>
|
||||
<select
|
||||
value={String(section.modelApiType ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'modelApiType')}
|
||||
>
|
||||
<option value="">未指定</option>
|
||||
{supportedApiTypes.map((apiType) => (
|
||||
<option key={apiType} value={apiType}>
|
||||
{MODEL_API_TYPES.find((item) => item.value === apiType)?.label ?? apiType}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="model-center-card-note">
|
||||
生效模型:<strong>{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}</strong>
|
||||
</p>
|
||||
{!providerKeys.length && (
|
||||
<p className="model-center-card-note">
|
||||
统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${section.enabled ? 'is-on' : ''}`}>
|
||||
{section.enabled ? `${backend.label} 已启用` : `${backend.label} 未启用`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
@@ -621,87 +799,9 @@ export function MemoryV2Page() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-form-grid">
|
||||
{backend.fields.map((field) => {
|
||||
const configured = Boolean(currentSection?.[`${field.key}Configured`]);
|
||||
const masked = String(currentSection?.[`${field.key}Masked`] ?? '');
|
||||
return (
|
||||
<label key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={field.secret ? 'password' : 'text'}
|
||||
value={String(section[field.key] ?? '')}
|
||||
placeholder={field.secret && configured ? `已配置:${masked || '******'};留空保持不变` : ''}
|
||||
onChange={updateBackendValue(backend.key, field.key)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isModelBackend(backend.key) && (
|
||||
<>
|
||||
<div className="admin-form-grid" style={{ marginTop: 12 }}>
|
||||
<label>
|
||||
<span>模型来源</span>
|
||||
<select
|
||||
value={String(section.modelProviderKeyId ?? '')}
|
||||
onChange={updateModelProvider(backend.key)}
|
||||
>
|
||||
<option value="">跟随统一模型中心默认</option>
|
||||
{providerKeys.map((item) => (
|
||||
<option key={item.id} value={item.id}>{providerLabel(item)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型</span>
|
||||
<select
|
||||
value={String(section.model ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'model')}
|
||||
>
|
||||
<option value="">
|
||||
{providerKey ? '使用当前 Provider 默认模型' : '跟随统一模型中心默认模型'}
|
||||
</option>
|
||||
{availableModels.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>模型类型</span>
|
||||
<select
|
||||
value={String(section.modelApiType ?? '')}
|
||||
onChange={updateBackendValue(backend.key, 'modelApiType')}
|
||||
>
|
||||
<option value="">未指定</option>
|
||||
{supportedApiTypes.map((apiType) => (
|
||||
<option key={apiType} value={apiType}>
|
||||
{MODEL_API_TYPES.find((item) => item.value === apiType)?.label ?? apiType}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p className="muted">
|
||||
当前生效模型链路:<strong>{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}</strong>
|
||||
</p>
|
||||
{!providerKeys.length && (
|
||||
<p className="muted">
|
||||
统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="ghost-btn" onClick={() => void load()} disabled={busyScope !== null && busyScope !== 'reload'}>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+215
-160
@@ -35,6 +35,14 @@ import type {
|
||||
|
||||
const CUSTOM_PROVIDER_ID = '__custom__';
|
||||
|
||||
const EXECUTOR_VISUALS: Record<string, { icon: string; accent: string }> = {
|
||||
goose: { icon: 'G', accent: 'blue' },
|
||||
aider: { icon: 'A', accent: 'amber' },
|
||||
openhands: { icon: 'O', accent: 'violet' },
|
||||
};
|
||||
|
||||
const PROVIDER_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
|
||||
|
||||
const defaultProviderForm = {
|
||||
providerId: CUSTOM_PROVIDER_ID,
|
||||
name: '',
|
||||
@@ -270,40 +278,41 @@ function ExecutorCard({
|
||||
}
|
||||
}, [model, selectedKey]);
|
||||
|
||||
const visual = EXECUTOR_VISUALS[binding.executor] ?? { icon: '✦', accent: 'blue' };
|
||||
|
||||
return (
|
||||
<article className="executor-card">
|
||||
<div className="admin-card-head">
|
||||
<section className={`asset-plugin-card asset-plugin-card--${visual.accent} model-center-card`}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
|
||||
<div>
|
||||
<h3>{binding.executorLabel}</h3>
|
||||
<p className="muted">{binding.executorDescription}</p>
|
||||
<p>{binding.executorDescription}</p>
|
||||
</div>
|
||||
<label className="inline-check">
|
||||
<label className="asset-mini-toggle" aria-label={`${binding.executorLabel} 启用状态`}>
|
||||
<input type="checkbox" checked={enabled} onChange={(event) => setEnabled(event.target.checked)} />
|
||||
启用
|
||||
<span>{enabled ? '启用' : '关闭'}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="executor-form-grid">
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || (enabled && (!keyId || !model))}
|
||||
onClick={() => void onSave(binding, keyId || null, model, enabled)}
|
||||
>
|
||||
保存绑定
|
||||
</button>
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
Provider
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
</label>
|
||||
<label>
|
||||
模型
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="executor-footer">
|
||||
<div className="model-center-card-body">
|
||||
<div className="executor-meta">
|
||||
<span>{statusText(launchStatus)}</span>
|
||||
{runtime?.ok ? <span>{runtime.providerName} · {runtime.model}</span> : <span>{runtime?.message ?? '运行配置未就绪'}</span>}
|
||||
@@ -348,10 +357,24 @@ function ExecutorCard({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
|
||||
</div>
|
||||
|
||||
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
|
||||
</article>
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${enabled ? 'is-on' : ''}`}>
|
||||
{enabled ? '绑定启用中' : '绑定已关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || (enabled && (!keyId || !model))}
|
||||
onClick={() => void onSave(binding, keyId || null, model, enabled)}
|
||||
>
|
||||
保存绑定
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -387,45 +410,61 @@ function VisionSection({
|
||||
}, [model, selectedKey]);
|
||||
|
||||
return (
|
||||
<section className="admin-card providers-panel">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>图片任务模型</h3>
|
||||
<p className="muted">发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)</p>
|
||||
<div className="asset-plugin-grid model-center-vision-grid">
|
||||
<section className="asset-plugin-card asset-plugin-card--green model-center-card">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">◉</div>
|
||||
<div>
|
||||
<h3>图片任务模型</h3>
|
||||
<p>发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)</p>
|
||||
</div>
|
||||
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
|
||||
{settings?.keyId ? `${settings.keyName} · ${settings.visionModel}` : '未配置'}
|
||||
</span>
|
||||
</div>
|
||||
{settings?.keyId ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
|
||||
清除
|
||||
</button>
|
||||
|
||||
<div className="asset-plugin-fields">
|
||||
<label>
|
||||
Provider
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
</label>
|
||||
<label>
|
||||
视觉模型
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择视觉模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!settings?.keyId ? (
|
||||
<p className="model-center-card-note">未配置时,图片消息将由默认模型处理。</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{settings?.keyId ? (
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>
|
||||
当前:<strong>{settings.keyName}</strong> · <span className="mono">{settings.visionModel}</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted" style={{ marginBottom: '0.75rem' }}>未配置,图片消息将由默认模型处理</p>
|
||||
)}
|
||||
|
||||
<div className="executor-form-grid">
|
||||
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
|
||||
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
|
||||
<option value="">选择视觉模型</option>
|
||||
{modelOptions.map((item) => (
|
||||
<option key={item} value={item}>{item}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || !keyId || !model}
|
||||
onClick={() => void onSave(keyId, model)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
|
||||
{settings?.keyId ? '已绑定视觉模型' : '等待配置'}
|
||||
</span>
|
||||
<div className="model-center-card-actions">
|
||||
{settings?.keyId ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
|
||||
清除
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={busy || !keyId || !model}
|
||||
onClick={() => void onSave(keyId, model)}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -723,123 +762,139 @@ export function ProvidersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const activeProviderCount = keys.filter((item) => item.status === 'active').length;
|
||||
|
||||
return (
|
||||
<div className="admin-page providers-page">
|
||||
<div className="admin-page-head providers-hero">
|
||||
<div>
|
||||
<h2>统一模型中心</h2>
|
||||
<p className="muted">管理 Provider、执行器绑定和默认模型</p>
|
||||
</div>
|
||||
<div className="providers-hero-meta">
|
||||
<div className="providers-hero-stat">
|
||||
<span className="providers-hero-label">当前默认</span>
|
||||
<strong>{globalSettings?.globalModel ?? '未设置'}</strong>
|
||||
</div>
|
||||
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
|
||||
添加 Provider
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>统一模型中心</h2>
|
||||
<p className="muted">管理 Provider、执行器绑定和默认模型</p>
|
||||
</div>
|
||||
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
|
||||
<section className="admin-card providers-panel providers-panel-primary">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>执行器绑定</h3>
|
||||
<p className="muted">把 Provider 和模型绑定到 Goose、Aider、OpenHands</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
|
||||
同步 Goose 绑定
|
||||
</button>
|
||||
<section className="asset-gateway-hero">
|
||||
<div>
|
||||
<div className="asset-kicker">MODEL CENTER</div>
|
||||
<h3>默认模型与 Provider 资源池</h3>
|
||||
<p>
|
||||
当前默认模型 <strong className="mono">{globalSettings?.globalModel ?? '未设置'}</strong>
|
||||
{' · '}
|
||||
Provider 资源池 {activeProviderCount}/{keys.length} 可用
|
||||
</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="muted">加载中...</p>
|
||||
) : (
|
||||
<div className="executor-grid">
|
||||
{bindings.map((binding) => (
|
||||
<ExecutorCard
|
||||
key={binding.executor}
|
||||
binding={binding}
|
||||
keys={activeKeys}
|
||||
runtime={runtimes.find((item) => item.executor === binding.executor)}
|
||||
plan={plans.find((item) => item.executor === binding.executor)}
|
||||
launchStatus={launchStatuses[binding.executor]}
|
||||
busy={busy}
|
||||
onSave={handleSaveBinding}
|
||||
onLaunch={handleLaunch}
|
||||
onStop={handleStop}
|
||||
onRestart={handleRestart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-card providers-panel providers-panel-secondary">
|
||||
<div className="admin-card-head providers-panel-head">
|
||||
<div>
|
||||
<h3>Provider 资源池</h3>
|
||||
<p className="muted">按名称纵向查看,更接近 list</p>
|
||||
</div>
|
||||
<div className="model-center-hero-actions">
|
||||
<button type="button" className="ghost-btn" disabled={busy || loading} onClick={() => void load()}>
|
||||
刷新
|
||||
</button>
|
||||
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
|
||||
添加 Provider
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="providers-list">
|
||||
{keys.map((row) => (
|
||||
<article key={row.id} className="provider-list-item">
|
||||
<div className="provider-list-main">
|
||||
<div className="provider-list-top">
|
||||
<div className="providers-name">
|
||||
<span>{row.name}</span>
|
||||
{row.isSelected ? <span className="risk-pill risk-low">默认</span> : null}
|
||||
</div>
|
||||
<span className={`provider-state ${row.status === 'active' ? 'is-active' : 'is-disabled'}`}>
|
||||
{row.status === 'active' ? '可用' : '禁用'}
|
||||
</span>
|
||||
<div className="model-center-section-head">
|
||||
<h3 className="model-center-section-title">执行器绑定</h3>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
|
||||
同步 Goose 绑定
|
||||
</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="muted">加载中...</p>
|
||||
) : (
|
||||
<div className="asset-plugin-grid">
|
||||
{bindings.map((binding) => (
|
||||
<ExecutorCard
|
||||
key={binding.executor}
|
||||
binding={binding}
|
||||
keys={activeKeys}
|
||||
runtime={runtimes.find((item) => item.executor === binding.executor)}
|
||||
plan={plans.find((item) => item.executor === binding.executor)}
|
||||
launchStatus={launchStatuses[binding.executor]}
|
||||
busy={busy}
|
||||
onSave={handleSaveBinding}
|
||||
onLaunch={handleLaunch}
|
||||
onStop={handleStop}
|
||||
onRestart={handleRestart}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="model-center-section-title">Provider 资源池</h3>
|
||||
<div className="asset-plugin-grid">
|
||||
{keys.map((row, index) => {
|
||||
const accent = PROVIDER_ACCENTS[index % PROVIDER_ACCENTS.length];
|
||||
return (
|
||||
<section key={row.id} className={`asset-plugin-card asset-plugin-card--${accent} model-center-card model-center-provider-card`}>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">✦</div>
|
||||
<div>
|
||||
<h3>{row.name}</h3>
|
||||
<p>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</p>
|
||||
</div>
|
||||
<div className="provider-list-meta">
|
||||
<span>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</span>
|
||||
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
|
||||
{row.status === 'active' ? '可用' : '禁用'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="model-center-provider-badges">
|
||||
{row.isSelected ? <span className="risk-pill risk-low">默认</span> : null}
|
||||
{row.isVisionSelected ? <span className="risk-pill risk-medium">图片任务</span> : null}
|
||||
</div>
|
||||
|
||||
<div className="asset-plugin-fields model-center-provider-meta">
|
||||
<div className="model-center-meta-row">
|
||||
<span>默认模型</span>
|
||||
<span className="mono">{row.defaultModel}</span>
|
||||
</div>
|
||||
<div className="model-center-meta-row">
|
||||
<span>API Key</span>
|
||||
<span className="mono">{row.apiKeyMasked}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-actions providers-actions provider-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy}
|
||||
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
|
||||
测试
|
||||
</button>
|
||||
{!row.isSelected && row.status === 'active' ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
|
||||
设为默认
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
|
||||
{row.isSelected ? '当前默认 Provider' : '备用配置'}
|
||||
</span>
|
||||
<div className="model-center-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy}
|
||||
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || row.isSelected}
|
||||
onClick={() => void handleToggleProviderStatus(row)}
|
||||
>
|
||||
{row.status === 'active' ? '禁用' : '恢复'}
|
||||
</button>
|
||||
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
|
||||
删除
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
|
||||
测试
|
||||
</button>
|
||||
{!row.isSelected && row.status === 'active' ? (
|
||||
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
|
||||
设为默认
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={busy || row.isSelected}
|
||||
onClick={() => void handleToggleProviderStatus(row)}
|
||||
>
|
||||
{row.status === 'active' ? '禁用' : '恢复'}
|
||||
</button>
|
||||
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">图片任务模型</h3>
|
||||
<VisionSection
|
||||
keys={activeKeys}
|
||||
settings={visionSettings}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AssetGatewayConfig,
|
||||
AdminDashboardSummary,
|
||||
AdminServiceRestartAction,
|
||||
AdminServiceRestartResult,
|
||||
@@ -279,6 +280,34 @@ export async function updateWechatScheduleLlmConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAssetGatewayConfig(): Promise<AssetGatewayConfig> {
|
||||
return portalFetch('/admin-api/asset-gateway/config');
|
||||
}
|
||||
|
||||
export async function updateAssetGatewayConfig(
|
||||
payload: Pick<AssetGatewayConfig, 'enabled'>,
|
||||
): Promise<AssetGatewayConfig> {
|
||||
return portalFetch('/admin-api/asset-gateway/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAssetPluginConfig(
|
||||
pluginId: string,
|
||||
payload: {
|
||||
enabled: boolean;
|
||||
provider?: string | null;
|
||||
llmProviderKeyId?: string | null;
|
||||
llmModel?: string | null;
|
||||
},
|
||||
): Promise<{ ok: boolean; config: AssetGatewayConfig }> {
|
||||
return portalFetch(`/admin-api/asset-gateway/plugins/${encodeURIComponent(pluginId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getMindSpaceAdminConfig(): Promise<MindSpaceAdminConfig> {
|
||||
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
||||
return result.config;
|
||||
|
||||
+405
@@ -950,6 +950,411 @@ body,
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Asset gateway / model center ────────────────────── */
|
||||
|
||||
.asset-gateway-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 72%, #7c5cff);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(120deg, rgba(75, 55, 146, .28), rgba(21, 27, 38, .78));
|
||||
}
|
||||
|
||||
.asset-kicker {
|
||||
color: #aaa1ff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .12em;
|
||||
}
|
||||
|
||||
.asset-gateway-hero h3 {
|
||||
margin: 5px 0 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.asset-gateway-hero p {
|
||||
max-width: 620px;
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-toggle,
|
||||
.asset-mini-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-toggle {
|
||||
padding: 9px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .07);
|
||||
}
|
||||
|
||||
.asset-plugin-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.asset-plugin-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-bg-surface);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.asset-plugin-grid--balanced .asset-plugin-card {
|
||||
min-height: 326px;
|
||||
}
|
||||
|
||||
.asset-plugin-card--violet { border-top: 3px solid #9374ff; }
|
||||
.asset-plugin-card--blue { border-top: 3px solid #54a8ff; }
|
||||
.asset-plugin-card--amber { border-top: 3px solid #e8af55; }
|
||||
.asset-plugin-card--green { border-top: 3px solid #55c79b; }
|
||||
|
||||
.asset-plugin-card-head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.asset-plugin-icon {
|
||||
display: grid;
|
||||
flex: 0 0 38px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: rgba(132, 110, 255, .17);
|
||||
color: #c4b8ff;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.asset-plugin-card-head h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.asset-plugin-card-head p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.asset-mini-toggle {
|
||||
margin-left: auto;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-plugin-fields {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.asset-plugin-fields label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-plugin-fields select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-plugin-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.asset-status {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-status.is-on {
|
||||
color: #68d8a7;
|
||||
}
|
||||
|
||||
.model-center-hero-actions,
|
||||
.model-center-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-center-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.model-center-section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.model-center-section-head .model-center-section-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-center-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.model-center-provider-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.model-center-card-body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-center-card-note {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-center-provider-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-center-provider-meta {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-center-meta-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-center-meta-row .mono {
|
||||
color: var(--color-text-primary);
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.model-center-vision-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.model-center-card .executor-instruction,
|
||||
.model-center-card .executor-meta,
|
||||
.model-center-card .executor-note,
|
||||
.model-center-card .executor-log {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.model-center-card .executor-actions {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.memory-v2-page {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.memory-v2-page > .asset-gateway-hero,
|
||||
.memory-v2-page > .asset-plugin-grid,
|
||||
.memory-v2-page > .model-center-section-title,
|
||||
.memory-v2-page > .model-center-card-note,
|
||||
.memory-v2-page > .banner {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.memory-v2-page .memory-v2-fields input,
|
||||
.memory-v2-page .memory-v2-fields select,
|
||||
.memory-v2-page .asset-plugin-fields select {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border-input);
|
||||
background: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.memory-v2-top-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-grid {
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-card {
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.memory-v2-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.memory-v2-backend-intro {
|
||||
margin: -4px 0 10px;
|
||||
}
|
||||
|
||||
.memory-v2-backend-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary-main {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.memory-v2-backend-summary-main h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.memory-v2-backend-body {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details > summary {
|
||||
padding: 8px 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.memory-v2-advanced-details .memory-v2-fields {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.memory-v2-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.memory-v2-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 10px 12px;
|
||||
}
|
||||
|
||||
.memory-v2-fields label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.memory-v2-fields label > span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-card .inline-check {
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-page .model-center-card-note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.memory-v2-backend-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.asset-gateway-hero,
|
||||
.model-center-section-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.asset-plugin-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.asset-plugin-footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Capability settings ─────────────────────────────── */
|
||||
|
||||
.capability-intro {
|
||||
|
||||
@@ -195,6 +195,27 @@ export type LlmExecutorLaunchState = {
|
||||
instruction: string | null;
|
||||
};
|
||||
|
||||
export type AssetPluginConfig = {
|
||||
pluginId: string;
|
||||
label: string;
|
||||
description: string;
|
||||
providers: string[];
|
||||
supportsLlm: boolean;
|
||||
enabled: boolean;
|
||||
provider: string | null;
|
||||
llmProviderKeyId: string | null;
|
||||
llmProviderName: string | null;
|
||||
llmProviderId: string | null;
|
||||
llmModel: string | null;
|
||||
updatedAt: number | null;
|
||||
};
|
||||
|
||||
export type AssetGatewayConfig = {
|
||||
enabled: boolean;
|
||||
updatedAt: number | null;
|
||||
plugins: AssetPluginConfig[];
|
||||
};
|
||||
|
||||
export type AdminServiceRestartAction = 'local_restart' | 'pro_restart';
|
||||
|
||||
export type AdminServiceRestartResult = {
|
||||
|
||||
Reference in New Issue
Block a user