feat(skill): Memory tab + auto-preflight after install

This commit is contained in:
matevip 2026-05-01 09:50:25 +08:00
parent 0b6d9faaf3
commit d76a5b994f
4 changed files with 89 additions and 5 deletions

View File

@ -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 {

View File

@ -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.',
},

View File

@ -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操作不可撤销。',
},

View File

@ -255,7 +255,7 @@
</div>
<!-- Import Hub Dialog -->
<ImportHubDialog v-model:visible="showImportDialog" @installed="loadAll" />
<ImportHubDialog v-model:visible="showImportDialog" @installed="onSkillInstalled" />
<!-- RFC-090 §4.4 Pre-flight install dialog -->
<PreflightInstallDialog
@ -288,6 +288,10 @@
<button class="detail-tab" :class="{ active: detailTab === 'lessons' }" @click="detailTab = 'lessons'">
{{ t('skills.detail.lessons') }}
</button>
<button class="detail-tab" :class="{ active: detailTab === 'memory' }" @click="detailTab = 'memory'">
{{ t('skills.detail.memory') }}
<span v-if="detailEmployees.length > 0" class="tab-count">{{ detailEmployees.length }}</span>
</button>
</div>
<!-- Manifest tab -->
<div v-if="detailTab === 'manifest'" class="detail-section">
@ -330,6 +334,26 @@
</li>
</ul>
</div>
<!-- RFC-090 §4.2 Memory tab: cross-reference bound agents +
pointer to per-agent memory pages. Programmatic
cross-reference (search MEMORY.md across agents) is a
future increment; v1 surfaces the agents so the user can
navigate. -->
<div v-if="detailTab === 'memory'" class="detail-section">
<p class="detail-hint">{{ t('skills.detail.memoryHint') }}</p>
<p v-if="detailEmployeesLoading" class="detail-empty">{{ t('common.loading') }}</p>
<p v-else-if="detailEmployees.length === 0" class="detail-empty">{{ t('skills.detail.noEmployees') }}</p>
<ul v-else class="memory-agent-list">
<li v-for="agent in detailEmployees" :key="agent.id" class="memory-agent-item">
<span class="memory-agent-icon">{{ agent.icon || '🤖' }}</span>
<span class="memory-agent-name">{{ agent.name }}</span>
<button class="memory-link-btn" @click="$router.push(`/memory?agentId=${agent.id}`)">
{{ t('skills.detail.openMemory') }}
</button>
</li>
</ul>
</div>
<!-- RFC-090 §11.4 Lessons tab -->
<div v-if="detailTab === 'lessons'" class="detail-section">
<div class="lessons-header">
@ -475,9 +499,11 @@ const rescanning = ref<Record<string, boolean>>({})
/** RFC-090 Phase 3 — detail drawer state. */
const detailDrawerVisible = ref(false)
const detailSkill = ref<Skill | null>(null)
const detailTab = ref<'manifest' | 'tools' | 'features' | 'lessons'>('manifest')
const detailTab = ref<'manifest' | 'tools' | 'features' | 'lessons' | 'memory'>('manifest')
const detailLessonsRaw = ref<string>('')
const detailLessonsLoading = ref(false)
const detailEmployees = ref<Array<{ id: number; name: string; icon?: string }>>([])
const detailEmployeesLoading = ref(false)
/** RFC-090 §4.2 card surface — per-skill side data (lessons count, used-by). */
const lessonsCountBySkill = ref<Record<string, number>>({})
@ -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); }