Merge branch 'codex/asset-gateway-controls'

This commit is contained in:
john
2026-07-12 12:18:01 +08:00
12 changed files with 1143 additions and 343 deletions
+28
View File
@@ -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());
+5
View File
@@ -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,
+2
View File
@@ -98,6 +98,7 @@ ready
pool,
userAuth,
llmProviderService,
assetGatewayConfigService,
plazaOps,
createOpsApi,
wechatAdmin,
@@ -120,6 +121,7 @@ ready
pool,
userAuth,
llmProviderService,
assetGatewayConfigService,
plazaOps,
createOpsApi,
wechatAdmin,
+3
View File
@@ -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';
}
+1
View File
@@ -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: '违禁词管理' },
],
},
+150
View File
@@ -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>
);
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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 GooseAiderOpenHands</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}
+29
View File
@@ -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
View File
@@ -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 {
+21
View File
@@ -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 = {