import { useState, useEffect, useCallback, useRef } from 'react'; import GlassCard from '@/components/digital/GlassCard'; import CardTitleBar from '@/components/digital/CardTitleBar'; import { adaptSkillSummaries, type SkillSummary } from '@/lib/consoleDataAdapter'; import { getSkills, pullSkillPolicies, setSkillEnabled } from '@/lib/api'; import { domSafeId, type DigitalNavigationTarget } from './navigation'; type TaskCadence = 'daily' | 'weekly' | 'monthly' | 'adhoc'; type SkillSection = { key: string; title: string; hint: string; skills: SkillSummary[]; emptyText: string; }; const cadenceOptions: Array<{ value: 'all' | TaskCadence; label: string }> = [ { value: 'all', label: '全部任务' }, { value: 'daily', label: '日常任务' }, { value: 'weekly', label: '周任务' }, { value: 'monthly', label: '月度任务' }, { value: 'adhoc', label: '临时任务' }, ]; const cadenceLabels: Record = { daily: '日常', weekly: '周', monthly: '月度', adhoc: '临时', }; const policyLabels: Record, string> = { allow: '白名单', deny: '黑名单', open: '开放', disabled: '停用', }; function getCadence(skill: SkillSummary): TaskCadence { const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase(); if (/weekly|week|周/.test(text)) return 'weekly'; if (/monthly|month|月报|月度/.test(text)) return 'monthly'; if (/monitor|crawler|巡检|监控|待办|日程|日报|日终|收件箱|工作台|客户跟进/.test(text)) return 'daily'; return 'adhoc'; } function isAdvancedSkill(skill: SkillSummary): boolean { const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase(); return ( skill.capabilityLevel === 'specialized' || skill.capabilityLevel === 'experimental' || /加密|深度|分析|自动化|部署|代码|审查|调研|批量|converter|crypto|github|deploy|review|research/.test(text) ); } function isRecommendedSkill(skill: SkillSummary): boolean { const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase(); return /日报|周报|报表|待办|日程|巡检|客户|知识库|经营|费用|收件箱|工作台/.test(text); } function getOutcome(skill: SkillSummary): string { const name = skill.name; const text = `${skill.id} ${skill.name} ${skill.description} ${skill.category}`.toLowerCase(); if (/report|报表|日报|周报|月报|汇报/.test(text)) return `自动整理${name}材料,减少手工汇总和重复填报。`; if (/monitor|巡检|监控|预警/.test(text)) return `按约定频率完成${name},及时提示异常和遗漏。`; if (/todo|待办|task|看板|日程/.test(text)) return `帮你梳理${name},把后续处理事项集中呈现。`; if (/客户|crm|回访|满意/.test(text)) return `汇总客户相关信息,形成可直接跟进的处理建议。`; if (/知识|文档|制度|模板/.test(text)) return `查找和核对资料内容,减少人工翻找和版本确认。`; return skill.description; } function learningStatus(skill: SkillSummary, recommended: boolean, advanced: boolean): string { if (skill.enabled) return '已学习'; if (!skill.enabled) return '暂不使用'; if (advanced) return '需确认后学习'; if (recommended) return '建议学习'; return '可学习'; } function skillMetaBadges(skill: SkillSummary): string[] { const badges: string[] = []; if (skill.serverId) badges.push(skill.serverId); if (skill.running !== null && skill.running !== undefined) badges.push(skill.running ? '运行中' : '未运行'); if (skill.policySource === 'remote') badges.push('远端策略'); if (skill.remotePolicy && skill.remotePolicy.enabled === false) badges.push('管理端禁用'); if (skill.governancePolicy) badges.push(policyLabels[skill.governancePolicy]); if (typeof skill.toolCount === 'number') badges.push(`${skill.toolCount} 工具`); if (skill.allowTools?.length) badges.push(`allow ${skill.allowTools.length}`); if (skill.denyTools?.length) badges.push(`deny ${skill.denyTools.length}`); return badges.slice(0, 5); } function recentCallText(skill: SkillSummary): string | null { const call = skill.recentCalls?.[0]; if (!call) return typeof skill.callCount === 'number' && skill.callCount > 0 ? `${skill.callCount} 次调用` : null; const tool = call.toolName ? `${call.toolName} · ` : ''; return `${tool}${call.message || call.status || '最近调用'}`; } function SkillCard({ skill, recommended = false, advanced = false, selected = false, updating = false, onSelect, onToggle, onAudit, }: { skill: SkillSummary; recommended?: boolean; advanced?: boolean; selected?: boolean; updating?: boolean; onSelect?: (skill: SkillSummary) => void; onToggle?: (skill: SkillSummary) => void; onAudit?: (skill: SkillSummary) => void; }) { const cadence = getCadence(skill); const status = learningStatus(skill, recommended, advanced); const metaBadges = skillMetaBadges(skill); const recentCall = recentCallText(skill); return (
onSelect?.(skill)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onSelect?.(skill); } }} >
{skill.name} {status}

{getOutcome(skill)}

