#!/usr/bin/env node /** * tag-skills.js — 批量为 Skill 打三维标签 (Role/Scene/Platform) * * 用法: * node scripts/tag-skills.js # 只对无标签的 Skill 打标 * node scripts/tag-skills.js --force # 强制重新打标(覆盖已有) * node scripts/tag-skills.js --dry-run # 预览模式,不写入 DB * node scripts/tag-skills.js --skill # 只对指定 Skill 打标 */ const { execFile } = require('child_process'); const fs = require('fs'); const path = require('path'); // ─── 配置 ─── const ROOT = path.resolve(__dirname, '..'); const SKILL_TAGGER_PATH = path.join(ROOT, 'skills/skill_tagger/SKILL.md'); const SKILLS_DIR = path.join(ROOT, 'skills'); // 加载项目模块 const skillDb = require(path.join(ROOT, 'server/services/skill-db')); const taxonomy = require(path.join(ROOT, 'server/config/taxonomy')); // ─── 参数解析 ─── const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const force = args.includes('--force'); const skillIdx = args.indexOf('--skill'); const targetSkill = skillIdx !== -1 ? args[skillIdx + 1] : null; async function main() { console.log('[tagger] 🏷️ Skill Tagger 启动'); console.log(`[tagger] Dry run: ${dryRun}`); console.log(`[tagger] Force: ${force}`); if (targetSkill) console.log(`[tagger] Target: ${targetSkill}`); // 获取所有 Skill let skills = skillDb.listSkills(); console.log(`[tagger] 总计 ${skills.length} 个 Skills`); if (targetSkill) { skills = skills.filter(s => s.skillName === targetSkill); if (skills.length === 0) { console.error(`[tagger] ❌ Skill "${targetSkill}" not found`); process.exit(1); } } // 过滤需要打标的 Skill if (!force) { skills = skills.filter(s => { const hasRoles = s.roles && s.roles.length > 0; const hasScenes = s.scenes && s.scenes.length > 0; const hasPlatforms = s.platforms && s.platforms.length > 0; return !(hasRoles && hasScenes && hasPlatforms); }); } console.log(`[tagger] 待打标: ${skills.length} 个`); if (skills.length === 0) { console.log('[tagger] ✅ 所有 Skill 已有标签,无需操作。'); return; } const skillTaggerContent = fs.readFileSync(SKILL_TAGGER_PATH, 'utf-8'); let successCount = 0; let failCount = 0; let fallbackCount = 0; for (const skill of skills) { console.log(`\n[tagger] ── ${skill.skillName} ──`); // 尝试读取 SKILL.md let skillMd = ''; const skillMdPath = path.join(SKILLS_DIR, skill.skillName, 'SKILL.md'); if (fs.existsSync(skillMdPath)) { skillMd = fs.readFileSync(skillMdPath, 'utf-8').substring(0, 2000); } // 构建输入 const input = [ `## Skill Name`, skill.skillName, '', `## Description`, skill.description || '(无)', '', `## Domain`, skill.domain || '(无)', '', `## Keywords`, (skill.keywords || []).join(', ') || '(无)', '', skillMd ? `## SKILL.md Content\n${skillMd}` : '', ].join('\n'); // 尝试调用 Claude CLI let tags = null; try { tags = await callClaudeForTags(skillTaggerContent, input); console.log(`[tagger] Claude 推荐: roles=${JSON.stringify(tags.roles)}, scenes=${JSON.stringify(tags.scenes)}, platforms=${JSON.stringify(tags.platforms)} (confidence: ${tags.confidence})`); successCount++; } catch (e) { console.log(`[tagger] ⚠️ Claude 调用失败 (${e.message}),使用关键词回退`); tags = taxonomy.autoTag(skill); tags.confidence = 0.3; tags.reasoning = 'keyword-based fallback'; console.log(`[tagger] 回退标签: roles=${JSON.stringify(tags.roles)}, scenes=${JSON.stringify(tags.scenes)}, platforms=${JSON.stringify(tags.platforms)}`); fallbackCount++; } // 写入 DB if (!dryRun) { try { skillDb.updateSkill(skill.skillName, { roles: tags.roles, scenes: tags.scenes, platforms: tags.platforms, }); console.log(`[tagger] ✅ 已写入`); } catch (e) { console.error(`[tagger] ❌ 写入失败: ${e.message}`); failCount++; } } else { console.log(`[tagger] [dry-run] 跳过写入`); } } console.log(`\n[tagger] 🏁 完成`); console.log(`[tagger] 成功 (Claude): ${successCount}`); console.log(`[tagger] 回退 (keyword): ${fallbackCount}`); console.log(`[tagger] 失败: ${failCount}`); } /** * 调用 Claude CLI 进行打标 */ function callClaudeForTags(skillContent, input) { return new Promise((resolve, reject) => { const prompt = [ '请按照以下 Skill 规范为目标 Skill 打标签:', '', '--- Skill 规范 ---', skillContent, '', '--- 目标 Skill ---', input, '', '请直接输出 JSON,不需要其他说明文字。', ].join('\n'); execFile('claude', ['--print', '-p', prompt], { timeout: 30000, maxBuffer: 512 * 1024 }, (err, stdout) => { if (err) return reject(err); const text = (stdout || '').trim(); // 提取 JSON const jsonMatch = text.match(/\{[\s\S]*\}/); if (!jsonMatch) { return reject(new Error('No JSON found in response')); } try { const result = JSON.parse(jsonMatch[0]); // 验证字段 if (!Array.isArray(result.roles) || !Array.isArray(result.scenes) || !Array.isArray(result.platforms)) { return reject(new Error('Invalid response structure')); } // 验证标签在预定义列表内 const validRoles = taxonomy.ROLES.map(r => r.id); const validScenes = taxonomy.SCENES.map(s => s.id); const validPlatforms = taxonomy.PLATFORMS.map(p => p.id); result.roles = result.roles.filter(r => validRoles.includes(r)); result.scenes = result.scenes.filter(s => validScenes.includes(s)); result.platforms = result.platforms.filter(p => validPlatforms.includes(p)); // 确保至少有一个标签 if (result.roles.length === 0) result.roles = ['developer']; if (result.scenes.length === 0) result.scenes = ['code_develop']; if (result.platforms.length === 0) result.platforms = ['general']; resolve(result); } catch (e) { reject(new Error(`JSON parse error: ${e.message}`)); } }); }); } main().catch(e => { console.error('[tagger] 💥 未预期错误:', e); process.exit(1); });