mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(skill): Memory tab + auto-preflight after install
This commit is contained in:
parent
0b6d9faaf3
commit
d76a5b994f
@ -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 {
|
||||
|
||||
@ -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.',
|
||||
},
|
||||
|
||||
@ -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?操作不可撤销。',
|
||||
},
|
||||
|
||||
@ -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); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user