#!/usr/bin/env node /** * OpenClaw Skills + Workshop CDN 上传脚本 v4.1 * * 功能: * 1. 将所有 skills 上传到七牛云 CDN,生成下载 URL * 2. 将每个 skill 的元数据写入 Parse 数据库 Skill 表 * 3. 将 workshop 文件(playbook + session + memory templates)上传到 CDN * 4. 生成 README.md(包含所有 skill 简介 + workshop 文件 + 下载链接)并上传到 CDN * * 使用方法: * node scripts/deploy/upload-skills.js # 上传全部(skills + workshop) * node scripts/deploy/upload-skills.js --skills-only # 只上传 skills * node scripts/deploy/upload-skills.js --workshop-only # 只上传 workshop * * 前置条件: * 1. npm install qiniu * 2. 配置下方 QINIU_ACCESS_KEY 和 QINIU_SECRET_KEY * 3. 先运行 deploy-to-openclaw.ps1 确保 ~/.openclaw 是最新的 * * 参考:hongben-server/modules/fmode-tikhub-server/upload.sh */ const qiniu = require('qiniu'); const fs = require('fs'); const path = require('path'); // ============================================ // 七牛云配置 // ============================================ const QINIU_ACCESS_KEY = process.env.QINIU_AK || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE'; const QINIU_SECRET_KEY = process.env.QINIU_SK || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX'; const BUCKET = 'nova-repos'; const CDN_DOMAIN = 'https://repos.fmode.cn'; const CDN_PREFIX = 'x/openclaw-skills'; // ============================================ // Parse 数据库配置 // ============================================ const PARSE_BASE = 'https://server.fmode.cn/parse'; const APP_ID = 'ncloudmaster'; const MASTER_KEY = process.env.PARSE_MASTER_KEY || 'SnkK12*&sunq2#@20!'; // 源目录(部署目录) const OPENCLAW_DIR = path.join(process.env.USERPROFILE || process.env.HOME, '.openclaw'); const DEPLOYED_SKILLS_DIR = path.join(OPENCLAW_DIR, 'skills'); const WORKSHOP_DIR = path.join(OPENCLAW_DIR, 'workspace'); const MEMORY_TEMPLATES_DIR = path.join(WORKSHOP_DIR, 'memory'); // CLI 参数 const args = process.argv.slice(2); const SKILLS_ONLY = args.includes('--skills-only'); const WORKSHOP_ONLY = args.includes('--workshop-only'); // 要上传的技能目录列表(如果为空,则上传所有 skills) const SKILL_FILTER = []; // 例如 ['tiktok-video-search', 'jimeng-img-v4'] 指定只上传某些 // ============================================ // 技能包分组定义 // ============================================ const SKILL_PACKAGES = { 'voc-core': { label: 'VOC 核心套件', description: 'Amazon 品类洞察 + 评论分析 + 竞品分析 + 数据合成 + 社媒VOC + 工作坊', categories: ['voc', 'review-analysis', 'competitor-analysis', 'synthesis', 'social-voc', 'workshop'], status: 'stable', // stable | testing | hidden emoji: '🔍' }, 'voc-social-data': { label: 'VOC 社媒数据套件', description: '小红书 / 抖音 / TikTok / Instagram 数据采集', categories: ['social-media', 'douyin', 'xiaohongshu'], status: 'stable', emoji: '📱', aliases: ['social-media'] }, 'ai-creation': { label: 'AI 创作套件', description: '即梦AI图片视频生成 + 一键成片(测试中)', categories: ['jimeng', 'video-creation'], status: 'testing', emoji: '🎨' }, '_other': { label: '其他/测试', description: '测试技能,不对外提供', categories: ['other'], status: 'hidden', emoji: '🧪' } }; const PACKAGE_ALIAS_TO_CANONICAL = Object.fromEntries( Object.entries(SKILL_PACKAGES).flatMap(([pkgId, pkg]) => (pkg.aliases || []).map(alias => [alias, pkgId]) ) ); function getPackageDefinition(pkgId) { return SKILL_PACKAGES[pkgId] || SKILL_PACKAGES[PACKAGE_ALIAS_TO_CANONICAL[pkgId]]; } function isLegacyPackageId(pkgId) { return Boolean(PACKAGE_ALIAS_TO_CANONICAL[pkgId]); } function getPackageLogLabel(pkgId) { const pkg = getPackageDefinition(pkgId); if (!pkg) return pkgId; const suffix = isLegacyPackageId(pkgId) ? `(兼容旧名 ${pkgId})` : ''; return `${pkg.emoji} ${pkg.label}${suffix}`; } // ============================================ // Workshop 文件定义 (v4.1) // ============================================ const WORKSHOP_FILES = { playbooks: [ 'workshop-voc-playbook.md', 'workshop-global-rules.md', 'workshop-session-0-intake.md', 'workshop-session-1-category.md', 'workshop-session-2-brand.md', 'workshop-session-3-voc.md', 'workshop-session-4-deep.md', 'workshop-session-5-report.md' ], supporting: [ 'product-analysis-playbook.md', 'synthesis-prompts.md', 'voc-report-schema-spec.md' ], memoryTemplates: [ 'workshop-progress.json', 'brand-context.json', 'analysis-request.json', 'calibration-notes.json', 'stage-1-output.json', 'stage-2-output.json', 'stage-3-output.json', 'stage-4-output.json', 'stage-5-output.json' ] }; function getPackageForCategory(category) { for (const [pkgId, pkg] of Object.entries(SKILL_PACKAGES)) { if (pkg.categories.includes(category || 'other')) return pkgId; } return '_other'; } // ============================================ // 七牛上传 // ============================================ function getUploadToken(bucket, key) { const mac = new qiniu.auth.digest.Mac(QINIU_ACCESS_KEY, QINIU_SECRET_KEY); const putPolicy = new qiniu.rs.PutPolicy({ scope: `${bucket}:${key}`, expires: 3600 }); return putPolicy.uploadToken(mac); } function uploadFile(localFile, cdnKey) { return new Promise((resolve, reject) => { const token = getUploadToken(BUCKET, cdnKey); const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 })); const putExtra = new qiniu.form_up.PutExtra(); formUploader.putFile(token, cdnKey, localFile, putExtra, (err, body, info) => { if (err) { reject(err); return; } if (info.statusCode === 200) { resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` }); } else { reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`)); } }); }); } function uploadBuffer(buffer, cdnKey) { return new Promise((resolve, reject) => { const token = getUploadToken(BUCKET, cdnKey); const formUploader = new qiniu.form_up.FormUploader(new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 })); const putExtra = new qiniu.form_up.PutExtra(); formUploader.put(token, cdnKey, buffer, putExtra, (err, body, info) => { if (err) { reject(err); return; } if (info.statusCode === 200) { resolve({ key: body.key, url: `${CDN_DOMAIN}/${body.key}` }); } else { reject(new Error(`Upload failed: ${info.statusCode} ${JSON.stringify(body)}`)); } }); }); } // ============================================ // Parse REST API helpers // ============================================ const PARSE_HEADERS = { 'X-Parse-Application-Id': APP_ID, 'X-Parse-Master-Key': MASTER_KEY, 'Content-Type': 'application/json' }; async function ensureSkillClass() { const resp = await fetch(`${PARSE_BASE}/schemas/Skill`, { headers: PARSE_HEADERS }); if (resp.ok) return; // Class 不存在,创建 const createResp = await fetch(`${PARSE_BASE}/schemas/Skill`, { method: 'POST', headers: PARSE_HEADERS, body: JSON.stringify({ className: 'Skill', fields: { name: { type: 'String' }, displayName: { type: 'String' }, description: { type: 'String' }, category: { type: 'String' }, version: { type: 'String' }, author: { type: 'String' }, cdnUrls: { type: 'Object' }, cdnPrefix: { type: 'String' } } }) }); const result = await createResp.json(); if (createResp.ok) { console.log(' ✅ Skill 表已创建'); } else { console.log(' ⚠️ 创建 Skill 表:', result.error || JSON.stringify(result)); } } async function parseQuery(className, where) { const qs = new URLSearchParams({ where: JSON.stringify(where), limit: '1' }); const resp = await fetch(`${PARSE_BASE}/classes/${className}?${qs}`, { headers: PARSE_HEADERS }); const data = await resp.json(); return data.results || []; } async function parseCreate(className, body) { const resp = await fetch(`${PARSE_BASE}/classes/${className}`, { method: 'POST', headers: PARSE_HEADERS, body: JSON.stringify(body) }); return resp.json(); } async function parseUpdate(className, objectId, body) { const resp = await fetch(`${PARSE_BASE}/classes/${className}/${objectId}`, { method: 'PUT', headers: PARSE_HEADERS, body: JSON.stringify(body) }); return resp.json(); } async function upsertSkill(skillData) { const existing = await parseQuery('Skill', { name: skillData.name }); if (existing.length > 0) { const result = await parseUpdate('Skill', existing[0].objectId, skillData); return { action: 'updated', objectId: existing[0].objectId, ...result }; } else { const result = await parseCreate('Skill', skillData); return { action: 'created', ...result }; } } // ============================================ // Skill metadata extraction // ============================================ function parseSkillMdFrontmatter(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return {}; const fm = {}; match[1].split('\n').forEach(line => { const idx = line.indexOf(':'); if (idx > 0) { const key = line.substring(0, idx).trim(); const val = line.substring(idx + 1).trim(); fm[key] = val; } }); return fm; } catch (e) { return {}; } } function parseApiConfig(filePath) { try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (e) { return {}; } } function extractSkillMeta(skillDir, skillName) { const skillMdPath = path.join(skillDir, 'SKILL.md'); const apiConfigPath = path.join(skillDir, 'api-config.json'); const fm = parseSkillMdFrontmatter(skillMdPath); const cfg = fs.existsSync(apiConfigPath) ? parseApiConfig(apiConfigPath) : {}; return { name: skillName, displayName: cfg.displayName || fm.name || skillName, description: cfg.description || fm.description || '', category: cfg.category || '', version: cfg.version || fm.version || '1.0.0', author: fm.author || 'nkkj-BrainHack' }; } // ============================================ // Collect workshop files // ============================================ function collectWorkshopFiles() { const files = []; // Playbooks + supporting docs const allMdFiles = [...WORKSHOP_FILES.playbooks, ...WORKSHOP_FILES.supporting]; for (const fileName of allMdFiles) { const filePath = path.join(WORKSHOP_DIR, fileName); if (fs.existsSync(filePath)) { files.push({ localPath: filePath, cdnKey: `${CDN_PREFIX}/workshop/${fileName}`, fileName, type: WORKSHOP_FILES.playbooks.includes(fileName) ? 'playbook' : 'supporting' }); } else { console.log(` ⚠️ 跳过 ${fileName}(不存在)`); } } // Memory templates for (const fileName of WORKSHOP_FILES.memoryTemplates) { const filePath = path.join(MEMORY_TEMPLATES_DIR, fileName); if (fs.existsSync(filePath)) { files.push({ localPath: filePath, cdnKey: `${CDN_PREFIX}/workshop/memory-templates/${fileName}`, fileName: `memory-templates/${fileName}`, type: 'memory-template' }); } else { console.log(` ⚠️ 跳过 memory-templates/${fileName}(不存在)`); } } return files; } function generateWorkshopManifest(workshopResults) { const playbooks = {}; const supporting = {}; const memoryTemplates = {}; for (const item of workshopResults) { if (item.status !== 'ok') continue; if (item.type === 'playbook') { playbooks[item.fileName] = item.url; } else if (item.type === 'supporting') { supporting[item.fileName] = item.url; } else if (item.type === 'memory-template') { const baseName = item.fileName.replace('memory-templates/', ''); memoryTemplates[baseName] = item.url; } } return { package: 'workshop', label: 'VOC虾工作坊', description: 'VOC 工作坊完整流程文件(v4.1 Side Quest 架构)', version: '4.1.0', generatedAt: new Date().toISOString(), architecture: { sessions: 6, sideQuests: 17, stateFlow: 'pending → in_progress → awaitingCalibration → exploring → completed' }, installTarget: { playbooks: '~/.openclaw/workspace/', memoryTemplates: '~/.openclaw/workspace/memory/' }, files: { playbooks, supporting, memoryTemplates } }; } // ============================================ // Collect skills // ============================================ function collectSkills() { const skills = []; const dir = DEPLOYED_SKILLS_DIR; if (!fs.existsSync(dir)) { console.error(`Skills 目录不存在: ${dir}`); process.exit(1); } const entries = fs.readdirSync(dir).filter(d => { const fullPath = path.join(dir, d); if (!fs.statSync(fullPath).isDirectory()) return false; if (SKILL_FILTER.length > 0 && !SKILL_FILTER.includes(d)) return false; return fs.existsSync(path.join(fullPath, 'SKILL.md')); }); for (const skillName of entries) { const skillDir = path.join(dir, skillName); const files = []; for (const fileName of ['SKILL.md', 'api-config.json']) { const filePath = path.join(skillDir, fileName); if (fs.existsSync(filePath)) { files.push({ localPath: filePath, cdnKey: `${CDN_PREFIX}/${skillName}/${fileName}`, fileName }); } } if (files.length > 0) { const meta = extractSkillMeta(skillDir, skillName); skills.push({ name: skillName, files, meta }); } } return skills; } // ============================================ // Generate README.md // ============================================ function generateReadme(skillEntries, workshopManifestUrl) { const now = new Date().toISOString().replace('T', ' ').substring(0, 19); // 按技能包分组 const pkgMap = {}; for (const entry of skillEntries) { const pkgId = getPackageForCategory(entry.meta.category); if (!pkgMap[pkgId]) pkgMap[pkgId] = []; pkgMap[pkgId].push(entry); } // 统计可见技能数 let visibleCount = 0; for (const [pkgId, entries] of Object.entries(pkgMap)) { if (SKILL_PACKAGES[pkgId]?.status !== 'hidden') visibleCount += entries.length; } const lines = [ '# OpenClaw Skills 技能市场 + Workshop', '', `> 自动生成于 ${now},共 ${visibleCount} 个技能(分 ${Object.keys(pkgMap).filter(k => SKILL_PACKAGES[k]?.status !== 'hidden').length} 个套件)+ VOC工作坊 v4.1`, '', ]; // 套件总览表 lines.push('## 📦 套件总览'); lines.push(''); lines.push('| 套件 | 说明 | 技能数 | 状态 | 安装清单 |'); lines.push('|------|------|--------|------|---------|'); for (const [pkgId, pkg] of Object.entries(SKILL_PACKAGES)) { if (pkg.status === 'hidden') continue; const count = (pkgMap[pkgId] || []).length; const statusBadge = pkg.status === 'stable' ? '✅ 正式' : '🧪 测试中'; const installLinks = [ `[${pkgId}.json](${CDN_DOMAIN}/${CDN_PREFIX}/packages/${pkgId}.json)`, ...(pkg.aliases || []).map(alias => `[${alias}.json](${CDN_DOMAIN}/${CDN_PREFIX}/packages/${alias}.json)`) ]; lines.push(`| ${pkg.emoji} **${pkg.label}** | ${pkg.description} | ${count} | ${statusBadge} | ${installLinks.join(' / ')} |`); } lines.push(''); lines.push('---'); lines.push(''); // 每个套件详细内容 for (const [pkgId, pkg] of Object.entries(SKILL_PACKAGES)) { if (pkg.status === 'hidden') continue; const entries = pkgMap[pkgId] || []; if (entries.length === 0) continue; const statusNote = pkg.status === 'testing' ? ' ⚠️ 测试中,功能可能变更' : ''; lines.push(`## ${pkg.emoji} ${pkg.label}${statusNote}`); lines.push(''); lines.push(`> ${pkg.description}`); lines.push('>'); const installUrl = `${CDN_DOMAIN}/${CDN_PREFIX}/packages/${pkgId}.json`; lines.push(`> 📥 一键安装清单:[\`${pkgId}.json\`](${installUrl})`); if (pkg.aliases && pkg.aliases.length > 0) { const aliasLinks = pkg.aliases.map(alias => `[\`${alias}.json\`](${CDN_DOMAIN}/${CDN_PREFIX}/packages/${alias}.json)`); lines.push(`> ↩️ 兼容旧清单:${aliasLinks.join(' / ')}`); } lines.push(''); // 按 category 子分组 const catMap = {}; for (const entry of entries) { const cat = entry.meta.category || '其他'; if (!catMap[cat]) catMap[cat] = []; catMap[cat].push(entry); } for (const [cat, catEntries] of Object.entries(catMap).sort()) { lines.push(`### ${cat}`); lines.push(''); lines.push('| 技能名 | 说明 | 版本 | 下载 |'); lines.push('|--------|------|------|------|'); for (const entry of catEntries) { const m = entry.meta; const desc = m.description.length > 60 ? m.description.substring(0, 60) + '...' : m.description; const skillMdUrl = entry.cdnUrls['SKILL.md'] || ''; const apiCfgUrl = entry.cdnUrls['api-config.json'] || ''; const links = []; if (skillMdUrl) links.push(`[SKILL.md](${skillMdUrl})`); if (apiCfgUrl) links.push(`[api-config.json](${apiCfgUrl})`); lines.push(`| **${m.displayName}** (\`${m.name}\`) | ${desc} | ${m.version} | ${links.join(' / ')} |`); } lines.push(''); } } lines.push('---'); lines.push(''); lines.push('## 快速使用'); lines.push(''); lines.push('每个 Skill 包含两个文件:'); lines.push('- **SKILL.md** — 技能说明文档(功能、入参、调用链、注意事项)'); lines.push('- **api-config.json** — API 配置(endpoint、参数 schema、错误处理)'); lines.push(''); lines.push('下载后放入 `~/.openclaw/skills//` 即可被 OpenClaw Agent 识别和调用。'); lines.push(''); lines.push('### 按套件安装'); lines.push(''); lines.push('下载对应套件的 `.json` 安装清单,包含该套件所有技能的 CDN 下载地址。'); lines.push(''); // Workshop section if (workshopManifestUrl) { lines.push('---'); lines.push(''); lines.push('## 🦐 VOC虾工作坊 v4.1'); lines.push(''); lines.push('> VOC 工作坊完整流程文件,含 Side Quest 支线探索系统'); lines.push('>'); lines.push(`> 📥 安装清单:[\`workshop.json\`](${workshopManifestUrl})`); lines.push(''); lines.push('**架构**:6个Session + 17个Side Quest,主线感知 + 支线深耕'); lines.push(''); lines.push('| 文件 | 说明 |'); lines.push('|------|------|'); lines.push('| `workshop-voc-playbook.md` | 入口路由 |'); lines.push('| `workshop-global-rules.md` | 全局规则 + Side Quest Protocol |'); lines.push('| `workshop-session-0~5-*.md` | Session 0-5 流程文件 |'); lines.push('| `memory-templates/*.json` | 记忆模板(9个JSON) |'); lines.push(''); lines.push('安装方式:将 playbook 文件放入 `~/.openclaw/workspace/`,memory templates 放入 `~/.openclaw/workspace/memory/`'); lines.push(''); } return lines.join('\n'); } // 生成每个技能包的 install.json 清单 function generatePackageManifests(skillEntries) { const pkgMap = {}; for (const entry of skillEntries) { const pkgId = getPackageForCategory(entry.meta.category); if (!pkgMap[pkgId]) pkgMap[pkgId] = []; pkgMap[pkgId].push(entry); } const manifests = {}; for (const [pkgId, pkg] of Object.entries(SKILL_PACKAGES)) { if (pkg.status === 'hidden') continue; const entries = pkgMap[pkgId] || []; if (entries.length === 0) continue; const manifest = { package: pkgId, label: pkg.label, description: pkg.description, status: pkg.status, aliases: pkg.aliases || [], skillCount: entries.length, generatedAt: new Date().toISOString(), skills: entries.map(e => ({ name: e.meta.name, displayName: e.meta.displayName, category: e.meta.category, version: e.meta.version, files: e.cdnUrls })) }; manifests[pkgId] = manifest; for (const alias of pkg.aliases || []) { manifests[alias] = { ...manifest, package: alias, canonicalPackage: pkgId, legacyPackage: true }; } } return manifests; } // ============================================ // Main // ============================================ async function main() { console.log('=================================================='); console.log(' OpenClaw Skills + Workshop CDN 上传 v4.1'); console.log('=================================================='); console.log(''); if (SKILLS_ONLY) console.log(' 📌 模式: 仅上传 Skills'); if (WORKSHOP_ONLY) console.log(' 📌 模式: 仅上传 Workshop'); console.log(''); // 检查凭证 if (QINIU_ACCESS_KEY === 'YOUR_ACCESS_KEY_HERE' || QINIU_SECRET_KEY === 'YOUR_SECRET_KEY_HERE') { console.error('❌ 请先配置七牛云凭证!'); console.error(' 方式1: 编辑脚本中的 QINIU_ACCESS_KEY / QINIU_SECRET_KEY'); console.error(' 方式2: 设置环境变量 QINIU_AK / QINIU_SK'); process.exit(1); } console.log(`☁️ Bucket: ${BUCKET}`); console.log(`🔗 CDN前缀: ${CDN_PREFIX}`); console.log(''); // 收集技能 const skills = WORKSHOP_ONLY ? [] : collectSkills(); if (!WORKSHOP_ONLY) { console.log(`📦 找到 ${skills.length} 个技能待上传`); console.log(`📂 Skills源: ${DEPLOYED_SKILLS_DIR}`); } // 收集 workshop 文件 const workshopFiles = SKILLS_ONLY ? [] : collectWorkshopFiles(); if (!SKILLS_ONLY) { console.log(`� 找到 ${workshopFiles.length} 个 workshop 文件待上传`); console.log(`📂 Workshop源: ${WORKSHOP_DIR}`); } let totalFiles = 0; skills.forEach(s => { totalFiles += s.files.length; }); totalFiles += workshopFiles.length; console.log(`📄 总计 ${totalFiles} 个文件`); console.log(''); // ── Step 1: 上传 Skill 文件到七牛 ── let successCount = 0; let failCount = 0; const results = []; if (!WORKSHOP_ONLY && skills.length > 0) { console.log('── Step 1: 上传 Skills 到七牛云 ──'); for (const skill of skills) { process.stdout.write(` 📦 ${skill.name} ... `); const skillResults = []; const cdnUrls = {}; for (const file of skill.files) { try { const result = await uploadFile(file.localPath, file.cdnKey); skillResults.push({ ...result, fileName: file.fileName, status: 'ok' }); cdnUrls[file.fileName] = result.url; successCount++; } catch (err) { skillResults.push({ fileName: file.fileName, status: 'fail', error: err.message }); failCount++; } } const allOk = skillResults.every(r => r.status === 'ok'); console.log(allOk ? '✅' : '⚠️'); results.push({ name: skill.name, files: skillResults, meta: skill.meta, cdnUrls }); } console.log(''); console.log(` Skills 上传完成!成功: ${successCount} 失败: ${failCount}`); console.log(''); } else if (!WORKSHOP_ONLY) { console.log('── Step 1: 跳过(无 Skills)──'); console.log(''); } // ── Step 1b: 上传 Workshop 文件到七牛 ── let wsSuccessCount = 0; let wsFailCount = 0; const workshopResults = []; if (!SKILLS_ONLY && workshopFiles.length > 0) { console.log('── Step 1b: 上传 Workshop 文件到七牛云 ──'); for (const file of workshopFiles) { process.stdout.write(` 📝 ${file.fileName} ... `); try { const result = await uploadFile(file.localPath, file.cdnKey); workshopResults.push({ ...result, fileName: file.fileName, type: file.type, status: 'ok' }); wsSuccessCount++; console.log('✅'); } catch (err) { workshopResults.push({ fileName: file.fileName, type: file.type, status: 'fail', error: err.message }); wsFailCount++; console.log(`❌ ${err.message}`); } } console.log(''); console.log(` Workshop 上传完成!成功: ${wsSuccessCount} 失败: ${wsFailCount}`); console.log(''); } // ── Step 2: 写入 Parse Skill 表 ── let dbSuccess = 0; let dbFail = 0; if (!WORKSHOP_ONLY && results.length > 0) { console.log('── Step 2: 同步到 Parse 数据库 (Skill 表) ──'); await ensureSkillClass(); for (const entry of results) { if (Object.keys(entry.cdnUrls).length === 0) continue; process.stdout.write(` 💾 ${entry.name} ... `); try { // Parse 不允许嵌套 key 含 '.',将文件名中的 '.' 替换为 '_' const safeCdnUrls = {}; for (const [k, v] of Object.entries(entry.cdnUrls)) { safeCdnUrls[k.replace(/\./g, '_')] = v; } const skillData = { name: entry.meta.name, displayName: entry.meta.displayName, description: entry.meta.description, category: entry.meta.category, version: entry.meta.version, author: entry.meta.author, cdnUrls: safeCdnUrls, cdnPrefix: `${CDN_DOMAIN}/${CDN_PREFIX}/${entry.name}` }; const result = await upsertSkill(skillData); console.log(`${result.action === 'created' ? '🆕 创建' : '🔄 更新'} (${result.objectId})`); dbSuccess++; } catch (err) { console.log(`❌ ${err.message}`); dbFail++; } } console.log(''); console.log(` 数据库同步完成!成功: ${dbSuccess} 失败: ${dbFail}`); console.log(''); } // ── Step 3: 生成清单 + README 并上传 ── console.log('── Step 3: 生成清单 + README 并上传 ──'); const packageUrls = {}; // 上传每个技能包的 install.json if (!WORKSHOP_ONLY && results.length > 0) { const manifests = generatePackageManifests(results); for (const [pkgId, manifest] of Object.entries(manifests)) { try { const key = `${CDN_PREFIX}/packages/${pkgId}.json`; const buf = Buffer.from(JSON.stringify(manifest, null, 2), 'utf8'); const res = await uploadBuffer(buf, key); packageUrls[pkgId] = res.url; const pkg = getPackageDefinition(pkgId); const statusTag = manifest.status === 'testing' ? ' 🧪' : ' ✅'; const legacyNote = isLegacyPackageId(pkgId) ? `(兼容旧名 ${pkgId})` : ''; console.log(` ${statusTag} ${pkg.label}${legacyNote} (${manifest.skillCount}个技能): ${res.url}`); } catch (err) { console.log(` ❌ ${pkgId}.json 上传失败: ${err.message}`); } } } // 上传 workshop manifest let workshopManifestUrl = ''; if (!SKILLS_ONLY && workshopResults.length > 0) { try { const wsManifest = generateWorkshopManifest(workshopResults); const key = `${CDN_PREFIX}/packages/workshop.json`; const buf = Buffer.from(JSON.stringify(wsManifest, null, 2), 'utf8'); const res = await uploadBuffer(buf, key); workshopManifestUrl = res.url; packageUrls['workshop'] = res.url; const okCount = workshopResults.filter(r => r.status === 'ok').length; console.log(` 🦐 VOC虾工作坊 (${okCount}个文件): ${res.url}`); } catch (err) { console.log(` ❌ workshop.json 上传失败: ${err.message}`); } } // 上传 README.md let readmeUrl = ''; try { const readme = generateReadme(results, workshopManifestUrl); const readmeKey = `${CDN_PREFIX}/README.md`; const readmeResult = await uploadBuffer(Buffer.from(readme, 'utf8'), readmeKey); readmeUrl = readmeResult.url; console.log(` ✅ README.md 已上传: ${readmeUrl}`); } catch (err) { console.log(` ❌ README.md 上传失败: ${err.message}`); } console.log(''); // ── 输出 URL 汇总 ── console.log('📋 下载地址:'); console.log(''); for (const [pkgId, url] of Object.entries(packageUrls)) { if (pkgId === 'workshop') { console.log(` 🦐 VOC虾工作坊`); } else { const pkg = getPackageDefinition(pkgId); if (!pkg) continue; const statusTag = pkg.status === 'testing' ? '🧪' : '✅'; const legacyNote = isLegacyPackageId(pkgId) ? `(兼容旧名 ${pkgId})` : ''; console.log(` ${statusTag} ${pkg.label}${legacyNote}`); } console.log(` 👉 ${url}`); } console.log(''); // 保存 URL 列表到文件(按技能包分组) const urlMap = { _packages: packageUrls }; if (readmeUrl) urlMap['_README'] = readmeUrl; for (const entry of results) { if (Object.keys(entry.cdnUrls).length > 0) { urlMap[entry.name] = entry.cdnUrls; } } if (workshopResults.length > 0) { urlMap['_workshop'] = {}; for (const item of workshopResults) { if (item.status === 'ok') urlMap['_workshop'][item.fileName] = item.url; } } const outputPath = path.resolve(__dirname, '..', 'skills-cdn-urls.json'); fs.writeFileSync(outputPath, JSON.stringify(urlMap, null, 2), 'utf8'); console.log(`💾 URL 列表已保存到: ${outputPath}`); console.log(''); console.log('=================================================='); console.log('📌 索引:'); console.log(` README: ${CDN_DOMAIN}/${CDN_PREFIX}/README.md`); for (const [pkgId, url] of Object.entries(packageUrls)) { if (pkgId === 'workshop') { console.log(` 🦐 VOC虾工作坊: ${url}`); } else { const pkg = getPackageDefinition(pkgId); if (pkg) console.log(` ${getPackageLogLabel(pkgId)}: ${url}`); } } console.log('=================================================='); } main().catch(err => { console.error('❌ 上传出错:', err); process.exit(1); });