| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898 |
- #!/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/<skill-name>/` 即可被 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);
- });
|