From d76a5b994fc3fcf0dffdce801db6f28753159a07 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 1 May 2026 09:50:25 +0800 Subject: [PATCH] feat(skill): Memory tab + auto-preflight after install --- .../src/components/skill/ImportHubDialog.vue | 13 +++- mateclaw-ui/src/i18n/locales/en-US.ts | 4 + mateclaw-ui/src/i18n/locales/zh-CN.ts | 4 + mateclaw-ui/src/views/SkillMarket.vue | 73 ++++++++++++++++++- 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/mateclaw-ui/src/components/skill/ImportHubDialog.vue b/mateclaw-ui/src/components/skill/ImportHubDialog.vue index c5c2b6a6..5043a24f 100644 --- a/mateclaw-ui/src/components/skill/ImportHubDialog.vue +++ b/mateclaw-ui/src/components/skill/ImportHubDialog.vue @@ -154,9 +154,13 @@ import { skillInstallApi } from '@/api/index' import type { InstallTask, HubSkillInfo } from '@/types/index' const props = defineProps<{ visible: boolean }>() +// RFC-090 §4.4 — when install completes, hand the parent the skill +// name so it can auto-open the pre-flight dialog if requirements +// aren't met. Backwards-compat: payload is optional, old listeners +// that ignore it keep working. const emit = defineEmits<{ (e: 'update:visible', val: boolean): void - (e: 'installed'): void + (e: 'installed', payload?: { name?: string }): void }>() const { t } = useI18n() @@ -228,7 +232,10 @@ function startPolling(taskId: string) { installing.value = false if (status === 'COMPLETED') { ElMessage.success(t('skills.import.installed')) - emit('installed') + // The install task's result envelope carries `{name, enabled, sourceUrl, ...}`. + // Hand the slug along so the parent can pop preflight for the + // freshly-installed skill if its requirements aren't met. + emit('installed', { name: res.data?.result?.name }) } } } catch { @@ -300,7 +307,7 @@ async function uploadZip() { }) ElMessage.success(t('skills.import.uploadSuccess')) zipFile.value = null - emit('installed') + emit('installed', { name: res?.data?.name }) } catch (e: any) { ElMessage.error(e?.response?.data?.msg || e?.message || t('skills.import.uploadFailed')) } finally { diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 582a338e..1bbf2dba 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -2038,12 +2038,16 @@ export default { tools: 'Tools', features: 'Features', lessons: 'Lessons', + memory: 'Memory', noManifest: 'This skill does not declare a v3 manifest. Legacy fields apply.', noTools: 'No tools advertised by active features.', noFeatures: 'No features[] matrix declared. The skill is treated as a single default feature.', noLessons: 'No lessons recorded yet. The skill writes here after each call when self-evolution is enabled.', + noEmployees: 'No agent has bound this skill yet.', toolsHint: 'These tool names are merged into the LLM allowed-tool list when this skill is bound to an agent. Tools owned by SETUP_NEEDED features stay hidden.', lessonsHint: 'Per-skill LESSONS.md. Auto-injected after SKILL.md body when this skill is loaded.', + memoryHint: 'Agents that have bound this skill. Click an agent to inspect its MEMORY.md / structured memory entries that may reference this skill.', + openMemory: 'Open agent memory', clearLessons: 'Clear all', clearLessonsConfirm: 'Delete all recorded lessons for this skill? This cannot be undone.', }, diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index a13eb55d..7799f22f 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -2040,12 +2040,16 @@ export default { tools: '工具', features: '特性', lessons: 'Lessons', + memory: '记忆', noManifest: '该技能未声明 v3 manifest,使用旧字段。', noTools: '当前激活特性未暴露任何工具。', noFeatures: '未声明 features[] 矩阵,技能被视为单一默认特性。', noLessons: '尚未记录任何 lesson。开启 self-evolution 后,每次调用结束 skill 可向此处写入经验。', + noEmployees: '暂无 Agent 绑定此 skill。', toolsHint: 'Skill 绑定到 agent 时,这些工具名将合并进 LLM 的 allowed-tools。SETUP_NEEDED 特性下的工具保持隐藏。', lessonsHint: '该 skill 的 LESSONS.md 内容。下次加载时自动注入到 SKILL.md 正文之后。', + memoryHint: '已绑定该 skill 的 Agents。点击进入 Agent 的 MEMORY.md / 结构化记忆查看与该 skill 相关的条目。', + openMemory: '查看 Agent 记忆', clearLessons: '全部清空', clearLessonsConfirm: '删除该 skill 的全部 lessons?操作不可撤销。', }, diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue index 5c0abc87..75f50db8 100644 --- a/mateclaw-ui/src/views/SkillMarket.vue +++ b/mateclaw-ui/src/views/SkillMarket.vue @@ -255,7 +255,7 @@ - + {{ t('skills.detail.lessons') }} +
@@ -330,6 +334,26 @@
+ +
+

