feat: add MindSearch admin controls
This commit is contained in:
@@ -98,6 +98,7 @@ export function createAdminApp(services) {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
@@ -368,6 +369,26 @@ export function createAdminApp(services) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/mindsearch/config', requireAdmin, async (_req, res) => {
|
||||
if (!mindSearchConfigService?.getAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.getAdminConfig());
|
||||
});
|
||||
|
||||
adminApi.patch('/mindsearch/config', requireAdmin, async (req, res) => {
|
||||
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
|
||||
});
|
||||
|
||||
adminApi.put('/mindsearch/config', requireAdmin, async (req, res) => {
|
||||
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
|
||||
});
|
||||
|
||||
adminApi.get('/mindsearch/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
||||
res.json(await mindSearchConfigService.getRuntimeState());
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => {
|
||||
const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
||||
try {
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function bootstrapAdminServices() {
|
||||
updateMindSpaceConfig,
|
||||
} = await importMemind('mindspace-config.mjs');
|
||||
const { createMemoryV2AdminConfigService } = await importMemind('memory-v2-admin-config.mjs');
|
||||
const { createMindSearchConfigService } = await importMemind('mindsearch-config.mjs');
|
||||
const { createPersonalMemoryCandidateStore } = await importMemind('memory-v2-personal-store.mjs');
|
||||
const { createSkillRuntimeAdminConfigService } = await importMemind('skill-runtime-admin-config.mjs');
|
||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||
@@ -95,6 +96,8 @@ export async function bootstrapAdminServices() {
|
||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
const mindSearchConfigService = createMindSearchConfigService(pool, { env: process.env });
|
||||
await mindSearchConfigService.ensureSchema();
|
||||
const personalMemoryCandidateStore = createPersonalMemoryCandidateStore(pool);
|
||||
const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, {
|
||||
env: process.env,
|
||||
@@ -152,6 +155,7 @@ export async function bootstrapAdminServices() {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
|
||||
@@ -105,6 +105,7 @@ ready
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
@@ -130,6 +131,7 @@ ready
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DashboardPage } from './admin/pages/DashboardPage';
|
||||
import { PoliciesPage } from './admin/pages/PoliciesPage';
|
||||
import { MindSpacePage } from './admin/pages/MindSpacePage';
|
||||
import { MemoryV2Page } from './admin/pages/MemoryV2Page';
|
||||
import { MindSearchPage } from './admin/pages/MindSearchPage';
|
||||
import { ProvidersPage } from './admin/pages/ProvidersPage';
|
||||
import { SkillsPage } from './admin/pages/SkillsPage';
|
||||
import { SystemTestsPage } from './admin/pages/SystemTestsPage';
|
||||
@@ -122,6 +123,7 @@ function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void }
|
||||
<Route path="policies" element={<PoliciesPage />} />
|
||||
<Route path="mindspace" element={<MindSpacePage />} />
|
||||
<Route path="memory-v2" element={<MemoryV2Page />} />
|
||||
<Route path="mindsearch" element={<MindSearchPage />} />
|
||||
<Route path="skill-runtime" element={<SkillRuntimePage />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
|
||||
@@ -29,6 +29,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
{ to: '/wechat', label: '服务号' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置' },
|
||||
{ to: '/memory-v2', label: 'Memory V2' },
|
||||
{ to: '/mindsearch', label: 'MindSearch' },
|
||||
{ to: '/skill-runtime', label: 'Skill Runtime' },
|
||||
{ to: '/system-tests', label: '系统测试验证' },
|
||||
{ to: '/capabilities', label: '能力' },
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMindSearchConfig, updateMindSearchConfig } from '../../api/client';
|
||||
import type { MindSearchConfig } from '../../types';
|
||||
|
||||
const initial: MindSearchConfig = {
|
||||
enabled: false,
|
||||
mode: 'off',
|
||||
providers: { searxng: false, github: false, reader: false },
|
||||
settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 },
|
||||
};
|
||||
|
||||
export function MindSearchPage() {
|
||||
const [config, setConfig] = useState(initial);
|
||||
const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({});
|
||||
const [message, setMessage] = useState('加载配置中…');
|
||||
|
||||
useEffect(() => {
|
||||
getMindSearchConfig()
|
||||
.then((result) => {
|
||||
setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } });
|
||||
setMeta(result);
|
||||
setMessage('');
|
||||
})
|
||||
.catch((error) => setMessage(error instanceof Error ? error.message : '配置加载失败'));
|
||||
}, []);
|
||||
|
||||
const save = async () => {
|
||||
setMessage('保存中…');
|
||||
try {
|
||||
const result = await updateMindSearchConfig(config);
|
||||
setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } });
|
||||
setMeta(result);
|
||||
setMessage('已保存到数据库;新会话生效,旧会话不受影响');
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProvider = (key: keyof MindSearchConfig['providers']) =>
|
||||
setConfig((current) => ({ ...current, providers: { ...current.providers, [key]: !current.providers[key] } }));
|
||||
const updateSetting = (key: keyof MindSearchConfig['settings'], value: string | number) =>
|
||||
setConfig((current) => ({ ...current, settings: { ...current.settings, [key]: value } }));
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-header"><div><h1>MindSearch</h1><p>可插拔外部搜索增强。关闭时完全保持原有搜索、记忆和上下文链路。</p></div></div>
|
||||
<div className="admin-card">
|
||||
<h2>总开关与模式</h2>
|
||||
<label><input type="checkbox" checked={config.enabled} onChange={(event) => setConfig({ ...config, enabled: event.target.checked, mode: event.target.checked ? (config.mode === 'off' ? 'shadow' : config.mode) : 'off' })} /> 启用 MindSearch</label>
|
||||
<label>运行模式 <select value={config.mode} onChange={(event) => setConfig({ ...config, mode: event.target.value as MindSearchConfig['mode'] })}><option value="off">关闭</option><option value="shadow">Shadow(只记录,不注入)</option><option value="assist">Assist(补充工具)</option></select></label>
|
||||
<h2>Provider</h2>
|
||||
<fieldset><legend>可用 Provider</legend>
|
||||
<label><input type="checkbox" checked={config.providers.searxng} onChange={() => toggleProvider('searxng')} /> SearXNG Web/News</label>
|
||||
<label><input type="checkbox" checked={config.providers.github} onChange={() => toggleProvider('github')} /> GitHub Code(Token 通过环境变量配置,不在页面回显)</label>
|
||||
<label><input type="checkbox" checked={config.providers.reader} onChange={() => toggleProvider('reader')} /> Reader(阻止 localhost/内网地址)</label>
|
||||
</fieldset>
|
||||
<h2>运行参数</h2>
|
||||
<label>SearXNG 地址 <input value={config.settings.searxngEndpoint} placeholder="http://127.0.0.1:8080/search" onChange={(event) => updateSetting('searxngEndpoint', event.target.value)} /></label>
|
||||
<label>最大结果数 <input type="number" min={1} max={20} value={config.settings.maxResults} onChange={(event) => updateSetting('maxResults', Number(event.target.value))} /></label>
|
||||
<label>Provider 超时(毫秒) <input type="number" min={1000} max={30000} value={config.settings.timeoutMs} onChange={(event) => updateSetting('timeoutMs', Number(event.target.value))} /></label>
|
||||
<label>Reader 最大字符数 <input type="number" min={1000} max={50000} value={config.settings.readerMaxChars} onChange={(event) => updateSetting('readerMaxChars', Number(event.target.value))} /></label>
|
||||
<button type="button" onClick={save}>保存配置</button>
|
||||
{message && <p role="status">{message}</p>}
|
||||
<p>配置来源:{meta.source === 'admin' ? '数据库' : '环境变量默认值'};最后更新:{meta.updatedAt ? new Date(meta.updatedAt).toLocaleString() : '尚未保存'}{meta.updatedBy ? `;操作人:${meta.updatedBy}` : ''}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import type {
|
||||
WechatScheduleLlmConfig,
|
||||
WechatMessage,
|
||||
WechatWebNotification,
|
||||
MindSearchConfig,
|
||||
} from '../types';
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -653,6 +654,14 @@ export async function clearUserCapabilityOverrides(userId: string): Promise<void
|
||||
await portalFetch(`/admin-api/users/${userId}/capabilities`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function getMindSearchConfig(): Promise<{ config: MindSearchConfig; source: string }> {
|
||||
return portalFetch('/admin-api/mindsearch/config');
|
||||
}
|
||||
|
||||
export async function updateMindSearchConfig(config: Partial<MindSearchConfig>): Promise<{ config: MindSearchConfig; source: string }> {
|
||||
return portalFetch('/admin-api/mindsearch/config', { method: 'PATCH', body: JSON.stringify(config) });
|
||||
}
|
||||
|
||||
// ── Policies ──────────────────────────────────────────
|
||||
|
||||
export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
|
||||
|
||||
@@ -597,3 +597,10 @@ export type AdminSubscription = {
|
||||
note: string | null;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type MindSearchConfig = {
|
||||
enabled: boolean;
|
||||
mode: 'off' | 'shadow' | 'assist';
|
||||
providers: { searxng: boolean; github: boolean; reader: boolean };
|
||||
settings: { searxngEndpoint: string; maxResults: number; timeoutMs: number; readerMaxChars: number };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user