{metaBadges.length > 0 && (
{metaBadges.map((badge) => {badge})}
)} {recentCall &&

最近:{recentCall}

}
适用任务:{cadenceLabels[cadence]}
); } export default function SkillLibrary({ focusSkillId, onNavigate }: { focusSkillId?: string | null; onNavigate?: (target: DigitalNavigationTarget) => void }) { const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [search, setSearch] = useState(''); const [cadenceFilter, setCadenceFilter] = useState<'all' | TaskCadence>('all'); const [selectedSkillId, setSelectedSkillId] = useState(focusSkillId ?? null); const [updatingSkillId, setUpdatingSkillId] = useState(null); const [pullingPolicies, setPullingPolicies] = useState(false); const [actionError, setActionError] = useState(null); const [actionNotice, setActionNotice] = useState(null); const mountedRef = useRef(false); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const fetchSkills = useCallback(async () => { setLoading(true); setError(false); try { const data = await getSkills(); if (!mountedRef.current) return; setSkills(Array.isArray(data) ? adaptSkillSummaries(data as Record[]) : []); } catch (err) { if (!mountedRef.current) return; console.error('[SkillLibrary] fetch failed:', err); setSkills([]); setError(true); } finally { if (mountedRef.current) setLoading(false); } }, []); useEffect(() => { fetchSkills(); }, [fetchSkills]); useEffect(() => { if (!focusSkillId) return; setSelectedSkillId(focusSkillId); setCadenceFilter('all'); setSearch(''); window.setTimeout(() => { document.getElementById(`skill-card-${domSafeId(focusSkillId)}`)?.scrollIntoView({ block: 'center', behavior: 'smooth', }); }, 80); }, [focusSkillId, skills.length]); const toggleSkill = useCallback(async (skill: SkillSummary) => { const nextEnabled = !skill.enabled; setUpdatingSkillId(skill.id); setActionError(null); setActionNotice(null); try { const result = await setSkillEnabled(skill.id, nextEnabled); if (!result.ok) { throw new Error(result.error || '技能状态变更失败'); } setSkills((current) => current.map((item) => ( item.id === skill.id ? { ...item, enabled: nextEnabled } : item ))); setSelectedSkillId(skill.id); } catch (err) { setActionError(err instanceof Error ? err.message : '技能状态变更失败'); } finally { setUpdatingSkillId(null); } }, []); const pullPolicies = useCallback(async () => { setPullingPolicies(true); setActionError(null); setActionNotice(null); try { const result = await pullSkillPolicies(); if (!result.ok) throw new Error(result.error || '技能策略拉取失败'); await fetchSkills(); setActionNotice(result.skipped ? '技能策略无需重复拉取。' : '技能策略已更新。'); } catch (err) { setActionError(err instanceof Error ? err.message : '技能策略拉取失败'); } finally { setPullingPolicies(false); } }, [fetchSkills]); const filtered = skills .filter(s => cadenceFilter === 'all' || getCadence(s) === cadenceFilter) .filter(s => { if (!search) return true; const haystack = [ s.name, s.description, s.category, s.serverId, ...(s.tools ?? []), ...(s.allowTools ?? []), ...(s.denyTools ?? []), ].filter(Boolean).join(' '); return haystack.includes(search); }); const advancedSkills = filtered.filter(isAdvancedSkill); const advancedIds = new Set(advancedSkills.map(s => s.id)); const learnedSkills = filtered.filter(s => s.enabled && !advancedIds.has(s.id)); const recommendedPool = filtered.filter(s => !s.enabled && isRecommendedSkill(s) && !isAdvancedSkill(s)); const baseLearnableSkills = filtered.filter(s => !s.enabled && !isAdvancedSkill(s)); const recommendedSkills = (recommendedPool.length > 0 ? recommendedPool : baseLearnableSkills).slice(0, 4); const recommendedIds = new Set(recommendedSkills.map(s => s.id)); const learnableSkills = baseLearnableSkills.filter(s => !recommendedIds.has(s.id)); const sections: SkillSection[] = [ { key: 'recommended', title: '推荐学习', hint: '优先补齐高频工作能力,适合让数字员工尽快接手重复事务。', skills: recommendedSkills, emptyText: '暂无推荐学习项。', }, { key: 'learnable', title: '可学习技能', hint: '可纳入数字员工日常工作的业务能力,学习入口待接入后启用。', skills: learnableSkills, emptyText: '当前没有可学习技能。', }, { key: 'learned', title: '已学技能', hint: '数字员工已经掌握并可被业务场景调用的能力。', skills: learnedSkills, emptyText: '当前还没有已学习技能。', }, { key: 'advanced', title: '高级技能', hint: '适合有明确边界或需要确认后再交给数字员工处理的能力。', skills: advancedSkills, emptyText: '暂无高级技能。', }, ]; const renderSection = (section: SkillSection) => (

{section.title}

{section.hint}

{section.skills.length} 项
{section.skills.length === 0 ? (

{section.emptyText}

) : (
{section.skills.map(s => ( setSelectedSkillId(skill.id)} onToggle={toggleSkill} onAudit={(skill) => onNavigate?.({ tab: 'skill-audit', auditSkillId: skill.id })} /> ))}
)}
); return (
{actionError && (
{actionError}
)} {actionNotice && (
{actionNotice}
)}
setSearch(e.target.value)} />
{cadenceOptions.map(option => ( ))}
{loading ? (
) : error ? (

技能目录暂不可用,请检查 qimingclaw bridge 或稍后刷新。

) : filtered.length === 0 && skills.length > 0 ? (

无匹配技能,请调整搜索或筛选条件

) : filtered.length === 0 ? (

暂无可展示的学习技能。

) : (
{sections.map(renderSection)}
)}
); }