mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-17 16:09:47 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cb54420e0 | |||
| 6363bd83e2 | |||
| 7b8247544d | |||
| a9576ebc67 | |||
| 79ca378229 | |||
| e1a1fe23cb | |||
| 3d85a7e663 | |||
| 10aabb39f8 |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.59.0"
|
"version": "1.59.1"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
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
|
## 1.59.0 - 2026-03-09
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[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
|
## 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
|
## 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
|
## Architecture
|
||||||
|
|
||||||
Skills are organized into three plugin categories in `marketplace.json`:
|
Skills are organized into three plugin categories in `.claude-plugin/marketplace.json`:
|
||||||
|
|
||||||
```
|
```
|
||||||
skills/
|
skills/
|
||||||
@@ -19,16 +19,27 @@ skills/
|
|||||||
│ ├── baoyu-article-illustrator/ # Smart illustration placement
|
│ ├── baoyu-article-illustrator/ # Smart illustration placement
|
||||||
│ ├── baoyu-comic/ # Knowledge comics (Logicomix/Ohmsha style)
|
│ ├── baoyu-comic/ # Knowledge comics (Logicomix/Ohmsha style)
|
||||||
│ ├── baoyu-post-to-x/ # X/Twitter posting automation
|
│ ├── 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
|
├── [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
|
└── [utility-skills] # Utility tools for content processing
|
||||||
├── baoyu-danger-x-to-markdown/ # X/Twitter content to markdown
|
├── 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**:
|
**Plugin Categories**:
|
||||||
| Category | Description |
|
| Category | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
@@ -37,9 +48,10 @@ skills/
|
|||||||
| `utility-skills` | Helper tools for content processing (conversion, compression) |
|
| `utility-skills` | Helper tools for content processing (conversion, compression) |
|
||||||
|
|
||||||
Each skill contains:
|
Each skill contains:
|
||||||
- `SKILL.md` - YAML front matter (name, description) + documentation
|
- `SKILL.md` - YAML front matter (name, description, version, openclaw metadata) + documentation
|
||||||
- `scripts/` - TypeScript implementations
|
- `scripts/` - TypeScript implementations (optional, pure-prompt skills omit this)
|
||||||
- `prompts/system.md` - AI generation guidelines (optional)
|
- `references/` - Extended reference files linked from SKILL.md (optional)
|
||||||
|
- `prompts/` - AI prompt templates (optional, used by some content skills)
|
||||||
|
|
||||||
## Running Skills
|
## Running Skills
|
||||||
|
|
||||||
@@ -88,8 +100,9 @@ ${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md
|
|||||||
## Key Dependencies
|
## Key Dependencies
|
||||||
|
|
||||||
- **Bun**: TypeScript runtime (native `bun` preferred, fallback `npx -y bun`)
|
- **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
|
- **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)
|
## Chrome Profile (Unified)
|
||||||
|
|
||||||
@@ -166,10 +179,40 @@ Skills that process external Markdown/HTML should treat content as untrusted:
|
|||||||
- Cookies cached in data directory
|
- Cookies cached in data directory
|
||||||
- Force refresh: `--login` flag
|
- Force refresh: `--login` flag
|
||||||
|
|
||||||
|
`baoyu-image-gen` uses official API keys configured in EXTEND.md (loaded at Step 0 before generation).
|
||||||
|
|
||||||
## Plugin Configuration
|
## Plugin Configuration
|
||||||
|
|
||||||
`.claude-plugin/marketplace.json` defines plugin metadata and skill paths. Version follows semver.
|
`.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
|
## Skill Loading Rules
|
||||||
|
|
||||||
**IMPORTANT**: When working in this project, follow these 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 |
|
| **References** | Keep one level deep from SKILL.md; avoid nested references |
|
||||||
| **No time-sensitive info** | Avoid dates/versions that become outdated |
|
| **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
|
### 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>`
|
- Directory name: `baoyu-<name>`
|
||||||
- SKILL.md `name` field: `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
|
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
|
||||||
4. **Choose the appropriate category** and register in `marketplace.json`:
|
4. **Choose the appropriate category** and register in `marketplace.json`:
|
||||||
- `content-skills`: For content generation/publishing (images, slides, posts)
|
- `content-skills`: For content generation/publishing (images, slides, posts)
|
||||||
- `ai-generation-skills`: For AI backend capabilities
|
- `ai-generation-skills`: For AI backend capabilities
|
||||||
- `utility-skills`: For helper tools (conversion, compression)
|
- `utility-skills`: For helper tools (conversion, compression)
|
||||||
- If none fit, create a new category with descriptive name
|
- 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
|
### Choosing a Category
|
||||||
|
|
||||||
| If your skill... | Use category |
|
| If your skill... | Use category |
|
||||||
|------------------|--------------|
|
|------------------|--------------|
|
||||||
| Generates visual content (images, slides, comics) | `content-skills` |
|
| Generates visual content (images, slides, comics, infographics) | `content-skills` |
|
||||||
| Publishes to platforms (X, WeChat, etc.) | `content-skills` |
|
| Publishes to platforms (X, WeChat, Weibo, etc.) | `content-skills` |
|
||||||
| Provides AI generation backend | `ai-generation-skills` |
|
| Provides AI generation backend (official API or reverse-engineered) | `ai-generation-skills` |
|
||||||
| Converts or processes content | `utility-skills` |
|
| Converts or processes content (URL→md, md→html, format) | `utility-skills` |
|
||||||
| Compresses or optimizes files | `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:
|
**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`)
|
- `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.
|
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
|
||||||
|
|
||||||
**Agent Execution Instructions**:
|
**Agent Execution Instructions**:
|
||||||
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
1. Determine this SKILL.md file's directory path as `{baseDir}`
|
||||||
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
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
|
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 Reference**:
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -278,7 +342,9 @@ Every SKILL.md with scripts MUST include this section after Usage:
|
|||||||
| `scripts/other.ts` | Other functionality |
|
| `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
|
### Progressive Disclosure
|
||||||
|
|
||||||
@@ -327,7 +393,7 @@ Use this template when implementing image generation in skills:
|
|||||||
### Step N: Generate Images
|
### Step N: Generate Images
|
||||||
|
|
||||||
**Skill Selection**:
|
**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
|
2. Read selected skill's SKILL.md for parameter reference
|
||||||
3. If multiple skills available, ask user to choose
|
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)
|
- Prompt file path (or inline prompt)
|
||||||
- Output image path
|
- Output image path
|
||||||
- Any skill-specific parameters (refer to skill's SKILL.md)
|
- 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"
|
3. After each image, output progress: "Generated X/N"
|
||||||
4. On failure, auto-retry once before reporting error
|
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
|
### Output Path Convention
|
||||||
|
|
||||||
Each session creates an independent directory. Even the same source file generates a new directory per session.
|
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)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
SKILLS_DIR="${ROOT_DIR}/skills"
|
SKILLS_DIR="${ROOT_DIR}/skills"
|
||||||
|
|
||||||
if command -v clawhub >/dev/null 2>&1; then
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
CLAWHUB_CMD=(clawhub)
|
echo "Error: node is required."
|
||||||
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"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -18,4 +13,4 @@ if [ "$#" -eq 0 ]; then
|
|||||||
set -- --all
|
set -- --all
|
||||||
fi
|
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) => {
|
if (import.meta.main ?? (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? ''))) {
|
||||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
await main().catch((err) => {
|
||||||
process.exit(1);
|
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}`,
|
`--user-data-dir=${profileDir}`,
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
'--no-default-browser-check',
|
'--no-default-browser-check',
|
||||||
'--disable-blink-features=AutomationControlled',
|
|
||||||
'--start-maximized',
|
'--start-maximized',
|
||||||
X_ARTICLES_URL,
|
X_ARTICLES_URL,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export async function postToX(options: XBrowserOptions): Promise<void> {
|
|||||||
`--user-data-dir=${profileDir}`,
|
`--user-data-dir=${profileDir}`,
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
'--no-default-browser-check',
|
'--no-default-browser-check',
|
||||||
'--disable-blink-features=AutomationControlled',
|
|
||||||
'--start-maximized',
|
'--start-maximized',
|
||||||
X_COMPOSE_URL,
|
X_COMPOSE_URL,
|
||||||
], { stdio: 'ignore' });
|
], { stdio: 'ignore' });
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export async function quotePost(options: QuoteOptions): Promise<void> {
|
|||||||
`--user-data-dir=${profileDir}`,
|
`--user-data-dir=${profileDir}`,
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
'--no-default-browser-check',
|
'--no-default-browser-check',
|
||||||
'--disable-blink-features=AutomationControlled',
|
|
||||||
'--start-maximized',
|
'--start-maximized',
|
||||||
tweetUrl,
|
tweetUrl,
|
||||||
], { stdio: 'ignore' });
|
], { stdio: 'ignore' });
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export async function postVideoToX(options: XVideoOptions): Promise<void> {
|
|||||||
`--user-data-dir=${profileDir}`,
|
`--user-data-dir=${profileDir}`,
|
||||||
'--no-first-run',
|
'--no-first-run',
|
||||||
'--no-default-browser-check',
|
'--no-default-browser-check',
|
||||||
'--disable-blink-features=AutomationControlled',
|
|
||||||
'--start-maximized',
|
'--start-maximized',
|
||||||
X_COMPOSE_URL,
|
X_COMPOSE_URL,
|
||||||
], { stdio: 'ignore' });
|
], { stdio: 'ignore' });
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ Before translating chunks:
|
|||||||
- Splits at markdown block boundaries to preserve structure
|
- Splits at markdown block boundaries to preserve structure
|
||||||
- If a single block exceeds the threshold, falls back to line splitting, then word splitting
|
- If a single block exceeds the threshold, falls back to line splitting, then word splitting
|
||||||
4. **Assemble translation prompt**:
|
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)
|
- Save as `02-prompt.md` in the output directory (shared context only, no task instructions)
|
||||||
5. **Draft translation via subagents** (if Agent tool available):
|
5. **Draft translation via subagents** (if Agent tool available):
|
||||||
- Spawn one subagent **per chunk**, all in parallel (Part 2 of the template)
|
- 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
|
- **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
|
- **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
|
- **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
|
#### Quick Mode
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ Translate directly → save to `translation.md`. No analysis file, but still app
|
|||||||
#### Normal Mode
|
#### Normal Mode
|
||||||
|
|
||||||
1. **Analyze** → `01-analysis.md` (domain, tone, audience, terminology, reader comprehension challenges, figurative language & metaphor mapping)
|
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`
|
3. **Translate** (following `02-prompt.md`) → `translation.md`
|
||||||
|
|
||||||
After completion, prompt user: "Translation saved. To further review and polish, reply **继续润色** or **refine**."
|
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
|
## 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).
|
This prompt is used by the subagent (chunked) or by the main agent itself (non-chunked).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user