# P3: 碰撞检测与诊断系统 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在 skill 扫描时收集诊断信息(名称碰撞、验证警告、环境缺失),通过 API 暴露,前端展示诊断横幅和 per-skill 诊断详情。 **Architecture:** `SkillLoaderService` 内部维护 `diagnostics: SkillDiagnostic[]` 数组,`scanSkills()` 时清空并重新收集。Admin controller 新增 `/diagnostics` 端点。前端 skills.vue 顶部新增诊断横幅,skill-detail.vue 新增诊断 tab。 **Tech Stack:** TypeORM, Midway.js, Element Plus **Spec:** `docs/superpowers/specs/2026-04-27-skill-system-evolution-design.md` Section 6 **Depends on:** P0 (envSchema for ENV_NOT_CONFIGURED check), P1 Tasks 1-2 (SkillConfigService integrated into SkillLoaderService), P2 Task 1 (validateSkillName function). Execute these prerequisites first. --- ### Task 1: 诊断数据结构和收集 **Files:** - Modify: `packages/shared/types/skill.types.ts` - Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts` - [ ] **Step 1: 在 shared/types/skill.types.ts 新增 SkillDiagnostic 类型** ```typescript export interface SkillDiagnostic { level: 'error' | 'warning' | 'info'; code: string; skillName: string; message: string; path?: string; detail?: Record; } ``` - [ ] **Step 2: 在 SkillLoaderService 中新增诊断收集** 在 class 内部新增: ```typescript private diagnostics: SkillDiagnostic[] = []; getDiagnostics(level?: string): SkillDiagnostic[] { if (level) return this.diagnostics.filter(d => d.level === level); return [...this.diagnostics]; } private addDiagnostic(diag: SkillDiagnostic): void { this.diagnostics.push(diag); } ``` - [ ] **Step 3: 在 scanSkills 开头清空诊断** 在 `scanSkills()` 方法的 `this.skills.clear()` 之后添加: ```typescript this.diagnostics = []; const realPathSet = new Set(); ``` - [ ] **Step 3.5: 在 scanSkills 循环中添加 symlink 去重(Spec 10.11)** 在解析每个 skill 目录时,碰撞检测之前添加: ```typescript // symlink 去重 const skillDirPath = path.join(this.skillsDir, entry.name); let realPath: string; try { realPath = await fs.realpath(skillDirPath); } catch { realPath = skillDirPath; } if (realPathSet.has(realPath)) continue; // 同一物理目录,静默跳过 realPathSet.add(realPath); ``` - [ ] **Step 4: 在 scanSkills 循环中添加诊断收集** 在解析每个 skill 的过程中,添加以下检查: ```typescript // 名称验证 const nameErrors = validateSkillName(skill.name, entry.name); for (const err of nameErrors) { if (err.includes('does not match')) { this.addDiagnostic({ level: 'warning', code: 'NAME_MISMATCH', skillName: skill.name, message: err, path: skillMdPath }); } else { this.addDiagnostic({ level: 'warning', code: 'NAME_INVALID', skillName: skill.name, message: err, path: skillMdPath }); } } // 碰撞检测 if (this.skills.has(skill.name)) { const existing = this.skills.get(skill.name)!; this.addDiagnostic({ level: 'warning', code: 'NAME_COLLISION', skillName: skill.name, message: `Name collision: "${skill.name}" already loaded`, detail: { winnerPath: path.join(this.skillsDir, existing.name), loserPath: skillMdPath }, }); continue; // 跳过,保留先发现的 } // description 检查 if (!skill.description) { this.addDiagnostic({ level: 'error', code: 'DESC_MISSING', skillName: skill.name, message: 'Missing description', path: skillMdPath }); continue; // 不加载 } if (skill.description.length > 1024) { this.addDiagnostic({ level: 'warning', code: 'DESC_TOO_LONG', skillName: skill.name, message: `Description exceeds 1024 chars (${skill.description.length})`, path: skillMdPath }); } ``` - [ ] **Step 5: 在 scanSkills 末尾添加 config 相关诊断** 在所有 skill 加载完成后,遍历检查 compute skill 的环境就绪状态: ```typescript // 扫描完成后检查 compute skill 状态 for (const [name, _skill] of this.skills) { const config = this.skillConfig.getConfig(name); if (!config) { // 检查是否有解析错误(区分"无 config 文件"和"config 解析失败") const parseErr = this.skillConfig.getParseError(name); if (parseErr) { this.addDiagnostic({ level: 'error', code: 'CONFIG_PARSE_ERROR', skillName: name, message: `skill.config.yaml parse failed: ${parseErr}` }); } continue; } // config 解析错误已在 loadConfig 中处理 // runtime 可用性检查 if (config.runtime === 'python') { const venvPath = path.join(this.skillsDir, name, '.venv'); try { await fs.access(venvPath); } catch { this.addDiagnostic({ level: 'warning', code: 'VENV_MISSING', skillName: name, message: 'Python .venv not found, run dependency install' }); } } if (config.runtime) { const runtimeChecks: Record = { python: 'python3 --version', node: 'node --version', dotnet: 'dotnet --version' }; const checkCmd = runtimeChecks[config.runtime]; if (checkCmd) { try { await execAsync(checkCmd, { timeout: 5000 }); } catch { this.addDiagnostic({ level: 'error', code: 'RUNTIME_UNAVAILABLE', skillName: name, message: `Runtime "${config.runtime}" not available on this system` }); } } } // 系统依赖检查 if (config.dependencies?.system) { for (const dep of config.dependencies.system) { try { await execAsync(dep.check, { timeout: 5000 }); } catch { this.addDiagnostic({ level: 'warning', code: 'SYSTEM_DEP_MISSING', skillName: name, message: `System dependency "${dep.name}" not found (check: ${dep.check})` }); } } } // fingerprint 变更检查 const lockfile = await this.registry?.readLockfile?.(); if (lockfile?.skills?.[name]) { const currentFp = await this.registry.computeFingerprint(path.join(this.skillsDir, name)); if (currentFp !== lockfile.skills[name].fingerprint) { this.addDiagnostic({ level: 'info', code: 'FINGERPRINT_CHANGED', skillName: name, message: 'SKILL.md content changed since last install/update' }); } } // env 配置检查 if (config.env) { const entity = await this.skillRepo.findOneBy({ name }); const hasSecrets = !!entity?.secrets; for (const envItem of config.env) { if (envItem.required && !hasSecrets) { this.addDiagnostic({ level: 'warning', code: 'ENV_NOT_CONFIGURED', skillName: name, message: `Required env "${envItem.name}" not configured` }); break; } } } } ``` - [ ] **Step 6: Commit** ```bash git add packages/shared/types/skill.types.ts packages/backend/src/modules/netaclaw/service/skill_loader.ts git commit -m "feat(skill): add diagnostic collection during skill scanning" ``` --- ### Task 2: Admin Controller 诊断端点 **Files:** - Modify: `packages/backend/src/modules/netaclaw/controller/admin/skill.ts` - [ ] **Step 1: 新增 diagnostics 端点** ```typescript @Get('/diagnostics', { summary: '获取 skill 诊断信息' }) async diagnostics(@Query('level') level?: string) { const diags = this.skillLoader.getDiagnostics(level); return this.ok(diags); } ``` - [ ] **Step 2: Commit** ```bash git add packages/backend/src/modules/netaclaw/controller/admin/skill.ts git commit -m "feat(skill): add diagnostics admin endpoint" ``` --- ### Task 3: 前端诊断横幅 **Files:** - Modify: `packages/frontend/src/modules/agent/views/skills.vue` - [ ] **Step 1: 在 skills.vue 中新增诊断数据加载** 在 script 区域添加: ```typescript const diagnostics = ref([]); const errorCount = computed(() => diagnostics.value.filter(d => d.level === 'error').length); const warningCount = computed(() => diagnostics.value.filter(d => d.level === 'warning').length); const showDiagDetails = ref(false); async function loadDiagnostics() { try { const res = await service.request({ url: '/admin/netaclaw/skill/diagnostics' }); diagnostics.value = res || []; } catch { /* ignore */ } } ``` 在 `refresh()` 函数末尾添加 `await loadDiagnostics();` - [ ] **Step 2: 在 template 中 skill-header 之后添加诊断横幅** ```vue
⚠ {{ errorCount }} 个错误需要处理
{{ warningCount }} 个警告
{{ d.code }} {{ d.skillName }} {{ d.message }}
``` - [ ] **Step 3: 添加样式** ```css .diag-banner { padding: 8px 16px; border-radius: 6px; margin-bottom: 16px; cursor: pointer; font-size: 13px; } .diag-error { background: #fef0f0; color: #f56c6c; border: 1px solid #fbc4c4; } .diag-warning { background: #fdf6ec; color: #e6a23c; border: 1px solid #f5dab1; } .diag-list { background: #f5f7fa; border-radius: 6px; padding: 12px; margin-bottom: 16px; max-height: 200px; overflow-y: auto; } .diag-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; } .diag-skill { font-weight: 600; color: #303133; } ``` - [ ] **Step 4: Commit** ```bash git add packages/frontend/src/modules/agent/views/skills.vue git commit -m "feat(skill): add diagnostic banner to skills management page" ``` --- ### Task 4: skill-detail 诊断 Tab **Files:** - Modify: `packages/frontend/src/modules/agent/components/skill-detail.vue` - [ ] **Step 1: 在 el-tabs 中新增诊断 tab** 在 P0 Task 6 已创建的 tab 结构中,添加第三个 tab: ```vue
无诊断信息
{{ d.code }} {{ d.message }}
``` - [ ] **Step 2: 新增诊断数据加载逻辑** ```typescript const skillDiagnostics = ref([]); async function loadSkillDiagnostics() { if (!props.skill?.name) return; try { const res = await service.request({ url: '/admin/netaclaw/skill/diagnostics' }); skillDiagnostics.value = (res || []).filter((d: any) => d.skillName === props.skill.name); } catch { /* ignore */ } } // 在 watch skill 的回调中添加 loadSkillDiagnostics(); ``` - [ ] **Step 3: Commit** ```bash git add packages/frontend/src/modules/agent/components/skill-detail.vue git commit -m "feat(skill): add diagnostics tab to skill-detail drawer" ``` --- ### Task 5: skills.vue 卡片 type 标签 **Files:** - Modify: `packages/frontend/src/modules/agent/views/skills.vue` - [ ] **Step 1: 在 metas API 返回中包含 skillTypeV2** 在 `SkillLoaderService.getSkillMetas()` 中,返回对象添加: ```typescript skillTypeV2: this.skillConfig.classify(fs.name), ``` - [ ] **Step 2: 在 skill 卡片的 tags 区域添加 type 标签** 在 `skill-tags` div 中,现有标签之前添加: ```vue {{ skill.skillTypeV2 || 'prompt' }} ``` - [ ] **Step 3: Commit** ```bash git add packages/backend/src/modules/netaclaw/service/skill_loader.ts \ packages/frontend/src/modules/agent/views/skills.vue git commit -m "feat(skill): show skill classification type tag on cards" ```