mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-15 06:59:48 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 661c74cce4 | |||
| 82445f2a39 | |||
| df9ce95c46 | |||
| 88b433d565 | |||
| 5acef7151b | |||
| e4d4ec8334 | |||
| 4935dc8b1a | |||
| c43ed57ffc | |||
| d96368492e | |||
| cfc647fd38 | |||
| 5144335916 | |||
| fa82c3d910 | |||
| 90730bc551 | |||
| 154ccfe3ff | |||
| 93efa5aeb1 | |||
| 40ccbed74a | |||
| fbf2bc02c5 | |||
| a37c80e142 | |||
| 2e68aa64a4 | |||
| a702513487 | |||
| 059ebc759c | |||
| d4a5a34865 | |||
| b3b2f8ea36 | |||
| f19ed00cb2 | |||
| 8e58855047 | |||
| 6b2b28b421 | |||
| 0df63c3a0b | |||
| 7834d632e0 | |||
| ca9a0a1404 | |||
| 366e7b5403 | |||
| 709e026be1 |
@@ -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.55.0"
|
"version": "1.59.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -161,3 +161,6 @@ posts/
|
|||||||
*.ipr
|
*.ipr
|
||||||
|
|
||||||
.claude/skills/baoyu-skill-evolution
|
.claude/skills/baoyu-skill-evolution
|
||||||
|
# ClawHub local state (current and legacy directory names from the official CLI)
|
||||||
|
.clawhub/
|
||||||
|
.clawdhub/
|
||||||
|
|||||||
@@ -2,6 +2,57 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.59.0 - 2026-03-09
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: add batch parallel image generation and provider-level throttling (by @SeamoonAO)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-image-gen`: restore Google as default provider when multiple keys available
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Improve skill documentation clarity (by @SeamoonAO)
|
||||||
|
|
||||||
|
## 1.58.0 - 2026-03-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add XDG config path support for EXTEND.md (by @liby)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: surface agent-browser startup errors
|
||||||
|
- `baoyu-post-to-wechat`: harden agent-browser command and eval handling (by @luojiyin1987)
|
||||||
|
- `baoyu-image-gen`: use execFileSync for google curl requests (by @luojiyin1987)
|
||||||
|
- `baoyu-format-markdown`: use spawnSync for autocorrect command (by @luojiyin1987)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Fix CLAUDE dependency statement (by @luojiyin1987)
|
||||||
|
- Add markdown-to-html to README utility skills (by @luojiyin1987)
|
||||||
|
|
||||||
|
## 1.57.0 - 2026-03-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add ClawHub/OpenClaw publishing support with sync script and README documentation
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- Add openclaw metadata to all skill frontmatter for ClawHub registry compatibility
|
||||||
|
- Rename `SKILL_DIR` to `baseDir` across all skills for consistency
|
||||||
|
- `baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`: dynamic script path in usage display
|
||||||
|
- `baoyu-comic`, `baoyu-xhs-images`: use skill interface instead of direct script invocation for image generation
|
||||||
|
|
||||||
|
## 1.56.1 - 2026-03-08
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-weibo`: simplify article image insertion with Backspace-based placeholder deletion for ProseMirror compatibility
|
||||||
|
|
||||||
|
## 1.56.0 - 2026-03-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-article-illustrator`: preset-first selection flow with categorized style presets by content type
|
||||||
|
- `baoyu-xhs-images`: streamline workflow from 6 to 4 steps with Smart Confirm (Quick/Customize/Detailed paths)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: improve image upload reliability with file chooser interception and fallback
|
||||||
|
|
||||||
## 1.55.0 - 2026-03-08
|
## 1.55.0 - 2026-03-08
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,57 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.59.0 - 2026-03-09
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:新增批量并行图片生成和提供商级别限流 (by @SeamoonAO)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-image-gen`:修复多个 API key 可用时恢复 Google 为默认提供商
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- 改进技能文档清晰度 (by @SeamoonAO)
|
||||||
|
|
||||||
|
## 1.58.0 - 2026-03-08
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- 新增 EXTEND.md 的 XDG 配置路径支持 (by @liby)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:暴露 agent-browser 启动错误信息
|
||||||
|
- `baoyu-post-to-wechat`:加固 agent-browser 命令和 eval 处理 (by @luojiyin1987)
|
||||||
|
- `baoyu-image-gen`:使用 execFileSync 替代 shell 执行 Google curl 请求 (by @luojiyin1987)
|
||||||
|
- `baoyu-format-markdown`:使用 spawnSync 替代 shell 执行 autocorrect 命令 (by @luojiyin1987)
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- 修正 CLAUDE 依赖说明 (by @luojiyin1987)
|
||||||
|
- 将 markdown-to-html 添加到 README 工具技能列表 (by @luojiyin1987)
|
||||||
|
|
||||||
|
## 1.57.0 - 2026-03-08
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- 新增 ClawHub/OpenClaw 发布支持,包含同步脚本和 README 文档
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- 为所有 skill 前言添加 openclaw 元数据,兼容 ClawHub 注册表
|
||||||
|
- 全部 skill 中将 `SKILL_DIR` 统一重命名为 `baseDir`
|
||||||
|
- `baoyu-danger-gemini-web`、`baoyu-danger-x-to-markdown`:使用动态脚本路径显示用法
|
||||||
|
- `baoyu-comic`、`baoyu-xhs-images`:通过 skill 接口调用图片生成,不再直接调用脚本
|
||||||
|
|
||||||
|
## 1.56.1 - 2026-03-08
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-weibo`:简化头条文章图片插入逻辑,使用 Backspace 按键替代复杂的 deleteContents 方案,兼容 ProseMirror 编辑器
|
||||||
|
|
||||||
|
## 1.56.0 - 2026-03-08
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-article-illustrator`:预设优先选择流程,按内容类型分类的风格预设
|
||||||
|
- `baoyu-xhs-images`:精简工作流从 6 步到 4 步,新增智能确认(快速/自定义/详细三种路径)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:通过文件选择器拦截改进图片上传可靠性
|
||||||
|
|
||||||
## 1.55.0 - 2026-03-08
|
## 1.55.0 - 2026-03-08
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ ${BUN_X} skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md
|
|||||||
|
|
||||||
- **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 and `baoyu-post-to-x` automation
|
||||||
- **No npm packages**: Self-contained TypeScript, no external dependencies
|
- **npm packages (per skill)**: Some skill subprojects include `package.json`/lockfiles and require dependency installation in their own `scripts/` directories
|
||||||
|
|
||||||
## Chrome Profile (Unified)
|
## Chrome Profile (Unified)
|
||||||
|
|
||||||
@@ -472,22 +472,27 @@ Check EXTEND.md existence (priority order):
|
|||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/<skill-name>/EXTEND.md && echo "project"
|
test -f .baoyu-skills/<skill-name>/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/<skill-name>/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/<skill-name>/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/<skill-name>/EXTEND.md" && echo "user"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
\`\`\`powershell
|
\`\`\`powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/<skill-name>/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/<skill-name>/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/<skill-name>/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/<skill-name>/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/<skill-name>/EXTEND.md") { "user" }
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
┌────────────────────────────────────────────┬───────────────────┐
|
┌────────────────────────────────────────────────────────┬──────────────────────────┐
|
||||||
│ Path │ Location │
|
│ Path │ Location │
|
||||||
├────────────────────────────────────────────┼───────────────────┤
|
├────────────────────────────────────────────────────────┼──────────────────────────┤
|
||||||
│ .baoyu-skills/<skill-name>/EXTEND.md │ Project directory │
|
│ .baoyu-skills/<skill-name>/EXTEND.md │ Project directory │
|
||||||
├────────────────────────────────────────────┼───────────────────┤
|
├────────────────────────────────────────────────────────┼──────────────────────────┤
|
||||||
│ $HOME/.baoyu-skills/<skill-name>/EXTEND.md │ User home │
|
│ $XDG_CONFIG_HOME/baoyu-skills/<skill-name>/EXTEND.md │ XDG config (~/.config) │
|
||||||
└────────────────────────────────────────────┴───────────────────┘
|
├────────────────────────────────────────────────────────┼──────────────────────────┤
|
||||||
|
│ $HOME/.baoyu-skills/<skill-name>/EXTEND.md │ User home (legacy) │
|
||||||
|
└────────────────────────────────────────────────────────┴──────────────────────────┘
|
||||||
|
|
||||||
┌───────────┬───────────────────────────────────────────────────────────────────────────┐
|
┌───────────┬───────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Result │ Action │
|
│ Result │ Action │
|
||||||
@@ -523,5 +528,6 @@ Custom configurations via EXTEND.md. See **Preferences** section for paths and s
|
|||||||
**Notes**:
|
**Notes**:
|
||||||
- Replace `<skill-name>` with actual skill name (e.g., `baoyu-cover-image`)
|
- Replace `<skill-name>` with actual skill name (e.g., `baoyu-cover-image`)
|
||||||
- Use `$HOME` instead of `~` for cross-platform compatibility (macOS/Linux/WSL/PowerShell)
|
- Use `$HOME` instead of `~` for cross-platform compatibility (macOS/Linux/WSL/PowerShell)
|
||||||
|
- `$XDG_CONFIG_HOME` defaults to `~/.config` when unset
|
||||||
- Use `test -f` (Bash) or `Test-Path` (PowerShell) for explicit file existence check
|
- Use `test -f` (Bash) or `Test-Path` (PowerShell) for explicit file existence check
|
||||||
- ASCII tables for clear visual formatting
|
- ASCII tables for clear visual formatting
|
||||||
|
|||||||
@@ -17,6 +17,27 @@ Skills shared by Baoyu for improving daily work efficiency with Claude Code.
|
|||||||
npx skills add jimliu/baoyu-skills
|
npx skills add jimliu/baoyu-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Publish to ClawHub / OpenClaw
|
||||||
|
|
||||||
|
This repository now supports publishing each `skills/baoyu-*` directory as an individual ClawHub skill.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview what would be published
|
||||||
|
./scripts/sync-clawhub.sh --dry-run
|
||||||
|
|
||||||
|
# Publish all changed skills from ./skills
|
||||||
|
./scripts/sync-clawhub.sh --all
|
||||||
|
```
|
||||||
|
|
||||||
|
ClawHub installs skills individually, not as one marketplace bundle. After publishing, users can install specific skills such as:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawhub install baoyu-image-gen
|
||||||
|
clawhub install baoyu-markdown-to-html
|
||||||
|
```
|
||||||
|
|
||||||
|
Publishing to ClawHub releases the published skill under `MIT-0`, per ClawHub's registry rules.
|
||||||
|
|
||||||
### Register as Plugin Marketplace
|
### Register as Plugin Marketplace
|
||||||
|
|
||||||
Run the following command in Claude Code:
|
Run the following command in Claude Code:
|
||||||
@@ -55,7 +76,7 @@ Simply tell Claude Code:
|
|||||||
|--------|-------------|--------|
|
|--------|-------------|--------|
|
||||||
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
|
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
|
||||||
| **ai-generation-skills** | AI-powered generation backends | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
|
| **ai-generation-skills** | AI-powered generation backends | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
|
||||||
| **utility-skills** | Utility tools for content processing | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [translate](#baoyu-translate) |
|
| **utility-skills** | Utility tools for content processing | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [markdown-to-html](#baoyu-markdown-to-html), [translate](#baoyu-translate) |
|
||||||
|
|
||||||
## Update Skills
|
## Update Skills
|
||||||
|
|
||||||
@@ -777,6 +798,21 @@ Format plain text or markdown files with proper frontmatter, titles, summaries,
|
|||||||
| Code/commands | `` `inline` `` or ` ```block``` ` |
|
| Code/commands | `` `inline` `` or ` ```block``` ` |
|
||||||
| Quotes | `>` blockquote |
|
| Quotes | `>` blockquote |
|
||||||
|
|
||||||
|
#### baoyu-markdown-to-html
|
||||||
|
|
||||||
|
Convert markdown files into styled HTML with WeChat-compatible themes, syntax highlighting, and optional bottom citations for external links.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic conversion
|
||||||
|
/baoyu-markdown-to-html article.md
|
||||||
|
|
||||||
|
# Theme + color
|
||||||
|
/baoyu-markdown-to-html article.md --theme grace --color red
|
||||||
|
|
||||||
|
# Convert ordinary external links to bottom citations
|
||||||
|
/baoyu-markdown-to-html article.md --cite
|
||||||
|
```
|
||||||
|
|
||||||
#### baoyu-translate
|
#### baoyu-translate
|
||||||
|
|
||||||
Translate articles and documents between languages with three modes: quick (direct), normal (analysis-informed), and refined (full publication-quality workflow with review and polish).
|
Translate articles and documents between languages with three modes: quick (direct), normal (analysis-informed), and refined (full publication-quality workflow with review and polish).
|
||||||
|
|||||||
+37
-1
@@ -17,6 +17,27 @@
|
|||||||
npx skills add jimliu/baoyu-skills
|
npx skills add jimliu/baoyu-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 发布到 ClawHub / OpenClaw
|
||||||
|
|
||||||
|
现在这个仓库支持把每个 `skills/baoyu-*` 目录作为独立 ClawHub skill 发布。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 预览将要发布的变更
|
||||||
|
./scripts/sync-clawhub.sh --dry-run
|
||||||
|
|
||||||
|
# 发布 ./skills 下所有已变更的 skill
|
||||||
|
./scripts/sync-clawhub.sh --all
|
||||||
|
```
|
||||||
|
|
||||||
|
ClawHub 按“单个 skill”安装,不是把整个 marketplace 一次性装进去。发布后,用户可以按需安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawhub install baoyu-image-gen
|
||||||
|
clawhub install baoyu-markdown-to-html
|
||||||
|
```
|
||||||
|
|
||||||
|
根据 ClawHub 的 registry 规则,发布到 ClawHub 的 skill 会以 `MIT-0` 许可分发。
|
||||||
|
|
||||||
### 注册插件市场
|
### 注册插件市场
|
||||||
|
|
||||||
在 Claude Code 中运行:
|
在 Claude Code 中运行:
|
||||||
@@ -55,7 +76,7 @@ npx skills add jimliu/baoyu-skills
|
|||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
|
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
|
||||||
| **ai-generation-skills** | AI 生成后端 | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
|
| **ai-generation-skills** | AI 生成后端 | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
|
||||||
| **utility-skills** | 内容处理工具 | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [translate](#baoyu-translate) |
|
| **utility-skills** | 内容处理工具 | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [markdown-to-html](#baoyu-markdown-to-html), [translate](#baoyu-translate) |
|
||||||
|
|
||||||
## 更新技能
|
## 更新技能
|
||||||
|
|
||||||
@@ -777,6 +798,21 @@ AI 驱动的生成后端。
|
|||||||
| 代码/命令 | `` `行内` `` 或 ` ```代码块``` ` |
|
| 代码/命令 | `` `行内` `` 或 ` ```代码块``` ` |
|
||||||
| 引用 | `>` 引用块 |
|
| 引用 | `>` 引用块 |
|
||||||
|
|
||||||
|
#### baoyu-markdown-to-html
|
||||||
|
|
||||||
|
将 Markdown 文件转换为样式化 HTML,支持微信公众号兼容主题、代码高亮,以及可选的外链底部引用。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基础转换
|
||||||
|
/baoyu-markdown-to-html article.md
|
||||||
|
|
||||||
|
# 主题 + 颜色
|
||||||
|
/baoyu-markdown-to-html article.md --theme grace --color red
|
||||||
|
|
||||||
|
# 将普通外链转换为文末引用
|
||||||
|
/baoyu-markdown-to-html article.md --cite
|
||||||
|
```
|
||||||
|
|
||||||
#### baoyu-translate
|
#### baoyu-translate
|
||||||
|
|
||||||
三模式翻译技能:快速(直接翻译)、标准(分析后翻译)、精翻(完整出版级工作流,含审校与润色)。
|
三模式翻译技能:快速(直接翻译)、标准(分析后翻译)、精翻(完整出版级工作流,含审校与润色)。
|
||||||
|
|||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
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"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
set -- --all
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "${CLAWHUB_CMD[@]}" sync --root "${SKILLS_DIR}" "$@"
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-article-illustrator
|
name: baoyu-article-illustrator
|
||||||
description: Analyzes article structure, identifies positions requiring visual aids, generates illustrations with Type × Style two-dimension approach. Use when user asks to "illustrate article", "add images", "generate images for article", or "为文章配图".
|
description: Analyzes article structure, identifies positions requiring visual aids, generates illustrations with Type × Style two-dimension approach. Use when user asks to "illustrate article", "add images", "generate images for article", or "为文章配图".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-article-illustrator
|
||||||
---
|
---
|
||||||
|
|
||||||
# Article Illustrator
|
# Article Illustrator
|
||||||
@@ -51,12 +55,15 @@ See [references/styles.md](references/styles.md) for Core Styles, full gallery,
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-article-illustrator/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-article-illustrator/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -82,13 +89,13 @@ Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--a
|
|||||||
|
|
||||||
### Step 3: Confirm Settings ⚠️
|
### Step 3: Confirm Settings ⚠️
|
||||||
|
|
||||||
**ONE AskUserQuestion, max 4 Qs. Q1-Q3 REQUIRED.**
|
**ONE AskUserQuestion, max 4 Qs. Q1-Q2 REQUIRED. Q3 required unless preset chosen.**
|
||||||
|
|
||||||
| Q | Options |
|
| Q | Options |
|
||||||
|---|---------|
|
|---|---------|
|
||||||
| **Q1: Type** | [Recommended], infographic, scene, flowchart, comparison, framework, timeline, mixed |
|
| **Q1: Preset or Type** | [Recommended preset], [alt preset], or manual: infographic, scene, flowchart, comparison, framework, timeline, mixed |
|
||||||
| **Q2: Density** | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
|
| **Q2: Density** | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
|
||||||
| **Q3: Style** | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, poster, Other |
|
| **Q3: Style** | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, poster, Other — **skip if preset chosen** |
|
||||||
| Q4: Language | When article language ≠ EXTEND.md setting |
|
| Q4: Language | When article language ≠ EXTEND.md setting |
|
||||||
|
|
||||||
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
|
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
|
||||||
@@ -111,6 +118,8 @@ Full template: [references/workflow.md](references/workflow.md#step-4-generate-o
|
|||||||
|
|
||||||
⛔ **BLOCKING: Prompt files MUST be saved before ANY image generation.**
|
⛔ **BLOCKING: Prompt files MUST be saved before ANY image generation.**
|
||||||
|
|
||||||
|
**Execution strategy**: When multiple illustrations have saved prompt files and the task is now plain generation, prefer `baoyu-image-gen` batch mode (`build-batch.ts` → `--batchfile`) over spawning subagents. Use subagents only when each image still needs separate prompt iteration or creative exploration.
|
||||||
|
|
||||||
1. For each illustration, create a prompt file per [references/prompt-construction.md](references/prompt-construction.md)
|
1. For each illustration, create a prompt file per [references/prompt-construction.md](references/prompt-construction.md)
|
||||||
2. Save to `prompts/NN-{type}-{slug}.md` with YAML frontmatter
|
2. Save to `prompts/NN-{type}-{slug}.md` with YAML frontmatter
|
||||||
3. Prompts **MUST** use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT)
|
3. Prompts **MUST** use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT)
|
||||||
|
|||||||
@@ -2,26 +2,67 @@
|
|||||||
|
|
||||||
`--preset X` expands to a type + style combination. Users can override either dimension.
|
`--preset X` expands to a type + style combination. Users can override either dimension.
|
||||||
|
|
||||||
| --preset | Type | Style |
|
## By Category
|
||||||
|----------|------|-------|
|
|
||||||
| `tech-explainer` | `infographic` | `blueprint` |
|
### Technical & Engineering
|
||||||
| `knowledge-base` | `infographic` | `vector-illustration` |
|
|
||||||
| `saas-guide` | `infographic` | `notion` |
|
| --preset | Type | Style | Best For |
|
||||||
| `data-report` | `infographic` | `editorial` |
|
|----------|------|-------|----------|
|
||||||
| `science-paper` | `infographic` | `scientific` |
|
| `tech-explainer` | `infographic` | `blueprint` | API docs, system metrics, technical deep-dives |
|
||||||
| `tutorial` | `flowchart` | `vector-illustration` |
|
| `system-design` | `framework` | `blueprint` | Architecture diagrams, system design |
|
||||||
| `process-flow` | `flowchart` | `notion` |
|
| `architecture` | `framework` | `vector-illustration` | Component relationships, module structure |
|
||||||
| `system-design` | `framework` | `blueprint` |
|
| `science-paper` | `infographic` | `scientific` | Research findings, lab results, academic |
|
||||||
| `architecture` | `framework` | `vector-illustration` |
|
|
||||||
| `versus` | `comparison` | `vector-illustration` |
|
### Knowledge & Education
|
||||||
| `business-compare` | `comparison` | `elegant` |
|
|
||||||
| `storytelling` | `scene` | `warm` |
|
| --preset | Type | Style | Best For |
|
||||||
| `lifestyle` | `scene` | `watercolor` |
|
|----------|------|-------|----------|
|
||||||
| `history` | `timeline` | `elegant` |
|
| `knowledge-base` | `infographic` | `vector-illustration` | Concept explainers, tutorials, how-to |
|
||||||
| `evolution` | `timeline` | `warm` |
|
| `saas-guide` | `infographic` | `notion` | Product guides, SaaS docs, tool walkthroughs |
|
||||||
| `opinion-piece` | `scene` | `screen-print` |
|
| `tutorial` | `flowchart` | `vector-illustration` | Step-by-step tutorials, setup guides |
|
||||||
| `editorial-poster` | `comparison` | `screen-print` |
|
| `process-flow` | `flowchart` | `notion` | Workflow documentation, onboarding flows |
|
||||||
| `cinematic` | `scene` | `screen-print` |
|
|
||||||
|
### Data & Analysis
|
||||||
|
|
||||||
|
| --preset | Type | Style | Best For |
|
||||||
|
|----------|------|-------|----------|
|
||||||
|
| `data-report` | `infographic` | `editorial` | Data journalism, metrics reports, dashboards |
|
||||||
|
| `versus` | `comparison` | `vector-illustration` | Tech comparisons, framework shootouts |
|
||||||
|
| `business-compare` | `comparison` | `elegant` | Product evaluations, strategy options |
|
||||||
|
|
||||||
|
### Narrative & Creative
|
||||||
|
|
||||||
|
| --preset | Type | Style | Best For |
|
||||||
|
|----------|------|-------|----------|
|
||||||
|
| `storytelling` | `scene` | `warm` | Personal essays, reflections, growth stories |
|
||||||
|
| `lifestyle` | `scene` | `watercolor` | Travel, wellness, lifestyle, creative |
|
||||||
|
| `history` | `timeline` | `elegant` | Historical overviews, milestones |
|
||||||
|
| `evolution` | `timeline` | `warm` | Progress narratives, growth journeys |
|
||||||
|
|
||||||
|
### Editorial & Opinion
|
||||||
|
|
||||||
|
| --preset | Type | Style | Best For |
|
||||||
|
|----------|------|-------|----------|
|
||||||
|
| `opinion-piece` | `scene` | `screen-print` | Op-eds, commentary, critical essays |
|
||||||
|
| `editorial-poster` | `comparison` | `screen-print` | Debate, contrasting viewpoints |
|
||||||
|
| `cinematic` | `scene` | `screen-print` | Dramatic narratives, cultural essays |
|
||||||
|
|
||||||
|
## Content Type → Preset Recommendations
|
||||||
|
|
||||||
|
Use this table during Step 3 to recommend presets based on Step 2 content analysis:
|
||||||
|
|
||||||
|
| Content Type (Step 2) | Primary Preset | Alternatives |
|
||||||
|
|------------------------|----------------|--------------|
|
||||||
|
| Technical | `tech-explainer` | `system-design`, `architecture` |
|
||||||
|
| Tutorial | `tutorial` | `process-flow`, `knowledge-base` |
|
||||||
|
| Methodology / Framework | `system-design` | `architecture`, `process-flow` |
|
||||||
|
| Data / Metrics | `data-report` | `versus`, `tech-explainer` |
|
||||||
|
| Comparison / Review | `versus` | `business-compare`, `editorial-poster` |
|
||||||
|
| Narrative / Personal | `storytelling` | `lifestyle`, `evolution` |
|
||||||
|
| Opinion / Editorial | `opinion-piece` | `cinematic`, `editorial-poster` |
|
||||||
|
| Historical / Timeline | `history` | `evolution` |
|
||||||
|
| Academic / Research | `science-paper` | `tech-explainer`, `data-report` |
|
||||||
|
| SaaS / Product | `saas-guide` | `knowledge-base`, `process-flow` |
|
||||||
|
|
||||||
## Override Examples
|
## Override Examples
|
||||||
|
|
||||||
|
|||||||
@@ -88,12 +88,15 @@ Check preferences and existing state, then ask ALL needed questions in ONE AskUs
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-article-illustrator/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-article-illustrator/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -165,9 +168,16 @@ For each reference image:
|
|||||||
|
|
||||||
**Do NOT skip.** Use ONE AskUserQuestion call with max 4 questions. **Q1, Q2, Q3 are ALL REQUIRED.**
|
**Do NOT skip.** Use ONE AskUserQuestion call with max 4 questions. **Q1, Q2, Q3 are ALL REQUIRED.**
|
||||||
|
|
||||||
### Q1: Illustration Type ⚠️ REQUIRED
|
### Q1: Preset or Type ⚠️ REQUIRED
|
||||||
- [Recommended based on analysis] (Recommended)
|
|
||||||
- infographic / scene / flowchart / comparison / framework / timeline / mixed
|
Based on Step 2 content analysis, recommend a preset first (sets both type & style). Look up [style-presets.md](style-presets.md) "Content Type → Preset Recommendations" table.
|
||||||
|
|
||||||
|
- [Recommended preset] — [brief: type + style + why] (Recommended)
|
||||||
|
- [Alternative preset] — [brief]
|
||||||
|
- Or choose type manually: infographic / scene / flowchart / comparison / framework / timeline / mixed
|
||||||
|
|
||||||
|
**If user picks a preset → skip Q3** (type & style both resolved).
|
||||||
|
**If user picks a type → Q3 is REQUIRED.**
|
||||||
|
|
||||||
### Q2: Density ⚠️ REQUIRED - DO NOT SKIP
|
### Q2: Density ⚠️ REQUIRED - DO NOT SKIP
|
||||||
- minimal (1-2) - Core concepts only
|
- minimal (1-2) - Core concepts only
|
||||||
@@ -175,7 +185,7 @@ For each reference image:
|
|||||||
- per-section - At least 1 per section/chapter (Recommended)
|
- per-section - At least 1 per section/chapter (Recommended)
|
||||||
- rich (6+) - Comprehensive coverage
|
- rich (6+) - Comprehensive coverage
|
||||||
|
|
||||||
### Q3: Style ⚠️ REQUIRED (ALWAYS ask, even with preferred_style in EXTEND.md)
|
### Q3: Style ⚠️ REQUIRED (skip if preset chosen in Q1)
|
||||||
|
|
||||||
If EXTEND.md has `preferred_style`:
|
If EXTEND.md has `preferred_style`:
|
||||||
- [Custom style name + brief description] (Recommended)
|
- [Custom style name + brief description] (Recommended)
|
||||||
@@ -191,14 +201,14 @@ If no `preferred_style` (present Core Styles first):
|
|||||||
|
|
||||||
**Core Styles** (simplified selection):
|
**Core Styles** (simplified selection):
|
||||||
|
|
||||||
| Core Style | Best For |
|
| Core Style | Maps To | Best For |
|
||||||
|------------|----------|
|
|------------|---------|----------|
|
||||||
| `minimal-flat` | General, knowledge sharing, SaaS |
|
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
|
||||||
| `sci-fi` | AI, frontier tech, system design |
|
| `sci-fi` | blueprint | AI, frontier tech, system design |
|
||||||
| `hand-drawn` | Relaxed, reflective, casual |
|
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual |
|
||||||
| `editorial` | Processes, data, journalism |
|
| `editorial` | editorial | Processes, data, journalism |
|
||||||
| `scene` | Narratives, emotional, lifestyle |
|
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
|
||||||
| `poster` | Opinion, editorial, cultural, cinematic |
|
| `poster` | screen-print | Opinion, editorial, cultural, cinematic |
|
||||||
|
|
||||||
Style selection based on Type × Style compatibility matrix (styles.md).
|
Style selection based on Type × Style compatibility matrix (styles.md).
|
||||||
Full specs: `styles/<style>.md`
|
Full specs: `styles/<style>.md`
|
||||||
@@ -305,6 +315,10 @@ Prompt Files:
|
|||||||
|
|
||||||
**DO NOT** pass ad-hoc inline text to `--prompt` without first saving prompt files. The generation command should either use `--promptfiles prompts/NN-{type}-{slug}.md` or read the saved file content for `--prompt`.
|
**DO NOT** pass ad-hoc inline text to `--prompt` without first saving prompt files. The generation command should either use `--promptfiles prompts/NN-{type}-{slug}.md` or read the saved file content for `--prompt`.
|
||||||
|
|
||||||
|
**Execution choice**:
|
||||||
|
- If multiple illustrations already have saved prompt files and the task is now plain generation, prefer `baoyu-image-gen` batch mode (`build-batch.ts` -> `main.ts --batchfile`)
|
||||||
|
- Use subagents only when each illustration still needs separate prompt rewriting, style exploration, or other per-image reasoning before generation
|
||||||
|
|
||||||
**CRITICAL - References in Frontmatter**:
|
**CRITICAL - References in Frontmatter**:
|
||||||
- Only add `references` field if files ACTUALLY EXIST in `references/` directory
|
- Only add `references` field if files ACTUALLY EXIST in `references/` directory
|
||||||
- If style/palette was extracted verbally (no file), append info to prompt BODY instead
|
- If style/palette was extracted verbally (no file), append info to prompt BODY instead
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
type CliArgs = {
|
||||||
|
outlinePath: string | null;
|
||||||
|
promptsDir: string | null;
|
||||||
|
outputPath: string | null;
|
||||||
|
imagesDir: string | null;
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
aspectRatio: string;
|
||||||
|
quality: string;
|
||||||
|
jobs: number | null;
|
||||||
|
help: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OutlineEntry = {
|
||||||
|
index: number;
|
||||||
|
filename: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function printUsage(): void {
|
||||||
|
console.log(`Usage:
|
||||||
|
npx -y tsx scripts/build-batch.ts --outline outline.md --prompts prompts --output batch.json --images-dir attachments
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--outline <path> Path to outline.md
|
||||||
|
--prompts <path> Path to prompts directory
|
||||||
|
--output <path> Path to output batch.json
|
||||||
|
--images-dir <path> Directory for generated images
|
||||||
|
--provider <name> Provider for baoyu-image-gen batch tasks (default: replicate)
|
||||||
|
--model <id> Model for baoyu-image-gen batch tasks (default: google/nano-banana-pro)
|
||||||
|
--ar <ratio> Aspect ratio for all tasks (default: 16:9)
|
||||||
|
--quality <level> Quality for all tasks (default: 2k)
|
||||||
|
--jobs <count> Recommended worker count metadata (optional)
|
||||||
|
-h, --help Show help`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv: string[]): CliArgs {
|
||||||
|
const args: CliArgs = {
|
||||||
|
outlinePath: null,
|
||||||
|
promptsDir: null,
|
||||||
|
outputPath: null,
|
||||||
|
imagesDir: null,
|
||||||
|
provider: "replicate",
|
||||||
|
model: "google/nano-banana-pro",
|
||||||
|
aspectRatio: "16:9",
|
||||||
|
quality: "2k",
|
||||||
|
jobs: null,
|
||||||
|
help: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const current = argv[i]!;
|
||||||
|
if (current === "--outline") args.outlinePath = argv[++i] ?? null;
|
||||||
|
else if (current === "--prompts") args.promptsDir = argv[++i] ?? null;
|
||||||
|
else if (current === "--output") args.outputPath = argv[++i] ?? null;
|
||||||
|
else if (current === "--images-dir") args.imagesDir = argv[++i] ?? null;
|
||||||
|
else if (current === "--provider") args.provider = argv[++i] ?? args.provider;
|
||||||
|
else if (current === "--model") args.model = argv[++i] ?? args.model;
|
||||||
|
else if (current === "--ar") args.aspectRatio = argv[++i] ?? args.aspectRatio;
|
||||||
|
else if (current === "--quality") args.quality = argv[++i] ?? args.quality;
|
||||||
|
else if (current === "--jobs") {
|
||||||
|
const value = argv[++i];
|
||||||
|
args.jobs = value ? parseInt(value, 10) : null;
|
||||||
|
} else if (current === "--help" || current === "-h") {
|
||||||
|
args.help = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOutline(content: string): OutlineEntry[] {
|
||||||
|
const entries: OutlineEntry[] = [];
|
||||||
|
const blocks = content.split(/^## Illustration\s+/m).slice(1);
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const indexMatch = block.match(/^(\d+)/);
|
||||||
|
const filenameMatch = block.match(/\*\*Filename\*\*:\s*(.+)/);
|
||||||
|
if (indexMatch && filenameMatch) {
|
||||||
|
entries.push({
|
||||||
|
index: parseInt(indexMatch[1]!, 10),
|
||||||
|
filename: filenameMatch[1]!.trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findPromptFile(promptsDir: string, entry: OutlineEntry): Promise<string | null> {
|
||||||
|
const files = await readdir(promptsDir);
|
||||||
|
const prefix = String(entry.index).padStart(2, "0");
|
||||||
|
const match = files.find((f) => f.startsWith(prefix) && f.endsWith(".md"));
|
||||||
|
return match ? path.join(promptsDir, match) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (args.help) {
|
||||||
|
printUsage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.outlinePath) {
|
||||||
|
console.error("Error: --outline is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!args.promptsDir) {
|
||||||
|
console.error("Error: --prompts is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!args.outputPath) {
|
||||||
|
console.error("Error: --output is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outlineContent = await readFile(args.outlinePath, "utf8");
|
||||||
|
const entries = parseOutline(outlineContent);
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
console.error("No illustration entries found in outline.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const promptFile = await findPromptFile(args.promptsDir, entry);
|
||||||
|
if (!promptFile) {
|
||||||
|
console.error(`Warning: No prompt file found for illustration ${entry.index}, skipping.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageDir = args.imagesDir ?? path.dirname(args.outputPath);
|
||||||
|
tasks.push({
|
||||||
|
id: `illustration-${String(entry.index).padStart(2, "0")}`,
|
||||||
|
promptFiles: [promptFile],
|
||||||
|
image: path.join(imageDir, entry.filename),
|
||||||
|
provider: args.provider,
|
||||||
|
model: args.model,
|
||||||
|
ar: args.aspectRatio,
|
||||||
|
quality: args.quality,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: Record<string, unknown> = { tasks };
|
||||||
|
if (args.jobs) output.jobs = args.jobs;
|
||||||
|
|
||||||
|
await writeFile(args.outputPath, JSON.stringify(output, null, 2) + "\n");
|
||||||
|
console.log(`Batch file written: ${args.outputPath} (${tasks.length} tasks)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+21
-17
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-comic
|
name: baoyu-comic
|
||||||
description: Knowledge comic creator supporting multiple art styles and tones. Creates original educational comics with detailed panel layouts and sequential image generation. Use when user asks to create "知识漫画", "教育漫画", "biography comic", "tutorial comic", or "Logicomix-style comic".
|
description: Knowledge comic creator supporting multiple art styles and tones. Creates original educational comics with detailed panel layouts and sequential image generation. Use when user asks to create "知识漫画", "教育漫画", "biography comic", "tutorial comic", or "Logicomix-style comic".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-comic
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Knowledge Comic Creator
|
# Knowledge Comic Creator
|
||||||
@@ -104,9 +112,9 @@ Details: [references/auto-selection.md](references/auto-selection.md)
|
|||||||
**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. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Replace all `{baseDir}` in this document with the actual path
|
||||||
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
@@ -208,12 +216,11 @@ Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard →
|
|||||||
|
|
||||||
**7.1 Generate character sheet first**:
|
**7.1 Generate character sheet first**:
|
||||||
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||||
```bash
|
- Invoke an installed image generation skill such as `baoyu-image-gen`
|
||||||
# Use Reference Sheet Prompt from characters/characters.md
|
- Read that skill's `SKILL.md` and follow its documented interface rather than calling its scripts directly
|
||||||
${BUN_X} ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
- Use `characters/characters.md` as the prompt-file input
|
||||||
--promptfiles characters/characters.md \
|
- Save output to `characters/characters.png`
|
||||||
--image characters/characters.png --ar 4:3
|
- Use aspect ratio `4:3`
|
||||||
```
|
|
||||||
|
|
||||||
**Compress character sheet** (recommended):
|
**Compress character sheet** (recommended):
|
||||||
Compress to reduce token usage when used as reference image:
|
Compress to reduce token usage when used as reference image:
|
||||||
@@ -231,14 +238,11 @@ Compress to reduce token usage when used as reference image:
|
|||||||
**Backup rules for page generation**:
|
**Backup rules for page generation**:
|
||||||
- If prompt file exists: rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
- If prompt file exists: rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
||||||
- If image file exists: rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
- If image file exists: rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
- Invoke the installed image generation skill for each page
|
||||||
```bash
|
- Use `prompts/01-page-xxx.md` as the prompt-file input
|
||||||
# Example: ALWAYS include --ref for consistency
|
- Save output to `01-page-xxx.png`
|
||||||
${BUN_X} ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
- Use aspect ratio `3:4`
|
||||||
--promptfiles prompts/01-page-xxx.md \
|
- If the chosen skill supports reference images, pass `characters/characters.png` as `--ref`
|
||||||
--image 01-page-xxx.png --ar 3:4 \
|
|
||||||
--ref characters/characters.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**Full workflow details**: [references/workflow.md](references/workflow.md)
|
**Full workflow details**: [references/workflow.md](references/workflow.md)
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-comic/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-comic/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-comic/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-comic/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-comic/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-comic/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-comic/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-comic/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-comic/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-comic/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -433,14 +436,12 @@ With confirmed prompts from Step 5/6:
|
|||||||
|
|
||||||
**Strategy A: Using `--ref` parameter** (e.g., baoyu-image-gen)
|
**Strategy A: Using `--ref` parameter** (e.g., baoyu-image-gen)
|
||||||
|
|
||||||
```bash
|
- Read the chosen image generation skill's `SKILL.md`
|
||||||
# Each page generation MUST include --ref
|
- Invoke that installed skill via its documented interface, not by calling its scripts directly
|
||||||
${BUN_X} ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
- For every page, use `prompts/01-page-xxx.md` as the prompt-file input
|
||||||
--promptfiles prompts/01-page-xxx.md \
|
- Save output to `01-page-xxx.png`
|
||||||
--image 01-page-xxx.png \
|
- Use aspect ratio `3:4`
|
||||||
--ar 3:4 \
|
- Pass `characters/characters.png` as `--ref` on every page generation
|
||||||
--ref characters/characters.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**Strategy B: Embedding character descriptions in prompt**
|
**Strategy B: Embedding character descriptions in prompt**
|
||||||
|
|
||||||
@@ -478,7 +479,7 @@ If image generation skill supports `--sessionId`:
|
|||||||
After all images generated:
|
After all images generated:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/merge-to-pdf.ts <comic-dir>
|
${BUN_X} {baseDir}/scripts/merge-to-pdf.ts <comic-dir>
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates `{topic-slug}.pdf` with all pages as full-page images.
|
Creates `{topic-slug}.pdf` with all pages as full-page images.
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-compress-image
|
name: baoyu-compress-image
|
||||||
description: Compresses images to WebP (default) or PNG with automatic tool selection. Use when user asks to "compress image", "optimize image", "convert to webp", or reduce image file size.
|
description: Compresses images to WebP (default) or PNG with automatic tool selection. Use when user asks to "compress image", "optimize image", "convert to webp", or reduce image file size.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-compress-image
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Image Compressor
|
# Image Compressor
|
||||||
@@ -9,7 +17,7 @@ Compresses images using best available tool (sips → cwebp → ImageMagick →
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
Scripts in `scripts/` subdirectory. `${SKILL_DIR}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
Scripts in `scripts/` subdirectory. `{baseDir}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -22,12 +30,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-compress-image/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-compress-image/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-compress-image/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-compress-image/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-compress-image/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-compress-image/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,7 +63,7 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md") { "user" }
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <input> [options]
|
${BUN_X} {baseDir}/scripts/main.ts <input> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
@@ -71,16 +82,16 @@ ${BUN_X} ${SKILL_DIR}/scripts/main.ts <input> [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Single file → WebP (replaces original)
|
# Single file → WebP (replaces original)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png
|
${BUN_X} {baseDir}/scripts/main.ts image.png
|
||||||
|
|
||||||
# Keep PNG format
|
# Keep PNG format
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png -f png --keep
|
${BUN_X} {baseDir}/scripts/main.ts image.png -f png --keep
|
||||||
|
|
||||||
# Directory recursive
|
# Directory recursive
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts ./images/ -r -q 75
|
${BUN_X} {baseDir}/scripts/main.ts ./images/ -r -q 75
|
||||||
|
|
||||||
# JSON output
|
# JSON output
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts image.png --json
|
${BUN_X} {baseDir}/scripts/main.ts image.png --json
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**:
|
**Output**:
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-cover-image
|
name: baoyu-cover-image
|
||||||
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 10 color palettes and 7 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
|
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 10 color palettes and 7 rendering styles. Supports cinematic (2.35:1), widescreen (16:9), and square (1:1) aspects. Use when user asks to "generate cover image", "create article cover", or "make cover".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-cover-image
|
||||||
---
|
---
|
||||||
|
|
||||||
# Cover Image Generator
|
# Cover Image Generator
|
||||||
@@ -130,12 +134,15 @@ Check EXTEND.md existence (priority: project → user):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-cover-image/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-cover-image/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-cover-image/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-cover-image/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-cover-image/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-cover-image/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-cover-image/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-danger-gemini-web
|
name: baoyu-danger-gemini-web
|
||||||
description: Generates images and text via reverse-engineered Gemini Web API. Supports text generation, image generation from prompts, reference images for vision input, and multi-turn conversations. Use when other skills need image generation backend, or when user requests "generate image with Gemini", "Gemini text generation", or needs vision-capable AI generation.
|
description: Generates images and text via reverse-engineered Gemini Web API. Supports text generation, image generation from prompts, reference images for vision input, and multi-turn conversations. Use when other skills need image generation backend, or when user requests "generate image with Gemini", "Gemini text generation", or needs vision-capable AI generation.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-danger-gemini-web
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Gemini Web Client
|
# Gemini Web Client
|
||||||
@@ -12,10 +20,10 @@ Text/image generation via Gemini Web API. Supports reference images and multi-tu
|
|||||||
**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 |
|
||||||
@@ -49,12 +57,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-danger-gemini-web/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,23 +91,23 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md") { "user"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Text generation
|
# Text generation
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Your prompt"
|
${BUN_X} {baseDir}/scripts/main.ts "Your prompt"
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash
|
||||||
|
|
||||||
# Image generation
|
# Image generation
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cute cat" --image cat.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cute cat" --image cat.png
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
# Vision input (reference images)
|
# Vision input (reference images)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Describe this" --reference image.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "Describe this" --reference image.png
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png
|
||||||
|
|
||||||
# Multi-turn conversation
|
# Multi-turn conversation
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Remember: 42" --sessionId session-abc
|
${BUN_X} {baseDir}/scripts/main.ts "Remember: 42" --sessionId session-abc
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts "What number?" --sessionId session-abc
|
${BUN_X} {baseDir}/scripts/main.ts "What number?" --sessionId session-abc
|
||||||
|
|
||||||
# JSON output
|
# JSON output
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts "Hello" --json
|
${BUN_X} {baseDir}/scripts/main.ts "Hello" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|||||||
@@ -59,16 +59,31 @@ function normalizeSessionMetadata(input: unknown): Array<string | null> {
|
|||||||
return [null, null, null];
|
return [null, null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatScriptCommand(fallback: string): string {
|
||||||
|
const raw = process.argv[1];
|
||||||
|
const displayPath = raw
|
||||||
|
? (() => {
|
||||||
|
const relative = path.relative(process.cwd(), raw);
|
||||||
|
return relative && !relative.startsWith("..") ? relative : raw;
|
||||||
|
})()
|
||||||
|
: fallback;
|
||||||
|
const quotedPath = displayPath.includes(" ")
|
||||||
|
? `"${displayPath.replace(/"/g, '\\"')}"`
|
||||||
|
: displayPath;
|
||||||
|
return `npx -y bun ${quotedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
function printUsage(cookiePath: string, profileDir: string): void {
|
function printUsage(cookiePath: string, profileDir: string): void {
|
||||||
|
const cmd = formatScriptCommand("scripts/main.ts");
|
||||||
console.log(`Usage:
|
console.log(`Usage:
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "Hello"
|
${cmd} --prompt "Hello"
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "Hello"
|
${cmd} "Hello"
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "A cute cat" --image generated.png
|
${cmd} --prompt "A cute cat" --image generated.png
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${cmd} --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
Multi-turn conversation (agent generates unique sessionId):
|
Multi-turn conversation (agent generates unique sessionId):
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "Remember 42" --sessionId abc123
|
${cmd} "Remember 42" --sessionId abc123
|
||||||
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "What number?" --sessionId abc123
|
${cmd} "What number?" --sessionId abc123
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-p, --prompt <text> Prompt text
|
-p, --prompt <text> Prompt text
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-danger-x-to-markdown
|
name: baoyu-danger-x-to-markdown
|
||||||
description: Converts X (Twitter) tweets and articles to markdown with YAML front matter. Uses reverse-engineered API requiring user consent. Use when user mentions "X to markdown", "tweet to markdown", "save tweet", or provides x.com/twitter.com URLs for conversion.
|
description: Converts X (Twitter) tweets and articles to markdown with YAML front matter. Uses reverse-engineered API requiring user consent. Use when user mentions "X to markdown", "tweet to markdown", "save tweet", or provides x.com/twitter.com URLs for conversion.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-danger-x-to-markdown
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# X to Markdown
|
# X to Markdown
|
||||||
@@ -14,8 +22,8 @@ Converts X content to markdown:
|
|||||||
Scripts located in `scripts/` subdirectory.
|
Scripts located in `scripts/` subdirectory.
|
||||||
|
|
||||||
**Path Resolution**:
|
**Path Resolution**:
|
||||||
1. `SKILL_DIR` = this SKILL.md's directory
|
1. `{baseDir}` = this SKILL.md's directory
|
||||||
2. Script path = `${SKILL_DIR}/scripts/main.ts`
|
2. Script path = `{baseDir}/scripts/main.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
|
||||||
|
|
||||||
## Consent Requirement
|
## Consent Requirement
|
||||||
@@ -75,12 +83,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -140,10 +151,10 @@ Full reference: [references/config/first-time-setup.md](references/config/first-
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url>
|
${BUN_X} {baseDir}/scripts/main.ts <url>
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
${BUN_X} {baseDir}/scripts/main.ts <url> -o output.md
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --download-media
|
${BUN_X} {baseDir}/scripts/main.ts <url> --download-media
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --json
|
${BUN_X} {baseDir}/scripts/main.ts <url> --json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|||||||
@@ -30,8 +30,22 @@ type ConsentRecord = {
|
|||||||
|
|
||||||
const DISCLAIMER_VERSION = "1.0";
|
const DISCLAIMER_VERSION = "1.0";
|
||||||
|
|
||||||
|
function formatScriptCommand(fallback: string): string {
|
||||||
|
const raw = process.argv[1];
|
||||||
|
const displayPath = raw
|
||||||
|
? (() => {
|
||||||
|
const relative = path.relative(process.cwd(), raw);
|
||||||
|
return relative && !relative.startsWith("..") ? relative : raw;
|
||||||
|
})()
|
||||||
|
: fallback;
|
||||||
|
const quotedPath = displayPath.includes(" ")
|
||||||
|
? `"${displayPath.replace(/"/g, '\\"')}"`
|
||||||
|
: displayPath;
|
||||||
|
return `npx -y bun ${quotedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
function printUsage(exitCode: number): never {
|
function printUsage(exitCode: number): never {
|
||||||
const cmd = "npx -y bun skills/baoyu-danger-x-to-markdown/scripts/main.ts";
|
const cmd = formatScriptCommand("scripts/main.ts");
|
||||||
console.log(`X (Twitter) to Markdown
|
console.log(`X (Twitter) to Markdown
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|||||||
@@ -36,6 +36,20 @@ function normalizeInputUrl(input: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatScriptCommand(fallback: string): string {
|
||||||
|
const raw = process.argv[1];
|
||||||
|
const displayPath = raw
|
||||||
|
? (() => {
|
||||||
|
const relative = path.relative(process.cwd(), raw);
|
||||||
|
return relative && !relative.startsWith("..") ? relative : raw;
|
||||||
|
})()
|
||||||
|
: fallback;
|
||||||
|
const quotedPath = displayPath.includes(" ")
|
||||||
|
? `"${displayPath.replace(/"/g, '\\"')}"`
|
||||||
|
: displayPath;
|
||||||
|
return `npx -y bun ${quotedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
function parseTweetId(input: string): string | null {
|
function parseTweetId(input: string): string | null {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
@@ -179,7 +193,7 @@ async function main() {
|
|||||||
const { url } = parseArgs();
|
const { url } = parseArgs();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
console.error("Usage:");
|
console.error("Usage:");
|
||||||
console.error(" npx -y bun skills/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.ts <tweet url>");
|
console.error(` ${formatScriptCommand("scripts/tweet-to-markdown.ts")} <tweet url>`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-format-markdown
|
name: baoyu-format-markdown
|
||||||
description: Formats plain text or markdown files with frontmatter, titles, summaries, headings, bold, lists, and code blocks. Use when user asks to "format markdown", "beautify article", "add formatting", or improve article layout. Outputs to {filename}-formatted.md.
|
description: Formats plain text or markdown files with frontmatter, titles, summaries, headings, bold, lists, and code blocks. Use when user asks to "format markdown", "beautify article", "add formatting", or improve article layout. Outputs to {filename}-formatted.md.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-format-markdown
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Markdown Formatter
|
# Markdown Formatter
|
||||||
@@ -11,7 +19,7 @@ Transforms plain text or markdown into well-structured, reader-friendly markdown
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
Scripts in `scripts/` subdirectory. `${SKILL_DIR}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
Scripts in `scripts/` subdirectory. `{baseDir}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -26,12 +34,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-format-markdown/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-format-markdown/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-format-markdown/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-format-markdown/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-format-markdown/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-format-markdown/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-format-markdown/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -296,7 +307,7 @@ fi
|
|||||||
Run the formatting script on the output file:
|
Run the formatting script on the output file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts {output-file-path} [options]
|
${BUN_X} {baseDir}/scripts/main.ts {output-file-path} [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Script Options:**
|
**Script Options:**
|
||||||
@@ -314,13 +325,13 @@ ${BUN_X} ${SKILL_DIR}/scripts/main.ts {output-file-path} [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default: spacing + emphasis enabled, quotes disabled
|
# Default: spacing + emphasis enabled, quotes disabled
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md
|
${BUN_X} {baseDir}/scripts/main.ts article.md
|
||||||
|
|
||||||
# Enable all features including quote replacement
|
# Enable all features including quote replacement
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --quotes
|
${BUN_X} {baseDir}/scripts/main.ts article.md --quotes
|
||||||
|
|
||||||
# Only fix emphasis issues, skip spacing
|
# Only fix emphasis issues, skip spacing
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --no-spacing
|
${BUN_X} {baseDir}/scripts/main.ts article.md --no-spacing
|
||||||
```
|
```
|
||||||
|
|
||||||
**Script performs (based on options):**
|
**Script performs (based on options):**
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { execSync } from "child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
export function applyAutocorrect(filePath: string): boolean {
|
export function applyAutocorrect(filePath: string): boolean {
|
||||||
try {
|
const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
||||||
execSync(`npx autocorrect-node --fix "${filePath}"`, { stdio: "inherit" });
|
const result = spawnSync(npxCmd, ["autocorrect-node", "--fix", filePath], {
|
||||||
return true;
|
stdio: "inherit",
|
||||||
} catch {
|
});
|
||||||
return false;
|
return result.status === 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-image-gen
|
name: baoyu-image-gen
|
||||||
description: AI image generation with OpenAI, Google, DashScope and Replicate APIs. Supports text-to-image, reference images, aspect ratios. Sequential by default; parallel generation available on request. Use when user asks to generate, create, or draw images.
|
description: AI image generation with OpenAI, Google, DashScope and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-image-gen
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Image Generation (AI SDK)
|
# Image Generation (AI SDK)
|
||||||
@@ -10,8 +18,8 @@ Official API-based image generation. Supports OpenAI, Google, DashScope (阿里
|
|||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
**Agent Execution**:
|
**Agent Execution**:
|
||||||
1. `SKILL_DIR` = this SKILL.md file's directory
|
1. `{baseDir}` = this SKILL.md file's directory
|
||||||
2. Script path = `${SKILL_DIR}/scripts/main.ts`
|
2. Script path = `{baseDir}/scripts/main.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
|
||||||
|
|
||||||
## Step 0: Load Preferences ⛔ BLOCKING
|
## Step 0: Load Preferences ⛔ BLOCKING
|
||||||
@@ -23,12 +31,15 @@ Check EXTEND.md existence (priority: project → user):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-image-gen/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-image-gen/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-image-gen/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-image-gen/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-image-gen/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,7 +55,7 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md") { "user" }
|
|||||||
| `.baoyu-skills/baoyu-image-gen/EXTEND.md` | Project directory |
|
| `.baoyu-skills/baoyu-image-gen/EXTEND.md` | Project directory |
|
||||||
| `$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md` | User home |
|
| `$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md` | User home |
|
||||||
|
|
||||||
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models
|
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models | Batch worker cap | Provider-specific batch limits
|
||||||
|
|
||||||
Schema: `references/config/preferences-schema.md`
|
Schema: `references/config/preferences-schema.md`
|
||||||
|
|
||||||
@@ -52,50 +63,85 @@ Schema: `references/config/preferences-schema.md`
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic
|
# Basic
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image cat.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image cat.png
|
||||||
|
|
||||||
# With aspect ratio
|
# With aspect ratio
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A landscape" --image out.png --ar 16:9
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A landscape" --image out.png --ar 16:9
|
||||||
|
|
||||||
# High quality
|
# High quality
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
|
||||||
|
|
||||||
# From prompt files
|
# From prompt files
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
# With reference images (Google multimodal or OpenAI edits)
|
# With reference images (Google multimodal or OpenAI edits)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
||||||
|
|
||||||
# With reference images (explicit provider/model)
|
# With reference images (explicit provider/model)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
||||||
|
|
||||||
# Specific provider
|
# Specific provider
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider openai
|
||||||
|
|
||||||
# DashScope (阿里通义万象)
|
# DashScope (阿里通义万象)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image out.png --provider dashscope
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "一只可爱的猫" --image out.png --provider dashscope
|
||||||
|
|
||||||
# Replicate (google/nano-banana-pro)
|
# Replicate (google/nano-banana-pro)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
||||||
|
|
||||||
# Replicate with specific model
|
# Replicate with specific model
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
||||||
|
|
||||||
|
# Batch mode with saved prompt files
|
||||||
|
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json
|
||||||
|
|
||||||
|
# Batch mode with explicit worker count
|
||||||
|
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4 --json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Batch File Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobs": 4,
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "hero",
|
||||||
|
"promptFiles": ["prompts/hero.md"],
|
||||||
|
"image": "out/hero.png",
|
||||||
|
"provider": "replicate",
|
||||||
|
"model": "google/nano-banana-pro",
|
||||||
|
"ar": "16:9",
|
||||||
|
"quality": "2k"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diagram",
|
||||||
|
"promptFiles": ["prompts/diagram.md"],
|
||||||
|
"image": "out/diagram.png",
|
||||||
|
"ref": ["references/original.png"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch file's directory. `jobs` is optional (overridden by CLI `--jobs`). Top-level array format (without `jobs` wrapper) is also accepted.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--prompt <text>`, `-p` | Prompt text |
|
| `--prompt <text>`, `-p` | Prompt text |
|
||||||
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
|
| `--promptfiles <files...>` | Read prompt from files (concatenated) |
|
||||||
| `--image <path>` | Output image path (required) |
|
| `--image <path>` | Output image path (required in single-image mode) |
|
||||||
| `--provider google\|openai\|dashscope\|replicate` | Force provider (default: google) |
|
| `--batchfile <path>` | JSON batch file for multi-image generation |
|
||||||
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`; OpenAI: `gpt-image-1.5`) |
|
| `--jobs <count>` | Worker count for batch mode (default: auto, max from config, built-in default 10) |
|
||||||
|
| `--provider google\|openai\|dashscope\|replicate` | Force provider (default: auto-detect) |
|
||||||
|
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`; OpenAI: `gpt-image-1.5`, `gpt-image-1`) |
|
||||||
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||||
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
||||||
| `--quality normal\|2k` | Quality preset (default: 2k) |
|
| `--quality normal\|2k` | Quality preset (default: `2k`) |
|
||||||
| `--imageSize 1K\|2K\|4K` | Image size for Google (default: from quality) |
|
| `--imageSize 1K\|2K\|4K` | Image size for Google (default: from quality) |
|
||||||
| `--ref <files...>` | Reference images. Supported by Google multimodal (`gemini-3-pro-image-preview`, `gemini-3-flash-preview`, `gemini-3.1-flash-image-preview`) and OpenAI edits (GPT Image models). If provider omitted: Google first, then OpenAI |
|
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, and Replicate |
|
||||||
| `--n <count>` | Number of images |
|
| `--n <count>` | Number of images |
|
||||||
| `--json` | JSON output |
|
| `--json` | JSON output |
|
||||||
|
|
||||||
@@ -115,6 +161,9 @@ ${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provide
|
|||||||
| `GOOGLE_BASE_URL` | Custom Google endpoint |
|
| `GOOGLE_BASE_URL` | Custom Google endpoint |
|
||||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
|
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
|
||||||
| `REPLICATE_BASE_URL` | Custom Replicate endpoint |
|
| `REPLICATE_BASE_URL` | Custom Replicate endpoint |
|
||||||
|
| `BAOYU_IMAGE_GEN_MAX_WORKERS` | Override batch worker cap |
|
||||||
|
| `BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY` | Override provider concurrency, e.g. `BAOYU_IMAGE_GEN_REPLICATE_CONCURRENCY` |
|
||||||
|
| `BAOYU_IMAGE_GEN_<PROVIDER>_START_INTERVAL_MS` | Override provider start gap, e.g. `BAOYU_IMAGE_GEN_REPLICATE_START_INTERVAL_MS` |
|
||||||
|
|
||||||
**Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
|
**Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
|
||||||
|
|
||||||
@@ -144,10 +193,10 @@ Examples:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Use Replicate default model
|
# Use Replicate default model
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate
|
||||||
|
|
||||||
# Override model explicitly
|
# Override model explicitly
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider replicate --model google/nano-banana
|
||||||
```
|
```
|
||||||
|
|
||||||
## Provider Selection
|
## Provider Selection
|
||||||
@@ -159,10 +208,10 @@ ${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provide
|
|||||||
|
|
||||||
## Quality Presets
|
## Quality Presets
|
||||||
|
|
||||||
| Preset | Google imageSize | OpenAI Size | Use Case |
|
| Preset | Google imageSize | OpenAI Size | Replicate resolution | Use Case |
|
||||||
|--------|------------------|-------------|----------|
|
|--------|------------------|-------------|----------------------|----------|
|
||||||
| `normal` | 1K | 1024px | Quick previews |
|
| `normal` | 1K | 1024px | 1K | Quick previews |
|
||||||
| `2k` (default) | 2K | 2048px | Covers, illustrations, infographics |
|
| `2k` (default) | 2K | 2048px | 2K | Covers, illustrations, infographics |
|
||||||
|
|
||||||
**Google imageSize**: Can be overridden with `--imageSize 1K|2K|4K`
|
**Google imageSize**: Can be overridden with `--imageSize 1K|2K|4K`
|
||||||
|
|
||||||
@@ -171,41 +220,48 @@ ${BUN_X} ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provide
|
|||||||
Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
|
Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`
|
||||||
|
|
||||||
- Google multimodal: uses `imageConfig.aspectRatio`
|
- Google multimodal: uses `imageConfig.aspectRatio`
|
||||||
- Google Imagen: uses `aspectRatio` parameter
|
|
||||||
- OpenAI: maps to closest supported size
|
- OpenAI: maps to closest supported size
|
||||||
|
- Replicate: passes `aspect_ratio` to model; when `--ref` is provided without `--ar`, defaults to `match_input_image`
|
||||||
|
|
||||||
## Generation Mode
|
## Generation Mode
|
||||||
|
|
||||||
**Default**: Sequential generation (one image at a time). This ensures stable output and easier debugging.
|
**Default**: Sequential generation.
|
||||||
|
|
||||||
**Parallel Generation**: Only use when user explicitly requests parallel/concurrent generation.
|
**Batch Parallel Generation**: When `--batchfile` contains 2 or more pending tasks, the script automatically enables parallel generation.
|
||||||
|
|
||||||
| Mode | When to Use |
|
| Mode | When to Use |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| Sequential (default) | Normal usage, single images, small batches |
|
| Sequential (default) | Normal usage, single images, small batches |
|
||||||
| Parallel | User explicitly requests, large batches (10+) |
|
| Parallel batch | Batch mode with 2+ tasks |
|
||||||
|
|
||||||
**Parallel Settings** (when requested):
|
Execution choice:
|
||||||
|
|
||||||
| Setting | Value |
|
| Situation | Preferred approach | Why |
|
||||||
|---------|-------|
|
|-----------|--------------------|-----|
|
||||||
| Recommended concurrency | 4 subagents |
|
| One image, or 1-2 simple images | Sequential | Lower coordination overhead and easier debugging |
|
||||||
| Max concurrency | 8 subagents |
|
| Multiple images already have saved prompt files | Batch (`--batchfile`) | Reuses finalized prompts, applies shared throttling/retries, and gives predictable throughput |
|
||||||
| Use case | Large batch generation when user requests parallel |
|
| Each image still needs separate reasoning, prompt writing, or style exploration | Subagents | The work is still exploratory, so each image may need independent analysis before generation |
|
||||||
|
| Output comes from `baoyu-article-illustrator` with `outline.md` + `prompts/` | Batch (`build-batch.ts` -> `--batchfile`) | That workflow already produces prompt files, so direct batch execution is the intended path |
|
||||||
|
|
||||||
**Agent Implementation** (parallel mode only):
|
Rule of thumb:
|
||||||
```
|
|
||||||
# Launch multiple generations in parallel using Task tool
|
- Prefer batch over subagents once prompt files are already saved and the task is "generate all of these"
|
||||||
# Each Task runs as background subagent with run_in_background=true
|
- Use subagents only when generation is coupled with per-image thinking, rewriting, or divergent creative exploration
|
||||||
# Collect results via TaskOutput when all complete
|
|
||||||
```
|
Parallel behavior:
|
||||||
|
|
||||||
|
- Default worker count is automatic, capped by config, built-in default 10
|
||||||
|
- Provider-specific throttling is applied only in batch mode, and the built-in defaults are tuned for faster throughput while still avoiding obvious RPM bursts
|
||||||
|
- You can override worker count with `--jobs <count>`
|
||||||
|
- Each image retries automatically up to 3 attempts
|
||||||
|
- Final output includes success count, failure count, and per-image failure reasons
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
- Missing API key → error with setup instructions
|
- Missing API key → error with setup instructions
|
||||||
- Generation failure → auto-retry once
|
- Generation failure → auto-retry up to 3 attempts per image
|
||||||
- Invalid aspect ratio → warning, proceed with default
|
- Invalid aspect ratio → warning, proceed with default
|
||||||
- Reference images with unsupported provider/model → error with fix hint (switch to Google multimodal: `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`; or OpenAI GPT Image edits)
|
- Reference images with unsupported provider/model → error with fix hint
|
||||||
|
|
||||||
## Extension Support
|
## Extension Support
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,25 @@ default_image_size: null # 1K|2K|4K|null (Google only, overrides quality)
|
|||||||
|
|
||||||
default_model:
|
default_model:
|
||||||
google: null # e.g., "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"
|
google: null # e.g., "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"
|
||||||
openai: null # e.g., "gpt-image-1.5"
|
openai: null # e.g., "gpt-image-1.5", "gpt-image-1"
|
||||||
dashscope: null # e.g., "z-image-turbo"
|
dashscope: null # e.g., "z-image-turbo"
|
||||||
replicate: null # e.g., "google/nano-banana-pro"
|
replicate: null # e.g., "google/nano-banana-pro"
|
||||||
|
|
||||||
|
batch:
|
||||||
|
max_workers: 10
|
||||||
|
provider_limits:
|
||||||
|
replicate:
|
||||||
|
concurrency: 5
|
||||||
|
start_interval_ms: 700
|
||||||
|
google:
|
||||||
|
concurrency: 3
|
||||||
|
start_interval_ms: 1100
|
||||||
|
openai:
|
||||||
|
concurrency: 3
|
||||||
|
start_interval_ms: 1100
|
||||||
|
dashscope:
|
||||||
|
concurrency: 3
|
||||||
|
start_interval_ms: 1100
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,6 +56,9 @@ default_model:
|
|||||||
| `default_model.openai` | string\|null | null | OpenAI default model |
|
| `default_model.openai` | string\|null | null | OpenAI default model |
|
||||||
| `default_model.dashscope` | string\|null | null | DashScope default model |
|
| `default_model.dashscope` | string\|null | null | DashScope default model |
|
||||||
| `default_model.replicate` | string\|null | null | Replicate default model |
|
| `default_model.replicate` | string\|null | null | Replicate default model |
|
||||||
|
| `batch.max_workers` | int\|null | 10 | Batch worker cap |
|
||||||
|
| `batch.provider_limits.<provider>.concurrency` | int\|null | provider default | Max simultaneous requests per provider |
|
||||||
|
| `batch.provider_limits.<provider>.start_interval_ms` | int\|null | provider default | Minimum gap between request starts per provider |
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -65,5 +84,11 @@ default_model:
|
|||||||
openai: "gpt-image-1.5"
|
openai: "gpt-image-1.5"
|
||||||
dashscope: "z-image-turbo"
|
dashscope: "z-image-turbo"
|
||||||
replicate: "google/nano-banana-pro"
|
replicate: "google/nano-banana-pro"
|
||||||
|
batch:
|
||||||
|
max_workers: 10
|
||||||
|
provider_limits:
|
||||||
|
replicate:
|
||||||
|
concurrency: 5
|
||||||
|
start_interval_ms: 700
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,34 +2,108 @@ import path from "node:path";
|
|||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import type { CliArgs, Provider, ExtendConfig } from "./types";
|
import type {
|
||||||
|
BatchFile,
|
||||||
|
BatchTaskInput,
|
||||||
|
CliArgs,
|
||||||
|
ExtendConfig,
|
||||||
|
Provider,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
type ProviderModule = {
|
||||||
|
getDefaultModel: () => string;
|
||||||
|
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreparedTask = {
|
||||||
|
id: string;
|
||||||
|
prompt: string;
|
||||||
|
args: CliArgs;
|
||||||
|
provider: Provider;
|
||||||
|
model: string;
|
||||||
|
outputPath: string;
|
||||||
|
providerModule: ProviderModule;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskResult = {
|
||||||
|
id: string;
|
||||||
|
provider: Provider;
|
||||||
|
model: string;
|
||||||
|
outputPath: string;
|
||||||
|
success: boolean;
|
||||||
|
attempts: number;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProviderRateLimit = {
|
||||||
|
concurrency: number;
|
||||||
|
startIntervalMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoadedBatchTasks = {
|
||||||
|
tasks: BatchTaskInput[];
|
||||||
|
jobs: number | null;
|
||||||
|
batchDir: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_ATTEMPTS = 3;
|
||||||
|
const DEFAULT_MAX_WORKERS = 10;
|
||||||
|
const POLL_WAIT_MS = 250;
|
||||||
|
const DEFAULT_PROVIDER_RATE_LIMITS: Record<Provider, ProviderRateLimit> = {
|
||||||
|
replicate: { concurrency: 5, startIntervalMs: 700 },
|
||||||
|
google: { concurrency: 3, startIntervalMs: 1100 },
|
||||||
|
openai: { concurrency: 3, startIntervalMs: 1100 },
|
||||||
|
dashscope: { concurrency: 3, startIntervalMs: 1100 },
|
||||||
|
};
|
||||||
|
|
||||||
function printUsage(): void {
|
function printUsage(): void {
|
||||||
console.log(`Usage:
|
console.log(`Usage:
|
||||||
npx -y bun scripts/main.ts --prompt "A cat" --image cat.png
|
npx -y bun scripts/main.ts --prompt "A cat" --image cat.png
|
||||||
npx -y bun scripts/main.ts --prompt "A landscape" --image landscape.png --ar 16:9
|
|
||||||
npx -y bun scripts/main.ts --promptfiles system.md content.md --image out.png
|
npx -y bun scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
npx -y bun scripts/main.ts --batchfile batch.json
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-p, --prompt <text> Prompt text
|
-p, --prompt <text> Prompt text
|
||||||
--promptfiles <files...> Read prompt from files (concatenated)
|
--promptfiles <files...> Read prompt from files (concatenated)
|
||||||
--image <path> Output image path (required)
|
--image <path> Output image path (required in single-image mode)
|
||||||
|
--batchfile <path> JSON batch file for multi-image generation
|
||||||
|
--jobs <count> Worker count for batch mode (default: auto, max from config, built-in default 10)
|
||||||
--provider google|openai|dashscope|replicate Force provider (auto-detect by default)
|
--provider google|openai|dashscope|replicate Force provider (auto-detect by default)
|
||||||
-m, --model <id> Model ID
|
-m, --model <id> Model ID
|
||||||
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
|
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
|
||||||
--size <WxH> Size (e.g., 1024x1024)
|
--size <WxH> Size (e.g., 1024x1024)
|
||||||
--quality normal|2k Quality preset (default: 2k)
|
--quality normal|2k Quality preset (default: 2k)
|
||||||
--imageSize 1K|2K|4K Image size for Google (default: from quality)
|
--imageSize 1K|2K|4K Image size for Google (default: from quality)
|
||||||
--ref <files...> Reference images (Google multimodal or OpenAI edits)
|
--ref <files...> Reference images (Google multimodal, OpenAI GPT Image edits, or Replicate)
|
||||||
--n <count> Number of images (default: 1)
|
--n <count> Number of images for the current task (default: 1)
|
||||||
--json JSON output
|
--json JSON output
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
|
|
||||||
|
Batch file format:
|
||||||
|
{
|
||||||
|
"jobs": 4,
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "hero",
|
||||||
|
"promptFiles": ["prompts/hero.md"],
|
||||||
|
"image": "out/hero.png",
|
||||||
|
"provider": "replicate",
|
||||||
|
"model": "google/nano-banana-pro",
|
||||||
|
"ar": "16:9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Batch mode automatically runs in parallel when pending tasks >= 2
|
||||||
|
- Each image retries automatically up to 3 attempts
|
||||||
|
- Batch summary reports success count, failure count, and per-image errors
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
OPENAI_API_KEY OpenAI API key
|
OPENAI_API_KEY OpenAI API key
|
||||||
GOOGLE_API_KEY Google API key
|
GOOGLE_API_KEY Google API key
|
||||||
GEMINI_API_KEY Gemini API key (alias for GOOGLE_API_KEY)
|
GEMINI_API_KEY Gemini API key (alias for GOOGLE_API_KEY)
|
||||||
DASHSCOPE_API_KEY DashScope API key (阿里云通义万象)
|
DASHSCOPE_API_KEY DashScope API key
|
||||||
REPLICATE_API_TOKEN Replicate API token
|
REPLICATE_API_TOKEN Replicate API token
|
||||||
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
|
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
|
||||||
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
|
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
|
||||||
@@ -40,6 +114,9 @@ Environment variables:
|
|||||||
GOOGLE_BASE_URL Custom Google endpoint
|
GOOGLE_BASE_URL Custom Google endpoint
|
||||||
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
||||||
REPLICATE_BASE_URL Custom Replicate endpoint
|
REPLICATE_BASE_URL Custom Replicate endpoint
|
||||||
|
BAOYU_IMAGE_GEN_MAX_WORKERS Override batch worker cap
|
||||||
|
BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY Override provider concurrency
|
||||||
|
BAOYU_IMAGE_GEN_<PROVIDER>_START_INTERVAL_MS Override provider start gap in ms
|
||||||
|
|
||||||
Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
|
Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
|
||||||
}
|
}
|
||||||
@@ -57,6 +134,8 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
imageSize: null,
|
imageSize: null,
|
||||||
referenceImages: [],
|
referenceImages: [],
|
||||||
n: 1,
|
n: 1,
|
||||||
|
batchFile: null,
|
||||||
|
jobs: null,
|
||||||
json: false,
|
json: false,
|
||||||
help: false,
|
help: false,
|
||||||
};
|
};
|
||||||
@@ -110,9 +189,26 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a === "--batchfile") {
|
||||||
|
const v = argv[++i];
|
||||||
|
if (!v) throw new Error("Missing value for --batchfile");
|
||||||
|
out.batchFile = v;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a === "--jobs") {
|
||||||
|
const v = argv[++i];
|
||||||
|
if (!v) throw new Error("Missing value for --jobs");
|
||||||
|
out.jobs = parseInt(v, 10);
|
||||||
|
if (isNaN(out.jobs) || out.jobs < 1) throw new Error(`Invalid worker count: ${v}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (a === "--provider") {
|
if (a === "--provider") {
|
||||||
const v = argv[++i];
|
const v = argv[++i];
|
||||||
if (v !== "google" && v !== "openai" && v !== "dashscope" && v !== "replicate") throw new Error(`Invalid provider: ${v}`);
|
if (v !== "google" && v !== "openai" && v !== "dashscope" && v !== "replicate") {
|
||||||
|
throw new Error(`Invalid provider: ${v}`);
|
||||||
|
}
|
||||||
out.provider = v;
|
out.provider = v;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -228,9 +324,11 @@ function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
|||||||
const config: Partial<ExtendConfig> = {};
|
const config: Partial<ExtendConfig> = {};
|
||||||
const lines = yaml.split("\n");
|
const lines = yaml.split("\n");
|
||||||
let currentKey: string | null = null;
|
let currentKey: string | null = null;
|
||||||
|
let currentProvider: Provider | null = null;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
||||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
|
||||||
if (trimmed.includes(":") && !trimmed.startsWith("-")) {
|
if (trimmed.includes(":") && !trimmed.startsWith("-")) {
|
||||||
@@ -247,18 +345,57 @@ function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
|||||||
} else if (key === "default_provider") {
|
} else if (key === "default_provider") {
|
||||||
config.default_provider = value === "null" ? null : (value as Provider);
|
config.default_provider = value === "null" ? null : (value as Provider);
|
||||||
} else if (key === "default_quality") {
|
} else if (key === "default_quality") {
|
||||||
config.default_quality = value === "null" ? null : (value as "normal" | "2k");
|
config.default_quality = value === "null" ? null : value as "normal" | "2k";
|
||||||
} else if (key === "default_aspect_ratio") {
|
} else if (key === "default_aspect_ratio") {
|
||||||
const cleaned = value.replace(/['"]/g, "");
|
const cleaned = value.replace(/['"]/g, "");
|
||||||
config.default_aspect_ratio = cleaned === "null" ? null : cleaned;
|
config.default_aspect_ratio = cleaned === "null" ? null : cleaned;
|
||||||
} else if (key === "default_image_size") {
|
} else if (key === "default_image_size") {
|
||||||
config.default_image_size = value === "null" ? null : (value as "1K" | "2K" | "4K");
|
config.default_image_size = value === "null" ? null : value as "1K" | "2K" | "4K";
|
||||||
} else if (key === "default_model") {
|
} else if (key === "default_model") {
|
||||||
config.default_model = { google: null, openai: null, dashscope: null, replicate: null };
|
config.default_model = { google: null, openai: null, dashscope: null, replicate: null };
|
||||||
currentKey = "default_model";
|
currentKey = "default_model";
|
||||||
} else if (currentKey === "default_model" && (key === "google" || key === "openai" || key === "dashscope" || key === "replicate")) {
|
currentProvider = null;
|
||||||
|
} else if (key === "batch") {
|
||||||
|
config.batch = {};
|
||||||
|
currentKey = "batch";
|
||||||
|
currentProvider = null;
|
||||||
|
} else if (currentKey === "batch" && indent >= 2 && key === "max_workers") {
|
||||||
|
config.batch ??= {};
|
||||||
|
config.batch.max_workers = value === "null" ? null : parseInt(value, 10);
|
||||||
|
} else if (currentKey === "batch" && indent >= 2 && key === "provider_limits") {
|
||||||
|
config.batch ??= {};
|
||||||
|
config.batch.provider_limits ??= {};
|
||||||
|
currentKey = "provider_limits";
|
||||||
|
currentProvider = null;
|
||||||
|
} else if (
|
||||||
|
currentKey === "provider_limits" &&
|
||||||
|
indent >= 4 &&
|
||||||
|
(key === "google" || key === "openai" || key === "dashscope" || key === "replicate")
|
||||||
|
) {
|
||||||
|
config.batch ??= {};
|
||||||
|
config.batch.provider_limits ??= {};
|
||||||
|
config.batch.provider_limits[key] ??= {};
|
||||||
|
currentProvider = key;
|
||||||
|
} else if (
|
||||||
|
currentKey === "default_model" &&
|
||||||
|
(key === "google" || key === "openai" || key === "dashscope" || key === "replicate")
|
||||||
|
) {
|
||||||
const cleaned = value.replace(/['"]/g, "");
|
const cleaned = value.replace(/['"]/g, "");
|
||||||
config.default_model![key] = cleaned === "null" ? null : cleaned;
|
config.default_model![key] = cleaned === "null" ? null : cleaned;
|
||||||
|
} else if (
|
||||||
|
currentKey === "provider_limits" &&
|
||||||
|
currentProvider &&
|
||||||
|
indent >= 6 &&
|
||||||
|
(key === "concurrency" || key === "start_interval_ms")
|
||||||
|
) {
|
||||||
|
config.batch ??= {};
|
||||||
|
config.batch.provider_limits ??= {};
|
||||||
|
const providerLimit = (config.batch.provider_limits[currentProvider] ??= {});
|
||||||
|
if (key === "concurrency") {
|
||||||
|
providerLimit.concurrency = value === "null" ? null : parseInt(value, 10);
|
||||||
|
} else {
|
||||||
|
providerLimit.start_interval_ms = value === "null" ? null : parseInt(value, 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +417,6 @@ async function loadExtendConfig(): Promise<Partial<ExtendConfig>> {
|
|||||||
const content = await readFile(p, "utf8");
|
const content = await readFile(p, "utf8");
|
||||||
const yaml = extractYamlFrontMatter(content);
|
const yaml = extractYamlFrontMatter(content);
|
||||||
if (!yaml) continue;
|
if (!yaml) continue;
|
||||||
|
|
||||||
return parseSimpleYaml(yaml);
|
return parseSimpleYaml(yaml);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
@@ -300,6 +436,57 @@ function mergeConfig(args: CliArgs, extend: Partial<ExtendConfig>): CliArgs {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePositiveInt(value: string | undefined): number | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const parsed = parseInt(value, 10);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveBatchInt(value: unknown): number | null {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Number.isInteger(value) && value > 0 ? value : null;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return parsePositiveInt(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfiguredMaxWorkers(extendConfig: Partial<ExtendConfig>): number {
|
||||||
|
const envValue = parsePositiveInt(process.env.BAOYU_IMAGE_GEN_MAX_WORKERS);
|
||||||
|
const configValue = extendConfig.batch?.max_workers ?? null;
|
||||||
|
return Math.max(1, envValue ?? configValue ?? DEFAULT_MAX_WORKERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfiguredProviderRateLimits(
|
||||||
|
extendConfig: Partial<ExtendConfig>
|
||||||
|
): Record<Provider, ProviderRateLimit> {
|
||||||
|
const configured: Record<Provider, ProviderRateLimit> = {
|
||||||
|
replicate: { ...DEFAULT_PROVIDER_RATE_LIMITS.replicate },
|
||||||
|
google: { ...DEFAULT_PROVIDER_RATE_LIMITS.google },
|
||||||
|
openai: { ...DEFAULT_PROVIDER_RATE_LIMITS.openai },
|
||||||
|
dashscope: { ...DEFAULT_PROVIDER_RATE_LIMITS.dashscope },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const provider of ["replicate", "google", "openai", "dashscope"] as Provider[]) {
|
||||||
|
const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase()}`;
|
||||||
|
const extendLimit = extendConfig.batch?.provider_limits?.[provider];
|
||||||
|
configured[provider] = {
|
||||||
|
concurrency:
|
||||||
|
parsePositiveInt(process.env[`${envPrefix}_CONCURRENCY`]) ??
|
||||||
|
extendLimit?.concurrency ??
|
||||||
|
configured[provider].concurrency,
|
||||||
|
startIntervalMs:
|
||||||
|
parsePositiveInt(process.env[`${envPrefix}_START_INTERVAL_MS`]) ??
|
||||||
|
extendLimit?.start_interval_ms ??
|
||||||
|
configured[provider].startIntervalMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
async function readPromptFromFiles(files: string[]): Promise<string> {
|
async function readPromptFromFiles(files: string[]): Promise<string> {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
@@ -311,9 +498,12 @@ async function readPromptFromFiles(files: string[]): Promise<string> {
|
|||||||
async function readPromptFromStdin(): Promise<string | null> {
|
async function readPromptFromStdin(): Promise<string | null> {
|
||||||
if (process.stdin.isTTY) return null;
|
if (process.stdin.isTTY) return null;
|
||||||
try {
|
try {
|
||||||
const t = await Bun.stdin.text();
|
const chunks: Buffer[] = [];
|
||||||
const v = t.trim();
|
for await (const chunk of process.stdin) {
|
||||||
return v.length > 0 ? v : null;
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
}
|
||||||
|
const value = Buffer.concat(chunks).toString("utf8").trim();
|
||||||
|
return value.length > 0 ? value : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -327,7 +517,13 @@ function normalizeOutputImagePath(p: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectProvider(args: CliArgs): Provider {
|
function detectProvider(args: CliArgs): Provider {
|
||||||
if (args.referenceImages.length > 0 && args.provider && args.provider !== "google" && args.provider !== "openai" && args.provider !== "replicate") {
|
if (
|
||||||
|
args.referenceImages.length > 0 &&
|
||||||
|
args.provider &&
|
||||||
|
args.provider !== "google" &&
|
||||||
|
args.provider !== "openai" &&
|
||||||
|
args.provider !== "replicate"
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), or --provider replicate."
|
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), or --provider replicate."
|
||||||
);
|
);
|
||||||
@@ -349,7 +545,12 @@ function detectProvider(args: CliArgs): Provider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = [hasGoogle && "google", hasOpenai && "openai", hasDashscope && "dashscope", hasReplicate && "replicate"].filter(Boolean) as Provider[];
|
const available = [
|
||||||
|
hasGoogle && "google",
|
||||||
|
hasOpenai && "openai",
|
||||||
|
hasDashscope && "dashscope",
|
||||||
|
hasReplicate && "replicate",
|
||||||
|
].filter(Boolean) as Provider[];
|
||||||
|
|
||||||
if (available.length === 1) return available[0]!;
|
if (available.length === 1) return available[0]!;
|
||||||
if (available.length > 1) return available[0]!;
|
if (available.length > 1) return available[0]!;
|
||||||
@@ -371,11 +572,6 @@ async function validateReferenceImages(referenceImages: string[]): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderModule = {
|
|
||||||
getDefaultModel: () => string;
|
|
||||||
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isRetryableGenerationError(error: unknown): boolean {
|
function isRetryableGenerationError(error: unknown): boolean {
|
||||||
const msg = error instanceof Error ? error.message : String(error);
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
const nonRetryableMarkers = [
|
const nonRetryableMarkers = [
|
||||||
@@ -384,26 +580,353 @@ function isRetryableGenerationError(error: unknown): boolean {
|
|||||||
"only supported",
|
"only supported",
|
||||||
"No API key found",
|
"No API key found",
|
||||||
"is required",
|
"is required",
|
||||||
|
"Invalid ",
|
||||||
|
"Unexpected ",
|
||||||
|
"API error (400)",
|
||||||
|
"API error (401)",
|
||||||
|
"API error (402)",
|
||||||
|
"API error (403)",
|
||||||
|
"API error (404)",
|
||||||
|
"temporarily disabled",
|
||||||
];
|
];
|
||||||
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
|
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
|
async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
|
||||||
if (provider === "google") {
|
if (provider === "google") return (await import("./providers/google")) as ProviderModule;
|
||||||
return (await import("./providers/google")) as ProviderModule;
|
if (provider === "dashscope") return (await import("./providers/dashscope")) as ProviderModule;
|
||||||
}
|
if (provider === "replicate") return (await import("./providers/replicate")) as ProviderModule;
|
||||||
if (provider === "dashscope") {
|
|
||||||
return (await import("./providers/dashscope")) as ProviderModule;
|
|
||||||
}
|
|
||||||
if (provider === "replicate") {
|
|
||||||
return (await import("./providers/replicate")) as ProviderModule;
|
|
||||||
}
|
|
||||||
return (await import("./providers/openai")) as ProviderModule;
|
return (await import("./providers/openai")) as ProviderModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadPromptForArgs(args: CliArgs): Promise<string | null> {
|
||||||
|
let prompt: string | null = args.prompt;
|
||||||
|
if (!prompt && args.promptFiles.length > 0) {
|
||||||
|
prompt = await readPromptFromFiles(args.promptFiles);
|
||||||
|
}
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModelForProvider(
|
||||||
|
provider: Provider,
|
||||||
|
requestedModel: string | null,
|
||||||
|
extendConfig: Partial<ExtendConfig>,
|
||||||
|
providerModule: ProviderModule
|
||||||
|
): string {
|
||||||
|
if (requestedModel) return requestedModel;
|
||||||
|
if (extendConfig.default_model) {
|
||||||
|
if (provider === "google" && extendConfig.default_model.google) return extendConfig.default_model.google;
|
||||||
|
if (provider === "openai" && extendConfig.default_model.openai) return extendConfig.default_model.openai;
|
||||||
|
if (provider === "dashscope" && extendConfig.default_model.dashscope) return extendConfig.default_model.dashscope;
|
||||||
|
if (provider === "replicate" && extendConfig.default_model.replicate) return extendConfig.default_model.replicate;
|
||||||
|
}
|
||||||
|
return providerModule.getDefaultModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConfig>): Promise<PreparedTask> {
|
||||||
|
if (!args.quality) args.quality = "2k";
|
||||||
|
|
||||||
|
const prompt = (await loadPromptForArgs(args)) ?? (await readPromptFromStdin());
|
||||||
|
if (!prompt) throw new Error("Prompt is required");
|
||||||
|
if (!args.imagePath) throw new Error("--image is required");
|
||||||
|
if (args.referenceImages.length > 0) await validateReferenceImages(args.referenceImages);
|
||||||
|
|
||||||
|
const provider = detectProvider(args);
|
||||||
|
const providerModule = await loadProviderModule(provider);
|
||||||
|
const model = getModelForProvider(provider, args.model, extendConfig, providerModule);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "single",
|
||||||
|
prompt,
|
||||||
|
args,
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
outputPath: normalizeOutputImagePath(args.imagePath),
|
||||||
|
providerModule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBatchTasks(batchFilePath: string): Promise<LoadedBatchTasks> {
|
||||||
|
const resolvedBatchFilePath = path.resolve(batchFilePath);
|
||||||
|
const content = await readFile(resolvedBatchFilePath, "utf8");
|
||||||
|
const parsed = JSON.parse(content.replace(/^\uFEFF/, "")) as BatchFile;
|
||||||
|
const batchDir = path.dirname(resolvedBatchFilePath);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return {
|
||||||
|
tasks: parsed,
|
||||||
|
jobs: null,
|
||||||
|
batchDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsed && typeof parsed === "object" && Array.isArray(parsed.tasks)) {
|
||||||
|
const jobs = parsePositiveBatchInt(parsed.jobs);
|
||||||
|
if (parsed.jobs !== undefined && parsed.jobs !== null && jobs === null) {
|
||||||
|
throw new Error("Invalid batch file. jobs must be a positive integer when provided.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tasks: parsed.tasks,
|
||||||
|
jobs,
|
||||||
|
batchDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error("Invalid batch file. Expected an array of tasks or an object with a tasks array.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBatchPath(batchDir: string, filePath: string): string {
|
||||||
|
return path.isAbsolute(filePath) ? filePath : path.resolve(batchDir, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir: string): CliArgs {
|
||||||
|
return {
|
||||||
|
...baseArgs,
|
||||||
|
prompt: task.prompt ?? null,
|
||||||
|
promptFiles: task.promptFiles ? task.promptFiles.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
|
||||||
|
imagePath: task.image ? resolveBatchPath(batchDir, task.image) : null,
|
||||||
|
provider: task.provider ?? baseArgs.provider ?? null,
|
||||||
|
model: task.model ?? baseArgs.model ?? null,
|
||||||
|
aspectRatio: task.ar ?? baseArgs.aspectRatio ?? null,
|
||||||
|
size: task.size ?? baseArgs.size ?? null,
|
||||||
|
quality: task.quality ?? baseArgs.quality ?? null,
|
||||||
|
imageSize: task.imageSize ?? baseArgs.imageSize ?? null,
|
||||||
|
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
|
||||||
|
n: task.n ?? baseArgs.n,
|
||||||
|
batchFile: null,
|
||||||
|
jobs: baseArgs.jobs,
|
||||||
|
json: baseArgs.json,
|
||||||
|
help: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareBatchTasks(
|
||||||
|
args: CliArgs,
|
||||||
|
extendConfig: Partial<ExtendConfig>
|
||||||
|
): Promise<{ tasks: PreparedTask[]; jobs: number | null }> {
|
||||||
|
if (!args.batchFile) throw new Error("--batchfile is required in batch mode");
|
||||||
|
const { tasks: taskInputs, jobs: batchJobs, batchDir } = await loadBatchTasks(args.batchFile);
|
||||||
|
if (taskInputs.length === 0) throw new Error("Batch file does not contain any tasks.");
|
||||||
|
|
||||||
|
const prepared: PreparedTask[] = [];
|
||||||
|
for (let i = 0; i < taskInputs.length; i++) {
|
||||||
|
const task = taskInputs[i]!;
|
||||||
|
const taskArgs = createTaskArgs(args, task, batchDir);
|
||||||
|
const prompt = await loadPromptForArgs(taskArgs);
|
||||||
|
if (!prompt) throw new Error(`Task ${i + 1} is missing prompt or promptFiles.`);
|
||||||
|
if (!taskArgs.imagePath) throw new Error(`Task ${i + 1} is missing image output path.`);
|
||||||
|
if (taskArgs.referenceImages.length > 0) await validateReferenceImages(taskArgs.referenceImages);
|
||||||
|
|
||||||
|
const provider = detectProvider(taskArgs);
|
||||||
|
const providerModule = await loadProviderModule(provider);
|
||||||
|
const model = getModelForProvider(provider, taskArgs.model, extendConfig, providerModule);
|
||||||
|
prepared.push({
|
||||||
|
id: task.id || `task-${String(i + 1).padStart(2, "0")}`,
|
||||||
|
prompt,
|
||||||
|
args: taskArgs,
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
outputPath: normalizeOutputImagePath(taskArgs.imagePath),
|
||||||
|
providerModule,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tasks: prepared,
|
||||||
|
jobs: args.jobs ?? batchJobs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeImage(outputPath: string, imageData: Uint8Array): Promise<void> {
|
||||||
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||||
|
await writeFile(outputPath, imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatePreparedTask(task: PreparedTask): Promise<TaskResult> {
|
||||||
|
console.error(`Using ${task.provider} / ${task.model} for ${task.id}`);
|
||||||
|
console.error(
|
||||||
|
`Switch model: --model <id> | EXTEND.md default_model.${task.provider} | env ${task.provider.toUpperCase()}_IMAGE_MODEL`
|
||||||
|
);
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
while (attempts < MAX_ATTEMPTS) {
|
||||||
|
attempts += 1;
|
||||||
|
try {
|
||||||
|
const imageData = await task.providerModule.generateImage(task.prompt, task.model, task.args);
|
||||||
|
await writeImage(task.outputPath, imageData);
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
provider: task.provider,
|
||||||
|
model: task.model,
|
||||||
|
outputPath: task.outputPath,
|
||||||
|
success: true,
|
||||||
|
attempts,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const canRetry = attempts < MAX_ATTEMPTS && isRetryableGenerationError(error);
|
||||||
|
if (canRetry) {
|
||||||
|
console.error(`[${task.id}] Attempt ${attempts}/${MAX_ATTEMPTS} failed, retrying...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
provider: task.provider,
|
||||||
|
model: task.model,
|
||||||
|
outputPath: task.outputPath,
|
||||||
|
success: false,
|
||||||
|
attempts,
|
||||||
|
error: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: task.id,
|
||||||
|
provider: task.provider,
|
||||||
|
model: task.model,
|
||||||
|
outputPath: task.outputPath,
|
||||||
|
success: false,
|
||||||
|
attempts: MAX_ATTEMPTS,
|
||||||
|
error: "Unknown failure",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProviderGate(providerRateLimits: Record<Provider, ProviderRateLimit>) {
|
||||||
|
const state = new Map<Provider, { active: number; lastStartedAt: number }>();
|
||||||
|
|
||||||
|
return async function acquire(provider: Provider): Promise<() => void> {
|
||||||
|
const limit = providerRateLimits[provider];
|
||||||
|
while (true) {
|
||||||
|
const current = state.get(provider) ?? { active: 0, lastStartedAt: 0 };
|
||||||
|
const now = Date.now();
|
||||||
|
const enoughCapacity = current.active < limit.concurrency;
|
||||||
|
const enoughGap = now - current.lastStartedAt >= limit.startIntervalMs;
|
||||||
|
if (enoughCapacity && enoughGap) {
|
||||||
|
state.set(provider, { active: current.active + 1, lastStartedAt: now });
|
||||||
|
return () => {
|
||||||
|
const latest = state.get(provider) ?? { active: 1, lastStartedAt: now };
|
||||||
|
state.set(provider, {
|
||||||
|
active: Math.max(0, latest.active - 1),
|
||||||
|
lastStartedAt: latest.lastStartedAt,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, POLL_WAIT_MS));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkerCount(taskCount: number, jobs: number | null, maxWorkers: number): number {
|
||||||
|
const requested = jobs ?? Math.min(taskCount, maxWorkers);
|
||||||
|
return Math.max(1, Math.min(requested, taskCount, maxWorkers));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBatchTasks(
|
||||||
|
tasks: PreparedTask[],
|
||||||
|
jobs: number | null,
|
||||||
|
extendConfig: Partial<ExtendConfig>
|
||||||
|
): Promise<TaskResult[]> {
|
||||||
|
if (tasks.length === 1) {
|
||||||
|
return [await generatePreparedTask(tasks[0]!)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWorkers = getConfiguredMaxWorkers(extendConfig);
|
||||||
|
const providerRateLimits = getConfiguredProviderRateLimits(extendConfig);
|
||||||
|
const acquireProvider = createProviderGate(providerRateLimits);
|
||||||
|
const workerCount = getWorkerCount(tasks.length, jobs, maxWorkers);
|
||||||
|
console.error(`Batch mode: ${tasks.length} tasks, ${workerCount} workers, parallel mode enabled.`);
|
||||||
|
for (const provider of ["replicate", "google", "openai", "dashscope"] as Provider[]) {
|
||||||
|
const limit = providerRateLimits[provider];
|
||||||
|
console.error(`- ${provider}: concurrency=${limit.concurrency}, startIntervalMs=${limit.startIntervalMs}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextIndex = 0;
|
||||||
|
const results: TaskResult[] = new Array(tasks.length);
|
||||||
|
|
||||||
|
const worker = async (): Promise<void> => {
|
||||||
|
while (true) {
|
||||||
|
const currentIndex = nextIndex;
|
||||||
|
nextIndex += 1;
|
||||||
|
if (currentIndex >= tasks.length) return;
|
||||||
|
|
||||||
|
const task = tasks[currentIndex]!;
|
||||||
|
const release = await acquireProvider(task.provider);
|
||||||
|
try {
|
||||||
|
results[currentIndex] = await generatePreparedTask(task);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printBatchSummary(results: TaskResult[]): void {
|
||||||
|
const successCount = results.filter((result) => result.success).length;
|
||||||
|
const failureCount = results.length - successCount;
|
||||||
|
|
||||||
|
console.error("");
|
||||||
|
console.error("Batch generation summary:");
|
||||||
|
console.error(`- Total: ${results.length}`);
|
||||||
|
console.error(`- Succeeded: ${successCount}`);
|
||||||
|
console.error(`- Failed: ${failureCount}`);
|
||||||
|
|
||||||
|
if (failureCount > 0) {
|
||||||
|
console.error("Failure reasons:");
|
||||||
|
for (const result of results.filter((item) => !item.success)) {
|
||||||
|
console.error(`- ${result.id}: ${result.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitJson(payload: unknown): void {
|
||||||
|
console.log(JSON.stringify(payload, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSingleMode(args: CliArgs, extendConfig: Partial<ExtendConfig>): Promise<void> {
|
||||||
|
const task = await prepareSingleTask(args, extendConfig);
|
||||||
|
const result = await generatePreparedTask(task);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Generation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.json) {
|
||||||
|
emitJson({
|
||||||
|
savedImage: result.outputPath,
|
||||||
|
provider: result.provider,
|
||||||
|
model: result.model,
|
||||||
|
attempts: result.attempts,
|
||||||
|
prompt: task.prompt.slice(0, 200),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(result.outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runBatchMode(args: CliArgs, extendConfig: Partial<ExtendConfig>): Promise<void> {
|
||||||
|
const { tasks, jobs } = await prepareBatchTasks(args, extendConfig);
|
||||||
|
const results = await runBatchTasks(tasks, jobs, extendConfig);
|
||||||
|
printBatchSummary(results);
|
||||||
|
|
||||||
|
if (args.json) {
|
||||||
|
emitJson({
|
||||||
|
mode: "batch",
|
||||||
|
total: results.length,
|
||||||
|
succeeded: results.filter((item) => item.success).length,
|
||||||
|
failed: results.filter((item) => !item.success).length,
|
||||||
|
results,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.some((item) => !item.success)) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const args = parseArgs(process.argv.slice(2));
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
printUsage();
|
printUsage();
|
||||||
return;
|
return;
|
||||||
@@ -412,86 +935,18 @@ async function main(): Promise<void> {
|
|||||||
await loadEnv();
|
await loadEnv();
|
||||||
const extendConfig = await loadExtendConfig();
|
const extendConfig = await loadExtendConfig();
|
||||||
const mergedArgs = mergeConfig(args, extendConfig);
|
const mergedArgs = mergeConfig(args, extendConfig);
|
||||||
|
|
||||||
if (!mergedArgs.quality) mergedArgs.quality = "2k";
|
if (!mergedArgs.quality) mergedArgs.quality = "2k";
|
||||||
|
|
||||||
let prompt: string | null = mergedArgs.prompt;
|
if (mergedArgs.batchFile) {
|
||||||
if (!prompt && mergedArgs.promptFiles.length > 0) prompt = await readPromptFromFiles(mergedArgs.promptFiles);
|
await runBatchMode(mergedArgs, extendConfig);
|
||||||
if (!prompt) prompt = await readPromptFromStdin();
|
|
||||||
|
|
||||||
if (!prompt) {
|
|
||||||
console.error("Error: Prompt is required");
|
|
||||||
printUsage();
|
|
||||||
process.exitCode = 1;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mergedArgs.imagePath) {
|
await runSingleMode(mergedArgs, extendConfig);
|
||||||
console.error("Error: --image is required");
|
|
||||||
printUsage();
|
|
||||||
process.exitCode = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mergedArgs.referenceImages.length > 0) {
|
|
||||||
await validateReferenceImages(mergedArgs.referenceImages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = detectProvider(mergedArgs);
|
|
||||||
const providerModule = await loadProviderModule(provider);
|
|
||||||
|
|
||||||
let model = mergedArgs.model;
|
|
||||||
if (!model && extendConfig.default_model) {
|
|
||||||
if (provider === "google") model = extendConfig.default_model.google ?? null;
|
|
||||||
if (provider === "openai") model = extendConfig.default_model.openai ?? null;
|
|
||||||
if (provider === "dashscope") model = extendConfig.default_model.dashscope ?? null;
|
|
||||||
if (provider === "replicate") model = extendConfig.default_model.replicate ?? null;
|
|
||||||
}
|
|
||||||
model = model || providerModule.getDefaultModel();
|
|
||||||
|
|
||||||
const outputPath = normalizeOutputImagePath(mergedArgs.imagePath);
|
|
||||||
|
|
||||||
let imageData: Uint8Array;
|
|
||||||
let retried = false;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
imageData = await providerModule.generateImage(prompt, model, mergedArgs);
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
if (!retried && isRetryableGenerationError(e)) {
|
|
||||||
retried = true;
|
|
||||||
console.error("Generation failed, retrying...");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = path.dirname(outputPath);
|
|
||||||
await mkdir(dir, { recursive: true });
|
|
||||||
await writeFile(outputPath, imageData);
|
|
||||||
|
|
||||||
if (mergedArgs.json) {
|
|
||||||
console.log(
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
savedImage: outputPath,
|
|
||||||
provider,
|
|
||||||
model,
|
|
||||||
prompt: prompt.slice(0, 200),
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(outputPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((error) => {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
console.error(msg);
|
console.error(message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { execSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import type { CliArgs } from "../types";
|
import type { CliArgs } from "../types";
|
||||||
|
|
||||||
const GOOGLE_MULTIMODAL_MODELS = [
|
const GOOGLE_MULTIMODAL_MODELS = [
|
||||||
@@ -76,14 +76,43 @@ async function postGoogleJsonViaCurl<T>(
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const proxy = getHttpProxy();
|
const proxy = getHttpProxy();
|
||||||
const bodyStr = JSON.stringify(body);
|
const bodyStr = JSON.stringify(body);
|
||||||
const proxyArgs = proxy ? `-x "${proxy}"` : "";
|
const args = [
|
||||||
|
"-s",
|
||||||
|
"--connect-timeout",
|
||||||
|
"30",
|
||||||
|
"--max-time",
|
||||||
|
"300",
|
||||||
|
...(proxy ? ["-x", proxy] : []),
|
||||||
|
url,
|
||||||
|
"-H",
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"-H",
|
||||||
|
`x-goog-api-key: ${apiKey}`,
|
||||||
|
"-d",
|
||||||
|
"@-",
|
||||||
|
];
|
||||||
|
|
||||||
const result = execSync(
|
let result = "";
|
||||||
`curl -s --connect-timeout 30 --max-time 300 ${proxyArgs} "${url}" -H "Content-Type: application/json" -H "x-goog-api-key: ${apiKey}" -d @-`,
|
try {
|
||||||
{ input: bodyStr, maxBuffer: 100 * 1024 * 1024, timeout: 310000 },
|
result = execFileSync("curl", args, {
|
||||||
);
|
input: bodyStr,
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer: 100 * 1024 * 1024,
|
||||||
|
timeout: 310000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const e = error as { message?: string; stderr?: string | Buffer };
|
||||||
|
const stderrText =
|
||||||
|
typeof e.stderr === "string"
|
||||||
|
? e.stderr
|
||||||
|
: e.stderr
|
||||||
|
? e.stderr.toString("utf8")
|
||||||
|
: "";
|
||||||
|
const details = stderrText.trim() || e.message || "curl request failed";
|
||||||
|
throw new Error(`Google API request failed via curl: ${details}`);
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = JSON.parse(result.toString()) as any;
|
const parsed = JSON.parse(result) as any;
|
||||||
if (parsed.error) {
|
if (parsed.error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Google API error (${parsed.error.code}): ${parsed.error.message}`,
|
`Google API error (${parsed.error.code}): ${parsed.error.message}`,
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ export async function generateImage(
|
|||||||
const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
|
const baseURL = process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
|
||||||
const apiKey = process.env.OPENAI_API_KEY;
|
const apiKey = process.env.OPENAI_API_KEY;
|
||||||
|
|
||||||
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
|
if (!apiKey) {
|
||||||
|
throw new Error(
|
||||||
|
"OPENAI_API_KEY is required. Codex/ChatGPT desktop login does not automatically grant OpenAI Images API access to this script."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.OPENAI_IMAGE_USE_CHAT === "true") {
|
if (process.env.OPENAI_IMAGE_USE_CHAT === "true") {
|
||||||
return generateWithChatCompletions(baseURL, apiKey, prompt, model);
|
return generateWithChatCompletions(baseURL, apiKey, prompt, model);
|
||||||
|
|||||||
@@ -36,22 +36,24 @@ function buildInput(prompt: string, args: CliArgs, referenceImages: string[]): R
|
|||||||
|
|
||||||
if (args.aspectRatio) {
|
if (args.aspectRatio) {
|
||||||
input.aspect_ratio = args.aspectRatio;
|
input.aspect_ratio = args.aspectRatio;
|
||||||
|
} else if (referenceImages.length > 0) {
|
||||||
|
input.aspect_ratio = "match_input_image";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.n > 1) {
|
if (args.n > 1) {
|
||||||
input.number_of_images = args.n;
|
input.number_of_images = args.n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.quality === "normal") {
|
||||||
|
input.resolution = "1K";
|
||||||
|
} else if (args.quality === "2k") {
|
||||||
|
input.resolution = "2K";
|
||||||
|
}
|
||||||
|
|
||||||
input.output_format = "png";
|
input.output_format = "png";
|
||||||
|
|
||||||
if (referenceImages.length > 0) {
|
if (referenceImages.length > 0) {
|
||||||
if (referenceImages.length === 1) {
|
input.image_input = referenceImages;
|
||||||
input.image = referenceImages[0];
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < referenceImages.length; i++) {
|
|
||||||
input[`image${i > 0 ? i + 1 : ""}`] = referenceImages[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
|
|||||||
@@ -13,10 +13,34 @@ export type CliArgs = {
|
|||||||
imageSize: string | null;
|
imageSize: string | null;
|
||||||
referenceImages: string[];
|
referenceImages: string[];
|
||||||
n: number;
|
n: number;
|
||||||
|
batchFile: string | null;
|
||||||
|
jobs: number | null;
|
||||||
json: boolean;
|
json: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BatchTaskInput = {
|
||||||
|
id?: string;
|
||||||
|
prompt?: string | null;
|
||||||
|
promptFiles?: string[];
|
||||||
|
image?: string;
|
||||||
|
provider?: Provider | null;
|
||||||
|
model?: string | null;
|
||||||
|
ar?: string | null;
|
||||||
|
size?: string | null;
|
||||||
|
quality?: Quality | null;
|
||||||
|
imageSize?: "1K" | "2K" | "4K" | null;
|
||||||
|
ref?: string[];
|
||||||
|
n?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BatchFile =
|
||||||
|
| BatchTaskInput[]
|
||||||
|
| {
|
||||||
|
tasks: BatchTaskInput[];
|
||||||
|
jobs?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExtendConfig = {
|
export type ExtendConfig = {
|
||||||
version: number;
|
version: number;
|
||||||
default_provider: Provider | null;
|
default_provider: Provider | null;
|
||||||
@@ -29,4 +53,16 @@ export type ExtendConfig = {
|
|||||||
dashscope: string | null;
|
dashscope: string | null;
|
||||||
replicate: string | null;
|
replicate: string | null;
|
||||||
};
|
};
|
||||||
|
batch?: {
|
||||||
|
max_workers?: number | null;
|
||||||
|
provider_limits?: Partial<
|
||||||
|
Record<
|
||||||
|
Provider,
|
||||||
|
{
|
||||||
|
concurrency?: number | null;
|
||||||
|
start_interval_ms?: number | null;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-infographic
|
name: baoyu-infographic
|
||||||
description: Generates professional infographics with 21 layout types and 20 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
|
description: Generates professional infographics with 21 layout types and 20 visual styles. Analyzes content, recommends layout×style combinations, and generates publication-ready infographics. Use when user asks to create "infographic", "信息图", "visual summary", "可视化", or "高密度信息大图".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-infographic
|
||||||
---
|
---
|
||||||
|
|
||||||
# Infographic Generator
|
# Infographic Generator
|
||||||
@@ -143,12 +147,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-infographic/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-infographic/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-infographic/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-infographic/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-infographic/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-infographic/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-markdown-to-html
|
name: baoyu-markdown-to-html
|
||||||
description: Converts Markdown to styled HTML with WeChat-compatible themes. Supports code highlighting, math, PlantUML, footnotes, alerts, infographics, and optional bottom citations for external links. Use when user asks for "markdown to html", "convert md to html", "md转html", "微信外链转底部引用", or needs styled HTML output from markdown.
|
description: Converts Markdown to styled HTML with WeChat-compatible themes. Supports code highlighting, math, PlantUML, footnotes, alerts, infographics, and optional bottom citations for external links. Use when user asks for "markdown to html", "convert md to html", "md转html", "微信外链转底部引用", or needs styled HTML output from markdown.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-markdown-to-html
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Markdown to HTML Converter
|
# Markdown to HTML Converter
|
||||||
@@ -9,7 +17,7 @@ Converts Markdown files to beautifully styled HTML with inline CSS, optimized fo
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
**Agent Execution**: Determine this SKILL.md directory as `SKILL_DIR`. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
**Agent Execution**: Determine this SKILL.md directory as `{baseDir}`. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -22,12 +30,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-markdown-to-html/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-markdown-to-html/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-markdown-to-html/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-markdown-to-html/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-markdown-to-html/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-markdown-to-html/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-markdown-to-html/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-markdown-to-html/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-markdown-to-html/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-markdown-to-html/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -117,7 +128,7 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md") { Select-Str
|
|||||||
### Step 2: Convert
|
### Step 2: Convert
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <markdown_file> --theme <theme> [--cite]
|
${BUN_X} {baseDir}/scripts/main.ts <markdown_file> --theme <theme> [--cite]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Report Result
|
### Step 3: Report Result
|
||||||
@@ -127,7 +138,7 @@ Display the output path from JSON result. If backup was created, mention it.
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
|
${BUN_X} {baseDir}/scripts/main.ts <markdown_file> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
@@ -165,22 +176,22 @@ ${BUN_X} ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic conversion (uses default theme, removes first heading)
|
# Basic conversion (uses default theme, removes first heading)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md
|
${BUN_X} {baseDir}/scripts/main.ts article.md
|
||||||
|
|
||||||
# With specific theme
|
# With specific theme
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --theme grace
|
${BUN_X} {baseDir}/scripts/main.ts article.md --theme grace
|
||||||
|
|
||||||
# Theme with custom color
|
# Theme with custom color
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --theme modern --color red
|
${BUN_X} {baseDir}/scripts/main.ts article.md --theme modern --color red
|
||||||
|
|
||||||
# Enable bottom citations for ordinary external links
|
# Enable bottom citations for ordinary external links
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --cite
|
${BUN_X} {baseDir}/scripts/main.ts article.md --cite
|
||||||
|
|
||||||
# Keep the first heading in content
|
# Keep the first heading in content
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --keep-title
|
${BUN_X} {baseDir}/scripts/main.ts article.md --keep-title
|
||||||
|
|
||||||
# Override title
|
# Override title
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts article.md --title "My Article"
|
${BUN_X} {baseDir}/scripts/main.ts article.md --title "My Article"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ function parseExtendYaml(yaml: string): Partial<ExtendConfig> {
|
|||||||
export function loadExtendConfig(): Partial<ExtendConfig> {
|
export function loadExtendConfig(): Partial<ExtendConfig> {
|
||||||
const paths = [
|
const paths = [
|
||||||
path.join(process.cwd(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
path.join(process.cwd(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
||||||
|
path.join(
|
||||||
|
process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config"),
|
||||||
|
"baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"
|
||||||
|
),
|
||||||
path.join(homedir(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
path.join(homedir(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
||||||
];
|
];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-post-to-wechat
|
name: baoyu-post-to-wechat
|
||||||
description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (贴图, formerly 图文) with multiple images. Markdown article workflows default to converting ordinary external links into bottom citations for WeChat-friendly output. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章".
|
description: Posts content to WeChat Official Account (微信公众号) via API or Chrome CDP. Supports article posting (文章) with HTML, markdown, or plain text input, and image-text posting (贴图, formerly 图文) with multiple images. Markdown article workflows default to converting ordinary external links into bottom citations for WeChat-friendly output. Use when user mentions "发布公众号", "post to wechat", "微信公众号", or "贴图/图文/文章".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-wechat
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Post to WeChat Official Account
|
# Post to WeChat Official Account
|
||||||
@@ -11,7 +19,7 @@ description: Posts content to WeChat Official Account (微信公众号) via API
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
**Agent Execution**: Determine this SKILL.md directory as `SKILL_DIR`, then use `${SKILL_DIR}/scripts/<name>.ts`. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun.
|
**Agent Execution**: Determine this SKILL.md directory as `{baseDir}`, then use `{baseDir}/scripts/<name>.ts`. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -28,12 +36,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-post-to-wechat/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-post-to-wechat/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-post-to-wechat/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-post-to-wechat/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-post-to-wechat/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -92,7 +103,7 @@ chrome_profile_path: /path/to/chrome/profile
|
|||||||
Before first use, suggest running the environment check. User can skip if they prefer.
|
Before first use, suggest running the environment check. User can skip if they prefer.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/check-permissions.ts
|
${BUN_X} {baseDir}/scripts/check-permissions.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, API credentials, Chrome conflicts.
|
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, API credentials, Chrome conflicts.
|
||||||
@@ -115,8 +126,8 @@ Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystrok
|
|||||||
For short posts with multiple images (up to 9):
|
For short posts with multiple images (up to 9):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/wechat-browser.ts --markdown article.md --images ./images/
|
${BUN_X} {baseDir}/scripts/wechat-browser.ts --markdown article.md --images ./images/
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "内容" --image img.png --submit
|
${BUN_X} {baseDir}/scripts/wechat-browser.ts --title "标题" --content "内容" --image img.png --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
See [references/image-text-posting.md](references/image-text-posting.md) for details.
|
See [references/image-text-posting.md](references/image-text-posting.md) for details.
|
||||||
@@ -260,7 +271,7 @@ WECHAT_APP_SECRET=<user_input>
|
|||||||
**API method** (accepts `.md` or `.html`):
|
**API method** (accepts `.md` or `.html`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/wechat-api.ts <file> --theme <theme> [--color <color>] [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>] [--no-cite]
|
${BUN_X} {baseDir}/scripts/wechat-api.ts <file> --theme <theme> [--color <color>] [--title <title>] [--summary <summary>] [--author <author>] [--cover <cover_path>] [--no-cite]
|
||||||
```
|
```
|
||||||
|
|
||||||
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md.
|
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md.
|
||||||
@@ -279,8 +290,8 @@ If script parameters do not expose the two comment fields, still ensure final AP
|
|||||||
**Browser method** (accepts `--markdown` or `--html`):
|
**Browser method** (accepts `--markdown` or `--html`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --markdown <markdown_file> --theme <theme> [--color <color>] [--no-cite]
|
${BUN_X} {baseDir}/scripts/wechat-article.ts --markdown <markdown_file> --theme <theme> [--color <color>] [--no-cite]
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/wechat-article.ts --html <html_file>
|
${BUN_X} {baseDir}/scripts/wechat-article.ts --html <html_file>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Completion Report
|
### Step 5: Completion Report
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ function parseExtendYaml(yaml: string): Partial<ExtendConfig> {
|
|||||||
export function loadExtendConfig(): Partial<ExtendConfig> {
|
export function loadExtendConfig(): Partial<ExtendConfig> {
|
||||||
const paths = [
|
const paths = [
|
||||||
path.join(process.cwd(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
path.join(process.cwd(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
||||||
|
path.join(
|
||||||
|
process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config"),
|
||||||
|
"baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"
|
||||||
|
),
|
||||||
path.join(homedir(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
path.join(homedir(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
|
||||||
];
|
];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { execSync, spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
@@ -10,30 +10,51 @@ function sleep(ms: number): Promise<void> {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
function ab(cmd: string, json = false): string {
|
function quoteForLog(arg: string): string {
|
||||||
const fullCmd = `agent-browser --session ${SESSION} ${cmd}${json ? ' --json' : ''}`;
|
return /[\s"'\\]/.test(arg) ? JSON.stringify(arg) : arg;
|
||||||
console.log(`[ab] ${fullCmd}`);
|
|
||||||
try {
|
|
||||||
const result = execSync(fullCmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
return result.trim();
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const err = e as { stdout?: string; stderr?: string; message?: string };
|
|
||||||
console.error(`[ab] Error: ${err.stderr || err.message}`);
|
|
||||||
return err.stdout || '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function abRaw(args: string[]): { success: boolean; output: string } {
|
function toSafeJsStringLiteral(value: string): string {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
.replace(/\u2028/g, '\\u2028')
|
||||||
|
.replace(/\u2029/g, '\\u2029');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAgentBrowser(args: string[]): {
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
spawnError?: string;
|
||||||
|
} {
|
||||||
const result = spawnSync('agent-browser', ['--session', SESSION, ...args], {
|
const result = spawnSync('agent-browser', ['--session', SESSION, ...args], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
const spawnError = result.error?.message?.trim();
|
||||||
|
const output = result.stdout || result.stderr || '';
|
||||||
return {
|
return {
|
||||||
success: result.status === 0,
|
success: result.status === 0,
|
||||||
output: result.stdout || result.stderr || ''
|
output: output || spawnError || '',
|
||||||
|
spawnError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ab(args: string[], json = false): string {
|
||||||
|
const fullArgs = json ? [...args, '--json'] : args;
|
||||||
|
console.log(`[ab] agent-browser --session ${SESSION} ${fullArgs.map(quoteForLog).join(' ')}`);
|
||||||
|
const result = runAgentBrowser(fullArgs);
|
||||||
|
if (result.spawnError) {
|
||||||
|
throw new Error(`agent-browser failed to start: ${result.spawnError}`);
|
||||||
|
}
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`[ab] Error: ${result.output.trim()}`);
|
||||||
|
}
|
||||||
|
return result.output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function abRaw(args: string[]): { success: boolean; output: string } {
|
||||||
|
return runAgentBrowser(args);
|
||||||
|
}
|
||||||
|
|
||||||
interface SnapshotElement {
|
interface SnapshotElement {
|
||||||
ref: string;
|
ref: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -99,17 +120,17 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[wechat] Opening WeChat Official Account...');
|
console.log('[wechat] Opening WeChat Official Account...');
|
||||||
ab(`open ${WECHAT_URL} --headed`);
|
ab(['open', WECHAT_URL, '--headed']);
|
||||||
await sleep(5000);
|
await sleep(5000);
|
||||||
|
|
||||||
console.log('[wechat] Checking login status...');
|
console.log('[wechat] Checking login status...');
|
||||||
let url = ab('get url');
|
let url = ab(['get', 'url']);
|
||||||
console.log(`[wechat] Current URL: ${url}`);
|
console.log(`[wechat] Current URL: ${url}`);
|
||||||
|
|
||||||
const waitForLogin = async (timeoutMs = 120_000): Promise<boolean> => {
|
const waitForLogin = async (timeoutMs = 120_000): Promise<boolean> => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
url = ab('get url');
|
url = ab(['get', 'url']);
|
||||||
if (url.includes('/cgi-bin/home')) return true;
|
if (url.includes('/cgi-bin/home')) return true;
|
||||||
console.log('[wechat] Waiting for login...');
|
console.log('[wechat] Waiting for login...');
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
@@ -126,7 +147,7 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
|
||||||
console.log('[wechat] Getting page snapshot...');
|
console.log('[wechat] Getting page snapshot...');
|
||||||
let snapshot = ab('snapshot');
|
let snapshot = ab(['snapshot']);
|
||||||
console.log(snapshot);
|
console.log(snapshot);
|
||||||
|
|
||||||
console.log('[wechat] Looking for "图文" menu...');
|
console.log('[wechat] Looking for "图文" menu...');
|
||||||
@@ -134,16 +155,16 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
|
|
||||||
if (!tuWenRef) {
|
if (!tuWenRef) {
|
||||||
console.log('[wechat] Using eval to find and click menu...');
|
console.log('[wechat] Using eval to find and click menu...');
|
||||||
ab(`eval "document.querySelectorAll('.new-creation__menu .new-creation__menu-item')[2].click()"`);
|
ab(['eval', "document.querySelectorAll('.new-creation__menu .new-creation__menu-item')[2].click()"]);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[wechat] Clicking menu ref: ${tuWenRef}`);
|
console.log(`[wechat] Clicking menu ref: ${tuWenRef}`);
|
||||||
ab(`click ${tuWenRef}`);
|
ab(['click', tuWenRef]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(4000);
|
await sleep(4000);
|
||||||
|
|
||||||
console.log('[wechat] Checking for new tab...');
|
console.log('[wechat] Checking for new tab...');
|
||||||
const tabsOutput = ab('tab');
|
const tabsOutput = ab(['tab']);
|
||||||
console.log(`[wechat] Tabs: ${tabsOutput}`);
|
console.log(`[wechat] Tabs: ${tabsOutput}`);
|
||||||
|
|
||||||
const tabLines = tabsOutput.split('\n');
|
const tabLines = tabsOutput.split('\n');
|
||||||
@@ -153,14 +174,14 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
const tabMatch = tabsOutput.match(/\[(\d+)\].*(?:appmsg|edit)/i);
|
const tabMatch = tabsOutput.match(/\[(\d+)\].*(?:appmsg|edit)/i);
|
||||||
if (tabMatch) {
|
if (tabMatch) {
|
||||||
console.log(`[wechat] Switching to editor tab ${tabMatch[1]}...`);
|
console.log(`[wechat] Switching to editor tab ${tabMatch[1]}...`);
|
||||||
ab(`tab ${tabMatch[1]}`);
|
ab(['tab', tabMatch[1]]);
|
||||||
} else {
|
} else {
|
||||||
const lastTabMatch = tabsOutput.match(/\[(\d+)\]/g);
|
const lastTabMatch = tabsOutput.match(/\[(\d+)\]/g);
|
||||||
if (lastTabMatch && lastTabMatch.length > 1) {
|
if (lastTabMatch && lastTabMatch.length > 1) {
|
||||||
const lastTab = lastTabMatch[lastTabMatch.length - 1].match(/\d+/)?.[0];
|
const lastTab = lastTabMatch[lastTabMatch.length - 1].match(/\d+/)?.[0];
|
||||||
if (lastTab) {
|
if (lastTab) {
|
||||||
console.log(`[wechat] Switching to last tab ${lastTab}...`);
|
console.log(`[wechat] Switching to last tab ${lastTab}...`);
|
||||||
ab(`tab ${lastTab}`);
|
ab(['tab', lastTab]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,38 +189,44 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
|
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
|
||||||
url = ab('get url');
|
url = ab(['get', 'url']);
|
||||||
console.log(`[wechat] Editor URL: ${url}`);
|
console.log(`[wechat] Editor URL: ${url}`);
|
||||||
|
|
||||||
console.log('[wechat] Getting editor snapshot...');
|
console.log('[wechat] Getting editor snapshot...');
|
||||||
snapshot = ab('snapshot');
|
snapshot = ab(['snapshot']);
|
||||||
console.log(snapshot.substring(0, 2000));
|
console.log(snapshot.substring(0, 2000));
|
||||||
|
|
||||||
console.log('[wechat] Uploading images...');
|
console.log('[wechat] Uploading images...');
|
||||||
const fileInputSelector = '.js_upload_btn_container input[type=file]';
|
const fileInputSelector = '.js_upload_btn_container input[type=file]';
|
||||||
|
const fileInputSelectorJs = toSafeJsStringLiteral(fileInputSelector);
|
||||||
|
|
||||||
ab(`eval "document.querySelector('${fileInputSelector}').style.display = 'block'"`);
|
ab(['eval', `{
|
||||||
|
const input = document.querySelector(${fileInputSelectorJs});
|
||||||
|
if (input) input.style.display = 'block';
|
||||||
|
}`]);
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
const uploadResult = abRaw(['upload', `"${fileInputSelector}"`, ...absoluteImages]);
|
const uploadResult = abRaw(['upload', fileInputSelector, ...absoluteImages]);
|
||||||
console.log(`[wechat] Upload result: ${uploadResult.output}`);
|
console.log(`[wechat] Upload result: ${uploadResult.output}`);
|
||||||
|
|
||||||
if (!uploadResult.success) {
|
if (!uploadResult.success) {
|
||||||
console.log('[wechat] Using alternative upload method...');
|
console.log('[wechat] Using alternative upload method...');
|
||||||
for (const img of absoluteImages) {
|
for (const img of absoluteImages) {
|
||||||
console.log(`[wechat] Uploading: ${img}`);
|
console.log(`[wechat] Uploading: ${img}`);
|
||||||
ab(`eval "
|
const imgUrlJs = toSafeJsStringLiteral(`file://${img}`);
|
||||||
const input = document.querySelector('${fileInputSelector}');
|
const imgFileNameJs = toSafeJsStringLiteral(path.basename(img));
|
||||||
|
ab(['eval', `
|
||||||
|
const input = document.querySelector(${fileInputSelectorJs});
|
||||||
if (input) {
|
if (input) {
|
||||||
const dt = new DataTransfer();
|
const dt = new DataTransfer();
|
||||||
fetch('file://${img}').then(r => r.blob()).then(b => {
|
fetch(${imgUrlJs}).then(r => r.blob()).then(b => {
|
||||||
const file = new File([b], '${path.basename(img)}', { type: 'image/png' });
|
const file = new File([b], ${imgFileNameJs}, { type: 'image/png' });
|
||||||
dt.items.add(file);
|
dt.items.add(file);
|
||||||
input.files = dt.files;
|
input.files = dt.files;
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"`);
|
`]);
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,13 +235,14 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
await sleep(10000);
|
await sleep(10000);
|
||||||
|
|
||||||
console.log('[wechat] Filling title...');
|
console.log('[wechat] Filling title...');
|
||||||
snapshot = ab('snapshot -i');
|
snapshot = ab(['snapshot', '-i']);
|
||||||
const titleRef = findElementByText(snapshot, 'title') || findElementByText(snapshot, '标题');
|
const titleRef = findElementByText(snapshot, 'title') || findElementByText(snapshot, '标题');
|
||||||
|
|
||||||
if (titleRef) {
|
if (titleRef) {
|
||||||
ab(`fill ${titleRef} "${title.replace(/"/g, '\\"')}"`);
|
ab(['fill', titleRef, title]);
|
||||||
} else {
|
} else {
|
||||||
ab(`eval "const t = document.querySelector('#title'); if(t) { t.value = '${title.replace(/'/g, "\\'")}'; t.dispatchEvent(new Event('input', {bubbles: true})); }"`);
|
const titleJs = toSafeJsStringLiteral(title);
|
||||||
|
ab(['eval', `const t = document.querySelector('#title'); if(t) { t.value = ${titleJs}; t.dispatchEvent(new Event('input', {bubbles: true})); }`]);
|
||||||
}
|
}
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@@ -222,9 +250,9 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
const editorRef = findElementByText(snapshot, 'js_pmEditorArea') || findElementByText(snapshot, 'textbox');
|
const editorRef = findElementByText(snapshot, 'js_pmEditorArea') || findElementByText(snapshot, 'textbox');
|
||||||
|
|
||||||
if (editorRef) {
|
if (editorRef) {
|
||||||
ab(`click ${editorRef}`);
|
ab(['click', editorRef]);
|
||||||
} else {
|
} else {
|
||||||
ab(`eval "document.querySelector('.js_pmEditorArea')?.click()"`);
|
ab(['eval', "document.querySelector('.js_pmEditorArea')?.click()"]);
|
||||||
}
|
}
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@@ -233,11 +261,11 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
if (line.length > 0) {
|
if (line.length > 0) {
|
||||||
const escapedLine = line.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
const lineJs = toSafeJsStringLiteral(line);
|
||||||
ab(`eval "document.execCommand('insertText', false, '${escapedLine}')"`);
|
ab(['eval', `document.execCommand('insertText', false, ${lineJs})`]);
|
||||||
}
|
}
|
||||||
if (i < lines.length - 1) {
|
if (i < lines.length - 1) {
|
||||||
ab('press Enter');
|
ab(['press', 'Enter']);
|
||||||
}
|
}
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
}
|
}
|
||||||
@@ -249,9 +277,9 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
console.log('[wechat] Saving as draft...');
|
console.log('[wechat] Saving as draft...');
|
||||||
const submitRef = findElementByText(snapshot, 'js_submit') || findElementByText(snapshot, '保存');
|
const submitRef = findElementByText(snapshot, 'js_submit') || findElementByText(snapshot, '保存');
|
||||||
if (submitRef) {
|
if (submitRef) {
|
||||||
ab(`click ${submitRef}`);
|
ab(['click', submitRef]);
|
||||||
} else {
|
} else {
|
||||||
ab(`eval "document.querySelector('#js_submit')?.click()"`);
|
ab(['eval', "document.querySelector('#js_submit')?.click()"]);
|
||||||
}
|
}
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
console.log('[wechat] Draft saved!');
|
console.log('[wechat] Draft saved!');
|
||||||
@@ -261,7 +289,7 @@ async function postToWeChat(options: WeChatOptions): Promise<void> {
|
|||||||
|
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
console.log('[wechat] Closing browser...');
|
console.log('[wechat] Closing browser...');
|
||||||
ab('close');
|
ab(['close']);
|
||||||
} else {
|
} else {
|
||||||
console.log('[wechat] Done. Browser window left open.');
|
console.log('[wechat] Done. Browser window left open.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -551,37 +551,126 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
const absolutePaths = images.map(p => path.isAbsolute(p) ? p : path.resolve(process.cwd(), p));
|
const absolutePaths = images.map(p => path.isAbsolute(p) ? p : path.resolve(process.cwd(), p));
|
||||||
console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`);
|
console.log(`[wechat-browser] Images: ${absolutePaths.join(', ')}`);
|
||||||
|
|
||||||
const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId });
|
// --- PRIMARY approach: intercept file chooser dialog ---
|
||||||
|
let uploadSuccess = false;
|
||||||
|
try {
|
||||||
|
console.log('[wechat-browser] [primary] Enabling file chooser interception...');
|
||||||
|
await cdp.send('Page.setInterceptFileChooserDialog', { enabled: true }, { sessionId });
|
||||||
|
|
||||||
// Try primary selector, then fallback to any multi-file image input
|
// Set up listener for file chooser opened event BEFORE clicking
|
||||||
let { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', {
|
const fileChooserPromise = new Promise<{ backendNodeId: number; mode: string }>((resolve, reject) => {
|
||||||
nodeId: root.nodeId,
|
const timeout = setTimeout(() => reject(new Error('File chooser dialog not opened within 10s')), 10_000);
|
||||||
selector: '.js_upload_btn_container input[type=file]',
|
cdp!.on('Page.fileChooserOpened', (params: unknown) => {
|
||||||
}, { sessionId });
|
clearTimeout(timeout);
|
||||||
|
const p = params as { backendNodeId: number; mode: string };
|
||||||
|
console.log(`[wechat-browser] [primary] File chooser opened: backendNodeId=${p.backendNodeId}, mode=${p.mode}`);
|
||||||
|
resolve(p);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (!nodeId) {
|
// Trigger file chooser by calling .click() on the file input with userGesture
|
||||||
console.log('[wechat-browser] Primary file input not found, trying fallback selector...');
|
const fileInputSelectors = [
|
||||||
const fallback = await cdp.send<{ nodeId: number }>('DOM.querySelector', {
|
'.js_upload_btn_container input[type=file]',
|
||||||
nodeId: root.nodeId,
|
'input[type=file][multiple][accept*="image"]',
|
||||||
selector: 'input[type=file][multiple][accept*="image"]',
|
'input[type=file][accept*="image"]',
|
||||||
|
'input[type=file][multiple]',
|
||||||
|
'input[type=file]',
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('[wechat-browser] [primary] Clicking file input via JS .click() with userGesture...');
|
||||||
|
const clickResult = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
|
expression: `
|
||||||
|
(function() {
|
||||||
|
const selectors = ${JSON.stringify(fileInputSelectors)};
|
||||||
|
for (const sel of selectors) {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (el) {
|
||||||
|
el.click();
|
||||||
|
return JSON.stringify({ clicked: sel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const debug = [];
|
||||||
|
document.querySelectorAll('input[type=file]').forEach((inp, i) => {
|
||||||
|
debug.push({ i, accept: inp.accept, multiple: inp.multiple, parentClass: inp.parentElement?.className?.slice(0, 60) });
|
||||||
|
});
|
||||||
|
return JSON.stringify({ error: 'no file input found', fileInputs: debug });
|
||||||
|
})()
|
||||||
|
`,
|
||||||
|
returnByValue: true,
|
||||||
|
userGesture: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
nodeId = fallback.nodeId;
|
console.log(`[wechat-browser] [primary] Click result: ${clickResult.result.value}`);
|
||||||
|
|
||||||
|
const clickStatus = JSON.parse(clickResult.result.value);
|
||||||
|
if (clickStatus.error) {
|
||||||
|
throw new Error(`File input not found: ${clickStatus.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the file chooser event
|
||||||
|
console.log('[wechat-browser] [primary] Waiting for file chooser dialog...');
|
||||||
|
const chooser = await fileChooserPromise;
|
||||||
|
|
||||||
|
console.log(`[wechat-browser] [primary] Setting files via backendNodeId=${chooser.backendNodeId}...`);
|
||||||
|
await cdp.send('DOM.setFileInputFiles', {
|
||||||
|
files: absolutePaths,
|
||||||
|
backendNodeId: chooser.backendNodeId,
|
||||||
|
}, { sessionId });
|
||||||
|
console.log('[wechat-browser] [primary] Files set successfully via file chooser interception');
|
||||||
|
uploadSuccess = true;
|
||||||
|
} catch (primaryErr) {
|
||||||
|
console.log(`[wechat-browser] [primary] File chooser approach failed: ${primaryErr instanceof Error ? primaryErr.message : String(primaryErr)}`);
|
||||||
|
// Disable interception before falling back
|
||||||
|
try { await cdp.send('Page.setInterceptFileChooserDialog', { enabled: false }, { sessionId }); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nodeId) throw new Error('File input not found');
|
// --- FALLBACK approach: direct DOM.setFileInputFiles on nodeId ---
|
||||||
|
if (!uploadSuccess) {
|
||||||
|
console.log('[wechat-browser] [fallback] Trying direct DOM.setFileInputFiles...');
|
||||||
|
const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId });
|
||||||
|
|
||||||
await cdp.send('DOM.setFileInputFiles', {
|
const fileInputSelectors = [
|
||||||
nodeId,
|
'.js_upload_btn_container input[type=file]',
|
||||||
files: absolutePaths,
|
'input[type=file][multiple][accept*="image"]',
|
||||||
}, { sessionId });
|
'input[type=file][accept*="image"]',
|
||||||
|
'input[type=file][multiple]',
|
||||||
|
'input[type=file]',
|
||||||
|
];
|
||||||
|
|
||||||
// Dispatch change event to trigger the upload
|
let nodeId = 0;
|
||||||
await cdp.send('Runtime.evaluate', {
|
for (const sel of fileInputSelectors) {
|
||||||
expression: `
|
const result = await cdp.send<{ nodeId: number }>('DOM.querySelector', { nodeId: root.nodeId, selector: sel }, { sessionId });
|
||||||
const fileInput = document.querySelector('.js_upload_btn_container input[type=file]') || document.querySelector('input[type=file][multiple][accept*="image"]');
|
if (result.nodeId) {
|
||||||
if (fileInput) fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
console.log(`[wechat-browser] [fallback] Found file input with selector: ${sel}`);
|
||||||
`,
|
nodeId = result.nodeId;
|
||||||
}, { sessionId });
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeId) throw new Error('File input not found with any selector');
|
||||||
|
|
||||||
|
await cdp.send('DOM.setFileInputFiles', { nodeId, files: absolutePaths }, { sessionId });
|
||||||
|
console.log('[wechat-browser] [fallback] Files set via nodeId');
|
||||||
|
|
||||||
|
// Dispatch change event
|
||||||
|
await cdp.send('Runtime.evaluate', {
|
||||||
|
expression: `
|
||||||
|
(function() {
|
||||||
|
const selectors = ${JSON.stringify(fileInputSelectors)};
|
||||||
|
for (const sel of selectors) {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (el) {
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
return 'dispatched on ' + sel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'no input found for event dispatch';
|
||||||
|
})()
|
||||||
|
`,
|
||||||
|
returnByValue: true,
|
||||||
|
}, { sessionId });
|
||||||
|
console.log('[wechat-browser] [fallback] Change event dispatched');
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for images to upload
|
// Wait for images to upload
|
||||||
console.log('[wechat-browser] Waiting for images to upload...');
|
console.log('[wechat-browser] Waiting for images to upload...');
|
||||||
@@ -589,11 +678,16 @@ export async function postToWeChat(options: WeChatBrowserOptions): Promise<void>
|
|||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
const uploadCheck = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
const uploadCheck = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||||
expression: `JSON.stringify({ uploaded: document.querySelectorAll('.weui-desktop-upload__thumb, .pic_item, [class*=upload_thumb]').length })`,
|
expression: `
|
||||||
|
JSON.stringify({
|
||||||
|
uploaded: document.querySelectorAll('.weui-desktop-upload__thumb, .pic_item, [class*=upload_thumb], [class*="pic_item"], [class*="upload__thumb"]').length,
|
||||||
|
loading: document.querySelectorAll('[class*="upload_loading"], [class*="uploading"], .weui-desktop-upload__loading').length
|
||||||
|
})
|
||||||
|
`,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
const status = JSON.parse(uploadCheck.result.value);
|
const status = JSON.parse(uploadCheck.result.value);
|
||||||
console.log(`[wechat-browser] Upload progress: ${status.uploaded}/${targetCount}`);
|
console.log(`[wechat-browser] Upload progress: ${status.uploaded}/${targetCount} (loading: ${status.loading})`);
|
||||||
if (status.uploaded >= targetCount) break;
|
if (status.uploaded >= targetCount) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-post-to-weibo
|
name: baoyu-post-to-weibo
|
||||||
description: Posts content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input via Chrome CDP. Use when user asks to "post to Weibo", "发微博", "发布微博", "publish to Weibo", "share on Weibo", "写微博", or "微博头条文章".
|
description: Posts content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input via Chrome CDP. Use when user asks to "post to Weibo", "发微博", "发布微博", "publish to Weibo", "share on Weibo", "写微博", or "微博头条文章".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-weibo
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Post to Weibo
|
# Post to Weibo
|
||||||
@@ -12,9 +20,9 @@ Posts text, images, videos, and long-form articles to Weibo via real Chrome brow
|
|||||||
**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. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Replace all `{baseDir}` in this document with the actual path
|
||||||
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
@@ -32,12 +40,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-post-to-weibo/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-post-to-weibo/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-post-to-weibo/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-post-to-weibo/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-post-to-weibo/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-post-to-weibo/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-post-to-weibo/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-post-to-weibo/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-weibo/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-weibo/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -72,8 +83,8 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-weibo/EXTEND.md") { "user" }
|
|||||||
Text + images/videos (max 18 files total). Posted on Weibo homepage.
|
Text + images/videos (max 18 files total). Posted on Weibo homepage.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/weibo-post.ts "Hello Weibo!" --image ./photo.png
|
${BUN_X} {baseDir}/scripts/weibo-post.ts "Hello Weibo!" --image ./photo.png
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/weibo-post.ts "Watch this" --video ./clip.mp4
|
${BUN_X} {baseDir}/scripts/weibo-post.ts "Watch this" --video ./clip.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -93,8 +104,8 @@ ${BUN_X} ${SKILL_DIR}/scripts/weibo-post.ts "Watch this" --video ./clip.mp4
|
|||||||
Long-form Markdown articles published at `https://card.weibo.com/article/v3/editor`.
|
Long-form Markdown articles published at `https://card.weibo.com/article/v3/editor`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/weibo-article.ts article.md
|
${BUN_X} {baseDir}/scripts/weibo-article.ts article.md
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/weibo-article.ts article.md --cover ./cover.jpg
|
${BUN_X} {baseDir}/scripts/weibo-article.ts article.md --cover ./cover.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
const parentElement = node.parentElement;
|
const parentElement = node.parentElement;
|
||||||
if (parentElement) {
|
if (parentElement) {
|
||||||
parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
parentElement.scrollIntoView({ behavior: 'instant', block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
@@ -500,112 +500,56 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Step A: Copy image to clipboard first (slow due to Swift compilation)
|
||||||
|
console.log(`[weibo-article] Copying image to clipboard: ${path.basename(img.localPath)}`);
|
||||||
|
if (!copyImageToClipboard(img.localPath)) {
|
||||||
|
console.warn(`[weibo-article] Failed to copy image to clipboard`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
// Step B: Select placeholder text (paste will replace the selection)
|
||||||
const selected = await selectPlaceholder(3);
|
const selected = await selectPlaceholder(3);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
console.warn(`[weibo-article] Skipping image - could not select placeholder: ${img.placeholder}`);
|
console.warn(`[weibo-article] Skipping image - could not select placeholder: ${img.placeholder}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[weibo-article] Copying image: ${path.basename(img.localPath)}`);
|
// Step C: Delete selected placeholder via Backspace (ProseMirror-compatible)
|
||||||
|
console.log(`[weibo-article] Deleting placeholder via Backspace...`);
|
||||||
if (!copyImageToClipboard(img.localPath)) {
|
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
||||||
console.warn(`[weibo-article] Failed to copy image to clipboard`);
|
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId });
|
||||||
continue;
|
await sleep(500);
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(1000);
|
|
||||||
|
|
||||||
// Delete placeholder by replacing selection with empty text
|
|
||||||
console.log(`[weibo-article] Deleting placeholder...`);
|
|
||||||
await cdp.send('Runtime.evaluate', {
|
|
||||||
expression: `(() => {
|
|
||||||
const sel = window.getSelection();
|
|
||||||
if (!sel || sel.isCollapsed) return false;
|
|
||||||
const range = sel.getRangeAt(0);
|
|
||||||
range.deleteContents();
|
|
||||||
sel.collapseToStart();
|
|
||||||
return true;
|
|
||||||
})()`,
|
|
||||||
returnByValue: true,
|
|
||||||
}, { sessionId });
|
|
||||||
|
|
||||||
await sleep(300);
|
|
||||||
|
|
||||||
// Fallback: send Backspace key if placeholder still exists
|
|
||||||
const stillExists = await cdp!.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
|
||||||
expression: `(() => {
|
|
||||||
const editor = document.querySelector('div[contenteditable="true"]');
|
|
||||||
if (!editor) return false;
|
|
||||||
const placeholder = ${JSON.stringify(img.placeholder)};
|
|
||||||
const regex = new RegExp(placeholder + '(?!\\\\d)');
|
|
||||||
return regex.test(editor.innerText);
|
|
||||||
})()`,
|
|
||||||
returnByValue: true,
|
|
||||||
}, { sessionId });
|
|
||||||
|
|
||||||
if (stillExists.result.value) {
|
|
||||||
console.log('[weibo-article] Placeholder survived deleteContents, trying Input.insertText replacement...');
|
|
||||||
// Re-select and replace with empty via Input.insertText
|
|
||||||
await cdp!.send('Runtime.evaluate', {
|
|
||||||
expression: `(() => {
|
|
||||||
const editor = document.querySelector('div[contenteditable="true"]');
|
|
||||||
if (!editor) return false;
|
|
||||||
const placeholder = ${JSON.stringify(img.placeholder)};
|
|
||||||
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
|
||||||
let node;
|
|
||||||
while ((node = walker.nextNode())) {
|
|
||||||
const text = node.textContent || '';
|
|
||||||
let searchStart = 0;
|
|
||||||
let idx;
|
|
||||||
while ((idx = text.indexOf(placeholder, searchStart)) !== -1) {
|
|
||||||
const afterIdx = idx + placeholder.length;
|
|
||||||
const charAfter = text[afterIdx];
|
|
||||||
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.setStart(node, idx);
|
|
||||||
range.setEnd(node, idx + placeholder.length);
|
|
||||||
const sel = window.getSelection();
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
searchStart = afterIdx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})()`,
|
|
||||||
}, { sessionId });
|
|
||||||
await sleep(200);
|
|
||||||
await cdp!.send('Input.insertText', { text: '' }, { sessionId });
|
|
||||||
await sleep(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(200);
|
|
||||||
|
|
||||||
// Verify placeholder was deleted
|
// Verify placeholder was deleted
|
||||||
const afterDelete = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
const placeholderGone = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||||
expression: `(() => {
|
expression: `(() => {
|
||||||
const editor = document.querySelector('div[contenteditable="true"]');
|
const editor = document.querySelector('div[contenteditable="true"]');
|
||||||
if (!editor) return true;
|
if (!editor) return true;
|
||||||
const text = editor.innerText;
|
|
||||||
const placeholder = ${JSON.stringify(img.placeholder)};
|
const placeholder = ${JSON.stringify(img.placeholder)};
|
||||||
const regex = new RegExp(placeholder + '(?!\\\\d)');
|
const regex = new RegExp(placeholder + '(?!\\\\d)');
|
||||||
return !regex.test(text);
|
return !regex.test(editor.innerText);
|
||||||
})()`,
|
})()`,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
|
||||||
if (afterDelete.result.value) {
|
if (placeholderGone.result.value) {
|
||||||
console.log(`[weibo-article] Placeholder deleted`);
|
console.log(`[weibo-article] Placeholder deleted`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[weibo-article] Placeholder may still exist after delete`);
|
console.warn(`[weibo-article] Placeholder may still exist, trying execCommand delete...`);
|
||||||
|
// Re-select and delete via execCommand
|
||||||
|
await selectPlaceholder(1);
|
||||||
|
await cdp.send('Runtime.evaluate', {
|
||||||
|
expression: `document.execCommand('delete')`,
|
||||||
|
}, { sessionId });
|
||||||
|
await sleep(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus editor for paste
|
// Step D: Focus editor and paste image
|
||||||
await cdp.send('Runtime.evaluate', {
|
await cdp.send('Runtime.evaluate', {
|
||||||
expression: `document.querySelector('div[contenteditable="true"]')?.focus()`,
|
expression: `document.querySelector('div[contenteditable="true"]')?.focus()`,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
await sleep(300);
|
await sleep(200);
|
||||||
|
|
||||||
// Count images before paste
|
// Count images before paste
|
||||||
const imgCountBefore = await cdp.send<{ result: { value: number } }>('Runtime.evaluate', {
|
const imgCountBefore = await cdp.send<{ result: { value: number } }>('Runtime.evaluate', {
|
||||||
@@ -613,7 +557,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
|||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
|
||||||
// Paste image
|
// Paste image at cursor position (where placeholder was)
|
||||||
console.log(`[weibo-article] Pasting image...`);
|
console.log(`[weibo-article] Pasting image...`);
|
||||||
if (pasteFromClipboard('Google Chrome', 5, 1000)) {
|
if (pasteFromClipboard('Google Chrome', 5, 1000)) {
|
||||||
console.log(`[weibo-article] Paste keystroke sent for: ${path.basename(img.localPath)}`);
|
console.log(`[weibo-article] Paste keystroke sent for: ${path.basename(img.localPath)}`);
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-post-to-x
|
name: baoyu-post-to-x
|
||||||
description: Posts content and articles to X (Twitter). Supports regular posts with images/videos and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation. Use when user asks to "post to X", "tweet", "publish to Twitter", or "share on X".
|
description: Posts content and articles to X (Twitter). Supports regular posts with images/videos and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation. Use when user asks to "post to X", "tweet", "publish to Twitter", or "share on X".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-x
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Post to X (Twitter)
|
# Post to X (Twitter)
|
||||||
@@ -12,9 +20,9 @@ Posts text, images, videos, and long-form articles to X via real Chrome browser
|
|||||||
**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. Replace all `${SKILL_DIR}` in this document with the actual path
|
3. Replace all `{baseDir}` in this document with the actual path
|
||||||
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
4. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun
|
||||||
|
|
||||||
**Script Reference**:
|
**Script Reference**:
|
||||||
@@ -36,12 +44,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-post-to-x/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-post-to-x/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-post-to-x/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-post-to-x/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-post-to-x/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-post-to-x/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-post-to-x/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-post-to-x/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-x/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-x/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,7 +85,7 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-x/EXTEND.md") { "user" }
|
|||||||
Before first use, suggest running the environment check. User can skip if they prefer.
|
Before first use, suggest running the environment check. User can skip if they prefer.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/check-paste-permissions.ts
|
${BUN_X} {baseDir}/scripts/check-paste-permissions.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, Chrome conflicts.
|
Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, Chrome conflicts.
|
||||||
@@ -107,7 +118,7 @@ Unless the user explicitly specifies the post type:
|
|||||||
## Regular Posts
|
## Regular Posts
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
${BUN_X} {baseDir}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -126,7 +137,7 @@ ${BUN_X} ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
|||||||
Text + video file.
|
Text + video file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
${BUN_X} {baseDir}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -147,7 +158,7 @@ ${BUN_X} ${SKILL_DIR}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
|||||||
Quote an existing tweet with comment.
|
Quote an existing tweet with comment.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123 "Great insight!"
|
${BUN_X} {baseDir}/scripts/x-quote.ts https://x.com/user/status/123 "Great insight!"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
@@ -166,8 +177,8 @@ ${BUN_X} ${SKILL_DIR}/scripts/x-quote.ts https://x.com/user/status/123 "Great in
|
|||||||
Long-form Markdown articles (requires X Premium).
|
Long-form Markdown articles (requires X Premium).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md
|
${BUN_X} {baseDir}/scripts/x-article.ts article.md
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
${BUN_X} {baseDir}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters**:
|
**Parameters**:
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Publish Markdown articles to X Articles editor with rich text formatting and ima
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Publish markdown article (preview mode)
|
# Publish markdown article (preview mode)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md
|
${BUN_X} {baseDir}/scripts/x-article.ts article.md
|
||||||
|
|
||||||
# With custom cover image
|
# With custom cover image
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
${BUN_X} {baseDir}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||||
|
|
||||||
# Actually publish
|
# Actually publish
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/x-article.ts article.md --submit
|
${BUN_X} {baseDir}/scripts/x-article.ts article.md --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Markdown Format
|
## Markdown Format
|
||||||
@@ -76,13 +76,13 @@ Convert markdown and inspect structure:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get JSON with all metadata
|
# Get JSON with all metadata
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/md-to-html.ts article.md
|
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md
|
||||||
|
|
||||||
# Output HTML only
|
# Output HTML only
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/md-to-html.ts article.md --html-only
|
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --html-only
|
||||||
|
|
||||||
# Save HTML to file
|
# Save HTML to file
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/md-to-html.ts article.md --save-html /tmp/article.html
|
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/article.html
|
||||||
```
|
```
|
||||||
|
|
||||||
JSON output:
|
JSON output:
|
||||||
@@ -161,7 +161,7 @@ Claude:
|
|||||||
- **No create button**: Ensure X Premium subscription is active
|
- **No create button**: Ensure X Premium subscription is active
|
||||||
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
||||||
- **Images not inserting**: Verify placeholders exist in pasted content
|
- **Images not inserting**: Verify placeholders exist in pasted content
|
||||||
- **Content not pasting**: Check HTML clipboard: `${BUN_X} ${SKILL_DIR}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
- **Content not pasting**: Check HTML clipboard: `${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ If you prefer step-by-step control:
|
|||||||
### Step 1: Copy Image to Clipboard
|
### Step 1: Copy Image to Clipboard
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/copy-to-clipboard.ts image /path/to/image.png
|
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /path/to/image.png
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Paste from Clipboard
|
### Step 2: Paste from Clipboard
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Simple paste to frontmost app
|
# Simple paste to frontmost app
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/paste-from-clipboard.ts
|
${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts
|
||||||
|
|
||||||
# Paste to Chrome with retries
|
# Paste to Chrome with retries
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
||||||
|
|
||||||
# Quick paste with shorter delay
|
# Quick paste with shorter delay
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/paste-from-clipboard.ts --delay 200
|
${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts --delay 200
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Use Playwright MCP (if Chrome session available)
|
### Step 3: Use Playwright MCP (if Chrome session available)
|
||||||
@@ -59,7 +59,7 @@ mcp__playwright__browser_take_screenshot filename="preview.png"
|
|||||||
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
||||||
|
|
||||||
Claude:
|
Claude:
|
||||||
1. Runs: ${BUN_X} ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
1. Runs: ${BUN_X} {baseDir}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
||||||
2. Chrome opens with X compose page
|
2. Chrome opens with X compose page
|
||||||
3. Text is typed into editor
|
3. Text is typed into editor
|
||||||
4. Image is copied to clipboard and pasted
|
4. Image is copied to clipboard and pasted
|
||||||
@@ -72,7 +72,7 @@ Claude:
|
|||||||
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
|
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
|
||||||
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
|
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
|
||||||
- **Image paste fails**:
|
- **Image paste fails**:
|
||||||
- Verify clipboard script: `${BUN_X} ${SKILL_DIR}/scripts/copy-to-clipboard.ts image <path>`
|
- Verify clipboard script: `${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image <path>`
|
||||||
- On macOS, grant "Accessibility" permission to Terminal/iTerm in System Settings > Privacy & Security > Accessibility
|
- On macOS, grant "Accessibility" permission to Terminal/iTerm in System Settings > Privacy & Security > Accessibility
|
||||||
- Keep Chrome window visible and in front during paste operations
|
- Keep Chrome window visible and in front during paste operations
|
||||||
- **osascript permission denied**: Grant Terminal accessibility permissions in System Preferences
|
- **osascript permission denied**: Grant Terminal accessibility permissions in System Preferences
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-slide-deck
|
name: baoyu-slide-deck
|
||||||
description: Generates professional slide deck images from content. Creates outlines with style instructions, then generates individual slide images. Use when user asks to "create slides", "make a presentation", "generate deck", "slide deck", or "PPT".
|
description: Generates professional slide deck images from content. Creates outlines with style instructions, then generates individual slide images. Use when user asks to "create slides", "make a presentation", "generate deck", "slide deck", or "PPT".
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-slide-deck
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Slide Deck Generator
|
# Slide Deck Generator
|
||||||
@@ -22,8 +30,8 @@ Transform content into professional slide deck images.
|
|||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
**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
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -195,12 +203,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-slide-deck/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-slide-deck/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-slide-deck/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-slide-deck/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-slide-deck/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-slide-deck/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-slide-deck/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-slide-deck/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-slide-deck/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-slide-deck/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -555,8 +566,8 @@ options:
|
|||||||
### Step 8: Merge to PPTX and PDF
|
### Step 8: Merge to PPTX and PDF
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/merge-to-pptx.ts <slide-deck-dir>
|
${BUN_X} {baseDir}/scripts/merge-to-pptx.ts <slide-deck-dir>
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/merge-to-pdf.ts <slide-deck-dir>
|
${BUN_X} {baseDir}/scripts/merge-to-pdf.ts <slide-deck-dir>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 9: Output Summary
|
### Step 9: Output Summary
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-translate
|
name: baoyu-translate
|
||||||
description: Translates articles and documents between languages with three modes - quick (direct), normal (analyze then translate), and refined (analyze, translate, review, polish). Supports custom glossaries and terminology consistency via EXTEND.md. Use when user asks to "translate", "翻译", "精翻", "translate article", "translate to Chinese/English", "改成中文", "改成英文", "convert to Chinese", "localize", "本地化", or needs any document translation. Also triggers for "refined translation", "精细翻译", "proofread translation", "快速翻译", "快翻", "这篇文章翻译一下", or when a URL or file is provided with translation intent.
|
description: Translates articles and documents between languages with three modes - quick (direct), normal (analyze then translate), and refined (analyze, translate, review, polish). Supports custom glossaries and terminology consistency via EXTEND.md. Use when user asks to "translate", "翻译", "精翻", "translate article", "translate to Chinese/English", "改成中文", "改成英文", "convert to Chinese", "localize", "本地化", or needs any document translation. Also triggers for "refined translation", "精细翻译", "proofread translation", "快速翻译", "快翻", "这篇文章翻译一下", or when a URL or file is provided with translation intent.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-translate
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Translator
|
# Translator
|
||||||
@@ -9,7 +17,7 @@ Three-mode translation skill: **quick** for direct translation, **normal** for a
|
|||||||
|
|
||||||
## Script Directory
|
## Script Directory
|
||||||
|
|
||||||
Scripts in `scripts/` subdirectory. `${SKILL_DIR}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `${SKILL_DIR}` and `${BUN_X}` with actual values.
|
Scripts in `scripts/` subdirectory. `{baseDir}` = this SKILL.md's directory path. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun. Replace `{baseDir}` and `${BUN_X}` with actual values.
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
@@ -22,12 +30,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-translate/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-translate/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-translate/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-translate/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-translate/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-translate/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-translate/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-translate/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-translate/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-translate/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,7 +183,7 @@ Before translating chunks:
|
|||||||
|
|
||||||
1. **Extract terminology**: Scan entire document for proper nouns, technical terms, recurring phrases
|
1. **Extract terminology**: Scan entire document for proper nouns, technical terms, recurring phrases
|
||||||
2. **Build session glossary**: Merge extracted terms with loaded glossaries, establish consistent translations
|
2. **Build session glossary**: Merge extracted terms with loaded glossaries, establish consistent translations
|
||||||
3. **Split into chunks**: Use `${BUN_X} ${SKILL_DIR}/scripts/chunk.ts <file> [--max-words <chunk_max_words>] [--output-dir <output-dir>]`
|
3. **Split into chunks**: Use `${BUN_X} {baseDir}/scripts/chunk.ts <file> [--max-words <chunk_max_words>] [--output-dir <output-dir>]`
|
||||||
- Parses markdown AST (headings, paragraphs, lists, code blocks, tables, etc.)
|
- Parses markdown AST (headings, paragraphs, lists, code blocks, tables, etc.)
|
||||||
- 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
|
||||||
@@ -201,6 +212,7 @@ Before translating chunks:
|
|||||||
- **Natural flow**: Use idiomatic target language word order and sentence patterns; break or restructure sentences freely when the source structure doesn't work naturally in the target language
|
- **Natural flow**: Use idiomatic target language word order and sentence patterns; break or restructure sentences freely when the source structure doesn't work naturally in the target language
|
||||||
- **Terminology**: Use standard translations; annotate with original term in parentheses on first occurrence
|
- **Terminology**: Use standard translations; annotate with original term in parentheses on first occurrence
|
||||||
- **Preserve format**: Keep all markdown formatting (headings, bold, italic, images, links, code blocks)
|
- **Preserve format**: Keep all markdown formatting (headings, bold, italic, images, links, code blocks)
|
||||||
|
- **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. Only add notes where genuinely needed; do not over-annotate obvious terms.
|
||||||
@@ -239,6 +251,20 @@ Each step reads the previous step's file and builds on it.
|
|||||||
|
|
||||||
Final translation is always at `translation.md` in the output directory.
|
Final translation is always at `translation.md` in the output directory.
|
||||||
|
|
||||||
|
After the final translation is written, do a lightweight image-language pass:
|
||||||
|
|
||||||
|
1. Collect image references from the translated article
|
||||||
|
2. Identify likely text-heavy images such as covers, screenshots, diagrams, charts, frameworks, and infographics
|
||||||
|
3. If any image likely contains a main text language that does not match the translated article language, proactively remind the user
|
||||||
|
4. The reminder must be a list only. Do not automatically localize those images unless the user asks
|
||||||
|
|
||||||
|
Reminder format (use whatever image syntax the article already uses — standard markdown or wikilink):
|
||||||
|
```text
|
||||||
|
Possible image localization needed:
|
||||||
|
- : likely still contains source-language text while the article is now in target language
|
||||||
|
- : likely text-heavy framework graphic, check whether labels need translation
|
||||||
|
```
|
||||||
|
|
||||||
Display summary:
|
Display summary:
|
||||||
```
|
```
|
||||||
**Translation complete** ({mode} mode)
|
**Translation complete** ({mode} mode)
|
||||||
@@ -250,6 +276,8 @@ Final: {output-dir}/translation.md
|
|||||||
Glossary terms applied: {count}
|
Glossary terms applied: {count}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If mismatched image-language candidates were found, append a short note after the summary telling the user that some embedded images may still need image-text localization, followed by the candidate list.
|
||||||
|
|
||||||
## Extension Support
|
## Extension Support
|
||||||
|
|
||||||
Custom configurations via EXTEND.md. See **Preferences** section for paths and supported options.
|
Custom configurations via EXTEND.md. See **Preferences** section for paths and supported options.
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-url-to-markdown
|
name: baoyu-url-to-markdown
|
||||||
description: Fetch any URL and convert to markdown using Chrome CDP. Saves the rendered HTML snapshot alongside the markdown, and automatically falls back to the pre-Defuddle HTML-to-Markdown pipeline when Defuddle fails. Supports two modes - auto-capture on page load, or wait for user signal (for pages requiring login). Use when user wants to save a webpage as markdown.
|
description: Fetch any URL and convert to markdown using Chrome CDP. Saves the rendered HTML snapshot alongside the markdown, and automatically falls back to the pre-Defuddle HTML-to-Markdown pipeline when Defuddle fails. Supports two modes - auto-capture on page load, or wait for user signal (for pages requiring login). Use when user wants to save a webpage as markdown.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-url-to-markdown
|
||||||
|
requires:
|
||||||
|
anyBins:
|
||||||
|
- bun
|
||||||
|
- npx
|
||||||
---
|
---
|
||||||
|
|
||||||
# URL to Markdown
|
# URL to Markdown
|
||||||
@@ -12,10 +20,10 @@ Fetches any URL via Chrome CDP, saves the rendered HTML snapshot, and converts i
|
|||||||
**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 |
|
||||||
@@ -30,12 +38,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-url-to-markdown/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-url-to-markdown/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-url-to-markdown/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-url-to-markdown/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-url-to-markdown/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-url-to-markdown/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -112,19 +123,19 @@ Full reference: [references/config/first-time-setup.md](references/config/first-
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Auto mode (default) - capture when page loads
|
# Auto mode (default) - capture when page loads
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url>
|
${BUN_X} {baseDir}/scripts/main.ts <url>
|
||||||
|
|
||||||
# Wait mode - wait for user signal before capture
|
# Wait mode - wait for user signal before capture
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --wait
|
${BUN_X} {baseDir}/scripts/main.ts <url> --wait
|
||||||
|
|
||||||
# Save to specific file
|
# Save to specific file
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> -o output.md
|
${BUN_X} {baseDir}/scripts/main.ts <url> -o output.md
|
||||||
|
|
||||||
# Save to a custom output directory (auto-generates filename)
|
# Save to a custom output directory (auto-generates filename)
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --output-dir ./posts/
|
${BUN_X} {baseDir}/scripts/main.ts <url> --output-dir ./posts/
|
||||||
|
|
||||||
# Download images and videos to local directories
|
# Download images and videos to local directories
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts <url> --download-media
|
${BUN_X} {baseDir}/scripts/main.ts <url> --download-media
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-xhs-images
|
name: baoyu-xhs-images
|
||||||
description: Generates Xiaohongshu (Little Red Book) infographic series with 11 visual styles and 8 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
|
description: Generates Xiaohongshu (Little Red Book) infographic series with 11 visual styles and 8 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
|
||||||
|
version: 1.56.1
|
||||||
|
metadata:
|
||||||
|
openclaw:
|
||||||
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-xhs-images
|
||||||
---
|
---
|
||||||
|
|
||||||
# Xiaohongshu Infographic Series Generator
|
# Xiaohongshu Infographic Series Generator
|
||||||
@@ -76,7 +80,7 @@ Detailed style definitions: `references/presets/<style>.md`
|
|||||||
|
|
||||||
## Preset Gallery
|
## Preset Gallery
|
||||||
|
|
||||||
Quick-start presets by content scenario. Use `--preset <name>` or recommend during Step 4.
|
Quick-start presets by content scenario. Use `--preset <name>` or recommend during Step 2.
|
||||||
|
|
||||||
**Knowledge & Learning**:
|
**Knowledge & Learning**:
|
||||||
|
|
||||||
@@ -237,11 +241,12 @@ XHS Infographic Progress:
|
|||||||
- [ ] Found → load preferences → continue
|
- [ ] Found → load preferences → continue
|
||||||
- [ ] Not found → run first-time setup → MUST complete before Step 1
|
- [ ] Not found → run first-time setup → MUST complete before Step 1
|
||||||
- [ ] Step 1: Analyze content → analysis.md
|
- [ ] Step 1: Analyze content → analysis.md
|
||||||
- [ ] Step 2: Confirmation 1 - Content understanding ⚠️ REQUIRED
|
- [ ] Step 2: Smart Confirm ⚠️ REQUIRED
|
||||||
- [ ] Step 3: Generate 3 outline + style variants
|
- [ ] Path A: Quick confirm → generate recommended outline
|
||||||
- [ ] Step 4: Confirmation 2 - Outline & style & elements selection ⚠️ REQUIRED
|
- [ ] Path B: Customize → adjust then generate outline
|
||||||
- [ ] Step 5: Generate images (sequential)
|
- [ ] Path C: Detailed → 3 outlines → second confirm → generate outline
|
||||||
- [ ] Step 6: Completion report
|
- [ ] Step 3: Generate images (sequential)
|
||||||
|
- [ ] Step 4: Completion report
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flow
|
### Flow
|
||||||
@@ -255,7 +260,11 @@ Input → [Step 0: Preferences] ─┬─ Found → Continue
|
|||||||
│
|
│
|
||||||
┌───────────────────────────────────────────────────────────────────────────┘
|
┌───────────────────────────────────────────────────────────────────────────┘
|
||||||
↓
|
↓
|
||||||
Analyze → [Confirm 1] → 3 Outlines → [Confirm 2: Outline + Style + Elements] → Generate → Complete
|
Analyze → [Smart Confirm] ─┬─ Quick: confirm recommended → outline.md → Generate → Complete
|
||||||
|
│
|
||||||
|
├─ Customize: adjust options → outline.md → Generate → Complete
|
||||||
|
│
|
||||||
|
└─ Detailed: 3 outlines → [Confirm 2] → outline.md → Generate → Complete
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
|
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
|
||||||
@@ -269,12 +278,15 @@ Check EXTEND.md existence (priority order):
|
|||||||
```bash
|
```bash
|
||||||
# macOS, Linux, WSL, Git Bash
|
# macOS, Linux, WSL, Git Bash
|
||||||
test -f .baoyu-skills/baoyu-xhs-images/EXTEND.md && echo "project"
|
test -f .baoyu-skills/baoyu-xhs-images/EXTEND.md && echo "project"
|
||||||
|
test -f "${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-xhs-images/EXTEND.md" && echo "xdg"
|
||||||
test -f "$HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md" && echo "user"
|
test -f "$HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md" && echo "user"
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# PowerShell (Windows)
|
# PowerShell (Windows)
|
||||||
if (Test-Path .baoyu-skills/baoyu-xhs-images/EXTEND.md) { "project" }
|
if (Test-Path .baoyu-skills/baoyu-xhs-images/EXTEND.md) { "project" }
|
||||||
|
$xdg = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { "$HOME/.config" }
|
||||||
|
if (Test-Path "$xdg/baoyu-skills/baoyu-xhs-images/EXTEND.md") { "xdg" }
|
||||||
if (Test-Path "$HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md") { "user" }
|
if (Test-Path "$HOME/.baoyu-skills/baoyu-xhs-images/EXTEND.md") { "user" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -323,18 +335,63 @@ Read source content, save it if needed, and perform deep analysis.
|
|||||||
- Swipe flow design
|
- Swipe flow design
|
||||||
4. Detect source language
|
4. Detect source language
|
||||||
5. Determine recommended image count (2-10)
|
5. Determine recommended image count (2-10)
|
||||||
6. **Generate clarifying questions** (see Step 2)
|
6. **Auto-recommend** best strategy + style + layout based on content signals
|
||||||
7. **Save to `analysis.md`**
|
7. **Save to `analysis.md`**
|
||||||
|
|
||||||
### Step 2: Confirmation 1 - Content Understanding ⚠️
|
### Step 2: Smart Confirm ⚠️
|
||||||
|
|
||||||
**Purpose**: Validate understanding + collect missing info. **Do NOT skip.**
|
**Purpose**: Present auto-recommended plan, let user confirm or adjust. **Do NOT skip.**
|
||||||
|
|
||||||
**Display summary**:
|
**Auto-Recommendation Logic**:
|
||||||
- Content type + topic identified
|
1. Use Auto Selection table to match content signals → best strategy + style + layout
|
||||||
- Key points extracted
|
2. Infer optimal image count from content density
|
||||||
- Tone detected
|
3. Load style's default elements from preset
|
||||||
- Source images count
|
|
||||||
|
**Display** (analysis summary + recommended plan):
|
||||||
|
|
||||||
|
```
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
📋 内容分析
|
||||||
|
主题:[topic] | 类型:[content_type]
|
||||||
|
要点:[key points summary]
|
||||||
|
受众:[target audience]
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
🎨 推荐方案(自动匹配)
|
||||||
|
策略:[A/B/C] [strategy name]([reason])
|
||||||
|
风格:[style] · 布局:[layout] · 预设:[preset]
|
||||||
|
图片:[N]张(封面+[N-2]内容+结尾)
|
||||||
|
元素:[background] / [decorations] / [emphasis]
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use AskUserQuestion** with single question:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 1. ✅ 确认,直接生成(推荐) | Trust auto-recommendation, proceed immediately |
|
||||||
|
| 2. 🎛️ 自定义调整 | Modify strategy/style/layout/count in one step |
|
||||||
|
| 3. 📋 详细模式 | Generate 3 outlines, then choose (two confirmations) |
|
||||||
|
|
||||||
|
#### Path A: Quick Confirm (Option 1)
|
||||||
|
|
||||||
|
Generate single outline using recommended strategy + style → save to `outline.md` → Step 3.
|
||||||
|
|
||||||
|
#### Path B: Customize (Option 2)
|
||||||
|
|
||||||
|
**Use AskUserQuestion** with adjustable options (leave blank = keep recommended):
|
||||||
|
|
||||||
|
1. **策略风格**: Current: [strategy + style]. Options: A Story-Driven(warm) | B Information-Dense(notion) | C Visual-First(screen-print). Or specify style directly: cute/fresh/warm/bold/minimal/retro/pop/notion/chalkboard/study-notes/screen-print. Or use preset: knowledge-card / checklist / tutorial / poster / cinematic / etc.
|
||||||
|
2. **布局**: Current: [layout]. Options: sparse | balanced | dense | list | comparison | flow | mindmap | quadrant
|
||||||
|
3. **图片数量**: Current: [N]. Range: 2-10
|
||||||
|
4. **补充说明**(可选): Selling point emphasis, audience adjustment, color preference, etc.
|
||||||
|
|
||||||
|
**After response**: Generate single outline with user's choices → save to `outline.md` → Step 3.
|
||||||
|
|
||||||
|
#### Path C: Detailed Mode (Option 3)
|
||||||
|
|
||||||
|
Full two-confirmation flow for maximum control:
|
||||||
|
|
||||||
|
**Step 2a: Content Understanding**
|
||||||
|
|
||||||
**Use AskUserQuestion** for:
|
**Use AskUserQuestion** for:
|
||||||
1. Core selling point (multiSelect: true)
|
1. Core selling point (multiSelect: true)
|
||||||
@@ -342,13 +399,9 @@ Read source content, save it if needed, and perform deep analysis.
|
|||||||
3. Style preference: Authentic sharing / Professional review / Aesthetic mood / Auto
|
3. Style preference: Authentic sharing / Professional review / Aesthetic mood / Auto
|
||||||
4. Additional context (optional)
|
4. Additional context (optional)
|
||||||
|
|
||||||
**After response**: Update `analysis.md` → Step 3
|
**After response**: Update `analysis.md`.
|
||||||
|
|
||||||
### Step 3: Generate 3 Outline + Style Variants
|
**Step 2b: Generate 3 Outline Variants**
|
||||||
|
|
||||||
Based on analysis + user context, create three distinct strategy variants. Each variant includes both **outline structure** and **visual style recommendation**.
|
|
||||||
|
|
||||||
**For each strategy**:
|
|
||||||
|
|
||||||
| Strategy | Filename | Outline | Recommended Style |
|
| Strategy | Filename | Outline | Recommended Style |
|
||||||
|----------|----------|---------|-------------------|
|
|----------|----------|---------|-------------------|
|
||||||
@@ -363,7 +416,7 @@ strategy: a # a, b, or c
|
|||||||
name: Story-Driven
|
name: Story-Driven
|
||||||
style: warm # recommended style for this strategy
|
style: warm # recommended style for this strategy
|
||||||
style_reason: "Warm tones enhance emotional storytelling and personal connection"
|
style_reason: "Warm tones enhance emotional storytelling and personal connection"
|
||||||
elements: # from style preset, can be customized in Step 4
|
elements: # from style preset, can be customized
|
||||||
background: solid-pastel
|
background: solid-pastel
|
||||||
decorations: [clouds, stars-sparkles]
|
decorations: [clouds, stars-sparkles]
|
||||||
emphasis: star-burst
|
emphasis: star-burst
|
||||||
@@ -391,50 +444,22 @@ image_count: 5
|
|||||||
- Each strategy MUST have different outline structure AND different recommended style
|
- Each strategy MUST have different outline structure AND different recommended style
|
||||||
- Adapt page count: A typically 4-6, B typically 3-5, C typically 3-4
|
- Adapt page count: A typically 4-6, B typically 3-5, C typically 3-4
|
||||||
- Include `style_reason` explaining why this style fits the strategy
|
- Include `style_reason` explaining why this style fits the strategy
|
||||||
- Consider user's style preference from Step 2
|
|
||||||
|
|
||||||
Reference: `references/workflows/outline-template.md`
|
Reference: `references/workflows/outline-template.md`
|
||||||
|
|
||||||
### Step 4: Confirmation 2 - Outline & Style & Elements Selection ⚠️
|
**Step 2c: Outline & Style Selection**
|
||||||
|
|
||||||
**Purpose**: User chooses outline strategy, confirms visual style, and customizes elements. **Do NOT skip.**
|
|
||||||
|
|
||||||
**Display each strategy**:
|
|
||||||
- Strategy name + page count + recommended style
|
|
||||||
- Page-by-page summary (P1 → P2 → P3...)
|
|
||||||
|
|
||||||
**Use AskUserQuestion** with three questions:
|
**Use AskUserQuestion** with three questions:
|
||||||
|
|
||||||
**Question 1: Outline Strategy**
|
**Q1: Outline Strategy**: A / B / C / Combine (specify pages from each)
|
||||||
- Strategy A (Recommended if "authentic sharing")
|
|
||||||
- Strategy B (Recommended if "professional review")
|
|
||||||
- Strategy C (Recommended if "aesthetic mood")
|
|
||||||
- Combine: specify pages from each
|
|
||||||
|
|
||||||
**Question 2: Visual Style**
|
**Q2: Visual Style**: Use recommended | Select preset | Select style | Custom description
|
||||||
- Use strategy's recommended style (show which style)
|
|
||||||
- Or use a preset: knowledge-card / checklist / tutorial / poster / cinematic / etc. (see [Style Presets](references/style-presets.md))
|
|
||||||
- Or select from: cute / fresh / warm / bold / minimal / retro / pop / notion / chalkboard / screen-print
|
|
||||||
- Or type custom style description
|
|
||||||
|
|
||||||
**Question 3: Visual Elements** (show after style selection)
|
**Q3: Visual Elements**: Use defaults (Recommended) | Adjust background | Adjust decorations | Custom
|
||||||
Display the selected style's default elements from preset, then ask:
|
|
||||||
- Use style defaults (Recommended) - show preview: background, decorations, emphasis
|
|
||||||
- Adjust background - options: solid-pastel / solid-saturated / gradient-linear / gradient-radial / paper-texture / grid
|
|
||||||
- Adjust decorations - options: hearts / stars-sparkles / flowers / clouds / leaves / confetti
|
|
||||||
- Type custom element preferences
|
|
||||||
|
|
||||||
**After response**:
|
**After response**: Save selected/merged outline to `outline.md` with confirmed style and elements → Step 3.
|
||||||
- Single strategy → copy to `outline.md` with confirmed style
|
|
||||||
- Combination → merge specified pages with confirmed style
|
|
||||||
- Custom request → regenerate based on feedback
|
|
||||||
- Style defaults → use preset's Element Combination as-is
|
|
||||||
- Background adjustment → update elements.background with user choice
|
|
||||||
- Decorations adjustment → update elements.decorations with user choice
|
|
||||||
- Custom elements → parse user's preferences into elements fields
|
|
||||||
- Update `outline.md` frontmatter with final style and elements
|
|
||||||
|
|
||||||
### Step 5: Generate Images
|
### Step 3: Generate Images
|
||||||
|
|
||||||
With confirmed outline + style + layout:
|
With confirmed outline + style + layout:
|
||||||
|
|
||||||
@@ -474,12 +499,13 @@ If image generation skill supports `--sessionId`:
|
|||||||
2. Use same session ID for all images
|
2. Use same session ID for all images
|
||||||
3. Combined with reference image chain, ensures maximum visual consistency
|
3. Combined with reference image chain, ensures maximum visual consistency
|
||||||
|
|
||||||
### Step 6: Completion Report
|
### Step 4: Completion Report
|
||||||
|
|
||||||
```
|
```
|
||||||
Xiaohongshu Infographic Series Complete!
|
Xiaohongshu Infographic Series Complete!
|
||||||
|
|
||||||
Topic: [topic]
|
Topic: [topic]
|
||||||
|
Mode: [Quick / Custom / Detailed]
|
||||||
Strategy: [A/B/C/Combined]
|
Strategy: [A/B/C/Combined]
|
||||||
Style: [style name]
|
Style: [style name]
|
||||||
Layout: [layout name or "varies"]
|
Layout: [layout name or "varies"]
|
||||||
@@ -487,10 +513,8 @@ Location: [directory path]
|
|||||||
Images: N total
|
Images: N total
|
||||||
|
|
||||||
✓ analysis.md
|
✓ analysis.md
|
||||||
✓ outline-strategy-a.md
|
✓ outline.md
|
||||||
✓ outline-strategy-b.md
|
✓ outline-strategy-a/b/c.md (detailed mode only)
|
||||||
✓ outline-strategy-c.md
|
|
||||||
✓ outline.md (selected: [strategy])
|
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
- 01-cover-[slug].png ✓ Cover (sparse)
|
- 01-cover-[slug].png ✓ Cover (sparse)
|
||||||
@@ -559,7 +583,7 @@ Detailed templates in `references/` directory:
|
|||||||
|
|
||||||
- Auto-retry once on failure | Cartoon alternatives for sensitive figures
|
- Auto-retry once on failure | Cartoon alternatives for sensitive figures
|
||||||
- Use confirmed language preference | Maintain style consistency
|
- Use confirmed language preference | Maintain style consistency
|
||||||
- **Two confirmation points required** (Steps 2 & 4) - do not skip
|
- **Smart Confirm required** (Step 2) - do not skip; detailed mode uses two sub-confirmations
|
||||||
|
|
||||||
## Extension Support
|
## Extension Support
|
||||||
|
|
||||||
|
|||||||
@@ -195,4 +195,4 @@ Before proceeding to outline generation:
|
|||||||
- [ ] Have I identified save/share triggers?
|
- [ ] Have I identified save/share triggers?
|
||||||
- [ ] Are there clear visual opportunities?
|
- [ ] Are there clear visual opportunities?
|
||||||
- [ ] Is the swipe flow planned?
|
- [ ] Is the swipe flow planned?
|
||||||
- [ ] Have I selected 3 style+layout combinations?
|
- [ ] Have I identified the best style+layout recommendation?
|
||||||
|
|||||||
@@ -211,13 +211,9 @@ If preferences include watermark:
|
|||||||
When generating multiple images in a series:
|
When generating multiple images in a series:
|
||||||
|
|
||||||
1. **Image 1 (cover)**: Generate without `--ref` — this establishes the visual anchor
|
1. **Image 1 (cover)**: Generate without `--ref` — this establishes the visual anchor
|
||||||
2. **Images 2+**: Always pass image 1 as `--ref` to the image generation skill:
|
2. **Images 2+**: Always pass image 1 as `--ref` to the installed image generation skill.
|
||||||
```bash
|
Read that skill's `SKILL.md` and use its documented interface rather than calling its scripts directly.
|
||||||
${BUN_X} ${SKILL_DIR}/scripts/main.ts \
|
For each later image, use the assembled prompt file as input, set the output image path, keep aspect ratio `3:4`, use quality `2k`, and pass image 1 as the reference.
|
||||||
--promptfiles prompts/02-content-xxx.md \
|
|
||||||
--ref path/to/01-cover-xxx.png \
|
|
||||||
--image 02-content-xxx.png --ar 3:4 --quality 2k
|
|
||||||
```
|
|
||||||
This ensures the AI maintains the same character design, illustration style, and color rendering across the series.
|
This ensures the AI maintains the same character design, illustration style, and color rendering across the series.
|
||||||
|
|
||||||
### Step 6: Combine
|
### Step 6: Combine
|
||||||
|
|||||||
Reference in New Issue
Block a user