merge: integrate MindSearch admin controls
This commit is contained in:
+128
@@ -98,6 +98,9 @@ export function createAdminApp(services) {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
@@ -366,6 +369,131 @@ 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 {
|
||||
const response = await fetch(`${portalBaseUrl}/api/runtime/status`, {
|
||||
headers: { accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(2500),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return res.status(502).json({
|
||||
ok: false,
|
||||
message: `Memory V2 运行时状态返回 HTTP ${response.status}`,
|
||||
});
|
||||
}
|
||||
const payload = await response.json();
|
||||
return res.json({
|
||||
ok: true,
|
||||
checkedAt: Date.now(),
|
||||
memory: payload?.memory ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(503).json({
|
||||
ok: false,
|
||||
checkedAt: Date.now(),
|
||||
message: err instanceof Error ? err.message : 'Memory V2 运行时不可用',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/candidates', requireAdmin, async (req, res) => {
|
||||
if (!personalMemoryCandidateStore?.listCandidates) {
|
||||
return res.status(503).json({ message: '候选记忆存储未启用' });
|
||||
}
|
||||
try {
|
||||
const status = String(req.query.status ?? 'candidate');
|
||||
const userId = String(req.query.userId ?? '').trim() || null;
|
||||
const limit = Number(req.query.limit ?? 50);
|
||||
const [items, counts] = await Promise.all([
|
||||
personalMemoryCandidateStore.listCandidates({ status, userId, limit }),
|
||||
personalMemoryCandidateStore.countByStatus(),
|
||||
]);
|
||||
return res.json({ items, counts });
|
||||
} catch (err) {
|
||||
const missingTable = err?.code === 'ER_NO_SUCH_TABLE';
|
||||
return res.status(missingTable ? 503 : 400).json({
|
||||
message: missingTable ? '候选记忆表尚未执行本地迁移' : (err instanceof Error ? err.message : '候选记忆读取失败'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.post('/memory-v2/candidates/:id/accept', requireAdmin, async (req, res) => {
|
||||
if (!personalMemoryCandidateStore?.reviewCandidate) {
|
||||
return res.status(503).json({ message: '候选记忆存储未启用' });
|
||||
}
|
||||
const result = await personalMemoryCandidateStore.reviewCandidate(req.params.id, 'accepted', {
|
||||
reviewedBy: req.currentUser.id,
|
||||
});
|
||||
if (!result.updated) return res.status(409).json({ message: '候选记忆已处理或不存在' });
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.post('/memory-v2/candidates/:id/reject', requireAdmin, async (req, res) => {
|
||||
if (!personalMemoryCandidateStore?.reviewCandidate) {
|
||||
return res.status(503).json({ message: '候选记忆存储未启用' });
|
||||
}
|
||||
const result = await personalMemoryCandidateStore.reviewCandidate(req.params.id, 'rejected', {
|
||||
reviewedBy: req.currentUser.id,
|
||||
});
|
||||
if (!result.updated) return res.status(409).json({ message: '候选记忆已处理或不存在' });
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
res.json(await skillRuntimeConfigService.getAdminConfig());
|
||||
});
|
||||
|
||||
const updateSkillRuntimeConfig = async (req, res) => {
|
||||
if (!skillRuntimeConfigService?.updateAdminConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
const result = await skillRuntimeConfigService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
|
||||
updatedBy: req.currentUser.id,
|
||||
});
|
||||
res.json(result);
|
||||
};
|
||||
|
||||
adminApi.put('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
||||
adminApi.patch('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
||||
|
||||
adminApi.get('/skill-runtime/catalog', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.listCatalogSummary) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
res.json({ catalog: await skillRuntimeConfigService.listCatalogSummary() });
|
||||
});
|
||||
|
||||
adminApi.get('/skill-runtime/runtime', requireAdmin, async (_req, res) => {
|
||||
if (!skillRuntimeConfigService?.getPublicRuntimeConfig) {
|
||||
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
||||
}
|
||||
res.json(await skillRuntimeConfigService.getPublicRuntimeConfig());
|
||||
});
|
||||
|
||||
adminApi.get('/system-tests/accounts', requireAdmin, async (_req, res) => {
|
||||
if (!systemTestAccountService) {
|
||||
return res.status(503).json({ message: '系统测试账号服务未启用' });
|
||||
|
||||
@@ -29,6 +29,9 @@ 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');
|
||||
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
||||
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
||||
@@ -93,6 +96,13 @@ 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,
|
||||
h5Root,
|
||||
});
|
||||
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
@@ -145,6 +155,9 @@ export async function bootstrapAdminServices() {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
|
||||
@@ -105,6 +105,9 @@ ready
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
@@ -128,6 +131,9 @@ ready
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
mindSearchConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
|
||||
@@ -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';
|
||||
@@ -16,6 +17,7 @@ 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 { SkillRuntimePage } from './admin/pages/SkillRuntimePage';
|
||||
import { defaultHomePath } from './lib/routes';
|
||||
import { OpsLayout } from './ops/components/OpsLayout';
|
||||
import { AnalyticsPage } from './ops/pages/AnalyticsPage';
|
||||
@@ -121,6 +123,8 @@ 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 />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
@@ -168,6 +172,7 @@ function loginRedirectPath(pathname: string, role: string | undefined) {
|
||||
|| pathname.startsWith('/policies')
|
||||
|| pathname.startsWith('/mindspace')
|
||||
|| pathname.startsWith('/memory-v2')
|
||||
|| pathname.startsWith('/skill-runtime')
|
||||
|| pathname.startsWith('/providers')
|
||||
|| pathname.startsWith('/wechat')
|
||||
|| pathname.startsWith('/asset-gateway')
|
||||
|
||||
@@ -29,6 +29,8 @@ 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: '能力' },
|
||||
{ to: '/skills', label: '技能' },
|
||||
|
||||
@@ -12,6 +12,7 @@ const QUICK_LINKS = [
|
||||
{ to: '/asset-gateway', label: '资产能力', desc: '可插拔素材插件、Provider 与专属 LLM 选择' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' },
|
||||
{ to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' },
|
||||
{ to: '/skill-runtime', label: 'Skill Runtime', desc: 'H5 manifest 关键词路由开关' },
|
||||
{ to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' },
|
||||
{ to: '/capabilities', label: '能力权限', desc: '扩展与工具开关' },
|
||||
] as const;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useCallback, useEffect, useState, type ChangeEvent } from 'react';
|
||||
import {
|
||||
getMemoryV2AdminConfig,
|
||||
getMemoryV2RuntimeStatus,
|
||||
listPersonalMemoryCandidates,
|
||||
listMemoryV2ModelOptions,
|
||||
updateMemoryV2AdminConfig,
|
||||
reviewPersonalMemoryCandidate,
|
||||
} from '../../api/client';
|
||||
import type {
|
||||
LlmGlobalSettings,
|
||||
@@ -10,6 +13,8 @@ import type {
|
||||
MemoryV2AdminConfig,
|
||||
MemoryV2AdminConfigResponse,
|
||||
MemoryV2ModelApiType,
|
||||
MemoryV2RuntimeStatusResponse,
|
||||
PersonalMemoryCandidateListResponse,
|
||||
} from '../../types';
|
||||
|
||||
type BackendKey =
|
||||
@@ -24,6 +29,154 @@ type BackendKey =
|
||||
|
||||
type ModelBackendKey = 'mem0' | 'letta' | 'langgraph';
|
||||
|
||||
type CapabilityKey =
|
||||
| 'candidateMemory'
|
||||
| 'policy'
|
||||
| 'retriever'
|
||||
| 'lifecycle'
|
||||
| 'persona'
|
||||
| 'graph'
|
||||
| 'userMemory'
|
||||
| 'pluginHealth';
|
||||
|
||||
type CapabilityField = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'boolean' | 'number' | 'select';
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
};
|
||||
|
||||
const CAPABILITIES: Array<{
|
||||
key: CapabilityKey;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
fields: CapabilityField[];
|
||||
}> = [
|
||||
{
|
||||
key: 'candidateMemory',
|
||||
label: '候选记忆',
|
||||
icon: 'C',
|
||||
description: '控制候选提取模式、自动接纳阈值与待处理数量。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用候选记忆', type: 'boolean' },
|
||||
{ key: 'mode', label: '运行模式', type: 'select', options: [
|
||||
{ value: 'off', label: 'Off · 关闭' },
|
||||
{ value: 'active', label: 'Active · 规则通过后自动接纳(推荐)' },
|
||||
{ value: 'canary', label: 'Canary · 仅高置信度自动接纳' },
|
||||
{ value: 'shadow', label: 'Shadow · 全部人工审核' },
|
||||
] },
|
||||
{ key: 'minImportance', label: '最低重要度', type: 'number' },
|
||||
{ key: 'minConfidence', label: '最低置信度', type: 'number' },
|
||||
{ key: 'maxPending', label: '最大待处理数', type: 'number' },
|
||||
{ key: 'persistenceEnabled', label: '持久化到 MySQL', type: 'boolean' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'policy',
|
||||
label: 'Policy',
|
||||
icon: 'P',
|
||||
description: '管理证据、敏感信息、显式记忆与保留期限。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用 Policy', type: 'boolean' },
|
||||
{ key: 'saveExplicit', label: '允许显式“记住”', type: 'boolean' },
|
||||
{ key: 'rejectSensitive', label: '拒绝敏感内容', type: 'boolean' },
|
||||
{ key: 'requireEvidence', label: '长期记忆必须有证据', type: 'boolean' },
|
||||
{ key: 'retentionDays', label: '默认保留天数', type: 'number' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'retriever',
|
||||
label: 'Retriever',
|
||||
icon: 'R',
|
||||
description: '组合事件、语义、偏好和目标召回,并限制上下文预算。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用组合召回', type: 'boolean' },
|
||||
{ key: 'episodicEnabled', label: '事件记忆', type: 'boolean' },
|
||||
{ key: 'semanticEnabled', label: '语义记忆', type: 'boolean' },
|
||||
{ key: 'preferenceEnabled', label: '用户偏好', type: 'boolean' },
|
||||
{ key: 'goalEnabled', label: '活跃目标', type: 'boolean' },
|
||||
{ key: 'limit', label: '召回条数', type: 'number' },
|
||||
{ key: 'tokenBudget', label: 'Token 预算', type: 'number' },
|
||||
{ key: 'timeoutMs', label: '超时毫秒', type: 'number' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'lifecycle',
|
||||
label: 'Lifecycle',
|
||||
icon: 'L',
|
||||
description: '控制去重、冲突审核、衰减、遗忘与压缩周期。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用生命周期管理', type: 'boolean' },
|
||||
{ key: 'dedupeEnabled', label: '自动去重', type: 'boolean' },
|
||||
{ key: 'conflictReview', label: '冲突进入审核', type: 'boolean' },
|
||||
{ key: 'decayEnabled', label: '启用记忆衰减', type: 'boolean' },
|
||||
{ key: 'forgettingEnabled', label: '启用遗忘处理', type: 'boolean' },
|
||||
{ key: 'compactIntervalHours', label: 'Compact 周期小时', type: 'number' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'persona',
|
||||
label: 'Persona',
|
||||
icon: 'A',
|
||||
description: '管理用户画像来源、Shadow 注入、缓存和上下文上限。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用 Persona', type: 'boolean' },
|
||||
{ key: 'provider', label: '画像来源', type: 'select', options: [
|
||||
{ value: 'none', label: '未配置' },
|
||||
{ value: 'ai-mind', label: 'AI Mind Bridge' },
|
||||
{ value: 'internal', label: '内部画像' },
|
||||
] },
|
||||
{ key: 'shadowMode', label: '仅 Shadow 不注入', type: 'boolean' },
|
||||
{ key: 'maxTokens', label: '最大 Token', type: 'number' },
|
||||
{ key: 'cacheTtlSeconds', label: '缓存秒数', type: 'number' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'graph',
|
||||
label: 'Graph',
|
||||
icon: 'G',
|
||||
description: '配置关系检索 Provider、最大深度和关系数量。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用关系检索', type: 'boolean' },
|
||||
{ key: 'provider', label: 'Graph Provider', type: 'select', options: [
|
||||
{ value: 'postgres', label: 'PostgreSQL 关系表' },
|
||||
{ value: 'neo4j', label: 'Neo4j' },
|
||||
{ value: 'none', label: '未配置' },
|
||||
] },
|
||||
{ key: 'maxDepth', label: '最大关系深度', type: 'number' },
|
||||
{ key: 'relationLimit', label: '最大关系数', type: 'number' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'userMemory',
|
||||
label: '用户记忆',
|
||||
icon: 'U',
|
||||
description: '管理用户查看、纠正、固定、遗忘和删除传播权限。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用用户记忆管理', type: 'boolean' },
|
||||
{ key: 'reviewEnabled', label: '允许用户查看', type: 'boolean' },
|
||||
{ key: 'correctionEnabled', label: '允许用户纠正', type: 'boolean' },
|
||||
{ key: 'pinEnabled', label: '允许固定记忆', type: 'boolean' },
|
||||
{ key: 'forgetEnabled', label: '允许遗忘记忆', type: 'boolean' },
|
||||
{ key: 'deletePropagation', label: '删除传播至索引和缓存', type: 'boolean' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'pluginHealth',
|
||||
label: '插件健康',
|
||||
icon: 'H',
|
||||
description: '配置健康检查、失败阈值和自动回退行为。',
|
||||
fields: [
|
||||
{ key: 'enabled', label: '启用插件健康检查', type: 'boolean' },
|
||||
{ key: 'intervalSeconds', label: '检查间隔秒', type: 'number' },
|
||||
{ key: 'timeoutMs', label: '检查超时毫秒', type: 'number' },
|
||||
{ key: 'failureThreshold', label: '失败阈值', type: 'number' },
|
||||
{ key: 'autoFallback', label: '异常时自动回退', type: 'boolean' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const BACKENDS: Array<{
|
||||
key: BackendKey;
|
||||
label: string;
|
||||
@@ -161,6 +314,14 @@ function defaultConfig(): MemoryV2AdminConfig {
|
||||
timeoutMs: '1500',
|
||||
fallbackRoute: 'agent_orchestration',
|
||||
},
|
||||
candidateMemory: { enabled: false, mode: 'active', minImportance: '0.7', minConfidence: '0.8', maxPending: '500', persistenceEnabled: false },
|
||||
policy: { enabled: false, saveExplicit: true, rejectSensitive: true, requireEvidence: true, retentionDays: '365' },
|
||||
retriever: { enabled: false, episodicEnabled: true, semanticEnabled: true, preferenceEnabled: true, goalEnabled: true, limit: '12', tokenBudget: '1800', timeoutMs: '1200' },
|
||||
lifecycle: { enabled: false, dedupeEnabled: true, conflictReview: true, decayEnabled: false, forgettingEnabled: true, compactIntervalHours: '24' },
|
||||
persona: { enabled: false, provider: 'none', shadowMode: true, maxTokens: '400', cacheTtlSeconds: '300' },
|
||||
graph: { enabled: false, provider: 'postgres', maxDepth: '2', relationLimit: '20' },
|
||||
userMemory: { enabled: false, reviewEnabled: true, correctionEnabled: true, pinEnabled: true, forgetEnabled: true, deletePropagation: true },
|
||||
pluginHealth: { enabled: false, intervalSeconds: '60', timeoutMs: '1500', failureThreshold: '3', autoFallback: true },
|
||||
pgvector: {},
|
||||
qdrant: {},
|
||||
weaviate: {},
|
||||
@@ -177,6 +338,14 @@ function normalizeConfig(config?: Partial<MemoryV2AdminConfig> | null): MemoryV2
|
||||
return {
|
||||
global: { ...base.global, ...(config?.global ?? {}) },
|
||||
chatIntentRouter: { ...base.chatIntentRouter, ...(config?.chatIntentRouter ?? {}) },
|
||||
candidateMemory: { ...base.candidateMemory, ...(config?.candidateMemory ?? {}) },
|
||||
policy: { ...base.policy, ...(config?.policy ?? {}) },
|
||||
retriever: { ...base.retriever, ...(config?.retriever ?? {}) },
|
||||
lifecycle: { ...base.lifecycle, ...(config?.lifecycle ?? {}) },
|
||||
persona: { ...base.persona, ...(config?.persona ?? {}) },
|
||||
graph: { ...base.graph, ...(config?.graph ?? {}) },
|
||||
userMemory: { ...base.userMemory, ...(config?.userMemory ?? {}) },
|
||||
pluginHealth: { ...base.pluginHealth, ...(config?.pluginHealth ?? {}) },
|
||||
pgvector: { ...base.pgvector, ...(config?.pgvector ?? {}) },
|
||||
qdrant: { ...base.qdrant, ...(config?.qdrant ?? {}) },
|
||||
weaviate: { ...base.weaviate, ...(config?.weaviate ?? {}) },
|
||||
@@ -234,12 +403,16 @@ function describeEffectiveModelSource(
|
||||
|
||||
export function MemoryV2Page() {
|
||||
const [payload, setPayload] = useState<MemoryV2AdminConfigResponse | null>(null);
|
||||
const [runtimeStatus, setRuntimeStatus] = useState<MemoryV2RuntimeStatusResponse | null>(null);
|
||||
const [candidatePayload, setCandidatePayload] = useState<PersonalMemoryCandidateListResponse | null>(null);
|
||||
const [candidateError, setCandidateError] = useState<string | null>(null);
|
||||
const [candidateBusyId, setCandidateBusyId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<MemoryV2AdminConfig>(defaultConfig());
|
||||
const [providerKeys, setProviderKeys] = useState<LlmProviderKeyRow[]>([]);
|
||||
const [globalModelSettings, setGlobalModelSettings] = useState<LlmGlobalSettings | null>(null);
|
||||
const [supportedApiTypes, setSupportedApiTypes] = useState<MemoryV2ModelApiType[]>(['chat', 'response']);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busyScope, setBusyScope] = useState<'global' | 'chatIntentRouter' | BackendKey | 'reload' | null>(null);
|
||||
const [busyScope, setBusyScope] = useState<'global' | 'chatIntentRouter' | CapabilityKey | 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>>({});
|
||||
@@ -250,15 +423,24 @@ export function MemoryV2Page() {
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
try {
|
||||
const [configResult, modelOptions] = await Promise.all([
|
||||
const [configResult, modelOptions, statusResult] = await Promise.all([
|
||||
getMemoryV2AdminConfig(),
|
||||
listMemoryV2ModelOptions(),
|
||||
getMemoryV2RuntimeStatus().catch(() => null),
|
||||
]);
|
||||
setPayload(configResult);
|
||||
setDraft(normalizeConfig(configResult.config));
|
||||
setProviderKeys(modelOptions.keys.filter((item) => item.status === 'active'));
|
||||
setGlobalModelSettings(modelOptions.global);
|
||||
setSupportedApiTypes(modelOptions.supportedApiTypes);
|
||||
setRuntimeStatus(statusResult);
|
||||
try {
|
||||
setCandidatePayload(await listPersonalMemoryCandidates());
|
||||
setCandidateError(null);
|
||||
} catch (candidateErr) {
|
||||
setCandidatePayload(null);
|
||||
setCandidateError(candidateErr instanceof Error ? candidateErr.message : '候选记忆加载失败');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载 Memory V2 配置失败');
|
||||
} finally {
|
||||
@@ -309,6 +491,22 @@ export function MemoryV2Page() {
|
||||
}));
|
||||
};
|
||||
|
||||
const updateCapabilityFlag = (capability: CapabilityKey, key: string) => (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[capability]: { ...current[capability], [key]: event.target.checked },
|
||||
}));
|
||||
};
|
||||
|
||||
const updateCapabilityValue = (capability: CapabilityKey, key: string) => (
|
||||
event: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[capability]: { ...current[capability], [key]: event.target.value },
|
||||
}));
|
||||
};
|
||||
|
||||
const updateBackendValue = (backend: BackendKey, field: string) => (
|
||||
event: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
@@ -368,7 +566,7 @@ export function MemoryV2Page() {
|
||||
|
||||
const saveConfigSection = async (
|
||||
patch: Partial<MemoryV2AdminConfig>,
|
||||
scope: 'global' | 'chatIntentRouter' | BackendKey,
|
||||
scope: 'global' | 'chatIntentRouter' | CapabilityKey | BackendKey,
|
||||
successMessage: string,
|
||||
) => {
|
||||
setBusyScope(scope);
|
||||
@@ -408,6 +606,28 @@ export function MemoryV2Page() {
|
||||
);
|
||||
};
|
||||
|
||||
const saveCapabilityConfig = async (capability: CapabilityKey) => {
|
||||
await saveConfigSection(
|
||||
{ [capability]: draft[capability] },
|
||||
capability,
|
||||
`${CAPABILITIES.find((item) => item.key === capability)?.label ?? capability} 配置已保存。`,
|
||||
);
|
||||
};
|
||||
|
||||
const reviewCandidate = async (id: string, action: 'accept' | 'reject') => {
|
||||
setCandidateBusyId(id);
|
||||
setCandidateError(null);
|
||||
try {
|
||||
await reviewPersonalMemoryCandidate(id, action);
|
||||
setCandidatePayload(await listPersonalMemoryCandidates());
|
||||
setMessage(action === 'accept' ? '候选记忆已接纳。' : '候选记忆已拒绝。');
|
||||
} catch (err) {
|
||||
setCandidateError(err instanceof Error ? err.message : '候选记忆处理失败');
|
||||
} finally {
|
||||
setCandidateBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const current = payload?.config ? normalizeConfig(payload.config) : draft;
|
||||
const routerSection = draft.chatIntentRouter;
|
||||
const currentRouterSection = current.chatIntentRouter;
|
||||
@@ -658,6 +878,170 @@ export function MemoryV2Page() {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">Personal Memory 能力管理</h3>
|
||||
<p className="model-center-card-note memory-v2-backend-intro">
|
||||
配置统一保存到 Memory V2 控制面。启用配置不代表运行插件已经健康,实际状态仍需结合插件健康与运行时状态判断。
|
||||
</p>
|
||||
<div className="asset-plugin-grid memory-v2-capability-grid">
|
||||
{CAPABILITIES.map((capability, index) => {
|
||||
const section = draft[capability.key];
|
||||
const currentSection = current[capability.key];
|
||||
const enabled = Boolean(section.enabled);
|
||||
const accent = BACKEND_ACCENTS[index % BACKEND_ACCENTS.length];
|
||||
return (
|
||||
<section
|
||||
key={capability.key}
|
||||
className={`asset-plugin-card asset-plugin-card--${accent} model-center-card memory-v2-card`}
|
||||
>
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">{capability.icon}</div>
|
||||
<div>
|
||||
<h3>{capability.label}</h3>
|
||||
<p>{capability.description}</p>
|
||||
</div>
|
||||
<span className={`asset-status ${enabled ? 'is-on' : ''}`}>
|
||||
{enabled ? '配置已启用' : '配置未启用'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-grid memory-v2-fields">
|
||||
{capability.fields.map((field) => {
|
||||
if (field.type === 'boolean') {
|
||||
return (
|
||||
<label key={field.key} className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(section[field.key])}
|
||||
onChange={updateCapabilityFlag(capability.key, field.key)}
|
||||
/>
|
||||
{field.label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
if (field.type === 'select') {
|
||||
return (
|
||||
<label key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<select
|
||||
value={String(section[field.key] ?? '')}
|
||||
onChange={updateCapabilityValue(capability.key, field.key)}
|
||||
>
|
||||
{(field.options ?? []).map((option) => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<label key={field.key}>
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type="number"
|
||||
value={String(section[field.key] ?? '')}
|
||||
onChange={updateCapabilityValue(capability.key, field.key)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="asset-plugin-footer">
|
||||
{capability.key === 'pluginHealth' && (
|
||||
<div className="memory-v2-runtime-health">
|
||||
<strong>运行时状态</strong>
|
||||
{runtimeStatus?.memory?.personalMemory ? (
|
||||
<span>
|
||||
{String(runtimeStatus.memory.personalMemory.health?.state ?? 'unknown')}
|
||||
{' · '}模式 {runtimeStatus.memory.personalMemory.effectiveMode ?? 'off'}
|
||||
{runtimeStatus.memory.personalMemory.autoReviewEnabled ? ' · 自动审核' : ' · 人工审核'}
|
||||
{' · '}候选 {runtimeStatus.memory.personalMemory.pendingCandidates ?? 0}
|
||||
{' · '}接纳 {runtimeStatus.memory.personalMemory.health?.accepted ?? 0}
|
||||
{' · '}自动接纳 {runtimeStatus.memory.personalMemory.health?.autoReviewed ?? 0}
|
||||
{' · '}拒绝 {runtimeStatus.memory.personalMemory.health?.rejected ?? 0}
|
||||
{' · '}去重 {runtimeStatus.memory.personalMemory.health?.deduped ?? 0}
|
||||
</span>
|
||||
) : (
|
||||
<span>主站尚未加载 Shadow Pipeline,或运行时状态暂不可用</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={`asset-status ${Boolean(currentSection.enabled) ? 'is-on' : ''}`}>
|
||||
当前保存:{Boolean(currentSection.enabled) ? '启用' : '关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={loading || busyScope !== null}
|
||||
onClick={() => void saveCapabilityConfig(capability.key)}
|
||||
>
|
||||
{busyScope === capability.key ? '保存中...' : `保存${capability.label}`}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3 className="model-center-section-title">候选记忆审核</h3>
|
||||
<section className="asset-plugin-card model-center-card memory-v2-card memory-v2-candidate-review">
|
||||
<div className="asset-plugin-card-head">
|
||||
<div className="asset-plugin-icon" aria-hidden="true">C</div>
|
||||
<div>
|
||||
<h3>待审核候选</h3>
|
||||
<p>
|
||||
待审核 {candidatePayload?.counts?.candidate ?? 0}
|
||||
{' · '}已接纳 {candidatePayload?.counts?.accepted ?? 0}
|
||||
{' · '}已拒绝 {candidatePayload?.counts?.rejected ?? 0}
|
||||
</p>
|
||||
<p className="model-center-card-note">
|
||||
Active 模式下规则通过的候选会自动接纳。此处仅显示 Shadow / Canary 模式下仍需人工确认的例外项。
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="ghost-btn" onClick={() => void load()} disabled={loading || candidateBusyId !== null}>
|
||||
刷新候选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{candidateError && <p className="banner banner-error">{candidateError}</p>}
|
||||
{!candidateError && (candidatePayload?.items.length ?? 0) === 0 && (
|
||||
<p className="model-center-card-note">当前没有待审核候选。Active 模式下候选会自动接纳;Shadow / Canary 模式下低置信度项会出现在这里。</p>
|
||||
)}
|
||||
<div className="memory-v2-candidate-list">
|
||||
{(candidatePayload?.items ?? []).map((candidate) => (
|
||||
<article key={candidate.id} className="memory-v2-candidate-item">
|
||||
<div className="memory-v2-candidate-meta">
|
||||
<span className="asset-status is-on">{candidate.memoryType}</span>
|
||||
<span>用户 {candidate.userId}</span>
|
||||
<span>重要度 {candidate.importance.toFixed(2)}</span>
|
||||
<span>置信度 {candidate.confidence.toFixed(2)}</span>
|
||||
<span>{new Date(candidate.createdAt).toLocaleString('zh-CN', { hour12: false })}</span>
|
||||
</div>
|
||||
<p>{candidate.content}</p>
|
||||
<small>Policy: {candidate.policyReason} · Evidence: {String(candidate.evidence?.sourceId ?? 'unknown')}</small>
|
||||
<div className="asset-plugin-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
disabled={candidateBusyId !== null}
|
||||
onClick={() => void reviewCandidate(candidate.id, 'reject')}
|
||||
>
|
||||
{candidateBusyId === candidate.id ? '处理中...' : '拒绝'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="send-btn"
|
||||
disabled={candidateBusyId !== null}
|
||||
onClick={() => void reviewCandidate(candidate.id, 'accept')}
|
||||
>
|
||||
{candidateBusyId === candidate.id ? '处理中...' : '接纳'}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3 className="model-center-section-title">Backend 配置</h3>
|
||||
<p className="model-center-card-note memory-v2-backend-intro">
|
||||
单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。未启用的 backend 默认折叠,点击标题可展开。
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
getSkillRuntimeAdminConfig,
|
||||
listSkillRuntimeCatalog,
|
||||
updateSkillRuntimeAdminConfig,
|
||||
} from '../../api/client';
|
||||
import type { SkillRuntimeAdminConfig, SkillRuntimeCatalogItem } from '../../types';
|
||||
|
||||
export function SkillRuntimePage() {
|
||||
const [config, setConfig] = useState<SkillRuntimeAdminConfig>({
|
||||
router: { v2Enabled: false, manifestRoutingEnabled: false },
|
||||
});
|
||||
const [catalog, setCatalog] = useState<SkillRuntimeCatalogItem[]>([]);
|
||||
const [source, setSource] = useState('default');
|
||||
const [updatedAt, setUpdatedAt] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [configResult, catalogRows] = await Promise.all([
|
||||
getSkillRuntimeAdminConfig(),
|
||||
listSkillRuntimeCatalog(),
|
||||
]);
|
||||
setConfig(configResult.config);
|
||||
setSource(configResult.source ?? 'default');
|
||||
setUpdatedAt(configResult.updatedAt ?? null);
|
||||
setCatalog(catalogRows);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载 Skill Runtime 配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
const result = await updateSkillRuntimeAdminConfig(config);
|
||||
setConfig(result.config);
|
||||
setSource(result.source ?? 'admin-db');
|
||||
setUpdatedAt(result.updatedAt ?? null);
|
||||
setNotice('Skill Runtime 配置已保存。H5 用户刷新或重新登录后会读取新开关。');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const manifestSkillCount = catalog.filter((item) => item.hasManifest && item.triggerKeywords.length > 0).length;
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>Skill Runtime</h2>
|
||||
<p className="muted">控制 H5 Skill Router v2 与 manifest 关键词路由(默认关闭)</p>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="muted">加载中…</p> : null}
|
||||
{error ? <p className="banner banner-error">{error}</p> : null}
|
||||
{notice ? <p className="banner banner-info">{notice}</p> : null}
|
||||
|
||||
{!loading ? (
|
||||
<>
|
||||
<section className="admin-card">
|
||||
<h3>Router 开关</h3>
|
||||
<p className="muted">
|
||||
当前来源:<strong>{source}</strong>
|
||||
{updatedAt
|
||||
? ` · 更新于 ${new Date(updatedAt).toLocaleString('zh-CN', { hour12: false })}`
|
||||
: ''}
|
||||
</p>
|
||||
<p className="muted">
|
||||
开启后仅对已授予 skill 的用户生效;manifest 匹配失败会回退 legacy 路由。
|
||||
</p>
|
||||
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.router.v2Enabled}
|
||||
onChange={(event) => {
|
||||
const v2Enabled = event.target.checked;
|
||||
setConfig({
|
||||
router: {
|
||||
v2Enabled,
|
||||
manifestRoutingEnabled: v2Enabled ? config.router.manifestRoutingEnabled : false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<strong>启用 Skill Router v2</strong>
|
||||
<span className="muted">总开关。关闭时 H5 走 legacy regex 路由。</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="inline-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.router.manifestRoutingEnabled}
|
||||
disabled={!config.router.v2Enabled}
|
||||
onChange={(event) => {
|
||||
setConfig({
|
||||
router: {
|
||||
...config.router,
|
||||
manifestRoutingEnabled: event.target.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<strong>启用 manifest 关键词路由</strong>
|
||||
<span className="muted">
|
||||
读取 skills/*/skill.yaml 的 trigger.keywords。当前已配置 manifest 的 skill:{manifestSkillCount}{' '}
|
||||
个。
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="admin-actions">
|
||||
<button type="button" className="send-btn" disabled={saving} onClick={() => void handleSave()}>
|
||||
{saving ? '保存中…' : '保存配置'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn-secondary"
|
||||
disabled={loading || saving}
|
||||
onClick={() => void load()}
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-card">
|
||||
<h3>技能 Catalog 预览</h3>
|
||||
<p className="muted">
|
||||
「Manifest」表示该 skill 目录下是否存在 <code>skill.yaml</code>(Router v2 关键词路由配置)。
|
||||
目前 Phase 1 仅 <strong>product-campaign-page</strong> 做了 manifest 试点;其余 skill 仍只有 legacy{' '}
|
||||
<code>SKILL.md</code>,因此 Manifest 显示「否」是正常现象。
|
||||
</p>
|
||||
<p className="muted">有 skill.yaml 且配置了关键词的条目会在 Router v2 开启后参与匹配。</p>
|
||||
<div className="admin-table-wrap">
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Skill</th>
|
||||
<th>Manifest</th>
|
||||
<th>关键词</th>
|
||||
<th>Prompt</th>
|
||||
<th>优先级</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{catalog.map((item) => (
|
||||
<tr key={item.name}>
|
||||
<td>
|
||||
<strong>{item.name}</strong>
|
||||
<div className="muted">{item.description}</div>
|
||||
</td>
|
||||
<td>{item.hasManifest ? '是' : '否'}</td>
|
||||
<td>{item.triggerKeywords.length ? item.triggerKeywords.join('、') : '—'}</td>
|
||||
<td>{item.routerPromptKey ?? '—'}</td>
|
||||
<td>{item.routerPriority || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
LlmProviderKeyRow,
|
||||
PlanDefinition,
|
||||
PlanSyncResult,
|
||||
PersonalMemoryCandidateListResponse,
|
||||
PolicyDefinition,
|
||||
PolicyMap,
|
||||
PortalUser,
|
||||
@@ -32,6 +33,10 @@ import type {
|
||||
MemoryV2AdminConfig,
|
||||
MemoryV2AdminConfigResponse,
|
||||
MemoryV2ModelApiType,
|
||||
MemoryV2RuntimeStatusResponse,
|
||||
SkillRuntimeAdminConfig,
|
||||
SkillRuntimeAdminConfigResponse,
|
||||
SkillRuntimeCatalogItem,
|
||||
SkillDefinition,
|
||||
SkillMap,
|
||||
UsageRecord,
|
||||
@@ -42,6 +47,7 @@ import type {
|
||||
WechatScheduleLlmConfig,
|
||||
WechatMessage,
|
||||
WechatWebNotification,
|
||||
MindSearchConfig,
|
||||
} from '../types';
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -327,6 +333,25 @@ export async function getMemoryV2AdminConfig(): Promise<MemoryV2AdminConfigRespo
|
||||
return portalFetch('/admin-api/memory-v2/config');
|
||||
}
|
||||
|
||||
export async function getMemoryV2RuntimeStatus(): Promise<MemoryV2RuntimeStatusResponse> {
|
||||
return portalFetch('/admin-api/memory-v2/status');
|
||||
}
|
||||
|
||||
export async function listPersonalMemoryCandidates(
|
||||
status = 'candidate',
|
||||
): Promise<PersonalMemoryCandidateListResponse> {
|
||||
return portalFetch(`/admin-api/memory-v2/candidates?status=${encodeURIComponent(status)}&limit=50`);
|
||||
}
|
||||
|
||||
export async function reviewPersonalMemoryCandidate(
|
||||
id: string,
|
||||
action: 'accept' | 'reject',
|
||||
): Promise<{ updated: boolean; status: string; reviewedAt: number }> {
|
||||
return portalFetch(`/admin-api/memory-v2/candidates/${encodeURIComponent(id)}/${action}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMemoryV2AdminConfig(
|
||||
payload: Partial<MemoryV2AdminConfig>,
|
||||
): Promise<MemoryV2AdminConfigResponse> {
|
||||
@@ -336,6 +361,24 @@ export async function updateMemoryV2AdminConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSkillRuntimeAdminConfig(): Promise<SkillRuntimeAdminConfigResponse> {
|
||||
return portalFetch('/admin-api/skill-runtime/config');
|
||||
}
|
||||
|
||||
export async function updateSkillRuntimeAdminConfig(
|
||||
config: SkillRuntimeAdminConfig,
|
||||
): Promise<SkillRuntimeAdminConfigResponse> {
|
||||
return portalFetch('/admin-api/skill-runtime/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSkillRuntimeCatalog(): Promise<SkillRuntimeCatalogItem[]> {
|
||||
const result = await portalFetch<{ catalog: SkillRuntimeCatalogItem[] }>('/admin-api/skill-runtime/catalog');
|
||||
return result.catalog ?? [];
|
||||
}
|
||||
|
||||
export async function listMemoryV2ModelOptions(): Promise<{
|
||||
global: LlmGlobalSettings;
|
||||
keys: LlmProviderKeyRow[];
|
||||
@@ -611,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[]> {
|
||||
|
||||
+57
-2
@@ -1217,6 +1217,10 @@ body,
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.memory-v2-capability-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.memory-v2-page .asset-plugin-grid {
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
@@ -1322,18 +1326,69 @@ body,
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-runtime-health {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
flex: 1 1 100%;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-runtime-health strong {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-review {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-item {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
|
||||
.memory-v2-candidate-item p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.memory-v2-candidate-item small {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.memory-v2-candidate-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.memory-v2-page .model-center-card-note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.memory-v2-backend-grid {
|
||||
.memory-v2-backend-grid,
|
||||
.memory-v2-capability-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.memory-v2-backend-grid {
|
||||
.memory-v2-backend-grid,
|
||||
.memory-v2-capability-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,14 @@ export type MemoryV2AdminConfig = {
|
||||
failOpen?: boolean;
|
||||
};
|
||||
chatIntentRouter: MemoryV2AdminSection;
|
||||
candidateMemory: MemoryV2AdminSection;
|
||||
policy: MemoryV2AdminSection;
|
||||
retriever: MemoryV2AdminSection;
|
||||
lifecycle: MemoryV2AdminSection;
|
||||
persona: MemoryV2AdminSection;
|
||||
graph: MemoryV2AdminSection;
|
||||
userMemory: MemoryV2AdminSection;
|
||||
pluginHealth: MemoryV2AdminSection;
|
||||
pgvector: MemoryV2AdminSection;
|
||||
qdrant: MemoryV2AdminSection;
|
||||
weaviate: MemoryV2AdminSection;
|
||||
@@ -312,6 +320,79 @@ export type MemoryV2AdminConfigResponse = {
|
||||
updatedBy: string | null;
|
||||
};
|
||||
|
||||
export type MemoryV2RuntimeStatusResponse = {
|
||||
ok: boolean;
|
||||
checkedAt?: number;
|
||||
message?: string;
|
||||
memory?: {
|
||||
enabled?: boolean;
|
||||
backend?: string;
|
||||
selectedBackend?: string | null;
|
||||
configSource?: string;
|
||||
configUpdatedAt?: number | null;
|
||||
personalMemory?: {
|
||||
enabled?: boolean;
|
||||
requestedMode?: string;
|
||||
effectiveMode?: string;
|
||||
autoReviewEnabled?: boolean;
|
||||
phase?: string;
|
||||
injectionEnabled?: boolean;
|
||||
persistence?: string;
|
||||
pendingCandidates?: number;
|
||||
health?: Record<string, string | number | boolean | null>;
|
||||
policy?: Record<string, string | number | boolean | null>;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type PersonalMemoryCandidate = {
|
||||
id: string;
|
||||
userId: string;
|
||||
sessionId: string | null;
|
||||
memoryType: string;
|
||||
content: string;
|
||||
importance: number;
|
||||
confidence: number;
|
||||
status: string;
|
||||
policyReason: string;
|
||||
evidence: Record<string, unknown>;
|
||||
reviewedBy: string | null;
|
||||
reviewedAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
export type PersonalMemoryCandidateListResponse = {
|
||||
items: PersonalMemoryCandidate[];
|
||||
counts: Record<string, number>;
|
||||
};
|
||||
|
||||
export type SkillRuntimeAdminConfig = {
|
||||
router: {
|
||||
v2Enabled: boolean;
|
||||
manifestRoutingEnabled: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type SkillRuntimeAdminConfigResponse = {
|
||||
config: SkillRuntimeAdminConfig;
|
||||
updatedAt: number | null;
|
||||
updatedBy: string | null;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type SkillRuntimeCatalogItem = {
|
||||
name: string;
|
||||
dirName: string;
|
||||
description: string;
|
||||
version: string | null;
|
||||
executors: string[];
|
||||
hasManifest: boolean;
|
||||
triggerKeywords: string[];
|
||||
routerPromptKey: string | null;
|
||||
routerPriority: number;
|
||||
};
|
||||
|
||||
export type AdminSystemTestStepStatus = 'passed' | 'warning' | 'failed';
|
||||
|
||||
export type AdminSystemTestStep = {
|
||||
@@ -516,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