feat(client): enrich digital skill catalog
This commit is contained in:
@@ -978,6 +978,9 @@
|
||||
.de-skill-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.de-skill-name { font-size: 15px; font-weight: 600; color: var(--de-text-primary); }
|
||||
.de-skill-desc { font-size: 13px; color: var(--de-text-body); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex: 1; }
|
||||
.de-skill-meta-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-height: 24px; }
|
||||
.de-skill-meta-chip { display: inline-flex; align-items: center; max-width: 160px; min-height: 22px; padding: 0 8px; border-radius: 999px; border: 1px solid rgba(79,172,254,0.14); background: rgba(79,172,254,0.08); color: #41627f; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.de-skill-recent { margin: 0; font-size: 12px; color: var(--de-text-muted); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.de-skill-card-foot { display: flex; align-items: center; justify-content: space-between; }
|
||||
.de-skill-version { font-size: 11px; color: var(--de-text-muted); }
|
||||
.de-skill-status { font-size: 11px; font-weight: 600; }
|
||||
|
||||
@@ -70,6 +70,24 @@ export interface SkillSummary {
|
||||
category: string;
|
||||
capabilityLevel: 'core' | 'standard' | 'specialized' | 'experimental';
|
||||
source: 'skill-api';
|
||||
serverId?: string | null;
|
||||
transport?: string | null;
|
||||
running?: boolean | null;
|
||||
toolCount?: number | null;
|
||||
tools?: string[];
|
||||
allowTools?: string[];
|
||||
denyTools?: string[];
|
||||
governancePolicy?: 'allow' | 'deny' | 'open' | 'disabled' | null;
|
||||
recentCalls?: Array<{
|
||||
eventId: string;
|
||||
message: string;
|
||||
status?: string | null;
|
||||
occurredAt: string;
|
||||
toolName?: string | null;
|
||||
projectId?: string | null;
|
||||
}>;
|
||||
callCount?: number;
|
||||
lastCalledAt?: string | null;
|
||||
}
|
||||
|
||||
export interface DataSourceStatus {
|
||||
@@ -87,6 +105,10 @@ function arr(v: unknown): Obj[] {
|
||||
return Array.isArray(v) ? (v as Obj[]) : [];
|
||||
}
|
||||
|
||||
function obj(v: unknown): Obj | null {
|
||||
return v && typeof v === 'object' && !Array.isArray(v) ? v as Obj : null;
|
||||
}
|
||||
|
||||
function str(v: unknown, fallback = ''): string {
|
||||
if (typeof v === 'string') return v;
|
||||
if (typeof v === 'number') return String(v);
|
||||
@@ -456,6 +478,7 @@ export function adaptSkillSummaries(skillsData: Obj[] | null): SkillSummary[] {
|
||||
if (!skillsData) return [];
|
||||
return skillsData.map((s: Obj) => {
|
||||
const sid = str(s.skill_id, str(s.id));
|
||||
const governance = obj(s.governance);
|
||||
return {
|
||||
id: sid,
|
||||
name: zhName(sid, str(s.name, sid)),
|
||||
@@ -465,10 +488,51 @@ export function adaptSkillSummaries(skillsData: Obj[] | null): SkillSummary[] {
|
||||
category: s.category ? str(s.category, categoryFor(sid)) : categoryFor(sid),
|
||||
capabilityLevel: capabilityLevelFor(s.capability_level ?? s.capabilityLevel),
|
||||
source: 'skill-api' as const,
|
||||
serverId: s.server_id ? str(s.server_id) : null,
|
||||
transport: s.transport ? str(s.transport) : null,
|
||||
running: typeof s.running === 'boolean' ? s.running : null,
|
||||
toolCount: num(s.tool_count),
|
||||
tools: stringList(s.tools),
|
||||
allowTools: stringList(s.allow_tools ?? governance?.server_allow_tools ?? governance?.global_allow_tools),
|
||||
denyTools: stringList(s.deny_tools ?? governance?.server_deny_tools ?? governance?.global_deny_tools),
|
||||
governancePolicy: governancePolicy(s.governance_policy ?? governance?.effective_policy),
|
||||
recentCalls: arr(s.recent_calls).map((call) => {
|
||||
const c = obj(call) ?? {};
|
||||
return {
|
||||
eventId: str(c.event_id, str(c.id)),
|
||||
message: str(c.message),
|
||||
status: c.status ? str(c.status) : null,
|
||||
occurredAt: str(c.occurred_at, str(c.occurredAt)),
|
||||
toolName: c.tool_name ? str(c.tool_name) : null,
|
||||
projectId: c.project_id ? str(c.project_id) : null,
|
||||
};
|
||||
}).filter((call) => call.eventId || call.message),
|
||||
callCount: num(s.call_count) ?? 0,
|
||||
lastCalledAt: s.last_called_at ? str(s.last_called_at) : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function stringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
|
||||
}
|
||||
|
||||
function num(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function governancePolicy(value: unknown): SkillSummary['governancePolicy'] {
|
||||
return value === 'allow' || value === 'deny' || value === 'open' || value === 'disabled'
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
export function adaptJournalEntries(storeData: Obj | null): JournalEntry[] {
|
||||
if (!storeData) return [];
|
||||
const tables = (storeData.tables ?? {}) as Obj;
|
||||
|
||||
@@ -30,6 +30,13 @@ const cadenceLabels: Record<TaskCadence, string> = {
|
||||
adhoc: '临时',
|
||||
};
|
||||
|
||||
const policyLabels: Record<NonNullable<SkillSummary['governancePolicy']>, 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';
|
||||
@@ -71,6 +78,26 @@ function learningStatus(skill: SkillSummary, recommended: boolean, advanced: boo
|
||||
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.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,
|
||||
@@ -90,6 +117,8 @@ function SkillCard({
|
||||
}) {
|
||||
const cadence = getCadence(skill);
|
||||
const status = learningStatus(skill, recommended, advanced);
|
||||
const metaBadges = skillMetaBadges(skill);
|
||||
const recentCall = recentCallText(skill);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -112,6 +141,12 @@ function SkillCard({
|
||||
</span>
|
||||
</div>
|
||||
<p className="de-skill-desc">{getOutcome(skill)}</p>
|
||||
{metaBadges.length > 0 && (
|
||||
<div className="de-skill-meta-row">
|
||||
{metaBadges.map((badge) => <span key={badge} className="de-skill-meta-chip">{badge}</span>)}
|
||||
</div>
|
||||
)}
|
||||
{recentCall && <p className="de-skill-recent">最近:{recentCall}</p>}
|
||||
<div className="de-skill-card-foot">
|
||||
<span className="de-skill-version">
|
||||
适用任务:{cadenceLabels[cadence]}
|
||||
@@ -205,7 +240,19 @@ export default function SkillLibrary({ focusSkillId }: { focusSkillId?: string |
|
||||
|
||||
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));
|
||||
.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));
|
||||
|
||||
Reference in New Issue
Block a user