Initial commit: tkmind admin frontend (React + Vite).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
clearUserSkillOverrides,
|
||||
getRoleSkills,
|
||||
getUserSkills,
|
||||
listSkillCatalog,
|
||||
updateRoleSkills,
|
||||
updateUserSkills,
|
||||
} from '../api/client';
|
||||
import type { AdminUserRow, SkillDefinition, SkillMap } from '../types';
|
||||
|
||||
function SkillGrid({
|
||||
catalog,
|
||||
values,
|
||||
disabled,
|
||||
onChange,
|
||||
}: {
|
||||
catalog: SkillDefinition[];
|
||||
values: SkillMap;
|
||||
disabled?: boolean;
|
||||
onChange: (name: string, enabled: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="capability-grid">
|
||||
<div className="capability-group">
|
||||
<h3>平台技能</h3>
|
||||
<ul>
|
||||
{catalog.map((item) => (
|
||||
<li key={item.name}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(values[item.name])}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(item.name, e.target.checked)}
|
||||
/>
|
||||
<span className="capability-label">
|
||||
{item.label}
|
||||
<code className="skill-name-tag">{item.name}</code>
|
||||
{item.requiresPublish && (
|
||||
<span className="risk-pill risk-medium">含页面发布</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="muted capability-desc">{item.description}</span>
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillSettings({
|
||||
users,
|
||||
userId,
|
||||
userOnly = false,
|
||||
}: {
|
||||
users: AdminUserRow[];
|
||||
userId?: string;
|
||||
userOnly?: boolean;
|
||||
}) {
|
||||
const [catalog, setCatalog] = useState<SkillDefinition[]>([]);
|
||||
const [roleSkills, setRoleSkills] = useState<SkillMap>({});
|
||||
const [selectedUserId, setSelectedUserId] = useState(userId ?? '');
|
||||
const [userSkills, setUserSkills] = useState<SkillMap>({});
|
||||
const [userOverrides, setUserOverrides] = useState<SkillMap>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [catalogItems, roleState] = await Promise.all([
|
||||
listSkillCatalog(),
|
||||
getRoleSkills('user'),
|
||||
]);
|
||||
setCatalog(catalogItems);
|
||||
setRoleSkills(roleState.skills);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载技能目录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUserSkills = useCallback(async (userId: string) => {
|
||||
if (!userId) {
|
||||
setUserSkills({});
|
||||
setUserOverrides({});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const state = await getUserSkills(userId);
|
||||
setUserSkills(state.skills);
|
||||
setUserOverrides(state.overrides);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载用户技能失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadUserSkills(selectedUserId);
|
||||
}, [loadUserSkills, selectedUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
setSelectedUserId(userId);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
const saveRoleDefaults = async () => {
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await updateRoleSkills('user', roleSkills);
|
||||
setRoleSkills(result.skills);
|
||||
setMessage('普通用户默认技能已保存,并已同步到各用户工作区');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const saveUserOverrides = async () => {
|
||||
if (!selectedUserId) return;
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await updateUserSkills(selectedUserId, userSkills);
|
||||
setUserSkills(result.skills);
|
||||
await loadUserSkills(selectedUserId);
|
||||
setMessage('用户技能已保存,并已安装到其 MindSpace 发布目录');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const resetUserOverrides = async () => {
|
||||
if (!selectedUserId) return;
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
try {
|
||||
await clearUserSkillOverrides(selectedUserId);
|
||||
await loadUserSkills(selectedUserId);
|
||||
setMessage('已恢复为角色默认技能');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '重置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const regularUsers = users.filter((user) => user.role === 'user');
|
||||
|
||||
return (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<h2>技能配置</h2>
|
||||
<button type="button" className="ghost-btn" onClick={() => void load()}>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<p className="muted capability-intro">
|
||||
勾选后技能会安装到用户 <code>MindSpace/用户名/.agents/skills/</code>,对话中可通过 load_skill
|
||||
使用。开通「静态页面发布」技能会自动启用页面写入能力。
|
||||
</p>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-info">{message}</p>}
|
||||
{loading ? (
|
||||
<p className="muted">加载中…</p>
|
||||
) : catalog.length === 0 ? (
|
||||
<p className="muted">暂无平台技能,请在 ui/h5/skills/ 下添加 SKILL.md</p>
|
||||
) : (
|
||||
<>
|
||||
{!userOnly && (
|
||||
<>
|
||||
<h3 className="capability-section-title">普通用户默认技能</h3>
|
||||
<SkillGrid
|
||||
catalog={catalog}
|
||||
values={roleSkills}
|
||||
onChange={(name, enabled) => setRoleSkills((prev) => ({ ...prev, [name]: enabled }))}
|
||||
/>
|
||||
<div className="capability-actions">
|
||||
<button type="button" className="send-btn" onClick={() => void saveRoleDefaults()}>
|
||||
保存默认技能
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!userOnly && <h3 className="capability-section-title">单用户技能覆盖</h3>}
|
||||
{!userOnly && (
|
||||
<div className="admin-form capability-user-picker">
|
||||
<select value={selectedUserId} onChange={(e) => setSelectedUserId(e.target.value)}>
|
||||
<option value="">选择用户(可选)</option>
|
||||
{regularUsers.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.displayName} (@{user.username})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{selectedUserId && (
|
||||
<>
|
||||
{Object.keys(userOverrides).length > 0 && (
|
||||
<p className="muted">
|
||||
已设置 {Object.keys(userOverrides).length} 项单独覆盖。
|
||||
</p>
|
||||
)}
|
||||
<SkillGrid
|
||||
catalog={catalog}
|
||||
values={userSkills}
|
||||
onChange={(name, enabled) =>
|
||||
setUserSkills((prev) => ({ ...prev, [name]: enabled }))
|
||||
}
|
||||
/>
|
||||
<div className="capability-actions">
|
||||
<button type="button" className="send-btn" onClick={() => void saveUserOverrides()}>
|
||||
保存用户技能
|
||||
</button>
|
||||
<button type="button" className="ghost-btn" onClick={() => void resetUserOverrides()}>
|
||||
恢复角色默认
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user