feat: add MindSearch admin controls

This commit is contained in:
john
2026-07-15 13:46:22 +08:00
parent 7df3ae35f1
commit 05681d758e
8 changed files with 114 additions and 0 deletions
+21
View File
@@ -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 {
+4
View File
@@ -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,
+2
View File
@@ -105,6 +105,7 @@ ready
loadMindSpaceConfig,
updateMindSpaceConfig,
memoryV2ConfigService,
mindSearchConfigService,
personalMemoryCandidateStore,
skillRuntimeConfigService,
wechatScheduleLlmConfigService,
@@ -130,6 +131,7 @@ ready
loadMindSpaceConfig,
updateMindSpaceConfig,
memoryV2ConfigService,
mindSearchConfigService,
personalMemoryCandidateStore,
skillRuntimeConfigService,
wechatScheduleLlmConfigService,
+2
View File
@@ -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 />} />
+1
View File
@@ -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: '能力' },
+68
View File
@@ -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 CodeToken </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>
);
}
+9
View File
@@ -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[]> {
+7
View File
@@ -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 };
};