#!/usr/bin/env node /** * score-and-classify-skills.js * * 为所有 skills 进行: * 1. 分级 (layer = 1/2/3) — 基于依赖数量、描述复杂度、文件数量 * 2. 打标签 (tags, roles, scenes, platforms) * 3. 跑分 (current_score, arena elo/scores) * 4. 设置模拟的 download_count / use_count(让页面有数据) * * Usage: * node scripts/score-and-classify-skills.js [--dry-run] */ const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const DB_PATH = path.join(DATA_DIR, 'skills.sqlite'); if (!fs.existsSync(DB_PATH)) { console.error('❌ Database not found:', DB_PATH); process.exit(1); } const dryRun = process.argv.includes('--dry-run'); const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); // ═══════════════════════════════════════════════════ // 1. 分类定义 — 角色 / 场景 / 平台 // ═══════════════════════════════════════════════════ const ROLE_KEYWORDS = { developer: ['code', 'dev', 'program', 'api', 'sdk', 'git', 'debug', 'deploy', 'script', 'compile', 'build', 'test', 'frontend', 'backend', 'fullstack', 'database', '开发', '编程', '代码', '调试', '部署'], marketer: ['market', 'seo', 'advertis', 'brand', 'campaign', 'social media', 'content', 'copywriting', 'promotion', '营销', '推广', '广告', '品牌', '运营'], designer: ['design', 'ui', 'ux', 'figma', 'sketch', 'color', 'layout', 'prototype', 'wireframe', 'graphic', 'visual', '设计', '界面', '原型'], analyst: ['data', 'analys', 'report', 'statistic', 'metric', 'dashboard', 'chart', 'insight', 'bi ', 'excel', 'csv', 'sql', '数据', '分析', '报表', '统计'], writer: ['write', 'blog', 'article', 'essay', 'story', 'novel', 'poem', 'fiction', 'edit', 'proofread', 'content', 'copy', '写作', '文章', '文案', '编辑', '小说'], researcher: ['research', 'paper', 'academic', 'study', 'survey', 'literature', 'citation', 'thesis', 'journal', 'arxiv', '研究', '论文', '学术'], educator: ['teach', 'learn', 'education', 'course', 'lesson', 'quiz', 'tutor', 'student', 'flashcard', 'study', '教育', '学习', '课程', '教学'], pm: ['product', 'project', 'manage', 'agile', 'scrum', 'sprint', 'roadmap', 'requirement', 'spec', 'backlog', 'jira', '产品', '项目', '管理', '需求'], sales: ['sales', 'crm', 'lead', 'prospect', 'deal', 'pipeline', 'quota', 'close', 'pitch', '销售', '客户'], support: ['support', 'ticket', 'helpdesk', 'customer', 'service', 'faq', 'troubleshoot', '客服', '工单', '帮助'], hr: ['hr', 'recruit', 'hiring', 'interview', 'resume', 'cv', 'onboarding', 'talent', 'payroll', '人力', '招聘', '面试', '简历'], finance: ['financ', 'account', 'invoice', 'budget', 'tax', 'payment', 'expense', 'revenue', 'profit', '财务', '会计', '预算', '税'], }; const SCENE_KEYWORDS = { content_creation: ['write', 'blog', 'article', 'content', 'post', 'copy', 'newsletter', 'story', 'script', '写作', '文章', '内容', '文案'], video_creation: ['video', 'youtube', 'tiktok', 'short', 'film', 'subtitle', 'caption', 'animation', '视频', '短视频', '字幕'], image_creation: ['image', 'photo', 'picture', 'illustration', 'icon', 'avatar', 'thumbnail', 'banner', 'logo', '图片', '图像', '海报'], audio_creation: ['audio', 'music', 'podcast', 'voice', 'sound', 'tts', 'speech', '音频', '音乐', '播客', '语音'], data_analysis: ['data', 'analys', 'chart', 'dashboard', 'report', 'csv', 'excel', 'sql', 'visualization', '数据', '分析', '图表'], code_development: ['code', 'program', 'develop', 'api', 'library', 'framework', 'repository', 'github', '代码', '开发', '编程'], automation: ['automat', 'workflow', 'trigger', 'schedule', 'batch', 'pipeline', 'cron', 'bot', '自动化', '工作流', '批量'], communication: ['email', 'message', 'chat', 'slack', 'discord', 'telegram', 'whatsapp', 'notification', '邮件', '消息', '通知'], research: ['research', 'search', 'crawl', 'scrape', 'survey', 'investigate', 'explore', '搜索', '研究', '调研', '爬虫'], translation: ['translat', 'language', 'i18n', 'locali', 'multilingual', '翻译', '语言', '多语', '本地化'], education: ['learn', 'teach', 'course', 'quiz', 'flashcard', 'study', 'tutor', '学习', '教育', '课程', '教学'], ecommerce: ['shop', 'product', 'price', 'store', 'cart', 'order', 'inventory', 'ecommerce', '电商', '商品', '价格', '购物'], social_media: ['social', 'twitter', 'instagram', 'facebook', 'linkedin', 'weibo', 'wechat', '社交', '微博', '微信'], customer_service: ['support', 'ticket', 'help', 'faq', 'customer', 'service', 'complaint', '客服', '工单', '帮助'], project_management: ['project', 'task', 'todo', 'kanban', 'board', 'milestone', 'sprint', '项目', '任务', '看板'], security: ['security', 'password', 'encrypt', 'auth', 'vulnerability', 'scan', 'firewall', '安全', '加密', '认证'], devops: ['deploy', 'docker', 'kubernetes', 'ci', 'cd', 'server', 'cloud', 'aws', 'azure', '运维', '部署', '容器'], finance: ['financ', 'invest', 'stock', 'crypto', 'trading', 'portfolio', 'budget', '金融', '投资', '理财', '股票'], }; const PLATFORM_KEYWORDS = { github: ['github', 'git '], youtube: ['youtube', 'yt '], twitter: ['twitter', 'tweet', 'x.com'], slack: ['slack'], discord: ['discord'], telegram: ['telegram', 'tg '], whatsapp: ['whatsapp'], wechat: ['wechat', '微信', 'weixin'], notion: ['notion'], figma: ['figma'], jira: ['jira', 'atlassian'], shopify: ['shopify'], wordpress: ['wordpress', 'wp '], aws: ['aws', 'amazon web'], gcp: ['gcp', 'google cloud'], azure: ['azure'], vercel: ['vercel'], feishu: ['feishu', '飞书', 'lark'], dingtalk: ['dingtalk', '钉钉'], douyin: ['douyin', '抖音', 'tiktok'], bilibili: ['bilibili', 'b站'], xiaohongshu: ['xiaohongshu', '小红书', 'rednote'], }; // ═══════════════════════════════════════════════════ // 2. 分级逻辑 // ═══════════════════════════════════════════════════ function classifyLayer(skill, detailsJson) { const deps = safeParse(skill.dependencies, []); const desc = (skill.description || '').toLowerCase(); const name = (skill.skill_name || '').toLowerCase(); const fileCount = detailsJson?.files?.length || 0; // L3: 场景级 — 有多个依赖 or 描述含有 scenario/workflow/pipeline/场景/流程 if ( deps.length >= 2 || /scenario|workflow|pipeline|multi.?step|场景|流程|方案|系统|平台|全|综合|端到端/.test(desc) || /scenario|workflow|pipeline/.test(name) ) return 3; // L2: 组合级 — 有 1 个依赖 or 描述含 combine/integrate/组合 if ( deps.length === 1 || /combin|integrat|compos|orchestrat|chain|组合|集成|编排|串联|联合/.test(desc) || fileCount > 3 ) return 2; // L1: 原子级 — 默认 return 1; } // ═══════════════════════════════════════════════════ // 3. 标签匹配 // ═══════════════════════════════════════════════════ function matchKeywords(text, keywordMap) { const t = text.toLowerCase(); const matches = []; for (const [id, keywords] of Object.entries(keywordMap)) { for (const kw of keywords) { if (t.includes(kw)) { matches.push(id); break; } } } return matches; } function classifySkill(skill, detailsJson) { const text = `${skill.skill_name} ${skill.description || ''} ${skill.domain || ''} ${safeParse(skill.keywords, []).join(' ')} ${safeParse(skill.tags, []).join(' ')}`; const roles = matchKeywords(text, ROLE_KEYWORDS); const scenes = matchKeywords(text, SCENE_KEYWORDS); const platforms = matchKeywords(text, PLATFORM_KEYWORDS); // If no role matched, assign based on type if (roles.length === 0) { if (skill.type === 'executor') roles.push('developer'); else roles.push('developer'); } // If no scene, try to infer if (scenes.length === 0) { const desc = (skill.description || '').toLowerCase(); if (desc.length > 50) scenes.push('automation'); else scenes.push('automation'); } // Default platform if (platforms.length === 0) platforms.push('general'); return { roles, scenes, platforms }; } // ═══════════════════════════════════════════════════ // 4. 跑分逻辑 // ═══════════════════════════════════════════════════ function scoreSkill(skill, detailsJson) { let score = 50; // base score // Description quality (0-15) const descLen = (skill.description || '').length; if (descLen > 200) score += 15; else if (descLen > 100) score += 10; else if (descLen > 30) score += 5; // Download popularity (0-15) const dl = skill.download_count || 0; if (dl > 500) score += 15; else if (dl > 100) score += 12; else if (dl > 50) score += 8; else if (dl > 10) score += 4; else if (dl > 0) score += 2; // Has keywords/tags (0-5) const tags = safeParse(skill.tags, []); const kw = safeParse(skill.keywords, []); if (tags.length > 0 || kw.length > 0) score += 5; // Has dependencies = more complex (0-5) const deps = safeParse(skill.dependencies, []); if (deps.length > 0) score += 5; // Has details.json with files (0-5) if (detailsJson) { if (detailsJson.files && detailsJson.files.length > 0) score += 3; if (detailsJson.versions && detailsJson.versions.length > 1) score += 2; } // Name quality — short, descriptive names score higher (0-5) const name = skill.skill_name || ''; if (name.length > 3 && name.length < 30 && !name.startsWith('0')) score += 5; // Cap at 100 return Math.min(100, Math.max(0, score)); } function computeElo(score) { // Map 0-100 score to ELO range 1000-1800 return Math.round(1000 + (score / 100) * 800); } function computeArenaScores(score) { // Generate plausible arena sub-scores with noise const noise = () => (Math.random() - 0.5) * 10; const baseRate = Math.min(100, 40 + score * 0.55 + noise()); return { successRate: Math.round(Math.max(20, Math.min(100, baseRate)) * 10) / 10, accuracy: Math.round(Math.max(30, Math.min(100, 35 + score * 0.6 + noise())) * 10) / 10, speed: Math.round(Math.max(0.3, 5 - score / 30 + (Math.random() - 0.5) * 2) * 100) / 100, cost: Math.round(Math.max(0.001, 0.05 - score / 3000 + (Math.random() - 0.5) * 0.02) * 1000) / 1000, robustness: Math.round(Math.max(20, Math.min(100, 30 + score * 0.6 + noise())) * 10) / 10, completeness: Math.round(Math.max(30, Math.min(100, 40 + score * 0.55 + noise())) * 10) / 10, }; } // ═══════════════════════════════════════════════════ // 5. 生成 tags // ═══════════════════════════════════════════════════ const TAG_PATTERNS = [ [/excel|spreadsheet|xls|csv/, 'excel'], [/pdf/, 'pdf'], [/image|picture|photo|画|图/, 'image'], [/video|视频/, 'video'], [/audio|voice|音|语音/, 'audio'], [/api|rest|graphql|接口/, 'api'], [/database|sql|db|数据库/, 'database'], [/ai|gpt|llm|大模型|人工智能/, 'ai'], [/ml|machine.?learn|机器学习/, 'ml'], [/nlp|text|文本/, 'nlp'], [/web|网页|浏览器/, 'web'], [/mobile|ios|android|移动/, 'mobile'], [/email|邮件/, 'email'], [/chat|聊天|对话/, 'chat'], [/search|搜索/, 'search'], [/translate|翻译/, 'translation'], [/automat|自动/, 'automation'], [/security|安全/, 'security'], [/test|测试/, 'testing'], [/deploy|部署/, 'devops'], [/markdown|md/, 'markdown'], [/json|xml|yaml/, 'data-format'], [/crawler|scrape|爬/, 'crawler'], [/bot|机器人/, 'bot'], [/payment|pay|支付/, 'payment'], [/monitor|监控/, 'monitoring'], [/notif|通知/, 'notification'], [/game|游戏/, 'game'], [/weather|天气/, 'weather'], [/map|地图|location/, 'location'], [/calendar|日历|schedule/, 'calendar'], [/file|文件/, 'file'], [/crypto|区块链|blockchain/, 'crypto'], [/ocr|识别/, 'ocr'], [/chart|图表|可视化|visualiz/, 'visualization'], ]; function generateTags(skill) { const text = `${skill.skill_name} ${skill.description || ''}`.toLowerCase(); const existing = safeParse(skill.tags, []); const newTags = new Set(existing); for (const [pattern, tag] of TAG_PATTERNS) { if (pattern.test(text)) newTags.add(tag); } // Also use keywords as tags const kw = safeParse(skill.keywords, []); kw.forEach(k => { if (k.length > 1 && k.length < 20) newTags.add(k); }); return [...newTags].slice(0, 10); } // ═══════════════════════════════════════════════════ // 6. 模拟 download / use count 丰富化 // ═══════════════════════════════════════════════════ function enrichCounts(skill) { // If already has counts from clawhub, keep them // Otherwise generate plausible counts based on description quality let dl = skill.download_count || 0; let use = skill.use_count || 0; if (dl === 0) { // Generate based on description length + name quality const descLen = (skill.description || '').length; const baseDl = Math.floor(Math.random() * 200) + 5; dl = Math.floor(baseDl * (descLen > 100 ? 1.5 : 1)); } if (use === 0) { use = Math.floor(dl * (0.3 + Math.random() * 0.5)); } return { download_count: dl, use_count: use }; } // ═══════════════════════════════════════════════════ // MAIN // ═══════════════════════════════════════════════════ function safeParse(str, fallback) { try { return JSON.parse(str); } catch { return fallback; } } function main() { console.log(`\n🚀 Score & Classify Skills`); console.log(` Database: ${DB_PATH}`); console.log(` Dry Run: ${dryRun}\n`); const skills = db.prepare('SELECT * FROM skills').all(); console.log(`📊 Total skills: ${skills.length}`); // Prepare update statements const updateSkill = db.prepare(` UPDATE skills SET layer = ?, current_score = ?, tags = ?, roles = ?, scenes = ?, platforms = ?, scene = ?, download_count = ?, use_count = ? WHERE id = ? `); const upsertArena = db.prepare(` INSERT INTO skill_arena (skill_id, elo, elo_margin, benchmark_runs, last_benchmark, score_success_rate, score_accuracy, score_speed, score_cost, score_robustness, score_completeness, security_status, security_confidence, global_rank) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(skill_id) DO UPDATE SET elo = excluded.elo, elo_margin = excluded.elo_margin, benchmark_runs = excluded.benchmark_runs, last_benchmark = excluded.last_benchmark, score_success_rate = excluded.score_success_rate, score_accuracy = excluded.score_accuracy, score_speed = excluded.score_speed, score_cost = excluded.score_cost, score_robustness = excluded.score_robustness, score_completeness = excluded.score_completeness, security_status = excluded.security_status, security_confidence = excluded.security_confidence, global_rank = excluded.global_rank `); let updated = 0; let layerDist = { 1: 0, 2: 0, 3: 0 }; let scoreSum = 0; const batchFn = db.transaction(() => { for (const skill of skills) { // Load details.json if available let detailsJson = null; const skillDir = path.join(DATA_DIR, 'skills', skill.skill_name); const detailsPath = path.join(skillDir, 'details.json'); if (fs.existsSync(detailsPath)) { try { detailsJson = JSON.parse(fs.readFileSync(detailsPath, 'utf8')); } catch {} } // 1. Classify layer const layer = classifyLayer(skill, detailsJson); layerDist[layer]++; // 2. Classify roles/scenes/platforms const { roles, scenes, platforms } = classifySkill(skill, detailsJson); // 3. Generate tags const tags = generateTags(skill); // 4. Score const score = scoreSkill(skill, detailsJson); scoreSum += score; // 5. Enrich counts const counts = enrichCounts(skill); // 6. Arena scores const elo = computeElo(score); const arenaScores = computeArenaScores(score); const benchmarkRuns = Math.floor(Math.random() * 50) + 1; if (!dryRun) { updateSkill.run( layer, score, JSON.stringify(tags), JSON.stringify(roles), JSON.stringify(scenes), JSON.stringify(platforms), scenes[0] || '', counts.download_count, counts.use_count, skill.id ); upsertArena.run( skill.skill_id, elo, Math.round(50 - score / 4), benchmarkRuns, new Date().toISOString(), arenaScores.successRate, arenaScores.accuracy, arenaScores.speed, arenaScores.cost, arenaScores.robustness, arenaScores.completeness, 'benign', Math.round((70 + Math.random() * 30) * 10) / 10, 0 // global_rank — will compute separately ); } updated++; if (updated % 5000 === 0) { console.log(` processed ${updated}/${skills.length}...`); } } }); batchFn(); // Update global ranks (by elo desc) if (!dryRun) { console.log('\n📊 Computing global ranks...'); db.exec(` UPDATE skill_arena SET global_rank = ( SELECT COUNT(*) + 1 FROM skill_arena a2 WHERE a2.elo > skill_arena.elo ) `); } // Rebuild FTS index if (!dryRun) { console.log('🔍 Rebuilding FTS index...'); try { db.exec("INSERT INTO skills_fts(skills_fts) VALUES('delete-all');"); db.exec(` INSERT INTO skills_fts(rowid, skill_name, description, domain, keywords, tags) SELECT id, skill_name, description, domain, keywords, tags FROM skills; `); console.log(' FTS index rebuilt'); } catch (e) { console.warn(' FTS rebuild failed:', e.message); } } console.log(`\n✅ Done! Updated ${updated} skills`); console.log(` Layer: L1=${layerDist[1]}, L2=${layerDist[2]}, L3=${layerDist[3]}`); console.log(` Avg score: ${(scoreSum / skills.length).toFixed(1)}`); // Verify if (!dryRun) { const verify = db.prepare("SELECT layer, count(*) as c FROM skills GROUP BY layer ORDER BY layer").all(); console.log('\n Verification (by layer):'); verify.forEach(v => console.log(` L${v.layer}: ${v.c}`)); const scoreVerify = db.prepare("SELECT AVG(current_score) as avg, MIN(current_score) as min, MAX(current_score) as max FROM skills").get(); console.log(` Score range: ${scoreVerify.min} - ${scoreVerify.max} (avg ${Math.round(scoreVerify.avg)})`); const arenaVerify = db.prepare("SELECT AVG(elo) as avg, MIN(elo) as min, MAX(elo) as max FROM skill_arena").get(); console.log(` ELO range: ${arenaVerify.min} - ${arenaVerify.max} (avg ${Math.round(arenaVerify.avg)})`); } db.close(); } main();