fix-missing-skills.js
8.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/env node
/**
* fix-missing-skills.js — 补导 data/skills/ 中存在但 SQLite 中缺失的 skill
*
* 逻辑:
* 1. 从 SQLite 获取所有已有 skill_name 集合
* 2. 扫描 data/skills/ 目录
* 3. 找出差集(目录中有,但 SQLite 中没有的)
* 4. 只导入这些缺失的 skill(从 SKILL.md/details.json 提取信息)
*
* 用法:
* node scripts/fix-missing-skills.js # 补导缺失
* node scripts/fix-missing-skills.js --dry-run # 预览,不写入
*/
const fs = require('fs');
const path = require('path');
// 复用 skill-db 的 schema 和数据库连接
const skillDb = require('../server/services/skill-db');
const args = process.argv.slice(2);
const DRY_RUN = args.includes('--dry-run');
const SKILLS_DIR = path.resolve(__dirname, '../../data/skills');
function extractDescription(dirPath) {
// 优先从 SKILL.md 提取
const skillMd = path.join(dirPath, 'SKILL.md');
if (fs.existsSync(skillMd)) {
const content = fs.readFileSync(skillMd, 'utf8');
// 提取 YAML frontmatter 中的 description
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (fmMatch) {
const descMatch = fmMatch[1].match(/description:\s*(.+)/);
if (descMatch) return descMatch[1].trim().slice(0, 500);
}
// fallback: 取第一行非标题非空文本
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
if (lines.length > 0) return lines[0].trim().slice(0, 500);
}
// 从 details.json 提取
const detailsJson = path.join(dirPath, 'details.json');
if (fs.existsSync(detailsJson)) {
try {
const details = JSON.parse(fs.readFileSync(detailsJson, 'utf8'));
return (details.skill?.summary || '').slice(0, 500);
} catch {}
}
return '';
}
function main() {
console.log('╔═══════════════════════════════════════════════╗');
console.log('║ 补导缺失 Skills → SQLite ║');
console.log('╚═══════════════════════════════════════════════╝');
console.log();
if (DRY_RUN) console.log('⚠️ DRY RUN 模式 — 不会实际写入数据库\n');
// 1. 检查目录
if (!fs.existsSync(SKILLS_DIR)) {
console.error(`❌ Skills 目录不存在: ${SKILLS_DIR}`);
process.exit(1);
}
// 2. 获取 DB 中已有的 skill_name 集合
const db = skillDb.getDb();
const existingNames = new Set(
db.prepare('SELECT skill_name FROM skills').all().map(r => r.skill_name)
);
console.log(`📊 数据库中已有: ${existingNames.size} 个 skill`);
// 3. 扫描目录
const allDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory() && !d.name.startsWith('.') && d.name !== '_meta')
.map(d => d.name);
console.log(`📁 目录中共有: ${allDirs.length} 个子目录`);
// 4. 找出缺失的
const missing = allDirs.filter(name => !existingNames.has(name));
console.log(`🔍 缺失数量: ${missing.length}`);
if (missing.length === 0) {
console.log('\n✅ 没有缺失的 skill,无需补导');
return;
}
// 5. 准备 SQL
const skillCols = [
'skill_id', 'skill_name', 'directory', 'description', 'domain', 'scope',
'status', 'version', 'benchmark_version', 'industry_avg_score', 'elite_avg_score',
'current_score', 'use_count', 'download_count', 'update_count', 'last_used',
'last_updated', 'created_at', 'keywords', 'type', 'agent_name', 'layer',
'dependencies', 'scene', 'author', 'license', 'source', 'tags',
'roles', 'scenes', 'platforms',
];
const placeholders = skillCols.map(() => '?').join(', ');
const insertSql = `INSERT OR IGNORE INTO skills (${skillCols.join(', ')}) VALUES (${placeholders})`;
const stmtInsert = db.prepare(insertSql);
const stmtArena = db.prepare(
`INSERT OR IGNORE INTO skill_arena (skill_id, elo, elo_margin, security_status) VALUES (?, 1200, 50, 'pending')`
);
// 6. 批量导入(事务)
let added = 0;
let errors = 0;
const startTime = Date.now();
console.log('\n--- 开始逐个导入 ---');
const batchSize = 500;
for (let i = 0; i < missing.length; i += batchSize) {
const batch = missing.slice(i, i + batchSize);
if (!DRY_RUN) {
const tx = db.transaction(() => {
for (const name of batch) {
try {
const dir = path.join(SKILLS_DIR, name);
const desc = extractDescription(dir);
// 检查是否有 details.json 来判断 source
const hasDetails = fs.existsSync(path.join(dir, 'details.json'));
const source = hasDetails ? 'clawhub' : 'local';
const skillId = hasDetails ? `crawled-${name}` : `skill-local-${name}`;
const now = new Date().toISOString();
const row = {
skill_id: skillId,
skill_name: name,
directory: dir,
description: desc,
domain: '',
scope: '',
status: 'enable',
version: '1.0.0',
benchmark_version: '1.0.0',
industry_avg_score: null,
elite_avg_score: null,
current_score: null,
use_count: 0,
download_count: 0,
update_count: 0,
last_used: null,
last_updated: now,
created_at: now,
keywords: '[]',
type: 'executor',
agent_name: null,
layer: null,
dependencies: '[]',
scene: '',
author: source === 'clawhub' ? 'clawhub' : 'local',
license: 'MIT',
source: source,
tags: '[]',
roles: '[]',
scenes: '[]',
platforms: '[]',
};
stmtInsert.run(...skillCols.map(c => row[c]));
stmtArena.run(skillId);
added++;
} catch (e) {
errors++;
if (errors <= 10) {
console.error(` ❌ ${name}: ${e.message}`);
}
}
}
});
tx();
} else {
added += batch.length;
}
const progress = Math.min(i + batchSize, missing.length);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
process.stdout.write(`\r 📦 已处理 ${progress}/${missing.length} | 用时 ${elapsed}s`);
}
console.log('\n');
// 7. 结果汇总
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
const totalInDb = DRY_RUN ? '(dry-run)' : db.prepare('SELECT COUNT(*) as c FROM skills').get().c;
console.log('┌──────────────────────────────────────┐');
console.log('│ 补导结果 │');
console.log('├──────────────────────────────────────┤');
console.log(`│ ✅ 新增导入: ${String(added).padStart(8)} │`);
console.log(`│ ❌ 错误: ${String(errors).padStart(8)} │`);
console.log(`│ ⏱️ 总耗时: ${String(totalTime + 's').padStart(8)} │`);
console.log(`│ 📊 数据库总数: ${String(totalInDb).padStart(7)} │`);
console.log('└──────────────────────────────────────┘');
// 8. 验证 tomato
if (!DRY_RUN) {
const t = db.prepare("SELECT skill_name, status, source FROM skills WHERE skill_name = 'tomato_short_story'").get();
console.log(`\n tomato_short_story: ${t ? `✅ 已导入 (source=${t.source}, status=${t.status})` : '❌ 未找到'}`);
// 重建 FTS5 索引
console.log('\n🔍 重建 FTS5 全文搜索索引...');
try {
db.exec("INSERT INTO skills_fts(skills_fts) VALUES('rebuild');");
const ftsCount = db.prepare('SELECT COUNT(*) as c FROM skills_fts').get().c;
console.log(` FTS5 索引: ${ftsCount} 条`);
} catch (e) {
console.warn(` FTS5 重建失败: ${e.message}`);
}
}
console.log('\n✨ 完成!');
}
main();