{{ t('skills.detail.memoryHint') }}

+

{{ t('common.loading') }}

+

{{ t('skills.detail.noEmployees') }}

+
    +
  • + {{ agent.icon || '🤖' }} + {{ agent.name }} + +
  • +
+
+
@@ -475,9 +499,11 @@ const rescanning = ref>({}) /** RFC-090 Phase 3 — detail drawer state. */ const detailDrawerVisible = ref(false) const detailSkill = ref(null) -const detailTab = ref<'manifest' | 'tools' | 'features' | 'lessons'>('manifest') +const detailTab = ref<'manifest' | 'tools' | 'features' | 'lessons' | 'memory'>('manifest') const detailLessonsRaw = ref('') const detailLessonsLoading = ref(false) +const detailEmployees = ref>([]) +const detailEmployeesLoading = ref(false) /** RFC-090 §4.2 card surface — per-skill side data (lessons count, used-by). */ const lessonsCountBySkill = ref>({}) @@ -494,6 +520,24 @@ function openPreflight(skill: Skill) { preflightVisible.value = true } +/** + * RFC-090 §4.4 — when ImportHubDialog reports a successful install, + * reload the catalog and, if the freshly-installed skill is not + * READY (has unresolved requirements), pop the preflight dialog so + * the user sees what they need to do next without hunting for the + * card. Backwards-compatible: if `payload.name` is missing (older + * dialog versions) we just reload silently. + */ +async function onSkillInstalled(payload?: { name?: string }) { + await loadAll() + if (!payload?.name) return + const installed = skills.value.find(s => s.name === payload.name) + if (!installed) return + if (needsSetup(installed)) { + openPreflight(installed) + } +} + const detailRuntime = computed(() => detailSkill.value ? runtimeStatusMap.value[detailSkill.value.name] || null : null, ) @@ -515,9 +559,23 @@ function openDetailDrawer(skill: Skill) { detailSkill.value = skill detailTab.value = 'manifest' detailLessonsRaw.value = '' + detailEmployees.value = [] detailDrawerVisible.value = true } +async function loadDetailEmployees() { + if (!detailSkill.value) return + detailEmployeesLoading.value = true + try { + const res: any = await skillApi.employees(detailSkill.value.id) + detailEmployees.value = res?.data || [] + } catch { + detailEmployees.value = [] + } finally { + detailEmployeesLoading.value = false + } +} + async function loadLessons() { if (!detailSkill.value) return detailLessonsLoading.value = true @@ -551,6 +609,9 @@ watch(detailTab, (tab) => { if (tab === 'lessons' && detailDrawerVisible.value && !detailLessonsRaw.value && !detailLessonsLoading.value) { loadLessons() } + if (tab === 'memory' && detailDrawerVisible.value && detailEmployees.value.length === 0 && !detailEmployeesLoading.value) { + loadDetailEmployees() + } }) const categoryTabs = computed(() => [ @@ -1425,6 +1486,14 @@ html.dark .scan-finding-item { background: rgba(255, 255, 255, 0.05); } .detail-feature-tag { padding: 2px 6px; background: var(--mc-bg-elevated); border-radius: 4px; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; color: var(--mc-text-primary); } .detail-feature-fallback { font-size: 11px; color: var(--mc-primary-hover); margin-top: 6px; font-style: italic; } +/* RFC-090 §4.2 Memory tab — bound agents list */ +.memory-agent-list { list-style: none; padding: 0; margin: 8px 0 0; display: flex; flex-direction: column; gap: 8px; } +.memory-agent-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--mc-border-light); border-radius: 10px; background: var(--mc-bg-muted); } +.memory-agent-icon { font-size: 20px; } +.memory-agent-name { flex: 1; font-weight: 600; color: var(--mc-text-primary); } +.memory-link-btn { padding: 4px 10px; border: 1px solid var(--mc-border); background: var(--mc-bg-elevated); color: var(--mc-primary); border-radius: 8px; font-size: 12px; cursor: pointer; font-weight: 500; } +.memory-link-btn:hover { background: var(--mc-primary-bg); border-color: var(--mc-primary); } + .lessons-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; } .lessons-clear-btn { padding: 6px 12px; border-radius: 8px; border: 1px solid var(--mc-border); background: var(--mc-bg-muted); color: var(--mc-text-secondary); cursor: pointer; font-size: 12px; } .lessons-clear-btn:hover:not(:disabled) { background: var(--mc-danger-bg); color: var(--mc-danger); border-color: var(--mc-danger); }