/** * 将 aiscri-xiong/resources/skills 和 resources/agents 导入 MongoDB * 用法:node scripts/import-resources.js [--owner ] [--dry-run] [--clear-versions] * * --clear-versions 更新已有 skill/agent 时清空历史 versions,只保留本次导入的一条记录(适合修复超大文档或重置历史) */ require('dotenv').config({ path: require('path').join(__dirname, '../.env') }) const fs = require('fs') const path = require('path') const { MongoClient } = require('mongodb') const MONGO_URL = process.env.MONGO_URL || 'mongodb://localhost:27017' const DB_NAME = process.env.DB_NAME || 'skills_market' const MAX_VERSION_HISTORY = Number.parseInt(process.env.MAX_VERSION_HISTORY || '10', 10) const args = process.argv.slice(2) const ownerIdx = args.indexOf('--owner') const OWNER = ownerIdx !== -1 ? args[ownerIdx + 1] : 'system' const DRY_RUN = args.includes('--dry-run') const CLEAR_VERSIONS = args.includes('--clear-versions') // 路径指向 aiscri-xiong/resources const RESOURCES_DIR = path.join(__dirname, '../../aiscri-xiong/resources') const SKILLS_DIR = path.join(RESOURCES_DIR, 'skills') const AGENTS_DIR = path.join(RESOURCES_DIR, 'agents') // ── 工具函数 ──────────────────────────────────────────────────────────────── function readFilesFromDir(dir) { const result = [] const walk = (curDir, base) => { for (const entry of fs.readdirSync(curDir, { withFileTypes: true })) { const rel = base ? `${base}/${entry.name}` : entry.name const full = path.join(curDir, entry.name) if (entry.isDirectory()) { walk(full, rel) } else { result.push({ path: rel, content: fs.readFileSync(full, 'utf-8') }) } } } walk(dir, '') return result } function extractFrontmatter(content) { const m = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/) if (!m) return {} const obj = {} for (const line of m[1].split('\n')) { const kv = line.match(/^(\w+):\s*(.+)$/) if (kv) obj[kv[1].trim()] = kv[2].trim().replace(/^["']|["']$/g, '') } return obj } // ── 技能导入 ──────────────────────────────────────────────────────────────── async function importSkills(skillsCollection, options) { const { clearVersions } = options if (!fs.existsSync(SKILLS_DIR)) { console.log(`[skills] 目录不存在:${SKILLS_DIR}`) return } const skillDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true }) .filter(e => e.isDirectory()) .map(e => e.name) console.log(`\n[skills] 发现 ${skillDirs.length} 个技能目录`) for (const dirName of skillDirs) { const skillDir = path.join(SKILLS_DIR, dirName) const files = readFilesFromDir(skillDir) if (files.length === 0) { console.log(` [跳过] ${dirName}:无文件`) continue } const skillMd = files.find(f => f.path === 'SKILL.md') const fm = skillMd ? extractFrontmatter(skillMd.content) : {} const name = (fm.name || dirName).toLowerCase().replace(/[^a-z0-9-]/g, '-') const description = fm.description || '' const now = new Date() const existing = await skillsCollection.findOne({ name }) if (DRY_RUN) { const note = existing && clearVersions ? ' [将清空旧 versions]' : '' console.log(` [dry-run] ${existing ? '更新' : '新建'} skill: ${name} (${files.length} 个文件)${note}`) continue } if (existing) { if (clearVersions) { const versionEntry = { version: 1, description: `Imported from resources by ${OWNER} (history cleared)`, file_count: files.length, created_at: now, created_by: OWNER } await skillsCollection.updateOne( { _id: existing._id }, { $set: { files, description: description || existing.description, updated_at: now, updated_by: OWNER, versions: [versionEntry] } } ) console.log(` [更新] skill: ${name} → 已清空历史,仅保留 v1`) } else { const versionEntry = { version: (existing.versions?.length || 0) + 1, description: `Imported from resources by ${OWNER}`, // Keep version snapshots lightweight to avoid MongoDB 16MB document limit. file_count: Array.isArray(existing.files) ? existing.files.length : 0, created_at: now, created_by: OWNER } await skillsCollection.updateOne( { _id: existing._id }, { $set: { files, description: description || existing.description, updated_at: now, updated_by: OWNER }, $push: { versions: { $each: [versionEntry], $slice: -MAX_VERSION_HISTORY } } } ) console.log(` [更新] skill: ${name} → v${versionEntry.version}`) } } else { await skillsCollection.insertOne({ name, description, owner: OWNER, files, downloads: 0, is_public: true, tags: [], versions: [{ version: 1, description: 'Initial import', file_count: files.length, created_at: now, created_by: OWNER }], created_at: now, updated_at: now, created_by: OWNER, updated_by: OWNER }) console.log(` [新建] skill: ${name} (${files.length} 个文件)`) } } } // ── Agent 导入 ──────────────────────────────────────────────────────────── async function importAgents(agentsCollection, options) { const { clearVersions } = options if (!fs.existsSync(AGENTS_DIR)) { console.log(`[agents] 目录不存在:${AGENTS_DIR}`) return } const agentFiles = fs.readdirSync(AGENTS_DIR, { withFileTypes: true }) .filter(e => e.isFile() && e.name.endsWith('.md')) .map(e => e.name) console.log(`\n[agents] 发现 ${agentFiles.length} 个 agent 文件`) for (const fileName of agentFiles) { const content = fs.readFileSync(path.join(AGENTS_DIR, fileName), 'utf-8') const fm = extractFrontmatter(content) const name = (fm.name || path.basename(fileName, '.md')).toLowerCase().replace(/[^a-z0-9-]/g, '-') const description = fm.description || '' const now = new Date() const existing = await agentsCollection.findOne({ name }) if (DRY_RUN) { const note = existing && clearVersions ? ' [将清空旧 versions]' : '' console.log(` [dry-run] ${existing ? '更新' : '新建'} agent: ${name}${note}`) continue } if (existing) { if (clearVersions) { const versionEntry = { version: 1, content_length: content.length, created_at: now, created_by: OWNER, note: 'history cleared on import' } await agentsCollection.updateOne( { _id: existing._id }, { $set: { content, description: description || existing.description, updated_at: now, updated_by: OWNER, versions: [versionEntry] } } ) console.log(` [更新] agent: ${name} → 已清空历史,仅保留 v1`) } else { const versionEntry = { version: (existing.versions?.length || 0) + 1, content_length: typeof existing.content === 'string' ? existing.content.length : 0, created_at: now, created_by: OWNER } await agentsCollection.updateOne( { _id: existing._id }, { $set: { content, description: description || existing.description, updated_at: now, updated_by: OWNER }, $push: { versions: { $each: [versionEntry], $slice: -MAX_VERSION_HISTORY } } } ) console.log(` [更新] agent: ${name} → v${versionEntry.version}`) } } else { await agentsCollection.insertOne({ name, description, owner: OWNER, content, is_public: true, versions: [{ version: 1, content_length: content.length, created_at: now, created_by: OWNER }], created_at: now, updated_at: now, created_by: OWNER, updated_by: OWNER }) console.log(` [新建] agent: ${name}`) } } } // ── 主流程 ──────────────────────────────────────────────────────────────── async function main() { console.log(`MONGO_URL: ${MONGO_URL}`) console.log(`DB: ${DB_NAME}`) console.log(`OWNER: ${OWNER}`) console.log(`DRY_RUN: ${DRY_RUN}`) console.log(`CLEAR_VERSIONS: ${CLEAR_VERSIONS}`) const client = new MongoClient(MONGO_URL) await client.connect() const db = client.db(DB_NAME) const skillsCollection = db.collection('skills') const agentsCollection = db.collection('agents') const importOpts = { clearVersions: CLEAR_VERSIONS } await importSkills(skillsCollection, importOpts) await importAgents(agentsCollection, importOpts) await client.close() console.log('\n✅ 导入完成') } main().catch(err => { console.error('❌ 导入失败:', err) process.exit(1) })