Files
memind_adm/src/admin/pages/SkillRuntimePage.tsx
T
john 731d93843d 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>
2026-07-13 14:00:20 +08:00

187 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}