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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user