import { useCallback, useEffect, useMemo, useState } from 'react';
import {
clearUserCapabilityOverrides,
clearUserSkillOverrides,
getRoleCapabilities,
getRoleSkills,
getUserCapabilities,
getUserSkills,
listCapabilityCatalog,
listSkillCatalog,
updateRoleCapabilities,
updateRoleSkills,
updateUserCapabilities,
updateUserSkills,
} from '../api/client';
import type { AdminUserRow, SkillDefinition, SkillMap } from '../types';
const WEB_CAPABILITY_KEY = 'web';
const WEB_SKILL_NAME = 'web';
function SkillGrid({
catalog,
values,
disabled,
onChange,
}: {
catalog: SkillDefinition[];
values: SkillMap;
disabled?: boolean;
onChange: (name: string, enabled: boolean) => void;
}) {
return (
);
}
function WebSearchToolPanel({
roleEnabled,
userEnabled,
showUser,
disabled,
onRoleChange,
onUserChange,
}: {
roleEnabled: boolean;
userEnabled: boolean;
showUser: boolean;
disabled?: boolean;
onRoleChange: (enabled: boolean) => void;
onUserChange: (enabled: boolean) => void;
}) {
return (
);
}
export function SkillSettings({
users,
userId,
userOnly = false,
}: {
users: AdminUserRow[];
userId?: string;
userOnly?: boolean;
}) {
const [catalog, setCatalog] = useState([]);
const [roleSkills, setRoleSkills] = useState({});
const [roleWebEnabled, setRoleWebEnabled] = useState(false);
const [hasWebCapabilityCatalog, setHasWebCapabilityCatalog] = useState(false);
const [selectedUserId, setSelectedUserId] = useState(userId ?? '');
const [userSkills, setUserSkills] = useState({});
const [userWebEnabled, setUserWebEnabled] = useState(false);
const [userWebOverride, setUserWebOverride] = useState(null);
const [userOverrides, setUserOverrides] = useState({});
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState(null);
const [error, setError] = useState(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [catalogItems, roleState, capabilityCatalog, roleCapabilities] = await Promise.all([
listSkillCatalog(),
getRoleSkills('user'),
listCapabilityCatalog(),
getRoleCapabilities('user'),
]);
setCatalog(catalogItems);
setRoleSkills(roleState.skills);
setHasWebCapabilityCatalog(capabilityCatalog.some((item) => item.key === WEB_CAPABILITY_KEY));
setRoleWebEnabled(Boolean(roleCapabilities.capabilities[WEB_CAPABILITY_KEY]));
} catch (err) {
setError(err instanceof Error ? err.message : '加载技能目录失败');
} finally {
setLoading(false);
}
}, []);
const loadUserSkills = useCallback(async (userId: string) => {
if (!userId) {
setUserSkills({});
setUserWebEnabled(false);
setUserWebOverride(null);
setUserOverrides({});
return;
}
try {
const [state, capabilityState] = await Promise.all([
getUserSkills(userId),
getUserCapabilities(userId),
]);
setUserSkills(state.skills);
setUserOverrides(state.overrides);
setUserWebEnabled(Boolean(capabilityState.capabilities[WEB_CAPABILITY_KEY]));
setUserWebOverride(
Object.prototype.hasOwnProperty.call(capabilityState.overrides, WEB_CAPABILITY_KEY)
? Boolean(capabilityState.overrides[WEB_CAPABILITY_KEY])
: null,
);
} 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 [skillsResult] = await Promise.all([
updateRoleSkills('user', roleSkills),
hasWebCapabilityCatalog
? updateRoleCapabilities('user', { [WEB_CAPABILITY_KEY]: roleWebEnabled })
: Promise.resolve(null),
]);
setRoleSkills(skillsResult.skills);
setMessage('普通用户默认技能与联网工具已保存,并已同步到各用户工作区');
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败');
}
};
const saveUserOverrides = async () => {
if (!selectedUserId) return;
setMessage(null);
setError(null);
try {
const [skillsResult] = await Promise.all([
updateUserSkills(selectedUserId, userSkills),
hasWebCapabilityCatalog
? updateUserCapabilities(selectedUserId, { [WEB_CAPABILITY_KEY]: userWebEnabled })
: Promise.resolve(null),
]);
setUserSkills(skillsResult.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 Promise.all([
clearUserSkillOverrides(selectedUserId),
hasWebCapabilityCatalog ? clearUserCapabilityOverrides(selectedUserId) : Promise.resolve(),
]);
await loadUserSkills(selectedUserId);
setMessage('已恢复为角色默认技能与联网工具');
} catch (err) {
setError(err instanceof Error ? err.message : '重置失败');
}
};
const regularUsers = users.filter((user) => user.role === 'user');
const webSkillEnabledForRole = Boolean(roleSkills[WEB_SKILL_NAME]);
const webSkillEnabledForUser = Boolean(userSkills[WEB_SKILL_NAME]);
const webMismatchRole = useMemo(
() => hasWebCapabilityCatalog && webSkillEnabledForRole && !roleWebEnabled,
[hasWebCapabilityCatalog, webSkillEnabledForRole, roleWebEnabled],
);
const webMismatchUser = useMemo(
() => hasWebCapabilityCatalog && webSkillEnabledForUser && !userWebEnabled,
[hasWebCapabilityCatalog, webSkillEnabledForUser, userWebEnabled],
);
return (
技能配置
勾选后技能会安装到用户 MindSpace/用户名/.agents/skills/,对话中可通过 load_skill
使用。开通「静态页面发布」技能会自动启用页面写入能力。联网搜索需在下方同时开启{' '}
网页搜索工具 与 web 技能。
{error && {error}
}
{message && {message}
}
{loading ? (
加载中…
) : !hasWebCapabilityCatalog ? (
当前 Portal 能力目录缺少 web,请更新 Memind 后端并重启 admin-api。
) : catalog.length === 0 ? (
暂无平台技能,请在 Memind/skills/ 下添加 SKILL.md
) : (
<>
{!userOnly && (
<>
普通用户默认
{webMismatchRole ? (
已开启 web 技能但未启用网页搜索工具,Agent 将无法使用 web_search。
) : null}
普通用户默认技能
setRoleSkills((prev) => ({ ...prev, [name]: enabled }))}
/>
>
)}
{!userOnly && 单用户覆盖
}
{!userOnly && (
)}
{selectedUserId && (
<>
{userWebOverride != null && (
联网工具已单独覆盖为 {userWebOverride ? '开启' : '关闭'}。
)}
{webMismatchUser ? (
该用户已开启 web 技能但未启用网页搜索工具。
) : null}
{Object.keys(userOverrides).length > 0 && (
已设置 {Object.keys(userOverrides).length} 项技能单独覆盖。
)}
用户技能
setUserSkills((prev) => ({ ...prev, [name]: enabled }))
}
/>
>
)}
>
)}
);
}