From 2c1e673fbac47edca5a8dc31eea3d8fbeefbd287 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 19 May 2026 09:56:08 +0800 Subject: [PATCH] feat(skill): SkillMarket lifecycle UI + curator control panel --- mateclaw-ui/src/api/index.ts | 31 ++ .../src/components/skill/SkillLineIcon.vue | 28 ++ mateclaw-ui/src/components/skill/lineIcons.ts | 20 ++ mateclaw-ui/src/i18n/locales/en-US.ts | 75 +++++ mateclaw-ui/src/i18n/locales/zh-CN.ts | 75 +++++ mateclaw-ui/src/router/index.ts | 6 + mateclaw-ui/src/types/index.ts | 8 + mateclaw-ui/src/views/Settings/Layout.vue | 6 + .../src/views/Settings/SkillCurator/index.vue | 308 ++++++++++++++++++ mateclaw-ui/src/views/SkillMarket.vue | 214 +++++++++++- 10 files changed, 759 insertions(+), 12 deletions(-) create mode 100644 mateclaw-ui/src/components/skill/SkillLineIcon.vue create mode 100644 mateclaw-ui/src/components/skill/lineIcons.ts create mode 100644 mateclaw-ui/src/views/Settings/SkillCurator/index.vue diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index adf87487..aa8609e7 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -185,6 +185,8 @@ export const skillApi = { enabled?: boolean /** 'PASSED' / 'FAILED' — filters by security_scan_status. */ scanStatus?: string + /** 'active' / 'stale' / 'archived' — filters by lifecycle_state. */ + lifecycleState?: string } = {}) => http.get('/skills', { params }), /** Tab count aggregate — returns { all, builtin, mcp, dynamic } */ counts: () => http.get('/skills/counts'), @@ -217,6 +219,35 @@ export const skillApi = { http.post(`/skills/${id}/secrets`, { key, value }), deleteSecret: (id: string | number, key: string) => http.delete(`/skills/${id}/secrets/${encodeURIComponent(key)}`), + + // ---- Lifecycle curator ---- + /** Pin / unpin a skill — pinned skills are never auto-archived. */ + pin: (id: string | number, pinned: boolean) => + http.post(`/skills/${id}/pin`, { pinned }), + /** + * Manually archive a skill. When the skill is bound to an enabled agent + * and {@code force} is false, the backend replies HTTP 409 with + * {@code code: 'BOUND_SKILL_CONFIRM_REQUIRED'} and a {@code boundAgents} + * list; retry with {@code force: true} to confirm. + */ + archive: (id: string | number, opts: { force?: boolean; reason?: string } = {}) => + http.post(`/skills/${id}/archive`, { reason: opts.reason ?? null }, + { params: { force: opts.force ?? false } }), + /** Restore an archived skill back to active. */ + restore: (id: string | number) => http.post(`/skills/${id}/restore`), + /** Curator control-panel status (config / control / counts / lastReport). */ + curatorStatus: () => http.get('/skills/curator/status'), + /** Run a curator dry-run preview immediately. */ + curatorDryRun: () => http.post('/skills/curator/dry-run'), + /** Activate (apply transitions) or deactivate (preview-only) the curator. */ + curatorActivate: (activate: boolean) => + http.post('/skills/curator/activate', null, { params: { activate } }), + curatorPause: () => http.post('/skills/curator/pause'), + curatorResume: () => http.post('/skills/curator/resume'), + /** List recent curator run report ids. */ + curatorReports: () => http.get('/skills/curator/reports'), + /** Read one curator run report (parsed run.json). */ + curatorReport: (runId: string) => http.get(`/skills/curator/reports/${runId}`), } /** Shape returned by GET /skills/{id}/secrets. */ diff --git a/mateclaw-ui/src/components/skill/SkillLineIcon.vue b/mateclaw-ui/src/components/skill/SkillLineIcon.vue new file mode 100644 index 00000000..3c46e7c9 --- /dev/null +++ b/mateclaw-ui/src/components/skill/SkillLineIcon.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/mateclaw-ui/src/components/skill/lineIcons.ts b/mateclaw-ui/src/components/skill/lineIcons.ts new file mode 100644 index 00000000..79b4d084 --- /dev/null +++ b/mateclaw-ui/src/components/skill/lineIcons.ts @@ -0,0 +1,20 @@ +/** + * Feather-style line-art icons for the skill catalog and lifecycle curator. + * Each glyph is drawn on a 24x24 canvas and stroked with `currentColor`. + * Consumed by SkillLineIcon.vue so the skill surfaces share one icon source + * instead of inlining raw SVG or emoji — mirrors the memory module's registry. + */ +export const skillLineIcons = { + // Catalog source tabs + all: '', + builtin: '', + mcp: '', + acp: '', + dynamic: '', + // Lifecycle + clock: '', + archive: '', + pin: '', +} as const + +export type SkillLineIconName = keyof typeof skillLineIcons diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 650f58b9..605b63b4 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -573,6 +573,7 @@ export default { featureFlags: 'Feature Flags', about: 'About', advanced: 'Advanced', + skillCurator: 'Skill Curator', }, models: { sidecar: { @@ -2980,6 +2981,34 @@ export default { mcp: 'MCP', acp: 'ACP', dynamic: 'Dynamic', + stale: 'Stale', + archived: 'Archived', + }, + lifecycle: { + section: 'Lifecycle', + state: 'State', + lastUsed: 'Last used', + neverUsed: 'Never used', + today: 'Today', + daysAgo: '{n}d ago', + states: { + active: 'Active', + stale: 'Stale', + archived: 'Archived', + }, + pin: 'Pin this skill', + pinHint: 'Pinned skills are never auto-archived', + archive: 'Archive', + restore: 'Restore', + pinFailed: 'Failed to update pin', + archiveTitle: 'Archive skill', + archiveConfirm: 'Archive "{name}"? The skill will be disabled and can be restored anytime.', + archiveSuccess: 'Skill archived', + archiveFailed: 'Failed to archive', + restoreSuccess: 'Skill restored', + restoreFailed: 'Failed to restore', + boundConfirmTitle: 'Skill is bound to agents', + boundConfirmBody: '"{name}" is explicitly bound to {count} enabled agent(s) ({agents}). Archiving removes that capability from them. Continue?', }, search: { placeholder: 'Search by name, description, or tags...', @@ -3209,6 +3238,52 @@ export default { tooLarge: 'File too large (max 50MB)', }, }, + skillCurator: { + title: 'Skill Curator', + desc: 'Automatically archives long-idle skills on a time window to keep the skill catalog tidy.', + stateEnabled: 'Deployed', + stateActivated: 'Activated', + statePreview: 'Preview only', + statePaused: 'Paused', + config: 'Configuration', + scope: 'Scope', + staleAfter: 'Stale after (days)', + archiveAfter: 'Archive after (days)', + cron: 'Schedule', + counts: 'Current counts', + active: 'Active', + stale: 'Stale', + archived: 'Archived', + pinned: 'Pinned', + blockedByBindings: 'Binding-protected', + control: 'Runtime control', + lastObservedAt: 'First observed', + lastDryRunAt: 'Last preview', + lastRunAt: 'Last run', + nextScheduledRun: 'Next run', + activate: 'Activate curator', + deactivate: 'Back to preview', + pause: 'Pause', + resume: 'Resume', + runDryRun: 'Preview now', + activatedHint: 'The curator is activated and archives idle skills on schedule.', + previewHint: 'The curator is in preview-only mode and never archives. Review a preview report, then activate.', + pausedHint: 'The curator is paused; the scheduled sweep will not run.', + reports: 'Run reports', + noReports: 'No run reports yet', + reportPlanned: 'Planned', + reportApplied: 'Applied', + reportScanned: 'Scanned', + reportDryRun: 'Preview', + reportTransitions: 'Transitions', + activateSuccess: 'Skill curator activated', + deactivateSuccess: 'Back to preview-only mode', + pauseSuccess: 'Skill curator paused', + resumeSuccess: 'Skill curator resumed', + dryRunSuccess: 'Preview report generated', + actionFailed: 'Action failed', + loadFailed: 'Failed to load skill curator status', + }, onboarding: { title: 'Welcome to MateClaw', subtitle: 'Set up your first AI model', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index ed90c4f9..2176baed 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -453,6 +453,7 @@ export default { featureFlags: '功能开关', about: '关于', advanced: '高级', + skillCurator: '技能管家', }, models: { sidecar: { @@ -3072,6 +3073,34 @@ export default { mcp: 'MCP', acp: 'ACP', dynamic: '动态', + stale: '待归档', + archived: '已归档', + }, + lifecycle: { + section: '生命周期', + state: '状态', + lastUsed: '上次使用', + neverUsed: '从未使用', + today: '今天', + daysAgo: '{n} 天前', + states: { + active: '活跃', + stale: '待归档', + archived: '已归档', + }, + pin: '钉住此技能', + pinHint: '钉住后永不会被自动归档', + archive: '归档', + restore: '恢复', + pinFailed: '钉住操作失败', + archiveTitle: '归档技能', + archiveConfirm: '确定归档「{name}」吗?归档后该技能将停用,可随时恢复。', + archiveSuccess: '技能已归档', + archiveFailed: '归档失败', + restoreSuccess: '技能已恢复', + restoreFailed: '恢复失败', + boundConfirmTitle: '该技能已被 Agent 绑定', + boundConfirmBody: '「{name}」被 {count} 个启用中的 Agent 显式绑定({agents})。归档后这些 Agent 将失去该能力,确定继续?', }, search: { placeholder: '搜索技能名称、描述或标签...', @@ -3301,6 +3330,52 @@ export default { tooLarge: '文件过大(最大 50MB)', }, }, + skillCurator: { + title: '技能管家', + desc: '按时间窗口自动归档长期闲置的技能,保持技能目录整洁。', + stateEnabled: '已部署', + stateActivated: '已激活', + statePreview: '仅预览', + statePaused: '已暂停', + config: '配置', + scope: '清理范围', + staleAfter: 'stale 阈值(天)', + archiveAfter: '归档阈值(天)', + cron: '调度表达式', + counts: '当前统计', + active: '活跃', + stale: '待归档', + archived: '已归档', + pinned: '已钉住', + blockedByBindings: '被绑定保护', + control: '运行控制', + lastObservedAt: '首次观测', + lastDryRunAt: '上次预览', + lastRunAt: '上次运行', + nextScheduledRun: '下次调度', + activate: '激活管家', + deactivate: '退回仅预览', + pause: '暂停', + resume: '恢复', + runDryRun: '立即预览', + activatedHint: '管家已激活,每日按调度真正归档闲置技能。', + previewHint: '管家处于仅预览模式,不会真正归档。查看预览报告满意后再激活。', + pausedHint: '管家已暂停,定时扫描不会运行。', + reports: '运行报告', + noReports: '暂无运行报告', + reportPlanned: '计划', + reportApplied: '执行', + reportScanned: '扫描', + reportDryRun: '预览', + reportTransitions: '状态变更', + activateSuccess: '技能管家已激活', + deactivateSuccess: '已退回仅预览模式', + pauseSuccess: '技能管家已暂停', + resumeSuccess: '技能管家已恢复', + dryRunSuccess: '预览报告已生成', + actionFailed: '操作失败', + loadFailed: '加载技能管家状态失败', + }, onboarding: { title: '欢迎使用 MateClaw', subtitle: '配置你的第一个 AI 模型', diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts index 13696a7f..3e05f4e1 100644 --- a/mateclaw-ui/src/router/index.ts +++ b/mateclaw-ui/src/router/index.ts @@ -182,6 +182,12 @@ const router = createRouter({ component: () => import('@/views/CronJobs.vue'), meta: { title: 'Settings - Cron Jobs', requiredCapability: 'manage:agents' }, }, + { + path: 'skill-curator', + name: 'SettingsSkillCurator', + component: () => import('@/views/Settings/SkillCurator/index.vue'), + meta: { title: 'Settings - Skill Curator', requiredCapability: 'manage:settings' }, + }, { path: 'workflows', name: 'SettingsWorkflows', diff --git a/mateclaw-ui/src/types/index.ts b/mateclaw-ui/src/types/index.ts index 83b970aa..9d0e6970 100644 --- a/mateclaw-ui/src/types/index.ts +++ b/mateclaw-ui/src/types/index.ts @@ -285,6 +285,14 @@ export interface Skill { securityScanResult?: string /** RFC-042 §2.3 — wall-clock time of the last scan */ securityScanTime?: string + /** Lifecycle curator state: 'active' / 'stale' / 'archived' */ + lifecycleState?: string + /** User-pinned skill — exempt from automatic archival */ + pinned?: boolean + /** Last activity timestamp — drives the curator's idle window */ + lastActivityAt?: string + /** When the curator moved this skill to archived state */ + archivedAt?: string } /** 运行时解析状态(来自 /runtime/status) */ diff --git a/mateclaw-ui/src/views/Settings/Layout.vue b/mateclaw-ui/src/views/Settings/Layout.vue index 684e990a..ef0635cc 100644 --- a/mateclaw-ui/src/views/Settings/Layout.vue +++ b/mateclaw-ui/src/views/Settings/Layout.vue @@ -167,6 +167,12 @@ const sections = computed(() => [ label: t('nav.triggers', 'Triggers'), icon: '', }, + { + id: 'skill-curator', + path: '/settings/skill-curator', + label: t('settings.sections.skillCurator', 'Skill Curator'), + icon: '', + }, { id: 'datasources', path: '/settings/datasources', diff --git a/mateclaw-ui/src/views/Settings/SkillCurator/index.vue b/mateclaw-ui/src/views/Settings/SkillCurator/index.vue new file mode 100644 index 00000000..698f024e --- /dev/null +++ b/mateclaw-ui/src/views/Settings/SkillCurator/index.vue @@ -0,0 +1,308 @@ + + + + + diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue index f0ac9d01..ff6b5238 100644 --- a/mateclaw-ui/src/views/SkillMarket.vue +++ b/mateclaw-ui/src/views/SkillMarket.vue @@ -41,10 +41,11 @@
@@ -132,6 +133,12 @@ {{ getSourceLabel(skill) }} v{{ skill.version }} + {{ lastUsedLabel(skill) }}