feat(client): vendor digital employee source

This commit is contained in:
baiyanyun
2026-06-05 15:02:42 +08:00
parent c6fdff20e8
commit 2eb9a99da3
66 changed files with 15160 additions and 2646 deletions

View File

@@ -0,0 +1,318 @@
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, setSkillEnabled } from '@/lib/api';
import { getDemoSkills } from './demoScenario';
import { domSafeId } 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<TaskCadence, string> = {
daily: '日常',
weekly: '周',
monthly: '月度',
adhoc: '临时',
};
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 SkillCard({
skill,
recommended = false,
advanced = false,
selected = false,
updating = false,
onSelect,
onToggle,
}: {
skill: SkillSummary;
recommended?: boolean;
advanced?: boolean;
selected?: boolean;
updating?: boolean;
onSelect?: (skill: SkillSummary) => void;
onToggle?: (skill: SkillSummary) => void;
}) {
const cadence = getCadence(skill);
const status = learningStatus(skill, recommended, advanced);
return (
<div
id={`skill-card-${domSafeId(skill.id)}`}
className={`de-skill-card ${selected ? 'de-skill-card--selected' : ''}`}
role="button"
tabIndex={0}
onClick={() => onSelect?.(skill)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onSelect?.(skill);
}
}}
>
<div className="de-skill-card-head">
<span className="de-skill-name">{skill.name}</span>
<span className={`de-skill-status ${skill.enabled ? 'enabled' : 'disabled'}`}>
{status}
</span>
</div>
<p className="de-skill-desc">{getOutcome(skill)}</p>
<div className="de-skill-card-foot">
<span className="de-skill-version">
{cadenceLabels[cadence]}
</span>
<button
className={`de-skill-btn ${skill.enabled ? 'de-skill-btn--danger' : ''}`}
disabled={updating}
title={skill.enabled ? '暂时停用此技能,后续路由不会选择它。' : '恢复使用此技能。'}
onClick={(event) => {
event.stopPropagation();
if (!updating) onToggle?.(skill);
}}
>
{updating ? '处理中...' : skill.enabled ? '暂不使用' : '恢复使用'}
</button>
</div>
</div>
);
}
export default function SkillLibrary({ focusSkillId }: { focusSkillId?: string | null }) {
const [skills, setSkills] = useState<SkillSummary[]>([]);
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<string | null>(focusSkillId ?? null);
const [updatingSkillId, setUpdatingSkillId] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(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) && data.length > 0
? adaptSkillSummaries(data as Record<string, unknown>[])
: getDemoSkills(),
);
} catch (err) {
if (!mountedRef.current) return;
console.error('[SkillLibrary] fetch failed:', err);
setSkills(getDemoSkills());
setError(false);
} 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);
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 filtered = skills
.filter(s => cadenceFilter === 'all' || getCadence(s) === cadenceFilter)
.filter(s => !search || s.name.includes(search) || s.description.includes(search) || s.category.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 key={section.key} style={{ display: 'grid', gap: 10 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
<div>
<h3 style={{ margin: 0, fontSize: 16, color: 'var(--de-text-primary)' }}>{section.title}</h3>
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'var(--de-text-muted)' }}>{section.hint}</p>
</div>
<span className="de-capability-badge de-capability-standard">{section.skills.length} </span>
</div>
{section.skills.length === 0 ? (
<p className="de-empty-text" style={{ margin: 0 }}>{section.emptyText}</p>
) : (
<div className="de-skill-grid">
{section.skills.map(s => (
<SkillCard
key={`${section.key}-${s.id}`}
skill={s}
recommended={section.key === 'recommended'}
advanced={section.key === 'advanced'}
selected={selectedSkillId === s.id}
updating={updatingSkillId === s.id}
onSelect={(skill) => setSelectedSkillId(skill.id)}
onToggle={toggleSkill}
/>
))}
</div>
)}
</section>
);
return (
<div style={{ padding: '14px 18px' }}>
<GlassCard>
<CardTitleBar title="学习技能">
<button onClick={fetchSkills} className="de-btn-secondary de-btn-sm"></button>
</CardTitleBar>
<div className="de-card-body">
{actionError && (
<div className="de-schedule-result de-schedule-result--error" style={{ marginBottom: 12 }}>
{actionError}
</div>
)}
<div className="de-skill-toolbar">
<input type="text" className="de-input" placeholder="搜索业务能力..." value={search} onChange={e => setSearch(e.target.value)} />
<div className="de-filter-row">
{cadenceOptions.map(option => (
<button key={option.value} className={`de-filter-chip ${cadenceFilter === option.value ? 'active' : ''}`} onClick={() => setCadenceFilter(option.value)}>
{option.label}
</button>
))}
</div>
</div>
{loading ? (
<div className="de-loading-skeleton" style={{ height: 300 }} />
) : error ? (
<p className="de-empty-text"></p>
) : filtered.length === 0 && skills.length > 0 ? (
<p className="de-empty-text"></p>
) : filtered.length === 0 ? (
<p className="de-empty-text"></p>
) : (
<div style={{ display: 'grid', gap: 18 }}>
{sections.map(renderSection)}
</div>
)}
</div>
</GlassCard>
</div>
);
}