|
|
@@ -43,6 +43,47 @@ const DEPLOYED_SKILLS_DIR = path.join(process.env.USERPROFILE || process.env.HOM
|
|
|
// 要上传的技能目录列表(如果为空,则上传所有 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: '🔍'
|
|
|
+ },
|
|
|
+ 'social-media': {
|
|
|
+ label: '社媒数据套件',
|
|
|
+ description: 'TikTok / Instagram / 抖音数据采集',
|
|
|
+ categories: ['social-media', 'douyin'],
|
|
|
+ status: 'stable',
|
|
|
+ emoji: '📱'
|
|
|
+ },
|
|
|
+ 'ai-creation': {
|
|
|
+ label: 'AI 创作套件',
|
|
|
+ description: '即梦AI图片视频生成 + 一键成片(测试中)',
|
|
|
+ categories: ['jimeng', 'video-creation'],
|
|
|
+ status: 'testing',
|
|
|
+ emoji: '🎨'
|
|
|
+ },
|
|
|
+ '_other': {
|
|
|
+ label: '其他/测试',
|
|
|
+ description: '测试技能,不对外提供',
|
|
|
+ categories: ['other'],
|
|
|
+ status: 'hidden',
|
|
|
+ emoji: '🧪'
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+function getPackageForCategory(category) {
|
|
|
+ for (const [pkgId, pkg] of Object.entries(SKILL_PACKAGES)) {
|
|
|
+ if (pkg.categories.includes(category || 'other')) return pkgId;
|
|
|
+ }
|
|
|
+ return '_other';
|
|
|
+}
|
|
|
+
|
|
|
// ============================================
|
|
|
// 七牛上传
|
|
|
// ============================================
|
|
|
@@ -281,40 +322,85 @@ function collectSkills() {
|
|
|
|
|
|
function generateReadme(skillEntries) {
|
|
|
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 目录',
|
|
|
+ '# OpenClaw Skills 技能市场',
|
|
|
'',
|
|
|
- `> 自动生成于 ${now},共 ${skillEntries.length} 个技能`,
|
|
|
+ `> 自动生成于 ${now},共 ${visibleCount} 个技能(分 ${Object.keys(pkgMap).filter(k => SKILL_PACKAGES[k]?.status !== 'hidden').length} 个套件)`,
|
|
|
'',
|
|
|
- '---',
|
|
|
- ''
|
|
|
];
|
|
|
|
|
|
- // 按 category 分组
|
|
|
- const grouped = {};
|
|
|
- for (const entry of skillEntries) {
|
|
|
- const cat = entry.meta.category || '其他';
|
|
|
- if (!grouped[cat]) grouped[cat] = [];
|
|
|
- grouped[cat].push(entry);
|
|
|
+ // 套件总览表
|
|
|
+ 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 installUrl = `${CDN_DOMAIN}/${CDN_PREFIX}/packages/${pkgId}.json`;
|
|
|
+ lines.push(`| ${pkg.emoji} **${pkg.label}** | ${pkg.description} | ${count} | ${statusBadge} | [${pkgId}.json](${installUrl}) |`);
|
|
|
}
|
|
|
+ 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;
|
|
|
|
|
|
- for (const [cat, entries] of Object.entries(grouped).sort()) {
|
|
|
- lines.push(`## ${cat}`);
|
|
|
+ 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})`);
|
|
|
lines.push('');
|
|
|
- lines.push('| 技能名 | 说明 | 版本 | 下载 |');
|
|
|
- lines.push('|--------|------|------|------|');
|
|
|
|
|
|
+ // 按 category 子分组
|
|
|
+ const catMap = {};
|
|
|
for (const entry of entries) {
|
|
|
- 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(' / ')} |`);
|
|
|
+ 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('---');
|
|
|
@@ -327,10 +413,48 @@ function generateReadme(skillEntries) {
|
|
|
lines.push('');
|
|
|
lines.push('下载后放入 `~/.openclaw/skills/<skill-name>/` 即可被 OpenClaw Agent 识别和调用。');
|
|
|
lines.push('');
|
|
|
+ lines.push('### 按套件安装');
|
|
|
+ lines.push('');
|
|
|
+ lines.push('下载对应套件的 `.json` 安装清单,包含该套件所有技能的 CDN 下载地址。');
|
|
|
+ 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;
|
|
|
+
|
|
|
+ manifests[pkgId] = {
|
|
|
+ package: pkgId,
|
|
|
+ label: pkg.label,
|
|
|
+ description: pkg.description,
|
|
|
+ status: pkg.status,
|
|
|
+ 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
|
|
|
+ }))
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return manifests;
|
|
|
+}
|
|
|
+
|
|
|
// ============================================
|
|
|
// Main
|
|
|
// ============================================
|
|
|
@@ -432,8 +556,8 @@ async function main() {
|
|
|
console.log(` 数据库同步完成!成功: ${dbSuccess} 失败: ${dbFail}`);
|
|
|
console.log('');
|
|
|
|
|
|
- // ── Step 3: 生成 README.md 并上传(包含在技能文件夹内)──
|
|
|
- console.log('── Step 3: 生成 README.md 并上传 ──');
|
|
|
+ // ── Step 3: 生成 README.md + 技能包清单 并上传 ──
|
|
|
+ console.log('── Step 3: 生成 README.md + 技能包清单 并上传 ──');
|
|
|
let readmeUrl = '';
|
|
|
try {
|
|
|
const readme = generateReadme(results);
|
|
|
@@ -444,42 +568,56 @@ async function main() {
|
|
|
} catch (err) {
|
|
|
console.log(` ❌ README.md 上传失败: ${err.message}`);
|
|
|
}
|
|
|
- console.log('');
|
|
|
|
|
|
- // ── 输出 URL 汇总(README 包含在内)──
|
|
|
- console.log('📋 CDN 下载地址列表:');
|
|
|
+ // 上传每个技能包的 install.json
|
|
|
+ const manifests = generatePackageManifests(results);
|
|
|
+ const packageUrls = {};
|
|
|
+ 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 = SKILL_PACKAGES[pkgId];
|
|
|
+ const statusTag = pkg.status === 'testing' ? ' 🧪' : ' ✅';
|
|
|
+ console.log(` ${statusTag} ${pkg.label} (${manifest.skillCount}个技能): ${res.url}`);
|
|
|
+ } catch (err) {
|
|
|
+ console.log(` ❌ ${pkgId}.json 上传失败: ${err.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
console.log('');
|
|
|
|
|
|
- const urlMap = {};
|
|
|
-
|
|
|
- // README 放在最前面
|
|
|
- if (readmeUrl) {
|
|
|
- urlMap['_README'] = { 'README.md': readmeUrl };
|
|
|
- console.log(` README.md`);
|
|
|
- console.log(` 👉 ${readmeUrl}`);
|
|
|
+ // ── 输出 URL 汇总 ──
|
|
|
+ console.log('📋 技能包下载地址:');
|
|
|
+ console.log('');
|
|
|
+ for (const [pkgId, url] of Object.entries(packageUrls)) {
|
|
|
+ const pkg = SKILL_PACKAGES[pkgId];
|
|
|
+ const statusTag = pkg.status === 'testing' ? '🧪' : '✅';
|
|
|
+ console.log(` ${statusTag} ${pkg.label}`);
|
|
|
+ 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;
|
|
|
- for (const [fn, url] of Object.entries(entry.cdnUrls)) {
|
|
|
- console.log(` ${entry.name}/${fn}`);
|
|
|
- console.log(` 👉 ${url}`);
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // 保存 URL 列表到文件
|
|
|
const outputPath = path.resolve(__dirname, '..', 'skills-cdn-urls.json');
|
|
|
fs.writeFileSync(outputPath, JSON.stringify(urlMap, null, 2), 'utf8');
|
|
|
- console.log('');
|
|
|
console.log(`💾 URL 列表已保存到: ${outputPath}`);
|
|
|
console.log('');
|
|
|
|
|
|
console.log('==================================================');
|
|
|
- console.log('📌 技能索引(README 已包含在文件夹内):');
|
|
|
- console.log(` README: ${CDN_DOMAIN}/${CDN_PREFIX}/README.md`);
|
|
|
- console.log(` CDN: ${CDN_DOMAIN}/${CDN_PREFIX}/`);
|
|
|
+ console.log('📌 技能市场索引:');
|
|
|
+ console.log(` README: ${CDN_DOMAIN}/${CDN_PREFIX}/README.md`);
|
|
|
+ for (const [pkgId, url] of Object.entries(packageUrls)) {
|
|
|
+ const pkg = SKILL_PACKAGES[pkgId];
|
|
|
+ console.log(` ${pkg.emoji} ${pkg.label}: ${url}`);
|
|
|
+ }
|
|
|
console.log('==================================================');
|
|
|
}
|
|
|
|