skills-market-server/scripts/import-resources.js
hjjjj abcd0a53d3 feat(server): enhance skill and agent import functionality
- Added support for clearing version history during skill and agent imports with the `--clear-versions` flag.
- Updated import scripts to handle versioning more effectively, ensuring only the latest version is retained when clearing history.
- Introduced new checks for user permissions in various API endpoints to restrict access based on roles.
- Normalized file paths during merges to ensure consistency across different operating systems.
- Updated `.gitignore` to exclude the `scripts/` directory.
2026-03-27 11:08:56 +08:00

291 lines
9.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 将 aiscri-xiong/resources/skills 和 resources/agents 导入 MongoDB
* 用法node scripts/import-resources.js [--owner <name>] [--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)
})