feat(admin): add Memory V2 personal controls and Skill Runtime page
Wire admin UI and API routes to restored Memind personal memory and skill runtime modules, including candidate review and runtime status display. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+107
@@ -98,6 +98,8 @@ export function createAdminApp(services) {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
@@ -366,6 +368,111 @@ export function createAdminApp(services) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
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,8 @@ export async function bootstrapAdminServices() {
|
||||
updateMindSpaceConfig,
|
||||
} = await importMemind('mindspace-config.mjs');
|
||||
const { createMemoryV2AdminConfigService } = await importMemind('memory-v2-admin-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 +95,11 @@ export async function bootstrapAdminServices() {
|
||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
const personalMemoryCandidateStore = createPersonalMemoryCandidateStore(pool);
|
||||
const skillRuntimeConfigService = createSkillRuntimeAdminConfigService(pool, {
|
||||
env: process.env,
|
||||
h5Root,
|
||||
});
|
||||
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
@@ -145,6 +152,8 @@ export async function bootstrapAdminServices() {
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
|
||||
@@ -105,6 +105,8 @@ ready
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
@@ -128,6 +130,8 @@ ready
|
||||
loadMindSpaceConfig,
|
||||
updateMindSpaceConfig,
|
||||
memoryV2ConfigService,
|
||||
personalMemoryCandidateStore,
|
||||
skillRuntimeConfigService,
|
||||
wechatScheduleLlmConfigService,
|
||||
adminSystemTestService,
|
||||
systemTestAccountService,
|
||||
|
||||
@@ -16,6 +16,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 +122,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="skill-runtime" element={<SkillRuntimePage />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
@@ -168,6 +170,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,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
{ to: '/wechat', label: '服务号' },
|
||||
{ to: '/mindspace', label: 'MindSpace 配置' },
|
||||
{ to: '/memory-v2', label: 'Memory V2' },
|
||||
{ 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: 'shadow', label: 'Shadow' },
|
||||
{ value: 'canary', label: 'Canary' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
] },
|
||||
{ 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: 'off', 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,165 @@ 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.pendingCandidates ?? 0}
|
||||
{' · '}接纳 {runtimeStatus.memory.personalMemory.health?.accepted ?? 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>
|
||||
</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">当前没有待审核候选。Shadow Pipeline 观察到稳定决策、偏好或目标后会写入这里。</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,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,
|
||||
@@ -327,6 +332,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 +360,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[];
|
||||
|
||||
+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,78 @@ 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;
|
||||
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 = {
|
||||
|
||||
Reference in New Issue
Block a user