mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cb54420e0 | |||
| 6363bd83e2 | |||
| 7b8247544d | |||
| a9576ebc67 | |||
| 79ca378229 | |||
| e1a1fe23cb | |||
| 3d85a7e663 | |||
| 10aabb39f8 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.59.0"
|
||||
"version": "1.59.1"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 1.59.1 - 2026-03-11
|
||||
|
||||
### Fixes
|
||||
- `baoyu-translate`: improve short text annotation density rule and add explicit style preset passing to 02-prompt.md
|
||||
- `baoyu-post-to-x`: remove `--disable-blink-features=AutomationControlled` Chrome flag
|
||||
|
||||
### Refactor
|
||||
- `baoyu-post-to-weibo`: add entry point guard to md-to-html.ts for module import compatibility
|
||||
- Replace clawhub CLI with local sync-clawhub.mjs script
|
||||
|
||||
### Documentation
|
||||
- Update CLAUDE.md to reflect v1.59.0 codebase state (by @jackL1020)
|
||||
|
||||
## 1.59.0 - 2026-03-09
|
||||
|
||||
### Features
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 1.59.1 - 2026-03-11
|
||||
|
||||
### 修复
|
||||
- `baoyu-translate`:改进短文本注释密度规则,补充风格预设到 02-prompt.md 的显式传递
|
||||
- `baoyu-post-to-x`:移除 `--disable-blink-features=AutomationControlled` Chrome 启动参数
|
||||
|
||||
### 重构
|
||||
- `baoyu-post-to-weibo`:为 md-to-html.ts 添加入口守卫,支持模块导入
|
||||
- 使用本地 sync-clawhub.mjs 脚本替代 clawhub CLI
|
||||
|
||||
### 文档
|
||||
- 更新 CLAUDE.md 以反映 v1.59.0 代码库状态 (by @jackL1020)
|
||||
|
||||
## 1.59.0 - 2026-03-09
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -4,11 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Skills use Gemini Web API (reverse-engineered) for text/image generation and Chrome CDP for browser automation.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Current version: **1.59.1**. Skills use official AI APIs (OpenAI, Google, DashScope, Replicate) or the reverse-engineered Gemini Web API for text/image generation, and Chrome CDP for browser automation.
|
||||
|
||||
## Architecture
|
||||
|
||||
Skills are organized into three plugin categories in `marketplace.json`:
|
||||
Skills are organized into three plugin categories in `.claude-plugin/marketplace.json`:
|
||||
|
||||
```
|
||||
skills/
|
||||
@@ -19,16 +19,27 @@ skills/
|
||||
│ ├── baoyu-article-illustrator/ # Smart illustration placement
|
||||
│ ├── baoyu-comic/ # Knowledge comics (Logicomix/Ohmsha style)
|
||||
│ ├── baoyu-post-to-x/ # X/Twitter posting automation
|
||||
│ └── baoyu-post-to-wechat/ # WeChat Official Account posting
|
||||
│ ├── baoyu-post-to-wechat/ # WeChat Official Account posting
|
||||
│ ├── baoyu-post-to-weibo/ # Weibo posting (text, images, videos, articles)
|
||||
│ └── baoyu-infographic/ # Professional infographics (21 layouts × 20 styles)
|
||||
│
|
||||
├── [ai-generation-skills] # AI-powered generation backends
|
||||
│ └── baoyu-danger-gemini-web/ # Gemini API wrapper (text + image gen)
|
||||
│ ├── baoyu-danger-gemini-web/ # Gemini Web API wrapper (text + image gen)
|
||||
│ └── baoyu-image-gen/ # Official API image gen (OpenAI, Google, DashScope, Replicate)
|
||||
│
|
||||
└── [utility-skills] # Utility tools for content processing
|
||||
├── baoyu-danger-x-to-markdown/ # X/Twitter content to markdown
|
||||
└── baoyu-compress-image/ # Image compression
|
||||
├── baoyu-compress-image/ # Image compression (WebP/PNG via sips/cwebp/Sharp)
|
||||
├── baoyu-url-to-markdown/ # Fetch any URL → markdown via Chrome CDP
|
||||
├── baoyu-format-markdown/ # Format/beautify plain text or markdown files
|
||||
├── baoyu-markdown-to-html/ # Markdown → styled HTML (WeChat-compatible themes)
|
||||
└── baoyu-translate/ # Multi-mode translation (quick/normal/refined)
|
||||
```
|
||||
|
||||
Top-level `scripts/` directory contains repository maintenance utilities:
|
||||
- `scripts/sync-clawhub.sh` - Publish skills to ClawHub/OpenClaw registry
|
||||
- `scripts/sync-md-to-wechat.sh` - Sync markdown content to WeChat
|
||||
|
||||
**Plugin Categories**:
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
@@ -37,9 +48,10 @@ skills/
|
||||
| `utility-skills` | Helper tools for content processing (conversion, compression) |
|
||||
|
||||
Each skill contains:
|
||||
- `SKILL.md` - YAML front matter (name, description) + documentation
|
||||
- `scripts/` - TypeScript implementations
|
||||
- `prompts/system.md` - AI generation guidelines (optional)
|
||||
- `SKILL.md` - YAML front matter (name, description, version, openclaw metadata) + documentation
|
||||
- `scripts/` - TypeScript implementations (optional, pure-prompt skills omit this)
|
||||
- `references/` - Extended reference files linked from SKILL.md (optional)
|
||||
- `prompts/` - AI prompt templates (optional, used by some content skills)
|
||||
|
||||
## Running Skills
|
||||
|
||||
@@ -88,8 +100,9 @@ ${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md
|
||||
## Key Dependencies
|
||||
|
||||
- **Bun**: TypeScript runtime (native `bun` preferred, fallback `npx -y bun`)
|
||||
- **Chrome**: Required for `baoyu-danger-gemini-web` auth and `baoyu-post-to-x` automation
|
||||
- **Chrome**: Required for `baoyu-danger-gemini-web` auth, `baoyu-post-to-x`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, and `baoyu-url-to-markdown`
|
||||
- **npm packages (per skill)**: Some skill subprojects include `package.json`/lockfiles and require dependency installation in their own `scripts/` directories
|
||||
- **Image generation APIs**: `baoyu-image-gen` requires at least one API key (OpenAI, Google, DashScope, or Replicate)
|
||||
|
||||
## Chrome Profile (Unified)
|
||||
|
||||
@@ -166,10 +179,40 @@ Skills that process external Markdown/HTML should treat content as untrusted:
|
||||
- Cookies cached in data directory
|
||||
- Force refresh: `--login` flag
|
||||
|
||||
`baoyu-image-gen` uses official API keys configured in EXTEND.md (loaded at Step 0 before generation).
|
||||
|
||||
## Plugin Configuration
|
||||
|
||||
`.claude-plugin/marketplace.json` defines plugin metadata and skill paths. Version follows semver.
|
||||
|
||||
The file contains:
|
||||
- `name`: Plugin name (`baoyu-skills`)
|
||||
- `owner`: Author info
|
||||
- `metadata.version`: Semver version (currently `1.59.1`)
|
||||
- `plugins[]`: Array of plugin categories, each with `name`, `description`, `source`, `strict`, and `skills[]` paths
|
||||
|
||||
## ClawHub / OpenClaw Publishing
|
||||
|
||||
Skills include `metadata.openclaw` in their YAML front matter for registry compatibility:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#<skill-name>
|
||||
requires: # optional — only for skills with scripts
|
||||
anyBins:
|
||||
- bun
|
||||
- npx
|
||||
```
|
||||
|
||||
To publish/sync skills to ClawHub registry:
|
||||
```bash
|
||||
bash scripts/sync-clawhub.sh # sync all skills
|
||||
bash scripts/sync-clawhub.sh <skill> # sync one skill
|
||||
```
|
||||
|
||||
Requires `clawhub` CLI or `npx` (auto-downloads via npx if not installed).
|
||||
|
||||
## Skill Loading Rules
|
||||
|
||||
**IMPORTANT**: When working in this project, follow these rules:
|
||||
@@ -212,29 +255,50 @@ Skills that process external Markdown/HTML should treat content as untrusted:
|
||||
| **References** | Keep one level deep from SKILL.md; avoid nested references |
|
||||
| **No time-sensitive info** | Avoid dates/versions that become outdated |
|
||||
|
||||
### SKILL.md Frontmatter Template
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: baoyu-<name>
|
||||
description: <Third-person description. What it does + when to use it.>
|
||||
version: <semver matching marketplace.json>
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-<name>
|
||||
requires: # include only if skill has scripts
|
||||
anyBins:
|
||||
- bun
|
||||
- npx
|
||||
---
|
||||
```
|
||||
|
||||
### Steps
|
||||
|
||||
1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter
|
||||
1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter (see template above)
|
||||
- Directory name: `baoyu-<name>`
|
||||
- SKILL.md `name` field: `baoyu-<name>`
|
||||
2. Add TypeScript in `skills/baoyu-<name>/scripts/`
|
||||
- Always include `metadata.openclaw.homepage`
|
||||
- Include `metadata.openclaw.requires.anyBins` only when the skill has executable scripts
|
||||
2. Add TypeScript in `skills/baoyu-<name>/scripts/` (if applicable)
|
||||
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
|
||||
4. **Choose the appropriate category** and register in `marketplace.json`:
|
||||
- `content-skills`: For content generation/publishing (images, slides, posts)
|
||||
- `ai-generation-skills`: For AI backend capabilities
|
||||
- `utility-skills`: For helper tools (conversion, compression)
|
||||
- If none fit, create a new category with descriptive name
|
||||
5. **Add Script Directory section** to SKILL.md (see template below)
|
||||
5. **Add Script Directory section** to SKILL.md (see template below) if skill has scripts
|
||||
6. **Add openclaw metadata** to SKILL.md frontmatter (required for ClawHub registry)
|
||||
|
||||
### Choosing a Category
|
||||
|
||||
| If your skill... | Use category |
|
||||
|------------------|--------------|
|
||||
| Generates visual content (images, slides, comics) | `content-skills` |
|
||||
| Publishes to platforms (X, WeChat, etc.) | `content-skills` |
|
||||
| Provides AI generation backend | `ai-generation-skills` |
|
||||
| Converts or processes content | `utility-skills` |
|
||||
| Generates visual content (images, slides, comics, infographics) | `content-skills` |
|
||||
| Publishes to platforms (X, WeChat, Weibo, etc.) | `content-skills` |
|
||||
| Provides AI generation backend (official API or reverse-engineered) | `ai-generation-skills` |
|
||||
| Converts or processes content (URL→md, md→html, format) | `utility-skills` |
|
||||
| Compresses or optimizes files | `utility-skills` |
|
||||
| Translates documents | `utility-skills` |
|
||||
|
||||
**Creating a new category**: If the skill doesn't fit existing categories, add a new plugin object to `marketplace.json` with:
|
||||
- `name`: Descriptive kebab-case name (e.g., `analytics-skills`)
|
||||
@@ -266,10 +330,10 @@ Every SKILL.md with scripts MUST include this section after Usage:
|
||||
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
|
||||
|
||||
**Agent Execution Instructions**:
|
||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||
1. Determine this SKILL.md file's directory path as `{baseDir}`
|
||||
2. Script path = `{baseDir}/scripts/<script-name>.ts`
|
||||
3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||
4. Replace all `${SKILL_DIR}` and `${BUN_X}` in this document with actual values
|
||||
4. Replace all `{baseDir}` and `${BUN_X}` in this document with actual values
|
||||
|
||||
**Script Reference**:
|
||||
| Script | Purpose |
|
||||
@@ -278,7 +342,9 @@ Every SKILL.md with scripts MUST include this section after Usage:
|
||||
| `scripts/other.ts` | Other functionality |
|
||||
```
|
||||
|
||||
When referencing scripts in workflow sections, use `${BUN_X} ${SKILL_DIR}/scripts/<name>.ts` so agents can resolve the correct runtime and path.
|
||||
When referencing scripts in workflow sections, use `${BUN_X} {baseDir}/scripts/<name>.ts` so agents can resolve the correct runtime and path.
|
||||
|
||||
> **Note**: The path variable was renamed from `SKILL_DIR` / `${SKILL_DIR}` to `baseDir` / `{baseDir}` (no `${}` wrapper) in v1.57.0. All existing SKILL.md files use `{baseDir}`.
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
@@ -327,7 +393,7 @@ Use this template when implementing image generation in skills:
|
||||
### Step N: Generate Images
|
||||
|
||||
**Skill Selection**:
|
||||
1. Check available image generation skills (e.g., `baoyu-danger-gemini-web`)
|
||||
1. Check available image generation skills (`baoyu-image-gen` default, or `baoyu-danger-gemini-web`)
|
||||
2. Read selected skill's SKILL.md for parameter reference
|
||||
3. If multiple skills available, ask user to choose
|
||||
|
||||
@@ -336,11 +402,14 @@ Use this template when implementing image generation in skills:
|
||||
- Prompt file path (or inline prompt)
|
||||
- Output image path
|
||||
- Any skill-specific parameters (refer to skill's SKILL.md)
|
||||
2. Generate images sequentially (one at a time)
|
||||
2. Generate images sequentially by default (one at a time)
|
||||
- Use batch parallel mode only when user already has multiple prompts or explicitly wants parallel throughput
|
||||
3. After each image, output progress: "Generated X/N"
|
||||
4. On failure, auto-retry once before reporting error
|
||||
```
|
||||
|
||||
**Batch Parallel Generation** (`baoyu-image-gen` only): supports concurrent workers with per-provider throttling. Configure via `batch.max_workers` and `batch.provider_limits` in EXTEND.md. Default: sequential.
|
||||
|
||||
### Output Path Convention
|
||||
|
||||
Each session creates an independent directory. Even the same source file generates a new directory per session.
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
|
||||
const DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
const TEXT_EXTENSIONS = new Set([
|
||||
"md",
|
||||
"mdx",
|
||||
"txt",
|
||||
"json",
|
||||
"json5",
|
||||
"yaml",
|
||||
"yml",
|
||||
"toml",
|
||||
"js",
|
||||
"cjs",
|
||||
"mjs",
|
||||
"ts",
|
||||
"tsx",
|
||||
"jsx",
|
||||
"py",
|
||||
"sh",
|
||||
"rb",
|
||||
"go",
|
||||
"rs",
|
||||
"swift",
|
||||
"kt",
|
||||
"java",
|
||||
"cs",
|
||||
"cpp",
|
||||
"c",
|
||||
"h",
|
||||
"hpp",
|
||||
"sql",
|
||||
"csv",
|
||||
"ini",
|
||||
"cfg",
|
||||
"env",
|
||||
"xml",
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"sass",
|
||||
"svg",
|
||||
]);
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const config = await readClawhubConfig();
|
||||
const registry = (
|
||||
process.env.CLAWHUB_REGISTRY ||
|
||||
process.env.CLAWDHUB_REGISTRY ||
|
||||
config.registry ||
|
||||
DEFAULT_REGISTRY
|
||||
).replace(/\/+$/, "");
|
||||
|
||||
if (!config.token) {
|
||||
throw new Error("Not logged in. Run: clawhub login");
|
||||
}
|
||||
|
||||
await apiJson(registry, config.token, "/api/v1/whoami");
|
||||
|
||||
const roots = options.roots.length > 0 ? options.roots : [path.resolve("skills")];
|
||||
const skills = await findSkills(roots);
|
||||
|
||||
if (skills.length === 0) {
|
||||
throw new Error("No skills found.");
|
||||
}
|
||||
|
||||
console.log("ClawHub sync");
|
||||
console.log(`Roots with skills: ${roots.join(", ")}`);
|
||||
|
||||
const locals = await mapWithConcurrency(skills, options.concurrency, async (skill) => {
|
||||
const files = await listTextFiles(skill.folder);
|
||||
const fingerprint = buildFingerprint(files);
|
||||
return {
|
||||
...skill,
|
||||
fileCount: files.length,
|
||||
fingerprint,
|
||||
};
|
||||
});
|
||||
|
||||
const candidates = await mapWithConcurrency(locals, options.concurrency, async (skill) => {
|
||||
const query = new URLSearchParams({
|
||||
slug: skill.slug,
|
||||
hash: skill.fingerprint,
|
||||
});
|
||||
const { status, body } = await apiJsonWithStatus(
|
||||
registry,
|
||||
config.token,
|
||||
`/api/v1/resolve?${query.toString()}`
|
||||
);
|
||||
|
||||
if (status === 404) {
|
||||
return {
|
||||
...skill,
|
||||
status: "new",
|
||||
latestVersion: null,
|
||||
matchVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error(body?.message || `Resolve failed for ${skill.slug} (HTTP ${status})`);
|
||||
}
|
||||
|
||||
const latestVersion = body?.latestVersion?.version ?? null;
|
||||
const matchVersion = body?.match?.version ?? null;
|
||||
|
||||
if (!latestVersion) {
|
||||
return {
|
||||
...skill,
|
||||
status: "new",
|
||||
latestVersion: null,
|
||||
matchVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...skill,
|
||||
status: matchVersion ? "synced" : "update",
|
||||
latestVersion,
|
||||
matchVersion,
|
||||
};
|
||||
});
|
||||
|
||||
const actionable = candidates.filter((candidate) => candidate.status !== "synced");
|
||||
if (actionable.length === 0) {
|
||||
console.log("Nothing to sync.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("To sync");
|
||||
for (const candidate of actionable) {
|
||||
console.log(`- ${formatCandidate(candidate, options.bump)}`);
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log("");
|
||||
console.log(`Dry run: would upload ${actionable.length} skill(s).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = options.tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const candidate of actionable) {
|
||||
const version =
|
||||
candidate.status === "new"
|
||||
? "1.0.0"
|
||||
: bumpSemver(candidate.latestVersion, options.bump);
|
||||
|
||||
console.log(`Publishing ${candidate.slug}@${version}`);
|
||||
const files = await listTextFiles(candidate.folder);
|
||||
await publishSkill({
|
||||
registry,
|
||||
token: config.token,
|
||||
skill: candidate,
|
||||
files,
|
||||
version,
|
||||
changelog: options.changelog,
|
||||
tags,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log(`Uploaded ${actionable.length} skill(s).`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
roots: [],
|
||||
dryRun: false,
|
||||
bump: "patch",
|
||||
changelog: "",
|
||||
tags: "latest",
|
||||
concurrency: 4,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--dry-run") {
|
||||
options.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--all") {
|
||||
continue;
|
||||
}
|
||||
if (arg === "--root") {
|
||||
const value = argv[index + 1];
|
||||
if (!value) throw new Error("--root requires a directory");
|
||||
options.roots.push(path.resolve(value));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--bump") {
|
||||
const value = argv[index + 1];
|
||||
if (!["patch", "minor", "major"].includes(value)) {
|
||||
throw new Error("--bump must be patch, minor, or major");
|
||||
}
|
||||
options.bump = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--changelog") {
|
||||
const value = argv[index + 1];
|
||||
if (value == null) throw new Error("--changelog requires text");
|
||||
options.changelog = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tags") {
|
||||
const value = argv[index + 1];
|
||||
if (value == null) throw new Error("--tags requires a value");
|
||||
options.tags = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--concurrency") {
|
||||
const value = Number(argv[index + 1]);
|
||||
if (!Number.isInteger(value) || value < 1 || value > 32) {
|
||||
throw new Error("--concurrency must be an integer between 1 and 32");
|
||||
}
|
||||
options.concurrency = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "-h" || arg === "--help") {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage: sync-clawhub.mjs [options]
|
||||
|
||||
Options:
|
||||
--root <dir> Extra skill root (repeatable)
|
||||
--all Accepted for compatibility
|
||||
--dry-run Show what would be uploaded
|
||||
--bump <type> patch | minor | major
|
||||
--changelog <text> Changelog for updates
|
||||
--tags <tags> Comma-separated tags
|
||||
--concurrency <n> Registry check concurrency (1-32)
|
||||
-h, --help Show help`);
|
||||
}
|
||||
|
||||
async function readClawhubConfig() {
|
||||
const configPath = getConfigPath();
|
||||
try {
|
||||
return JSON.parse(await fs.readFile(configPath, "utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigPath() {
|
||||
const override =
|
||||
process.env.CLAWHUB_CONFIG_PATH?.trim() || process.env.CLAWDHUB_CONFIG_PATH?.trim();
|
||||
if (override) {
|
||||
return path.resolve(override);
|
||||
}
|
||||
|
||||
const home = os.homedir();
|
||||
if (process.platform === "darwin") {
|
||||
const clawhub = path.join(home, "Library", "Application Support", "clawhub", "config.json");
|
||||
const clawdhub = path.join(home, "Library", "Application Support", "clawdhub", "config.json");
|
||||
return pathForExistingConfig(clawhub, clawdhub);
|
||||
}
|
||||
|
||||
const xdg = process.env.XDG_CONFIG_HOME;
|
||||
if (xdg) {
|
||||
const clawhub = path.join(xdg, "clawhub", "config.json");
|
||||
const clawdhub = path.join(xdg, "clawdhub", "config.json");
|
||||
return pathForExistingConfig(clawhub, clawdhub);
|
||||
}
|
||||
|
||||
if (process.platform === "win32" && process.env.APPDATA) {
|
||||
const clawhub = path.join(process.env.APPDATA, "clawhub", "config.json");
|
||||
const clawdhub = path.join(process.env.APPDATA, "clawdhub", "config.json");
|
||||
return pathForExistingConfig(clawhub, clawdhub);
|
||||
}
|
||||
|
||||
const clawhub = path.join(home, ".config", "clawhub", "config.json");
|
||||
const clawdhub = path.join(home, ".config", "clawdhub", "config.json");
|
||||
return pathForExistingConfig(clawhub, clawdhub);
|
||||
}
|
||||
|
||||
function pathForExistingConfig(primary, legacy) {
|
||||
if (existsSync(primary)) return path.resolve(primary);
|
||||
if (existsSync(legacy)) return path.resolve(legacy);
|
||||
return path.resolve(primary);
|
||||
}
|
||||
|
||||
async function findSkills(roots) {
|
||||
const deduped = new Map();
|
||||
for (const root of roots) {
|
||||
const folders = await findSkillFolders(root);
|
||||
for (const folder of folders) {
|
||||
deduped.set(folder.slug, folder);
|
||||
}
|
||||
}
|
||||
return [...deduped.values()].sort((left, right) => left.slug.localeCompare(right.slug));
|
||||
}
|
||||
|
||||
async function findSkillFolders(root) {
|
||||
const stat = await safeStat(root);
|
||||
if (!stat?.isDirectory()) return [];
|
||||
|
||||
if (await hasSkillMarker(root)) {
|
||||
return [buildSkillEntry(root)];
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(root, { withFileTypes: true });
|
||||
const found = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const folder = path.join(root, entry.name);
|
||||
if (await hasSkillMarker(folder)) {
|
||||
found.push(buildSkillEntry(folder));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function buildSkillEntry(folder) {
|
||||
const base = path.basename(folder);
|
||||
return {
|
||||
folder,
|
||||
slug: sanitizeSlug(base),
|
||||
displayName: titleCase(base),
|
||||
};
|
||||
}
|
||||
|
||||
async function hasSkillMarker(folder) {
|
||||
return Boolean(
|
||||
(await safeStat(path.join(folder, "SKILL.md")))?.isFile() ||
|
||||
(await safeStat(path.join(folder, "skill.md")))?.isFile()
|
||||
);
|
||||
}
|
||||
|
||||
async function listTextFiles(root) {
|
||||
const files = [];
|
||||
|
||||
async function walk(folder) {
|
||||
const entries = await fs.readdir(folder, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
if (entry.name === "node_modules") continue;
|
||||
if (entry.name === ".clawhub" || entry.name === ".clawdhub") continue;
|
||||
|
||||
const fullPath = path.join(folder, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
|
||||
const relPath = path.relative(root, fullPath).split(path.sep).join("/");
|
||||
const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
|
||||
if (!TEXT_EXTENSIONS.has(ext)) continue;
|
||||
|
||||
const bytes = await fs.readFile(fullPath);
|
||||
files.push({ relPath, bytes });
|
||||
}
|
||||
}
|
||||
|
||||
await walk(root);
|
||||
files.sort((left, right) => left.relPath.localeCompare(right.relPath));
|
||||
return files;
|
||||
}
|
||||
|
||||
function buildFingerprint(files) {
|
||||
const payload = files
|
||||
.map((file) => `${file.relPath}:${sha256(file.bytes)}`)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.join("\n");
|
||||
return crypto.createHash("sha256").update(payload).digest("hex");
|
||||
}
|
||||
|
||||
function sha256(bytes) {
|
||||
return crypto.createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
async function publishSkill({ registry, token, skill, files, version, changelog, tags }) {
|
||||
const form = new FormData();
|
||||
form.set(
|
||||
"payload",
|
||||
JSON.stringify({
|
||||
slug: skill.slug,
|
||||
displayName: skill.displayName,
|
||||
version,
|
||||
changelog,
|
||||
tags,
|
||||
acceptLicenseTerms: true,
|
||||
})
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
form.append("files", new Blob([file.bytes], { type: "text/plain" }), file.relPath);
|
||||
}
|
||||
|
||||
const response = await fetch(`${registry}/api/v1/skills`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(text || `Publish failed for ${skill.slug} (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
const result = text ? JSON.parse(text) : {};
|
||||
console.log(`OK. Published ${skill.slug}@${version}${result.versionId ? ` (${result.versionId})` : ""}`);
|
||||
}
|
||||
|
||||
async function apiJson(registry, token, requestPath) {
|
||||
const { status, body } = await apiJsonWithStatus(registry, token, requestPath);
|
||||
if (status < 200 || status >= 300) {
|
||||
throw new Error(body?.message || `HTTP ${status}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
async function apiJsonWithStatus(registry, token, requestPath) {
|
||||
const response = await fetch(`${registry}${requestPath}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let body = null;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
body = { message: text };
|
||||
}
|
||||
return { status: response.status, body };
|
||||
}
|
||||
|
||||
async function mapWithConcurrency(items, limit, fn) {
|
||||
const results = new Array(items.length);
|
||||
let cursor = 0;
|
||||
|
||||
async function worker() {
|
||||
while (cursor < items.length) {
|
||||
const index = cursor;
|
||||
cursor += 1;
|
||||
results[index] = await fn(items[index], index);
|
||||
}
|
||||
}
|
||||
|
||||
const count = Math.min(Math.max(limit, 1), Math.max(items.length, 1));
|
||||
await Promise.all(Array.from({ length: count }, () => worker()));
|
||||
return results;
|
||||
}
|
||||
|
||||
function formatCandidate(candidate, bump) {
|
||||
if (candidate.status === "new") {
|
||||
return `${candidate.slug} NEW (${candidate.fileCount} files)`;
|
||||
}
|
||||
return `${candidate.slug} UPDATE ${candidate.latestVersion} -> ${bumpSemver(
|
||||
candidate.latestVersion,
|
||||
bump
|
||||
)} (${candidate.fileCount} files)`;
|
||||
}
|
||||
|
||||
function bumpSemver(version, bump) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version ?? "");
|
||||
if (!match) {
|
||||
throw new Error(`Invalid semver: ${version}`);
|
||||
}
|
||||
const major = Number(match[1]);
|
||||
const minor = Number(match[2]);
|
||||
const patch = Number(match[3]);
|
||||
|
||||
if (bump === "major") return `${major + 1}.0.0`;
|
||||
if (bump === "minor") return `${major}.${minor + 1}.0`;
|
||||
return `${major}.${minor}.${patch + 1}`;
|
||||
}
|
||||
|
||||
function sanitizeSlug(value) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, "-")
|
||||
.replace(/^-+/, "")
|
||||
.replace(/-+$/, "")
|
||||
.replace(/--+/g, "-");
|
||||
}
|
||||
|
||||
function titleCase(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/[-_]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
async function safeStat(filePath) {
|
||||
try {
|
||||
return await fs.stat(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -4,13 +4,8 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SKILLS_DIR="${ROOT_DIR}/skills"
|
||||
|
||||
if command -v clawhub >/dev/null 2>&1; then
|
||||
CLAWHUB_CMD=(clawhub)
|
||||
elif command -v npx >/dev/null 2>&1; then
|
||||
CLAWHUB_CMD=(npx -y clawhub)
|
||||
else
|
||||
echo "Error: neither clawhub nor npx is available."
|
||||
echo "See https://docs.openclaw.ai/zh-CN/tools/clawhub"
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "Error: node is required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -18,4 +13,4 @@ if [ "$#" -eq 0 ]; then
|
||||
set -- --all
|
||||
fi
|
||||
|
||||
exec "${CLAWHUB_CMD[@]}" sync --root "${SKILLS_DIR}" "$@"
|
||||
exec node "${ROOT_DIR}/scripts/sync-clawhub.mjs" --root "${SKILLS_DIR}" "$@"
|
||||
|
||||
@@ -446,7 +446,9 @@ Options:
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch((err) => {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
if (import.meta.main ?? (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? ''))) {
|
||||
await main().catch((err) => {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,7 +114,6 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||
`--user-data-dir=${profileDir}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--start-maximized',
|
||||
X_ARTICLES_URL,
|
||||
];
|
||||
|
||||
@@ -41,7 +41,6 @@ export async function postToX(options: XBrowserOptions): Promise<void> {
|
||||
`--user-data-dir=${profileDir}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--start-maximized',
|
||||
X_COMPOSE_URL,
|
||||
], { stdio: 'ignore' });
|
||||
|
||||
@@ -45,7 +45,6 @@ export async function quotePost(options: QuoteOptions): Promise<void> {
|
||||
`--user-data-dir=${profileDir}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--start-maximized',
|
||||
tweetUrl,
|
||||
], { stdio: 'ignore' });
|
||||
|
||||
@@ -45,7 +45,6 @@ export async function postVideoToX(options: XVideoOptions): Promise<void> {
|
||||
`--user-data-dir=${profileDir}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--start-maximized',
|
||||
X_COMPOSE_URL,
|
||||
], { stdio: 'ignore' });
|
||||
|
||||
@@ -188,7 +188,7 @@ Before translating chunks:
|
||||
- Splits at markdown block boundaries to preserve structure
|
||||
- If a single block exceeds the threshold, falls back to line splitting, then word splitting
|
||||
4. **Assemble translation prompt**:
|
||||
- Main agent reads `01-analysis.md` (if exists) and assembles shared context using Part 1 of [references/subagent-prompt-template.md](references/subagent-prompt-template.md) — inlining content background, merged glossary, and comprehension challenges
|
||||
- Main agent reads `01-analysis.md` (if exists) and assembles shared context using Part 1 of [references/subagent-prompt-template.md](references/subagent-prompt-template.md) — inlining the resolved style preset (from `--style` flag, EXTEND.md `style` setting, or default `storytelling`), content background, merged glossary, and comprehension challenges
|
||||
- Save as `02-prompt.md` in the output directory (shared context only, no task instructions)
|
||||
5. **Draft translation via subagents** (if Agent tool available):
|
||||
- Spawn one subagent **per chunk**, all in parallel (Part 2 of the template)
|
||||
@@ -215,7 +215,7 @@ Before translating chunks:
|
||||
- **Image-language awareness**: Preserve image references exactly during translation, but after the translation is complete, review referenced images and check whether their likely main text language still matches the translated article language
|
||||
- **Frontmatter transformation**: If the source has YAML frontmatter, preserve it in the translation with these changes: (1) Rename metadata fields that describe the *source* article — `url`→`sourceUrl`, `title`→`sourceTitle`, `description`→`sourceDescription`, `author`→`sourceAuthor`, `date`→`sourceDate`, and any similar origin-metadata fields — by adding a `source` prefix (camelCase). (2) Translate the values of text fields (title, description, etc.) and add them as new top-level fields. (3) Keep other fields (tags, categories, custom fields) as-is, translating their values where appropriate
|
||||
- **Respect original**: Maintain original meaning and intent; do not add, remove, or editorialize — but sentence structure and imagery may be adapted freely to serve the meaning
|
||||
- **Translator's notes**: For terms, concepts, or cultural references that target readers may not understand — due to jargon, cultural gaps, or domain-specific knowledge — add a concise explanatory note in parentheses immediately after the term. The note should explain *what it means* in plain language, not just provide the English original. Format: `译文(English original,通俗解释)`. Calibrate annotation depth to the target audience: general readers need more notes than technical readers. Only add notes where genuinely needed; do not over-annotate obvious terms.
|
||||
- **Translator's notes**: For terms, concepts, or cultural references that target readers may not understand — due to jargon, cultural gaps, or domain-specific knowledge — add a concise explanatory note in parentheses immediately after the term. The note should explain *what it means* in plain language, not just provide the English original. Format: `译文(English original,通俗解释)`. Calibrate annotation depth to the target audience: general readers need more notes than technical readers. For short texts (< 5 sentences), further reduce annotations — only annotate non-common terms that the target audience is unlikely to know; skip terms that are widely recognized or self-explanatory in context. Only add notes where genuinely needed; do not over-annotate obvious terms.
|
||||
|
||||
#### Quick Mode
|
||||
|
||||
@@ -224,7 +224,7 @@ Translate directly → save to `translation.md`. No analysis file, but still app
|
||||
#### Normal Mode
|
||||
|
||||
1. **Analyze** → `01-analysis.md` (domain, tone, audience, terminology, reader comprehension challenges, figurative language & metaphor mapping)
|
||||
2. **Assemble prompt** → `02-prompt.md` (translation instructions with inlined context)
|
||||
2. **Assemble prompt** → `02-prompt.md` (translation instructions with inlined style preset, content background, glossary, and comprehension challenges)
|
||||
3. **Translate** (following `02-prompt.md`) → `translation.md`
|
||||
|
||||
After completion, prompt user: "Translation saved. To further review and polish, reply **继续润色** or **refine**."
|
||||
|
||||
@@ -121,7 +121,7 @@ Implicit assumptions: [unstated premises]
|
||||
|
||||
## Step 2: Assemble Translation Prompt
|
||||
|
||||
Main agent reads `01-analysis.md` and assembles a complete translation prompt using [references/subagent-prompt-template.md](subagent-prompt-template.md). Inline content background, merged glossary, and comprehension challenges into the prompt. Save to `02-prompt.md`.
|
||||
Main agent reads `01-analysis.md` and assembles a complete translation prompt using [references/subagent-prompt-template.md](subagent-prompt-template.md). Inline the resolved style preset (from `--style` flag, EXTEND.md `style` setting, or default `storytelling`), content background, merged glossary, and comprehension challenges into the prompt. Save to `02-prompt.md`.
|
||||
|
||||
This prompt is used by the subagent (chunked) or by the main agent itself (non-chunked).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user