mirror of
https://github.com/LeoYeAI/openclaw-master-skills.git
synced 2026-07-27 22:15:43 +00:00
fix: switch weekly collector to local archive + quality filter
This commit is contained in:
+311
-284
@@ -1,325 +1,352 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# collect.sh - Weekly OpenClaw Master Skills collector
|
||||
# Sources: skills.sh (top 200) + GitHub (openclaw-skill topic) + ClaWHub CLI
|
||||
# Usage: ./collect.sh [--dry-run]
|
||||
# Env: GITHUB_TOKEN (required for GitHub scan)
|
||||
# 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"
|
||||
PENDING_DIR="$REPO_ROOT/pending"
|
||||
CHANGELOG="$REPO_ROOT/CHANGELOG.md"
|
||||
RELEASES="$REPO_ROOT/RELEASES.md"
|
||||
README="$REPO_ROOT/README.md"
|
||||
LOG_FILE="/tmp/openclaw-collect-$(date +%Y-%m-%d).log"
|
||||
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:-}"
|
||||
WEEK=$(date +%Y-%m-%d)
|
||||
|
||||
mkdir -p "$PENDING_DIR"
|
||||
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 "ℹ️ $1"; }
|
||||
ok() { log "✅ $1"; }
|
||||
warn() { log "⚠️ $1"; }
|
||||
info() { log "INFO $1"; }
|
||||
ok() { log "OK $1"; }
|
||||
warn() { log "WARN $1"; }
|
||||
fail() { log "FAIL $1"; exit 1; }
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fetch_json() {
|
||||
curl -s --max-time 15 \
|
||||
${GH_TOKEN:+-H "Authorization: token $GH_TOKEN"} \
|
||||
"$@"
|
||||
require_bin() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "Missing required binary: $1"
|
||||
}
|
||||
|
||||
# Clone or update a GitHub repo skill folder into pending/
|
||||
fetch_github_skill() {
|
||||
local repo="$1" # e.g. vercel-labs/agent-skills
|
||||
local skill="$2" # e.g. vercel-react-best-practices
|
||||
local dest="$PENDING_DIR/${skill}"
|
||||
|
||||
if [ -d "$dest" ]; then
|
||||
info "Already fetched: $skill"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local url="https://api.github.com/repos/${repo}/contents/${skill}"
|
||||
local files
|
||||
files=$(fetch_json "$url" 2>/dev/null) || { warn "Cannot fetch $repo/$skill"; return 1; }
|
||||
|
||||
local has_skill_md
|
||||
has_skill_md=$(echo "$files" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
names = [f['name'] for f in data if isinstance(data, list)]
|
||||
print('yes' if 'SKILL.md' in names else 'no')
|
||||
except: print('no')
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ "$has_skill_md" = "yes" ]; then
|
||||
mkdir -p "$dest"
|
||||
# Download each file
|
||||
echo "$files" | python3 -c "
|
||||
import sys, json, urllib.request, os
|
||||
data = json.load(sys.stdin)
|
||||
dest = sys.argv[1]
|
||||
for f in data:
|
||||
if f.get('type') == 'file' and f.get('download_url'):
|
||||
out = os.path.join(dest, f['name'])
|
||||
urllib.request.urlretrieve(f['download_url'], out)
|
||||
print(f' → {f[\"name\"]}')
|
||||
" "$dest" 2>/dev/null && ok "Fetched: $skill (from $repo)" || warn "Failed to download files for $skill"
|
||||
else
|
||||
warn "No SKILL.md in $repo/$skill, skipping"
|
||||
fi
|
||||
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}"
|
||||
}
|
||||
|
||||
# ── Source 1: skills.sh top 200 ──────────────────────────────────────────────
|
||||
scan_skills_sh() {
|
||||
info "=== Scanning skills.sh top 200 ==="
|
||||
|
||||
local html
|
||||
html=$(curl -s --max-time 30 "https://skills.sh/" 2>/dev/null) || { warn "skills.sh unreachable"; return; }
|
||||
|
||||
# Extract repo/skill pairs from the leaderboard
|
||||
echo "$html" | python3 -c "
|
||||
import re, sys
|
||||
html = sys.stdin.read()
|
||||
# Pattern: /owner/repo/skill-name in links
|
||||
matches = re.findall(r'/([a-z0-9_-]+)/([a-z0-9_.-]+)/([a-z0-9_:-]+)', html)
|
||||
seen = set()
|
||||
for owner, repo, skill in matches:
|
||||
key = f'{owner}/{repo}/{skill}'
|
||||
if key not in seen and owner not in ('http', 'https', 'www'):
|
||||
seen.add(key)
|
||||
print(f'{owner}/{repo}\t{skill}')
|
||||
" 2>/dev/null | head -60 | while IFS=$'\t' read -r repo skill; do
|
||||
fetch_github_skill "$repo" "$skill" || true
|
||||
done
|
||||
refresh_archive() {
|
||||
info "Checking local archive mirror"
|
||||
[ -d "$ARCHIVE_DIR/skills" ] || fail "Local archive not found: $ARCHIVE_DIR"
|
||||
ok "Archive ready: $ARCHIVE_DIR"
|
||||
}
|
||||
|
||||
# ── Source 2: GitHub topic:openclaw-skill ────────────────────────────────────
|
||||
scan_github_topic() {
|
||||
info "=== Scanning GitHub topic: openclaw-skill ==="
|
||||
|
||||
if [ -z "$GH_TOKEN" ]; then
|
||||
warn "GITHUB_TOKEN not set, skipping GitHub scan (rate limits apply)"
|
||||
return
|
||||
fi
|
||||
|
||||
local results
|
||||
results=$(fetch_json \
|
||||
"https://api.github.com/search/repositories?q=topic:openclaw-skill&sort=stars&per_page=30" \
|
||||
2>/dev/null) || { warn "GitHub search failed"; return; }
|
||||
|
||||
echo "$results" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for r in data.get('items', []):
|
||||
print(r['full_name'], r.get('description',''), r.get('stargazers_count',0))
|
||||
" 2>/dev/null | while read -r full_name desc stars; do
|
||||
info " Found: $full_name (⭐ $stars)"
|
||||
skill_name=$(echo "$full_name" | sed 's|.*/||')
|
||||
fetch_github_skill "$full_name" "$skill_name" || true
|
||||
done
|
||||
|
||||
# Also search for repos containing SKILL.md with openclaw in description
|
||||
fetch_json \
|
||||
"https://api.github.com/search/repositories?q=openclaw+skill+in:description&sort=stars&per_page=20" \
|
||||
2>/dev/null | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for r in data.get('items', []):
|
||||
print(r['full_name'])
|
||||
" 2>/dev/null | while read -r full_name; do
|
||||
skill_name=$(echo "$full_name" | sed 's|.*/||')
|
||||
fetch_github_skill "$full_name" "$skill_name" || true
|
||||
done
|
||||
}
|
||||
|
||||
# ── Source 3: ClaWHub CLI ─────────────────────────────────────────────────────
|
||||
scan_clawhub() {
|
||||
info "=== Scanning ClaWHub ==="
|
||||
|
||||
if ! command -v clawhub &>/dev/null; then
|
||||
warn "clawhub CLI not installed (npm i -g clawhub), skipping"
|
||||
return
|
||||
fi
|
||||
|
||||
clawhub explore --limit 50 2>/dev/null | while read -r line; do
|
||||
info " ClaWHub: $line"
|
||||
slug=$(echo "$line" | awk '{print $1}')
|
||||
[ -z "$slug" ] && continue
|
||||
dest="$PENDING_DIR/$slug"
|
||||
if [ ! -d "$dest" ]; then
|
||||
clawhub install "$slug" --workdir "$PENDING_DIR" --force 2>/dev/null && ok "Installed from ClaWHub: $slug" || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── Validate pending skills ───────────────────────────────────────────────────
|
||||
validate_pending() {
|
||||
info "=== Validating pending skills ==="
|
||||
local approved=0
|
||||
local rejected=0
|
||||
|
||||
for skill_dir in "$PENDING_DIR"/*/; do
|
||||
[ -d "$skill_dir" ] || continue
|
||||
skill_name=$(basename "$skill_dir")
|
||||
|
||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||
# Check frontmatter has name + description
|
||||
valid=$(python3 -c "
|
||||
import sys
|
||||
content = open('$skill_dir/SKILL.md').read()
|
||||
has_name = 'name:' in content
|
||||
has_desc = 'description:' in content
|
||||
print('ok' if has_name and has_desc else 'fail')
|
||||
" 2>/dev/null)
|
||||
if [ "$valid" = "ok" ]; then
|
||||
if [ "$DRY_RUN" = "--dry-run" ]; then
|
||||
ok "[DRY RUN] Would approve: $skill_name"
|
||||
else
|
||||
cp -r "$skill_dir" "$SKILLS_DIR/$skill_name" && ok "Approved: $skill_name"
|
||||
fi
|
||||
approved=$((approved+1))
|
||||
else
|
||||
warn "Invalid SKILL.md (missing name/description): $skill_name"
|
||||
rejected=$((rejected+1))
|
||||
fi
|
||||
else
|
||||
warn "No SKILL.md found: $skill_name"
|
||||
rejected=$((rejected+1))
|
||||
fi
|
||||
done
|
||||
|
||||
info "Results: ✅ $approved approved, ❌ $rejected rejected"
|
||||
echo "$approved"
|
||||
}
|
||||
|
||||
# ── Update CHANGELOG ─────────────────────────────────────────────────────────
|
||||
update_changelog() {
|
||||
local count="$1"
|
||||
info "=== Updating CHANGELOG ==="
|
||||
|
||||
local new_skills=""
|
||||
for skill_dir in "$SKILLS_DIR"/*/; do
|
||||
[ -d "$skill_dir" ] || continue
|
||||
skill_name=$(basename "$skill_dir")
|
||||
desc=$(python3 -c "
|
||||
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
|
||||
try:
|
||||
content = open('$skill_dir/SKILL.md').read()
|
||||
m = re.search(r'description:\s*(.+)', content)
|
||||
print(m.group(1).strip()[:80] if m else '')
|
||||
except: print('')
|
||||
" 2>/dev/null)
|
||||
new_skills="$new_skills\n| \`$skill_name\` | $desc |"
|
||||
done
|
||||
|
||||
local entry="## [Week of $WEEK]\n\n### Skills Added This Week: $count\n\n| Skill | Description |\n|---|---|\n$new_skills\n\n---\n"
|
||||
skills_dir = os.environ['SKILLS_DIR']
|
||||
archive_dir = os.environ['ARCHIVE_DIR']
|
||||
|
||||
# Prepend to changelog after header
|
||||
python3 -c "
|
||||
import re, sys
|
||||
entry = sys.argv[1]
|
||||
with open('$CHANGELOG', 'r') as f:
|
||||
content = f.read()
|
||||
# Insert after first '---'
|
||||
content = content.replace('---\n', '---\n\n' + entry, 1)
|
||||
with open('$CHANGELOG', 'w') as f:
|
||||
f.write(content)
|
||||
" "$entry" 2>/dev/null && ok "CHANGELOG updated"
|
||||
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"
|
||||
}
|
||||
|
||||
# ── Update README skill index ─────────────────────────────────────────────────
|
||||
update_readme() {
|
||||
info "=== Updating README skill index ==="
|
||||
local rows=""
|
||||
local count=0
|
||||
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
|
||||
|
||||
for skill_dir in "$SKILLS_DIR"/*/; do
|
||||
[ -d "$skill_dir" ] || continue
|
||||
skill_name=$(basename "$skill_dir")
|
||||
desc=$(python3 -c "
|
||||
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
|
||||
try:
|
||||
content = open('$skill_dir/SKILL.md').read()
|
||||
m = re.search(r'description:\s*(.+)', content)
|
||||
print(m.group(1).strip()[:70] if m else '')
|
||||
except: print('')
|
||||
" 2>/dev/null)
|
||||
rows="$rows\n| [\`$skill_name\`](skills/$skill_name/) | $desc | — | — | — |"
|
||||
count=$((count+1))
|
||||
done
|
||||
|
||||
python3 -c "
|
||||
import re, sys
|
||||
rows = sys.argv[1]
|
||||
count = sys.argv[2]
|
||||
with open('$README', 'r') as f:
|
||||
content = f.read()
|
||||
# Replace table content between | Skill | header and next ---
|
||||
table_header = '| Skill | Description | Category | Source | Added |'
|
||||
table_sep = '|---|---|---|---|---|'
|
||||
new_table = table_header + '\n' + table_sep + rows
|
||||
content = re.sub(
|
||||
r'\| Skill \| Description \| Category \| Source \| Added \|.*?(?=\n---|\n##)',
|
||||
new_table,
|
||||
content, flags=re.DOTALL
|
||||
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
|
||||
)
|
||||
with open('$README', 'w') as f:
|
||||
f.write(content)
|
||||
print(f'README updated: {count} skills in index')
|
||||
" "$rows" "$count" 2>/dev/null && ok "README updated ($count skills)"
|
||||
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 commit & push ─────────────────────────────────────────────────────────
|
||||
git_push() {
|
||||
local count="$1"
|
||||
info "=== Committing and pushing ==="
|
||||
git_publish() {
|
||||
if [ "$DRY_RUN" = "--dry-run" ]; then
|
||||
warn "Dry-run: skipping git publish"
|
||||
return
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
info "No changes to commit"
|
||||
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 commit -m "chore: weekly update $WEEK — $count new skills collected"
|
||||
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"
|
||||
|
||||
if [ -n "$GH_TOKEN" ]; then
|
||||
remote_url=$(git remote get-url origin 2>/dev/null | sed "s|https://|https://$GH_TOKEN@|")
|
||||
git push "$remote_url" main 2>/dev/null && ok "Pushed to GitHub" || warn "Push failed"
|
||||
else
|
||||
warn "GITHUB_TOKEN not set, skipping push"
|
||||
fi
|
||||
[ -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 ──────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
log "======================================"
|
||||
log "OpenClaw Master Skills Weekly Collector"
|
||||
log "Week: $WEEK | Dry-run: ${DRY_RUN:-no}"
|
||||
log "======================================"
|
||||
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"
|
||||
|
||||
scan_skills_sh
|
||||
scan_github_topic
|
||||
scan_clawhub
|
||||
if [ "$import_count" = "0" ]; then
|
||||
warn "No new skills found; exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
count=$(validate_pending)
|
||||
if [ "$DRY_RUN" = "--dry-run" ]; then
|
||||
warn "Dry-run complete: skipping docs and publish steps"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" != "--dry-run" ]; then
|
||||
update_changelog "$count"
|
||||
update_readme
|
||||
git_push "$count"
|
||||
# Clear pending after approval
|
||||
rm -rf "$PENDING_DIR"/*
|
||||
fi
|
||||
|
||||
log "======================================"
|
||||
log "Done! Log: $LOG_FILE"
|
||||
log "======================================"
|
||||
update_docs
|
||||
git_publish
|
||||
publish_clawhub
|
||||
ok "Done. Log: $LOG_FILE"
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
Reference in New Issue
Block a user