Files
openclaw-master-skills/scripts/collect.sh
T

353 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# collect.sh - Weekly OpenClaw Master Skills collector
# Source of truth: https://github.com/openclaw/skills-archive
# Strategy: clone/update archive -> diff by slug -> quality filter -> import
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SKILLS_DIR="$REPO_ROOT/skills"
CHANGELOG="$REPO_ROOT/CHANGELOG.md"
RELEASES="$REPO_ROOT/RELEASES.md"
README="$REPO_ROOT/README.md"
LOG_FILE="/tmp/openclaw-master-skills-collect-$(date +%Y-%m-%d).log"
DRY_RUN="${1:-}"
WEEK="$(date -u +%Y-%m-%d)"
ARCHIVE_DIR="${OPENCLAW_SKILLS_ARCHIVE_DIR:-/tmp/openclaw-skills-archive}"
TMP_NEW_LIST="/tmp/openclaw-master-skills-new-slugs.txt"
TMP_IMPORT_LIST="/tmp/openclaw-master-skills-imported-slugs.txt"
TMP_RELEASE_BODY="/tmp/openclaw-master-skills-release-body.md"
GH_TOKEN="${GITHUB_TOKEN:-}"
CLAWHUB_TOKEN="${CLAWHUB_TOKEN:-}"
CLAWHUB_REGISTRY="${CLAWHUB_REGISTRY:-https://clawhub.ai}"
MAX_WEEKLY_IMPORTS="${MAX_WEEKLY_IMPORTS:-100}"
NEW_VERSION=""
NEW_COUNT="0"
TOTAL_COUNT="0"
log() { echo "[$(date '+%H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
info() { log "INFO $1"; }
ok() { log "OK $1"; }
warn() { log "WARN $1"; }
fail() { log "FAIL $1"; exit 1; }
require_bin() {
command -v "$1" >/dev/null 2>&1 || fail "Missing required binary: $1"
}
setup() {
require_bin git
require_bin python3
mkdir -p "$SKILLS_DIR"
: > "$TMP_NEW_LIST"
: > "$TMP_IMPORT_LIST"
info "Repo root: $REPO_ROOT"
info "Week: $WEEK | Dry-run: ${DRY_RUN:-no}"
}
refresh_archive() {
info "Checking local archive mirror"
[ -d "$ARCHIVE_DIR/skills" ] || fail "Local archive not found: $ARCHIVE_DIR"
ok "Archive ready: $ARCHIVE_DIR"
}
find_new_candidates() {
info "Finding new candidate skills"
SKILLS_DIR="$SKILLS_DIR" ARCHIVE_DIR="$ARCHIVE_DIR" MAX_WEEKLY_IMPORTS="$MAX_WEEKLY_IMPORTS" python3 <<'PY' > "$TMP_NEW_LIST"
import os
import re
skills_dir = os.environ['SKILLS_DIR']
archive_dir = os.environ['ARCHIVE_DIR']
existing = set()
for name in os.listdir(skills_dir):
path = os.path.join(skills_dir, name)
if os.path.isdir(path):
existing.add(name)
best_by_slug = {}
root = os.path.join(archive_dir, 'skills')
for publisher in sorted(os.listdir(root)):
publisher_dir = os.path.join(root, publisher)
if not os.path.isdir(publisher_dir):
continue
for slug in sorted(os.listdir(publisher_dir)):
if slug in existing:
continue
skill_md = os.path.join(publisher_dir, slug, 'SKILL.md')
if not os.path.isfile(skill_md):
continue
try:
size = os.path.getsize(skill_md)
if size < 800 or size > 30000:
continue
with open(skill_md, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(5000)
if not content.lstrip().startswith('---'):
continue
if not re.search(r'^name:\s*\S', content, re.M):
continue
desc_match = re.search(r'^description:\s*(.+?)(?=\n[a-zA-Z_]+:|\n---|\Z)', content, re.M | re.S)
if not desc_match:
continue
desc = ' '.join(desc_match.group(1).strip().split())
if len(desc) < 50 or len(desc) > 1024:
continue
prev = best_by_slug.get(slug)
row = (size, publisher, slug, desc)
if prev is None or row[0] > prev[0]:
best_by_slug[slug] = row
except Exception:
continue
rows = sorted(best_by_slug.values(), key=lambda x: (-x[0], x[2]))
limit = int(os.environ.get('MAX_WEEKLY_IMPORTS', '100'))
for size, publisher, slug, desc in rows[:limit]:
print(f"{slug}\t{publisher}\t{size}\t{desc}")
PY
local total
total=$(wc -l < "$TMP_NEW_LIST" | tr -d ' ')
ok "Candidate skills found: $total"
}
import_candidates() {
info "Importing candidates"
REPO_ROOT="$REPO_ROOT" SKILLS_DIR="$SKILLS_DIR" ARCHIVE_DIR="$ARCHIVE_DIR" TMP_NEW_LIST="$TMP_NEW_LIST" TMP_IMPORT_LIST="$TMP_IMPORT_LIST" DRY_RUN="$DRY_RUN" python3 <<'PY'
import os
import shutil
skills_dir = os.environ['SKILLS_DIR']
archive_dir = os.environ['ARCHIVE_DIR']
tmp_new = os.environ['TMP_NEW_LIST']
tmp_import = os.environ['TMP_IMPORT_LIST']
dry_run = os.environ.get('DRY_RUN', '') == '--dry-run'
count = 0
with open(tmp_new, 'r', encoding='utf-8') as src, open(tmp_import, 'w', encoding='utf-8') as out:
for line in src:
slug, publisher, size, desc = line.rstrip('\n').split('\t', 3)
source_dir = os.path.join(archive_dir, 'skills', publisher, slug)
dest_dir = os.path.join(skills_dir, slug)
if os.path.exists(dest_dir):
continue
if dry_run:
out.write(f"{slug}\t{publisher}\t{desc}\n")
count += 1
continue
shutil.copytree(source_dir, dest_dir)
out.write(f"{slug}\t{publisher}\t{desc}\n")
count += 1
print(count)
PY
}
update_docs() {
NEW_COUNT=$(wc -l < "$TMP_IMPORT_LIST" | tr -d ' ')
TOTAL_COUNT=$(find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
NEW_VERSION=$(python3 <<'PY'
import re
from pathlib import Path
content = Path('CHANGELOG.md').read_text(encoding='utf-8', errors='ignore')
match = re.search(r'v(\d+)\.(\d+)\.(\d+)', content)
if match:
major, minor, patch = map(int, match.groups())
print(f"{major}.{minor + 1}.0")
else:
print('0.1.0')
PY
)
export NEW_COUNT TOTAL_COUNT NEW_VERSION
info "Updating docs for v$NEW_VERSION (+$NEW_COUNT => $TOTAL_COUNT total)"
RELEASES="$RELEASES" WEEK="$WEEK" NEW_COUNT="$NEW_COUNT" TOTAL_COUNT="$TOTAL_COUNT" TMP_IMPORT_LIST="$TMP_IMPORT_LIST" NEW_VERSION="$NEW_VERSION" python3 <<'PY'
import os
from pathlib import Path
releases = Path(os.environ['RELEASES'])
week = os.environ['WEEK']
new_count = os.environ['NEW_COUNT']
total_count = os.environ['TOTAL_COUNT']
import_file = Path(os.environ['TMP_IMPORT_LIST'])
version = os.environ['NEW_VERSION']
items = []
if import_file.exists():
with import_file.open('r', encoding='utf-8') as f:
for idx, line in enumerate(f):
slug, publisher, desc = line.rstrip('\n').split('\t', 2)
if idx < 30:
items.append(f"- `{slug}` — {desc[:120]}")
body = f"## v{version} — {week}\n\n### 🚀 周更:新增 {new_count} 个 Skills,总计 {total_count}\n\n来源:openclaw/skills-archive 官方镜像,按质量规则筛选(SKILL.md 800B-30KB、完整 YAML 元数据、有效 description)。\n\n#### 部分新增亮点(前 30 个)\n" + ("\n".join(items) if items else "- 本周无新增") + "\n\n---\n\n"
content = releases.read_text(encoding='utf-8', errors='ignore')
if content.startswith('# Release Notes'):
parts = content.split('\n', 4)
prefix = '\n'.join(parts[:4]) + '\n\n'
rest = parts[4] if len(parts) > 4 else ''
releases.write_text(prefix + body + rest, encoding='utf-8')
PY
CHANGELOG="$CHANGELOG" WEEK="$WEEK" NEW_COUNT="$NEW_COUNT" TOTAL_COUNT="$TOTAL_COUNT" NEW_VERSION="$NEW_VERSION" python3 <<'PY'
import os
from pathlib import Path
changelog = Path(os.environ['CHANGELOG'])
week = os.environ['WEEK']
new_count = os.environ['NEW_COUNT']
total_count = os.environ['TOTAL_COUNT']
version = os.environ['NEW_VERSION']
entry = (
f"## [v{version}] — {week}\n\n"
f"### 🚀 周更:新增 {new_count} 个 Skills,总计 {total_count}\n\n"
"来源:openclaw/skills-archive 官方镜像,按质量规则筛选。详见 RELEASES.md。\n\n"
"---\n"
)
content = changelog.read_text(encoding='utf-8', errors='ignore')
marker = '---\n'
pos = content.find(marker)
if pos != -1:
content = content[:pos + len(marker)] + '\n' + entry + content[pos + len(marker):]
else:
content += '\n\n' + entry
changelog.write_text(content, encoding='utf-8')
PY
TOTAL_COUNT="$TOTAL_COUNT" python3 <<'PY'
import os
import re
from pathlib import Path
for filename in ['README.md', 'SKILL.md', 'README.zh-CN.md', 'README.fr.md', 'README.de.md', 'README.ru.md', 'README.ja.md', 'README.it.md', 'README.es.md']:
path = Path(filename)
if not path.exists():
continue
content = path.read_text(encoding='utf-8', errors='ignore')
content = re.sub(r'Skills-[0-9]+%2B', f"Skills-{os.environ['TOTAL_COUNT']}%2B", content)
content = re.sub(r'collection of [0-9]+\+ best OpenClaw skills', f"collection of {os.environ['TOTAL_COUNT']}+ best OpenClaw skills", content)
content = re.sub(r'[0-9]+\+ best OpenClaw skills', f"{os.environ['TOTAL_COUNT']}+ best OpenClaw skills", content)
path.write_text(content, encoding='utf-8')
PY
cat > "$TMP_RELEASE_BODY" <<EOF
## 🚀 周更:新增 ${NEW_COUNT} 个 Skills,总计 ${TOTAL_COUNT}
来源:openclaw/skills-archive 官方镜像,按质量规则筛选。
详见仓库内 RELEASES.md。
EOF
}
git_publish() {
if [ "$DRY_RUN" = "--dry-run" ]; then
warn "Dry-run: skipping git publish"
return
fi
info "Committing changes"
git -C "$REPO_ROOT" add -A
if git -C "$REPO_ROOT" diff --cached --quiet; then
warn "No changes to commit"
return
fi
git -C "$REPO_ROOT" config user.name "The Doctor (MyClaw)"
git -C "$REPO_ROOT" config user.email "doctor@myclaw.ai"
git -C "$REPO_ROOT" commit -m "feat(v${NEW_VERSION}): weekly update ${WEEK}${NEW_COUNT} new skills (${TOTAL_COUNT} total)" >> "$LOG_FILE" 2>&1 || fail "git commit failed"
ok "Commit created"
[ -n "$GH_TOKEN" ] || fail "GITHUB_TOKEN is required for push"
local remote_url="https://${GH_TOKEN}@github.com/LeoYeAI/openclaw-master-skills.git"
git -C "$REPO_ROOT" push "$remote_url" main >> "$LOG_FILE" 2>&1 || fail "git push failed"
ok "GitHub push succeeded"
GH_TOKEN="$GH_TOKEN" NEW_VERSION="$NEW_VERSION" TMP_RELEASE_BODY="$TMP_RELEASE_BODY" python3 <<'PY'
import json
import os
import urllib.request
version = os.environ['NEW_VERSION']
body = open(os.environ['TMP_RELEASE_BODY'], 'r', encoding='utf-8').read()
payload = json.dumps({
'tag_name': f'v{version}',
'name': f'v{version} — Weekly Update',
'body': body,
'draft': False,
'prerelease': False,
}).encode()
req = urllib.request.Request(
'https://api.github.com/repos/LeoYeAI/openclaw-master-skills/releases',
data=payload,
headers={
'Authorization': f"token {os.environ['GH_TOKEN']}",
'Content-Type': 'application/json',
'Accept': 'application/vnd.github+json',
},
method='POST',
)
try:
with urllib.request.urlopen(req) as resp:
print(resp.status)
except Exception as exc:
print(f'WARN release creation failed: {exc}')
PY
}
publish_clawhub() {
[ "$DRY_RUN" = "--dry-run" ] && { warn "Dry-run: skipping ClawHub publish"; return; }
[ -n "$CLAWHUB_TOKEN" ] || { warn "CLAWHUB_TOKEN missing: skipping ClawHub publish"; return; }
command -v clawhub >/dev/null 2>&1 || { warn "clawhub CLI missing: skipping ClawHub publish"; return; }
info "Publishing lightweight package to ClawHub"
local pkg_dir="/tmp/openclaw-master-skills-clawhub-pkg"
rm -rf "$pkg_dir"
mkdir -p "$pkg_dir/references"
cp "$REPO_ROOT/SKILL.md" "$pkg_dir/"
cp "$REPO_ROOT/README.md" "$pkg_dir/"
[ -f "$REPO_ROOT/LICENSE" ] && cp "$REPO_ROOT/LICENSE" "$pkg_dir/"
cat > "$pkg_dir/references/install-guide.md" <<EOF
# Install OpenClaw Master Skills
Full collection:
https://github.com/LeoYeAI/openclaw-master-skills
EOF
clawhub logout >> "$LOG_FILE" 2>&1 || true
clawhub login --token "$CLAWHUB_TOKEN" >> "$LOG_FILE" 2>&1 || fail "ClawHub login failed"
clawhub publish "$pkg_dir" \
--registry "$CLAWHUB_REGISTRY" \
--slug openclaw-master-skills \
--version "$NEW_VERSION" \
--changelog "Weekly update: +${NEW_COUNT} skills, total ${TOTAL_COUNT}. Source: openclaw/skills-archive." \
>> "$LOG_FILE" 2>&1 || fail "ClawHub publish failed"
ok "ClawHub publish succeeded"
}
main() {
setup
refresh_archive
find_new_candidates
import_candidates >/tmp/openclaw-master-skills-import-count.txt
local import_count
import_count=$(tail -1 /tmp/openclaw-master-skills-import-count.txt)
ok "Imported candidates: $import_count"
if [ "$import_count" = "0" ]; then
warn "No new skills found; exiting"
exit 0
fi
if [ "$DRY_RUN" = "--dry-run" ]; then
warn "Dry-run complete: skipping docs and publish steps"
exit 0
fi
update_docs
git_publish
publish_clawhub
ok "Done. Log: $LOG_FILE"
}
main