Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled
- 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.
291 lines
9.6 KiB
JavaScript
291 lines
9.6 KiB
JavaScript
/**
|
||
* 将 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)
|
||
})
|