mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 22:09:48 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fd9e51fc2 | |||
| a5d227b4e8 | |||
| 81377416b4 | |||
| a44bb08360 | |||
| a07669136c | |||
| b7298a60c6 | |||
| 309f078efb | |||
| 8958ba5409 | |||
| e6612628dc | |||
| bd5745f837 | |||
| cb26732559 | |||
| 9baf570caa | |||
| f99815cec9 | |||
| 8f0663d515 | |||
| 20ebf6126c | |||
| 64db328e61 | |||
| a3819b8e30 | |||
| 234c2a832b | |||
| 7943eb7b05 | |||
| 4b4a4d3863 | |||
| 919849c863 | |||
| 1faee80da4 | |||
| dea9322d0d | |||
| 86b1be83b8 | |||
| a1b935b2d8 | |||
| 9968d79a26 | |||
| 6f75fb17e4 | |||
| adba24281b | |||
| 61342ecfea | |||
| 5f753dd584 | |||
| 076192d58e | |||
| d6d434e714 | |||
| 6f3600d8e5 | |||
| 045fe5e57e | |||
| 0b3b7d13b5 | |||
| aa1a967a9f | |||
| d643bad53c | |||
| 0d977787b5 | |||
| 516803feb4 | |||
| 4af0506fa3 | |||
| 80a1c2970a | |||
| cdfa0dbff9 | |||
| 505a7e10ce | |||
| 6d063734ae | |||
| 31d728b505 | |||
| f6d5df0594 | |||
| 4bd5fe573e | |||
| 8c17d77209 | |||
| fb749aae36 | |||
| a23f2e6a4b | |||
| ae32c56a3c | |||
| 4b2ad3ad18 | |||
| 512b6d2e15 | |||
| 60d0b9c95b | |||
| acf1d42a64 | |||
| adb783ae6d | |||
| a4bd37c390 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.109.0"
|
||||
"version": "1.117.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -33,7 +33,8 @@
|
||||
"./skills/baoyu-translate",
|
||||
"./skills/baoyu-url-to-markdown",
|
||||
"./skills/baoyu-image-cards",
|
||||
"./skills/baoyu-youtube-transcript"
|
||||
"./skills/baoyu-youtube-transcript",
|
||||
"./skills/baoyu-wechat-summary"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: release-skills
|
||||
description: Universal release workflow. Auto-detects version files and changelogs. Supports Node.js, Python, Rust, Claude Plugin, and generic projects. Use when user says "release", "发布", "new version", "bump version", "push", "推送".
|
||||
description: Universal release workflow. Auto-detects version files and changelogs. Supports Node.js, Python, Rust, Claude Plugin, GitHub Releases, annotated tags, historical release backfill, and generic projects. Use when user says "release", "发布", "new version", "bump version", "push", "推送", "release notes", "GitHub Release", or "回填 Release".
|
||||
---
|
||||
|
||||
# Release Skills
|
||||
@@ -39,6 +39,7 @@ Just run `/release-skills` - auto-detects your project configuration.
|
||||
| `--major` | Force major version bump |
|
||||
| `--minor` | Force minor version bump |
|
||||
| `--patch` | Force patch version bump |
|
||||
| `--backfill-releases` | Create missing GitHub Releases for existing tags from changelog sections |
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -57,7 +58,11 @@ Just run `/release-skills` - auto-detects your project configuration.
|
||||
- `HISTORY*.md`
|
||||
- `CHANGES*.md`
|
||||
4. Identify language of each changelog by filename suffix
|
||||
5. Display detected configuration
|
||||
5. Detect GitHub release support:
|
||||
- Check whether `origin` points to GitHub
|
||||
- Check whether `gh` is installed and authenticated
|
||||
- Check existing releases with `gh release list --limit 5` when available
|
||||
6. Display detected configuration
|
||||
|
||||
**Project Hook Contract**:
|
||||
|
||||
@@ -310,6 +315,14 @@ git commit -m "docs(project): update architecture documentation"
|
||||
- Read version file (JSON/TOML/text)
|
||||
- Update version number
|
||||
- Write back (preserve formatting)
|
||||
3. **Create release notes file**:
|
||||
- Prefer the new version section from `CHANGELOG.md`
|
||||
- If no English/default changelog exists, use the first detected changelog
|
||||
- Extract only the exact `## {VERSION} - {YYYY-MM-DD}` section through the next `##`
|
||||
- Match both plain version and tag-prefixed headings when needed, e.g. `1.2.3` and `v1.2.3`
|
||||
- Keep breaking changes near the top; if needed, add a short highlight before other sections
|
||||
- Write notes to a UTF-8 temp file and reuse it for annotated tag messages, GitHub Releases, and `publish_artifact`
|
||||
- In normal mode, stop rather than creating an empty tag or GitHub Release when notes cannot be found
|
||||
|
||||
**Version Paths by File Type**:
|
||||
|
||||
@@ -325,7 +338,7 @@ git commit -m "docs(project): update architecture documentation"
|
||||
|
||||
Before creating the release commit, ask user to confirm:
|
||||
|
||||
**Use AskUserQuestion with two questions**:
|
||||
**Use AskUserQuestion with three questions**:
|
||||
|
||||
1. **Version bump** (single select):
|
||||
- Show recommended version based on Step 3 analysis
|
||||
@@ -335,6 +348,11 @@ Before creating the release commit, ask user to confirm:
|
||||
2. **Push to remote** (single select):
|
||||
- Options: "Yes, push after commit", "No, keep local only"
|
||||
|
||||
3. **Publish GitHub Release** (single select):
|
||||
- Offer this only when GitHub release support is available
|
||||
- Default to "Yes, publish after tag push" when the user also chose push
|
||||
- If the user keeps the release local, do not create or edit a GitHub Release
|
||||
|
||||
**Example Output Before Confirmation**:
|
||||
```
|
||||
Commits created:
|
||||
@@ -349,10 +367,11 @@ Changelog preview (en):
|
||||
### Fixes
|
||||
- Improve panel layout for long dialogues in comic
|
||||
|
||||
Ready to create release commit and tag.
|
||||
Release notes source: CHANGELOG.md#1.3.0
|
||||
Ready to create release commit, annotated tag, and GitHub Release.
|
||||
```
|
||||
|
||||
### Step 9: Create Release Commit and Tag
|
||||
### Step 9: Create Release Commit and Annotated Tag
|
||||
|
||||
After user confirmation:
|
||||
|
||||
@@ -367,10 +386,11 @@ After user confirmation:
|
||||
git commit -m "chore: release v{VERSION}"
|
||||
```
|
||||
|
||||
3. **Create tag**:
|
||||
3. **Create annotated tag**:
|
||||
```bash
|
||||
git tag v{VERSION}
|
||||
git tag -a v{VERSION} -F <release-notes-file>
|
||||
```
|
||||
If `.releaserc.yml` sets `tag.sign: true`, use `git tag -s` with the same notes file.
|
||||
|
||||
4. **Push if user confirmed** (Step 8):
|
||||
```bash
|
||||
@@ -380,6 +400,28 @@ After user confirmation:
|
||||
|
||||
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
|
||||
|
||||
### Step 10: Publish Release Artifacts and GitHub Release
|
||||
|
||||
Project artifact publishing and GitHub Releases are separate outputs:
|
||||
|
||||
1. **Project artifacts**:
|
||||
- If `release.hooks.publish_artifact` exists, run it once per prepared target
|
||||
- Pass the same `{release_notes_file}` used for the tag and GitHub Release
|
||||
- In dry-run mode, pass `{dry_run}=true` and report what would be published
|
||||
|
||||
2. **GitHub Release**:
|
||||
- Run only if the user confirmed remote publishing and GitHub support is available
|
||||
- Ensure the tag exists on the remote before creating the release
|
||||
- Create or update using the extracted notes:
|
||||
```bash
|
||||
if gh release view v{VERSION} >/dev/null 2>&1; then
|
||||
gh release edit v{VERSION} --title "v{VERSION}" --notes-file <release-notes-file>
|
||||
else
|
||||
gh release create v{VERSION} --title "v{VERSION}" --notes-file <release-notes-file> --verify-tag
|
||||
fi
|
||||
```
|
||||
- Never inline multiline release notes into shell commands
|
||||
|
||||
**Post-Release Output**:
|
||||
```
|
||||
Release v1.3.0 created.
|
||||
@@ -391,9 +433,32 @@ Commits:
|
||||
4. chore: release v1.3.0
|
||||
|
||||
Tag: v1.3.0
|
||||
Tag type: annotated
|
||||
GitHub Release: published # or "skipped/local only"
|
||||
Status: Pushed to origin # or "Local only - run git push when ready"
|
||||
```
|
||||
|
||||
## Backfill Existing GitHub Releases
|
||||
|
||||
Use this mode when the user asks to backfill historical releases or passes `--backfill-releases`.
|
||||
|
||||
1. Do not bump versions, edit changelogs, or create release commits.
|
||||
2. List existing tags in version order and detect missing releases:
|
||||
```bash
|
||||
git tag --sort=v:refname
|
||||
gh release view <tag>
|
||||
```
|
||||
3. For each tag without a GitHub Release:
|
||||
- Normalize the changelog lookup by stripping the configured tag prefix, e.g. `v1.2.3` -> `1.2.3`
|
||||
- Extract the matching section from `CHANGELOG.md`; fall back to the first matching changelog file
|
||||
- Skip or ask before publishing if no matching changelog section exists
|
||||
- Create the release with:
|
||||
```bash
|
||||
gh release create <tag> --title "<tag>" --notes-file <release-notes-file> --verify-tag
|
||||
```
|
||||
4. Detect lightweight tags with `git cat-file -t <tag>` (`commit` means lightweight, `tag` means annotated).
|
||||
5. Do not rewrite public lightweight tags by default. Converting an existing remote tag to an annotated tag requires explicit user confirmation because it rewrites a published reference.
|
||||
|
||||
## Configuration (.releaserc.yml)
|
||||
|
||||
Optional config file in project root to override defaults:
|
||||
@@ -498,6 +563,7 @@ No changes made. Run without --dry-run to execute.
|
||||
/release-skills --minor # Force minor bump
|
||||
/release-skills --patch # Force patch bump
|
||||
/release-skills --major # Force major bump (with confirmation)
|
||||
/release-skills --backfill-releases # Create missing GitHub Releases for existing tags
|
||||
```
|
||||
|
||||
## When to Use
|
||||
@@ -506,6 +572,7 @@ Trigger this skill when user requests:
|
||||
- "release", "发布", "create release", "new version", "新版本"
|
||||
- "bump version", "update version", "更新版本"
|
||||
- "prepare release"
|
||||
- "release notes", "GitHub Release", "回填 Release"
|
||||
- "push to remote" (with uncommitted changes)
|
||||
|
||||
**Important**: If user says "just push" or "直接 push" with uncommitted changes, STILL follow all steps above first.
|
||||
|
||||
@@ -173,3 +173,6 @@ diagram/
|
||||
.worktrees/
|
||||
youtube-transcript/
|
||||
.omx/
|
||||
.codex-tmp/
|
||||
outputs/
|
||||
wechat/
|
||||
|
||||
+118
-1
@@ -2,6 +2,123 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 1.117.0 - 2026-05-16
|
||||
|
||||
### Features
|
||||
- `baoyu-article-illustrator`: add batch generation policy — backend native batch first, runtime parallel calls second, sequential fallback; configurable `generation_batch_size` and `--batch-size` option
|
||||
- `baoyu-comic`: add batch generation policy with dependency-aware ordering (character sheet before pages) and configurable `--batch-size`
|
||||
- `baoyu-image-cards`: add batch generation policy honoring image-1 anchor chain, with configurable `--batch-size`
|
||||
- `baoyu-slide-deck`: add batch generation policy for slide image rendering with configurable `--batch-size`
|
||||
- `baoyu-xhs-images`: sync batch generation policy from baoyu-image-cards
|
||||
|
||||
## 1.116.5 - 2026-05-14
|
||||
|
||||
### Features
|
||||
- `baoyu-post-to-wechat`: send WeChat login QR code to Telegram when `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` env vars are set, enabling headless / remote login flows (by @beforesun)
|
||||
|
||||
### Refactor
|
||||
- `baoyu-post-to-wechat`: harden Telegram QR notification — add 10s fetch timeout, defer the 2s QR-render wait until env vars are configured, and use viewport screenshot as fallback
|
||||
|
||||
## 1.116.4 - 2026-05-14
|
||||
|
||||
### Refactor
|
||||
- `baoyu-wechat-summary`: streamline roast version prompts in output-formats.md (99 → 23 lines), add roast-specific profile usage bullets to SKILL.md Round 2
|
||||
|
||||
## 1.116.3 - 2026-05-13
|
||||
|
||||
### Documentation
|
||||
- Replace Claude Code references with generic Agent wording in READMEs to reflect multi-agent support (Claude Code, Codex, etc.)
|
||||
|
||||
## 1.116.2 - 2026-05-13
|
||||
|
||||
### Documentation
|
||||
- `baoyu-wechat-summary`: update example group name in SKILL.md
|
||||
|
||||
## 1.116.1 - 2026-05-13
|
||||
|
||||
### Features
|
||||
- `baoyu-wechat-summary`: add `data_root` option to first-time setup flow, allowing users to customize the digest output directory during initialization
|
||||
|
||||
## 1.116.0 - 2026-05-13
|
||||
|
||||
### Features
|
||||
- Add `baoyu-wechat-summary` skill: summarize WeChat group chat highlights into structured digests with topic extraction, message leaderboards, and per-user profiles. Supports normal and roast (毒舌) versions, incremental mode, and profile backfill. Requires [wx-cli](https://github.com/jackwener/wx-cli).
|
||||
|
||||
## 1.115.4 - 2026-05-11
|
||||
|
||||
### Documentation
|
||||
- Image generation backend selection: emphasize Codex `imagegen` as the priority runtime-native tool (invoke via the `Skill` tool with `skill: "imagegen"`) and forbid SVG/HTML/canvas substitution when no raster backend can be resolved — fall through to asking the user instead of silently emitting code-based art. Updated in `docs/image-generation-tools.md` and inlined into `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-image-cards`, `baoyu-infographic`, `baoyu-slide-deck`, and `baoyu-xhs-images`.
|
||||
|
||||
## 1.115.3 - 2026-05-11
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-wechat`: ensure tab activation before copy/paste in WeChat editor (by @fengxiaodong28)
|
||||
- `baoyu-post-to-x`: use toolbar media upload instead of image clipboard paste for X Articles
|
||||
|
||||
## 1.115.2 - 2026-05-10
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-x`: honor explicit Codex Chrome plugin requests as a distinct browser-control mode, keep Chrome Computer Use and CDP fallbacks from silently taking over, and improve X Articles draft creation detection.
|
||||
|
||||
## 1.115.1 - 2026-05-10
|
||||
|
||||
### Fixes
|
||||
- `baoyu-imagine`: change the default MiniMax image API endpoint to `https://api.minimaxi.com` to match the current official image generation documentation, while keeping `https://api.minimax.io` available through `MINIMAX_BASE_URL` overrides.
|
||||
- `baoyu-image-gen`: sync the deprecated image-generation entrypoint with the same MiniMax default endpoint and regression coverage.
|
||||
|
||||
## 1.115.0 - 2026-05-09
|
||||
|
||||
### Features
|
||||
- `baoyu-post-to-x`: add Chrome Computer Use as the preferred execution mode in Codex. When Computer Use tools are available, all X UI actions (compose, article, quote, video) go through the user's real Chrome window instead of CDP scripts. CDP scripts become a fallback when Computer Use is unavailable or explicitly not requested.
|
||||
|
||||
## 1.114.1 - 2026-05-08
|
||||
|
||||
### Fixes
|
||||
- `baoyu-danger-gemini-web`: restore generated-image extraction for current Gemini Web responses where image URLs appear as `https://lh3.googleusercontent.com/gg-dl/` without the legacy generated-image markers. Adds regression coverage for the fallback response shape. (by @evilstar2016)
|
||||
|
||||
## 1.114.0 - 2026-05-05
|
||||
|
||||
### Features
|
||||
- `baoyu-infographic`: add `retro-popup-pop` style — retro pixel popup × pop-art collage. Renders content as a stack of 80/90s desktop popup windows (title bars, close buttons, ERROR / ALERT dialogs, file windows like `PROBLEMS.EXE`, progress bars, OK / CANCEL / FIX IT buttons) with thick black outlines, flat color fills, and bright cyan (#12B8DE) or vintage cream (#F5F0E6) backgrounds. Pairs especially well with the `dense-modules` layout; promoted as a recommended style for the `高密度信息大图` keyword shortcut and the `Product/Buying Guide` content type. Style Gallery count goes from 21 to 22.
|
||||
Credit to AJ@WaytoAGI.
|
||||
|
||||
### Documentation
|
||||
- `release-skills`: document GitHub Release publishing in the release workflow, including release-notes extraction from changelog sections, annotated tag creation, `gh release create/edit`, and historical release backfill for existing tags.
|
||||
|
||||
## 1.113.0 - 2026-04-25
|
||||
|
||||
### Features
|
||||
- `baoyu-imagine`: add DashScope Wan 2.7 image model support (`wan2.7-image-pro` and `wan2.7-image`) directly through the official Aliyun (Bailian) API. Supports text-to-image, image editing, and multi-image fusion with up to 9 reference images, with documented `[1:8, 8:1]` aspect ratio validation and per-mode pixel-budget rules. Forces `parameters.n: 1` to match baoyu-imagine's single-image save semantics and explicitly rejects `--n > 1` to prevent silent multi-image billing (the API defaults to `n=4` in non-collage mode). Allows `--provider dashscope --ref ...` opt-in for Wan 2.7 reference workflows.
|
||||
|
||||
## 1.112.0 - 2026-04-24
|
||||
|
||||
### Features
|
||||
- `baoyu-article-illustrator`: make `hand-drawn-edu` (infographic + sketch-notes + macaron) the universal fallback preset when content analysis surfaces no strong signal — warm cream paper, black hand-drawn lines, soft pastel section blocks. Elevate `sketch-notes` to primary style across infographic / flowchart / comparison / framework auto-selection; rewrite the sketch-notes style spec (macaron palette, canonical single-page layout, diagram-only rule); add matching prompt block and workflow defaults.
|
||||
- `baoyu-article-illustrator`: add `hand-drawn-edu-flow` (flowchart) and `hand-drawn-edu-compare` (comparison) presets for the same warm educational style.
|
||||
|
||||
### Breaking Changes
|
||||
- `baoyu-article-illustrator`: `hand-drawn-edu` preset now maps to `infographic` instead of `flowchart`. Users relying on the previous flowchart behavior should switch to the new `hand-drawn-edu-flow` preset.
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-x`: add entry point guard to `scripts/md-to-html.ts` so that importing `parseMarkdown` from `x-article.ts` no longer triggers the CLI entry point. Mirrors the same fix applied to `baoyu-post-to-weibo`.
|
||||
|
||||
## 1.111.1 - 2026-04-21
|
||||
|
||||
### Documentation
|
||||
- Add a top-level `## Confirmation Policy` section to every image-generating skill (`baoyu-infographic`, `baoyu-cover-image`, `baoyu-slide-deck`, `baoyu-image-cards`, `baoyu-xhs-images`, `baoyu-article-illustrator`) as a single source of truth: explicit skill invocation, keyword shortcuts, EXTEND.md defaults, and auto-recommendations are recommendation inputs only — they never authorize skipping the confirmation step. Opt-out requires an explicit current-request signal (`--no-confirm` / `--quick` / `--yes` / "直接生成" / equivalent).
|
||||
- `baoyu-infographic`: consolidate the scattered reminders (previously repeated across Step 5, Step 6, Default combination, Keyword Shortcuts, and the preferences docs) into a single policy section referenced from Step 4's hard gate.
|
||||
|
||||
## 1.111.0 - 2026-04-21
|
||||
|
||||
### Refactor
|
||||
- Unify image-backend resolution across all image-consuming skills (`baoyu-infographic`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-image-cards`, `baoyu-article-illustrator`, `baoyu-slide-deck`, `baoyu-xhs-images`): add a single `preferred_image_backend` preference field (`auto | ask | <backend-id>`) and replace the stateless ask-once rule with a 4-step resolution (current-request override → saved preference → auto-select → ask). Runtime-native tools (Codex `imagegen`, Hermes `image_generate`) are preferred by default; existing `EXTEND.md` files without the field are treated as `auto` with no schema bump.
|
||||
- Add a top-level `## Changing Preferences` section to each image-consuming skill as a first-class surface for pinning the backend and editing common one-line preferences.
|
||||
|
||||
## 1.110.0 - 2026-04-21
|
||||
|
||||
### Features
|
||||
- `baoyu-imagine`: add `gpt-image-2` support for OpenAI image generation and edits, make it the default OpenAI model, and document the official size/quality mapping, custom-size constraints, and Azure deployment guidance
|
||||
|
||||
## 1.109.0 - 2026-04-21
|
||||
|
||||
### Features
|
||||
@@ -648,7 +765,7 @@ English | [中文](./CHANGELOG.zh.md)
|
||||
- `baoyu-format-markdown`: add reader-perspective content analysis phase — analyzes highlights, structure, and formatting issues before applying formatting
|
||||
- `baoyu-format-markdown`: restructure workflow from 8 steps to 7 with explicit do/don't formatting principles and completion report
|
||||
- `baoyu-translate`: extract Step 2 workflow mechanics to separate reference file for cleaner SKILL.md
|
||||
- `baoyu-translate`: expand trigger keywords (改成中文, 快翻, 本地化, etc.) for better skill activation
|
||||
- `baoyu-translate`: expand trigger keywords (改成中文,快翻,本地化,etc.) for better skill activation
|
||||
- `baoyu-translate`: add proactive warning for long content in quick mode
|
||||
- `baoyu-translate`: save frontmatter to `chunks/frontmatter.md` during chunking
|
||||
|
||||
|
||||
+120
-3
@@ -2,6 +2,123 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 1.117.0 - 2026-05-16
|
||||
|
||||
### 新功能
|
||||
- `baoyu-article-illustrator`:新增批量生成策略 —— 优先使用后端原生批量接口,其次运行时并行调用,最后顺序生成;支持 `generation_batch_size` 配置和 `--batch-size` 参数
|
||||
- `baoyu-comic`:新增批量生成策略,支持依赖感知排序(角色图先于页面)和 `--batch-size` 参数
|
||||
- `baoyu-image-cards`:新增批量生成策略,遵循 image-1 锚定链,支持 `--batch-size` 参数
|
||||
- `baoyu-slide-deck`:新增幻灯片图片批量生成策略,支持 `--batch-size` 参数
|
||||
- `baoyu-xhs-images`:同步 baoyu-image-cards 的批量生成策略
|
||||
|
||||
## 1.116.5 - 2026-05-14
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-wechat`:当设置 `TELEGRAM_BOT_TOKEN` 和 `TELEGRAM_CHAT_ID` 环境变量时,自动将微信登录二维码发送到 Telegram,支持无显示器/远程登录场景 (by @beforesun)
|
||||
|
||||
### 重构
|
||||
- `baoyu-post-to-wechat`:加固 Telegram QR 通知逻辑 —— 增加 10 秒 fetch 超时、未配置环境变量时不再无谓等待 2 秒、回退截图改为视口范围以减小体积
|
||||
|
||||
## 1.116.4 - 2026-05-14
|
||||
|
||||
### 重构
|
||||
- `baoyu-wechat-summary`:精简毒舌版提示词(99 → 23 行),在 SKILL.md Round 2 中增加 roast 专用的画像使用指引
|
||||
|
||||
## 1.116.3 - 2026-05-13
|
||||
|
||||
### 文档
|
||||
- README 中将 Claude Code 替换为通用的 Agent 表述,体现多 Agent 支持(Claude Code、Codex 等)
|
||||
|
||||
## 1.116.2 - 2026-05-13
|
||||
|
||||
### 文档
|
||||
- `baoyu-wechat-summary`:更新 SKILL.md 中的示例群名
|
||||
|
||||
## 1.116.1 - 2026-05-13
|
||||
|
||||
### 新功能
|
||||
- `baoyu-wechat-summary`:初始化设置流程中新增 `data_root` 选项,允许用户在首次配置时自定义摘要输出目录
|
||||
|
||||
## 1.116.0 - 2026-05-13
|
||||
|
||||
### 新功能
|
||||
- 新增 `baoyu-wechat-summary` 技能:将微信群聊精华提炼为结构化简报,支持话题提取、发言排行榜和群友画像。可生成正常版和毒舌版,支持增量模式和画像回溯初始化。需安装 [wx-cli](https://github.com/jackwener/wx-cli)。
|
||||
|
||||
## 1.115.4 - 2026-05-11
|
||||
|
||||
### 文档
|
||||
- 图片生成后端选择规则强化:明确将 Codex `imagegen` 作为运行时原生工具的优先项(通过 `Skill` 工具调用,`skill: "imagegen"`),并禁止在无可用光栅后端时降级为 SVG/HTML/canvas 等代码渲染 —— 应退回到询问用户,而非静默输出代码绘图。规则同步更新到 `docs/image-generation-tools.md`,并按自包含规则内联到 `baoyu-article-illustrator`、`baoyu-comic`、`baoyu-cover-image`、`baoyu-image-cards`、`baoyu-infographic`、`baoyu-slide-deck`、`baoyu-xhs-images`。
|
||||
|
||||
## 1.115.3 - 2026-05-11
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-wechat`:修复微信编辑器中复制粘贴前未激活标签页的问题 (by @fengxiaodong28)
|
||||
- `baoyu-post-to-x`:X 文章图片插入改用工具栏媒体上传替代剪贴板粘贴方式
|
||||
|
||||
## 1.115.2 - 2026-05-10
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-x`:将显式请求 Codex Chrome 插件的场景作为独立浏览器控制模式处理,避免 Chrome Computer Use 或 CDP 回退流程静默接管;同时改进 X Articles 草稿创建按钮检测。
|
||||
|
||||
## 1.115.1 - 2026-05-10
|
||||
|
||||
### 修复
|
||||
- `baoyu-imagine`:将默认 MiniMax 图片 API 端点改为 `https://api.minimaxi.com`,与当前官方图片生成文档保持一致;仍可通过 `MINIMAX_BASE_URL` 覆盖为 `https://api.minimax.io`。
|
||||
- `baoyu-image-gen`:同步已废弃图片生成入口的 MiniMax 默认端点和回归测试。
|
||||
|
||||
## 1.115.0 - 2026-05-09
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-x`:新增 Chrome Computer Use 作为 Codex 环境下的首选执行模式。当 Computer Use 工具可用时,所有 X 界面操作(发帖、文章、引用、视频)均通过用户真实 Chrome 窗口完成,不再使用 CDP 脚本。CDP 脚本降级为 Computer Use 不可用或用户明确要求时的回退方案。
|
||||
|
||||
## 1.114.1 - 2026-05-08
|
||||
|
||||
### 修复
|
||||
- `baoyu-danger-gemini-web`:修复当前 Gemini Web 响应中生成图 URL 以 `https://lh3.googleusercontent.com/gg-dl/` 形式出现、但不再包含旧版生成图 marker 时的图片提取失败问题。补充该响应形态的回归测试。 (by @evilstar2016)
|
||||
|
||||
## 1.114.0 - 2026-05-05
|
||||
|
||||
### 新功能
|
||||
- `baoyu-infographic`:新增 `retro-popup-pop` 风格 —— 复古像素弹窗 × 波普信息图。画面由多个 80/90 年代桌面弹窗叠加而成(标题栏、关闭按钮、ERROR / ALERT 报错对话框、`PROBLEMS.EXE` 等复古文件窗、进度条、OK / CANCEL / FIX IT 按钮),统一粗黑描边、平涂色块,背景使用亮青蓝(#12B8DE)或复古奶油色(#F5F0E6)。与 `dense-modules` 布局尤其契合;同时升级为 `高密度信息大图` 关键词快捷方式与 `Product/Buying Guide` 内容类型的推荐风格。风格库从 21 个扩展至 22 个。
|
||||
Credit to AJ@WaytoAGI.
|
||||
|
||||
### 文档
|
||||
- `release-skills`:补充 GitHub Release 发布流程,包括从 changelog 段落提取 release notes、创建 annotated tag、执行 `gh release create/edit`,以及为已有 tag 回填历史 GitHub Releases。
|
||||
|
||||
## 1.113.0 - 2026-04-25
|
||||
|
||||
### 新功能
|
||||
- `baoyu-imagine`:新增 DashScope Wan 2.7 图像模型支持(`wan2.7-image-pro` 与 `wan2.7-image`),通过阿里云百炼官方 API 直接调用,无需经 Replicate 转发。支持文生图、图像编辑、多图融合(最多 9 张参考图),按官方文档校验 `[1:8, 8:1]` 宽高比范围,并按模式应用不同的像素预算规则。强制 `parameters.n: 1` 以匹配 baoyu-imagine 的单图保存语义,显式拒绝 `--n > 1`,避免在用户不知情的情况下产生多图计费(API 在非拼图模式下默认 `n=4`)。允许通过 `--provider dashscope --ref ...` 显式启用 Wan 2.7 参考图工作流。
|
||||
|
||||
## 1.112.0 - 2026-04-24
|
||||
|
||||
### 新功能
|
||||
- `baoyu-article-illustrator`:当内容分析未检测到明确信号时,将 `hand-drawn-edu`(infographic + sketch-notes + macaron)作为通用默认预设 —— 暖奶油色纸面背景、黑色手绘线条、柔和马卡龙色块。`sketch-notes` 升级为 infographic / flowchart / comparison / framework 自动选择的首选风格;重写 sketch-notes 风格规范(马卡龙调色板、标准单页布局、仅限示意图的规则);新增对应的 prompt 模板块和默认工作流规则。
|
||||
- `baoyu-article-illustrator`:新增 `hand-drawn-edu-flow`(flowchart)和 `hand-drawn-edu-compare`(comparison)两个预设,保持相同的温暖教育风格。
|
||||
|
||||
### 破坏性变更
|
||||
- `baoyu-article-illustrator`:`hand-drawn-edu` 预设的类型由 `flowchart` 改为 `infographic`。依赖原有流程图行为的用户请改用新增的 `hand-drawn-edu-flow` 预设。
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-x`:为 `scripts/md-to-html.ts` 添加入口守卫,确保 `x-article.ts` 导入 `parseMarkdown` 时不再触发 CLI 入口逻辑。与 `baoyu-post-to-weibo` 此前的修复保持一致。
|
||||
|
||||
## 1.111.1 - 2026-04-21
|
||||
|
||||
### 文档
|
||||
- 为每个图片生成类技能(`baoyu-infographic`、`baoyu-cover-image`、`baoyu-slide-deck`、`baoyu-image-cards`、`baoyu-xhs-images`、`baoyu-article-illustrator`)新增顶级 `## Confirmation Policy` 章节作为单一事实源:显式调用技能、关键词快捷方式、EXTEND.md 偏好、自动推荐都只是"推荐输入",不授权跳过确认步骤。跳过确认必须由当前请求中的明确信号触发(`--no-confirm` / `--quick` / `--yes` / "直接生成" / 同义表达)。
|
||||
- `baoyu-infographic`:将原先散落在 Step 5、Step 6、Default combination、Keyword Shortcuts 及 preferences 文档中的重复提醒合并为单一策略章节,由 Step 4 的 hard gate 引用。
|
||||
|
||||
## 1.111.0 - 2026-04-21
|
||||
|
||||
### 重构
|
||||
- 统一所有图片生成类技能(`baoyu-infographic`、`baoyu-comic`、`baoyu-cover-image`、`baoyu-image-cards`、`baoyu-article-illustrator`、`baoyu-slide-deck`、`baoyu-xhs-images`)的后端选择规则:新增单一 `preferred_image_backend` 偏好字段(`auto | ask | <backend-id>`),用 4 步解析规则(当前请求覆盖 → 已保存偏好 → 自动选择 → 询问用户)替换原有的无状态询问规则。默认优先使用运行时原生工具(如 Codex `imagegen`、Hermes `image_generate`);未设置该字段的现有 `EXTEND.md` 文件视为 `auto`,无需升级 schema 版本。
|
||||
- 在每个图片技能中新增顶级 `## Changing Preferences` 章节,作为固定后端和修改常用偏好的一级入口。
|
||||
|
||||
## 1.110.0 - 2026-04-21
|
||||
|
||||
### 新功能
|
||||
- `baoyu-imagine`:新增 `gpt-image-2` 支持,用于 OpenAI 图像生成与编辑;将其设为默认 OpenAI 模型,并补齐官方尺寸/质量映射、自定义尺寸约束与 Azure 部署说明
|
||||
|
||||
## 1.109.0 - 2026-04-21
|
||||
|
||||
### 新功能
|
||||
@@ -604,7 +721,7 @@
|
||||
## 1.52.0 - 2026-03-06
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-weibo`:新增 `--video` 视频上传支持(图片+视频最多 18 个文件)
|
||||
- `baoyu-post-to-weibo`:新增 `--video` 视频上传支持(图片 + 视频最多 18 个文件)
|
||||
- `baoyu-post-to-weibo`:上传方式从剪贴板粘贴改为 `DOM.setFileInputFiles`,提升上传可靠性
|
||||
|
||||
### 修复
|
||||
@@ -1067,7 +1184,7 @@
|
||||
## 1.20.0 - 2026-01-24
|
||||
|
||||
### 新功能
|
||||
- `baoyu-cover-image`:从类型 × 风格二维系统升级为**四维系统**——新增 `--text` 维度(none 无文字、title-only 仅标题、title-subtitle 标题+副标题、text-rich 丰富文字)控制文字密度,新增 `--mood` 维度(subtle 低调、balanced 平衡、bold 醒目)控制情感强度。新增 `--quick` 标志跳过确认,直接使用自动选择。
|
||||
- `baoyu-cover-image`:从类型 × 风格二维系统升级为**四维系统**——新增 `--text` 维度(none 无文字、title-only 仅标题、title-subtitle 标题 + 副标题、text-rich 丰富文字)控制文字密度,新增 `--mood` 维度(subtle 低调、balanced 平衡、bold 醒目)控制情感强度。新增 `--quick` 标志跳过确认,直接使用自动选择。
|
||||
|
||||
### 文档
|
||||
- `baoyu-cover-image`:新增维度参考文件——`references/dimensions/text.md`(文字密度级别)和 `references/dimensions/mood.md`(氛围强度级别)。
|
||||
@@ -1087,7 +1204,7 @@
|
||||
- `baoyu-image-gen`:代码模块化——类型定义提取至 `types.ts`,provider 实现提取至 `providers/google.ts` 和 `providers/openai.ts`。
|
||||
|
||||
### 文档
|
||||
- `baoyu-comic`:改进 ohmsha 预设文档,明确默认哆啦A梦角色定义和视觉描述。
|
||||
- `baoyu-comic`:改进 ohmsha 预设文档,明确默认哆啦 A 梦角色定义和视觉描述。
|
||||
|
||||
## 1.18.3 - 2026-01-23
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](./README.zh.md)
|
||||
|
||||
Skills shared by Baoyu for improving daily work efficiency with Claude Code.
|
||||
Skills shared by Baoyu for improving daily work efficiency with AI Agents (Claude Code, Codex, etc.).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -40,7 +40,7 @@ Publishing to ClawHub releases the published skill under `MIT-0`, per ClawHub's
|
||||
|
||||
### Register as Plugin Marketplace
|
||||
|
||||
Run the following command in Claude Code:
|
||||
Run the following command in the Agent:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add JimLiu/baoyu-skills
|
||||
@@ -64,7 +64,7 @@ Run the following command in Claude Code:
|
||||
|
||||
**Option 3: Ask the Agent**
|
||||
|
||||
Simply tell Claude Code:
|
||||
Simply tell the Agent:
|
||||
|
||||
> Please install Skills from github.com/JimLiu/baoyu-skills
|
||||
|
||||
@@ -80,7 +80,7 @@ The marketplace now exposes a single plugin so each skill is registered exactly
|
||||
|
||||
To update skills to the latest version:
|
||||
|
||||
1. Run `/plugin` in Claude Code
|
||||
1. Run `/plugin` in the Agent
|
||||
2. Switch to **Marketplaces** tab (use arrow keys or Tab)
|
||||
3. Select **baoyu-skills**
|
||||
4. Choose **Update marketplace**
|
||||
@@ -579,7 +579,7 @@ Plain text input is treated as a regular post. Markdown files are treated as X A
|
||||
|
||||
```bash
|
||||
# Post with text
|
||||
/baoyu-post-to-x "Hello from Claude Code!"
|
||||
/baoyu-post-to-x "Hello from AI Agent!"
|
||||
|
||||
# Post with images
|
||||
/baoyu-post-to-x "Check this out" --image photo.png
|
||||
@@ -714,7 +714,7 @@ AI-powered generation backends.
|
||||
|
||||
#### baoyu-imagine
|
||||
|
||||
AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), MiniMax, Jimeng (即梦), Seedream (豆包), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, custom sizes, batch generation, and quality presets.
|
||||
AI SDK-based image generation using OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), MiniMax, Jimeng (即梦), Seedream (豆包), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, custom sizes, batch generation, and quality presets.
|
||||
|
||||
```bash
|
||||
# Basic generation (auto-detect provider)
|
||||
@@ -727,10 +727,10 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
/baoyu-imagine --prompt "A banner" --image banner.png --quality 2k
|
||||
|
||||
# Specific provider
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider openai
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider openai --model gpt-image-2
|
||||
|
||||
# Azure OpenAI (model = deployment name)
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider azure --model gpt-image-1.5
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider azure --model gpt-image-2
|
||||
|
||||
# OpenRouter
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider openrouter
|
||||
@@ -786,7 +786,7 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `zai`, `minimax`, `jimeng`, `seedream`, or `replicate` |
|
||||
| `--model`, `-m` | Model ID or deployment name. Azure uses deployment name; OpenRouter uses full model IDs; Z.AI uses `glm-image`; MiniMax uses `image-01` / `image-01-live` |
|
||||
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||
| `--size` | Size (e.g., `1024x1024`) |
|
||||
| `--size` | Size (e.g., `1024x1024`; `gpt-image-2` accepts valid custom sizes up to 3840px max edge) |
|
||||
| `--quality` | `normal` or `2k` (default: `2k`) |
|
||||
| `--imageSize` | `1K`, `2K`, or `4K` for Google/OpenRouter |
|
||||
| `--imageApiDialect` | `openai-native` or `ratio-metadata` for OpenAI-compatible gateways |
|
||||
@@ -810,9 +810,9 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `JIMENG_ACCESS_KEY_ID` | Jimeng Volcengine access key | - |
|
||||
| `JIMENG_SECRET_ACCESS_KEY` | Jimeng Volcengine secret key | - |
|
||||
| `ARK_API_KEY` | Seedream Volcengine ARK API key | - |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-2` |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Azure default deployment name | - |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | Backward-compatible Azure deployment/model alias | `gpt-image-1.5` |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | Backward-compatible Azure deployment/model alias | `gpt-image-2` |
|
||||
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model | `google/gemini-3.1-flash-image-preview` |
|
||||
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
|
||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `qwen-image-2.0-pro` |
|
||||
@@ -834,7 +834,7 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
|
||||
| `ZAI_BASE_URL` | Custom Z.AI endpoint | `https://api.z.ai/api/paas/v4` |
|
||||
| `BIGMODEL_BASE_URL` | Backward-compatible alias for Z.AI endpoint | - |
|
||||
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint | `https://api.minimax.io` |
|
||||
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint | `https://api.minimaxi.com` |
|
||||
| `REPLICATE_BASE_URL` | Custom Replicate endpoint | - |
|
||||
| `JIMENG_BASE_URL` | Custom Jimeng endpoint | `https://visual.volcengineapi.com` |
|
||||
| `JIMENG_REGION` | Jimeng region | `cn-north-1` |
|
||||
@@ -1112,6 +1112,36 @@ Custom style descriptions are also accepted, e.g., `--style "poetic and lyrical"
|
||||
- Translator's notes for cultural/domain-specific references
|
||||
- Output directory with all intermediate files preserved
|
||||
|
||||
#### baoyu-wechat-summary
|
||||
|
||||
Summarize WeChat group chat highlights into a structured digest. Extracts topics, quotes, and stats from group messages using [wx-cli](https://github.com/jackwener/wx-cli). Maintains per-group history and per-user profiles across runs. Supports normal and roast (毒舌) versions.
|
||||
|
||||
```bash
|
||||
# Summarize a group's recent messages
|
||||
/baoyu-wechat-summary 相亲相爱一家人 最近 1 天
|
||||
|
||||
# Weekly summary
|
||||
/baoyu-wechat-summary AI 技术群 最近 7 天
|
||||
|
||||
# Incremental (since last digest)
|
||||
/baoyu-wechat-summary 相亲相爱一家人
|
||||
|
||||
# Roast version
|
||||
/baoyu-wechat-summary 相亲相爱一家人 最近 3 天 毒舌版
|
||||
```
|
||||
|
||||
**Requirements**:
|
||||
- [wx-cli](https://github.com/jackwener/wx-cli) installed (`npm install -g @jackwener/wx-cli`)
|
||||
- WeChat 4.x running and logged in on macOS
|
||||
|
||||
**Features**:
|
||||
- Topic extraction with attribution and quotes
|
||||
- Message leaderboard and per-user profiles
|
||||
- Incremental mode (picks up where last digest left off)
|
||||
- Multi-day range splitting for large batches
|
||||
- Normal and roast (毒舌) digest versions
|
||||
- Profile backfill from historical digests
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Some skills require API keys or custom configuration. Environment variables can be set in `.env` files:
|
||||
@@ -1132,14 +1162,14 @@ mkdir -p ~/.baoyu-skills
|
||||
cat > ~/.baoyu-skills/.env << 'EOF'
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
OPENAI_IMAGE_MODEL=gpt-image-1.5
|
||||
OPENAI_IMAGE_MODEL=gpt-image-2
|
||||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# OPENAI_IMAGE_USE_CHAT=false
|
||||
|
||||
# Azure OpenAI
|
||||
AZURE_OPENAI_API_KEY=xxx
|
||||
AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-image-1.5
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-image-2
|
||||
# AZURE_API_VERSION=2025-04-01-preview
|
||||
|
||||
# OpenRouter
|
||||
@@ -1167,7 +1197,7 @@ ZAI_IMAGE_MODEL=glm-image
|
||||
# MiniMax
|
||||
MINIMAX_API_KEY=xxx
|
||||
MINIMAX_IMAGE_MODEL=image-01
|
||||
# MINIMAX_BASE_URL=https://api.minimax.io
|
||||
# MINIMAX_BASE_URL=https://api.minimaxi.com
|
||||
|
||||
# Replicate
|
||||
REPLICATE_API_TOKEN=r8_xxx
|
||||
|
||||
+45
-15
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
宝玉分享的 Claude Code 技能集,提升日常工作效率。
|
||||
宝玉分享的 AI Agent 技能集(适用于 Claude Code、Codex 等),提升日常工作效率。
|
||||
|
||||
## 前置要求
|
||||
|
||||
@@ -40,7 +40,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
### 注册插件市场
|
||||
|
||||
在 Claude Code 中运行:
|
||||
在 Agent 中运行:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add JimLiu/baoyu-skills
|
||||
@@ -64,7 +64,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
**方式三:告诉 Agent**
|
||||
|
||||
直接告诉 Claude Code:
|
||||
直接告诉 Agent:
|
||||
|
||||
> 请帮我安装 github.com/JimLiu/baoyu-skills 中的 Skills
|
||||
|
||||
@@ -80,7 +80,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
更新技能到最新版本:
|
||||
|
||||
1. 在 Claude Code 中运行 `/plugin`
|
||||
1. 在 Agent 中运行 `/plugin`
|
||||
2. 切换到 **Marketplaces** 标签页(使用方向键或 Tab)
|
||||
3. 选择 **baoyu-skills**
|
||||
4. 选择 **Update marketplace**
|
||||
@@ -579,7 +579,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
```bash
|
||||
# 发布文字
|
||||
/baoyu-post-to-x "Hello from Claude Code!"
|
||||
/baoyu-post-to-x "Hello from AI Agent!"
|
||||
|
||||
# 发布带图片
|
||||
/baoyu-post-to-x "看看这个" --image photo.png
|
||||
@@ -714,7 +714,7 @@ AI 驱动的生成后端。
|
||||
|
||||
#### baoyu-imagine
|
||||
|
||||
基于 AI SDK 的图像生成,支持 OpenAI、Azure OpenAI、Google、OpenRouter、DashScope(阿里通义万相)、MiniMax、即梦(Jimeng)、豆包(Seedream)和 Replicate API。支持文生图、参考图、宽高比、自定义尺寸、批量生成和质量预设。
|
||||
基于 AI SDK 的图像生成,支持 OpenAI GPT Image 2、Azure OpenAI、Google、OpenRouter、DashScope(阿里通义万相)、MiniMax、即梦(Jimeng)、豆包(Seedream)和 Replicate API。支持文生图、参考图、宽高比、自定义尺寸、批量生成和质量预设。
|
||||
|
||||
```bash
|
||||
# 基础生成(自动检测服务商)
|
||||
@@ -727,10 +727,10 @@ AI 驱动的生成后端。
|
||||
/baoyu-imagine --prompt "横幅图" --image banner.png --quality 2k
|
||||
|
||||
# 指定服务商
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider openai
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider openai --model gpt-image-2
|
||||
|
||||
# Azure OpenAI(model 为部署名称)
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider azure --model gpt-image-1.5
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider azure --model gpt-image-2
|
||||
|
||||
# OpenRouter
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider openrouter
|
||||
@@ -786,7 +786,7 @@ AI 驱动的生成后端。
|
||||
| `--provider` | `google`、`openai`、`azure`、`openrouter`、`dashscope`、`zai`、`minimax`、`jimeng`、`seedream` 或 `replicate` |
|
||||
| `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 ID;Z.AI 使用 `glm-image`;MiniMax 使用 `image-01` / `image-01-live` |
|
||||
| `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) |
|
||||
| `--size` | 尺寸(如 `1024x1024`) |
|
||||
| `--size` | 尺寸(如 `1024x1024`;`gpt-image-2` 支持最长边不超过 3840px 的有效自定义尺寸) |
|
||||
| `--quality` | `normal` 或 `2k`(默认:`2k`) |
|
||||
| `--imageSize` | Google/OpenRouter 使用的 `1K`、`2K`、`4K` |
|
||||
| `--imageApiDialect` | OpenAI 兼容网关的图像 API 方言(`openai-native` 或 `ratio-metadata`) |
|
||||
@@ -810,9 +810,9 @@ AI 驱动的生成后端。
|
||||
| `JIMENG_ACCESS_KEY_ID` | 即梦火山引擎 Access Key | - |
|
||||
| `JIMENG_SECRET_ACCESS_KEY` | 即梦火山引擎 Secret Key | - |
|
||||
| `ARK_API_KEY` | 豆包火山引擎 ARK API 密钥 | - |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-2` |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Azure 默认部署名 | - |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | 兼容旧配置的 Azure 部署/模型别名 | `gpt-image-1.5` |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | 兼容旧配置的 Azure 部署/模型别名 | `gpt-image-2` |
|
||||
| `OPENROUTER_IMAGE_MODEL` | OpenRouter 模型 | `google/gemini-3.1-flash-image-preview` |
|
||||
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
|
||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `qwen-image-2.0-pro` |
|
||||
@@ -834,7 +834,7 @@ AI 驱动的生成后端。
|
||||
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
|
||||
| `ZAI_BASE_URL` | 自定义 Z.AI 端点 | `https://api.z.ai/api/paas/v4` |
|
||||
| `BIGMODEL_BASE_URL` | Z.AI 端点向后兼容别名 | - |
|
||||
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimax.io` |
|
||||
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimaxi.com` |
|
||||
| `REPLICATE_BASE_URL` | 自定义 Replicate 端点 | - |
|
||||
| `JIMENG_BASE_URL` | 自定义即梦端点 | `https://visual.volcengineapi.com` |
|
||||
| `JIMENG_REGION` | 即梦区域 | `cn-north-1` |
|
||||
@@ -1112,6 +1112,36 @@ AI 驱动的生成后端。
|
||||
- 为文化/专业术语添加译注
|
||||
- 输出目录保留所有中间文件
|
||||
|
||||
#### baoyu-wechat-summary
|
||||
|
||||
微信群聊精华提取。使用 [wx-cli](https://github.com/jackwener/wx-cli) 从群消息中提取话题、引言和统计数据,生成结构化简报。支持跨次运行的群聊历史和群友画像维护,可生成正常版和毒舌版。
|
||||
|
||||
```bash
|
||||
# 总结群最近消息
|
||||
/baoyu-wechat-summary 相亲相爱一家人 最近 1 天
|
||||
|
||||
# 周报
|
||||
/baoyu-wechat-summary AI 技术群 最近 7 天
|
||||
|
||||
# 增量模式(从上次摘要继续)
|
||||
/baoyu-wechat-summary 相亲相爱一家人
|
||||
|
||||
# 毒舌版
|
||||
/baoyu-wechat-summary 相亲相爱一家人 最近 3 天 毒舌版
|
||||
```
|
||||
|
||||
**前置要求**:
|
||||
- 安装 [wx-cli](https://github.com/jackwener/wx-cli)(`npm install -g @jackwener/wx-cli`)
|
||||
- macOS 上运行并登录 WeChat 4.x
|
||||
|
||||
**特性**:
|
||||
- 话题提取,带归属和引言
|
||||
- 发言排行榜和群友画像
|
||||
- 增量模式(从上次摘要断点继续)
|
||||
- 大批量消息自动按天分割
|
||||
- 正常版和毒舌版两种风格
|
||||
- 支持从历史摘要回溯初始化画像
|
||||
|
||||
## 环境配置
|
||||
|
||||
部分技能需要 API 密钥或自定义配置。环境变量可以在 `.env` 文件中设置:
|
||||
@@ -1132,14 +1162,14 @@ mkdir -p ~/.baoyu-skills
|
||||
cat > ~/.baoyu-skills/.env << 'EOF'
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
OPENAI_IMAGE_MODEL=gpt-image-1.5
|
||||
OPENAI_IMAGE_MODEL=gpt-image-2
|
||||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# OPENAI_IMAGE_USE_CHAT=false
|
||||
|
||||
# Azure OpenAI
|
||||
AZURE_OPENAI_API_KEY=xxx
|
||||
AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-image-1.5
|
||||
AZURE_OPENAI_DEPLOYMENT=gpt-image-2
|
||||
# AZURE_API_VERSION=2025-04-01-preview
|
||||
|
||||
# OpenRouter
|
||||
@@ -1167,7 +1197,7 @@ ZAI_IMAGE_MODEL=glm-image
|
||||
# MiniMax
|
||||
MINIMAX_API_KEY=xxx
|
||||
MINIMAX_IMAGE_MODEL=image-01
|
||||
# MINIMAX_BASE_URL=https://api.minimax.io
|
||||
# MINIMAX_BASE_URL=https://api.minimaxi.com
|
||||
|
||||
# Replicate
|
||||
REPLICATE_API_TOKEN=r8_xxx
|
||||
|
||||
@@ -4,13 +4,32 @@ Skills in this repo are loaded by multiple agent runtimes (Claude Code, Codex, H
|
||||
|
||||
## The Rule
|
||||
|
||||
When a skill needs to render an image:
|
||||
When a skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if the skill's `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
No explicit priority between runtime-native tools and repo skills — treat them equivalently and let the user decide when there's a choice. No persisted preference mechanism; the question is cheap, and the rule is stateless.
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends.
|
||||
|
||||
## The Preference Field
|
||||
|
||||
Each image-consuming skill's `EXTEND.md` carries a single `preferred_image_backend` field:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `auto` (default) | Apply the auto-select rule — runtime-native preferred, fall back to only installed backend, ask if multiple non-native. |
|
||||
| `ask` | Always confirm the backend on every run, even when a runtime-native tool exists. |
|
||||
| `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) | Pin this backend when available; fall back to `auto` if it isn't. |
|
||||
|
||||
The field is **absent-equals-auto**: older `EXTEND.md` files without this field behave exactly as if `preferred_image_backend: auto` were set. No schema version bump is needed to introduce it.
|
||||
|
||||
## Prompt File Requirement (hard)
|
||||
|
||||
@@ -20,6 +39,8 @@ Regardless of which backend is chosen, every skill that renders images MUST writ
|
||||
|
||||
Each `SKILL.md` that renders images includes **exactly one** `## Image Generation Tools` section (near the top, after `## User Input Tools` and before the main workflow) that **inlines** this rule. Skills are self-contained and cannot link to `docs/` — each skill folder must ship the rule inside its own `SKILL.md`. See [CLAUDE.md → Skill Self-Containment](../CLAUDE.md).
|
||||
|
||||
Each skill's `references/config/preferences-schema.md` (and its `EXTEND.md` template in `first-time-setup.md`) lists `preferred_image_backend` alongside other preference fields. First-time setup does NOT ask the user about the backend — `auto` is set silently. Users who want to pin a specific backend edit `EXTEND.md` later, and each skill's `## Changing Preferences` section documents the common one-line edits.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) in this document and in SKILL.md are **examples** — agents in other runtimes apply the rule above and substitute the local equivalent. Skill-specific parameters for these backends are illustrative; runtimes without those knobs can omit them.
|
||||
|
||||
## Backend Skills Are Exempt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-article-illustrator
|
||||
description: Analyzes article structure, identifies positions requiring visual aids, generates illustrations with Type × Style × Palette three-dimension approach. Use when user asks to "illustrate article", "add images", "generate images for article", or "为文章配图".
|
||||
version: 1.57.0
|
||||
version: 1.59.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-article-illustrator
|
||||
@@ -23,16 +23,50 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The backend receives the prompt file (or its content); the file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
|
||||
## Batch Generation Policy
|
||||
|
||||
After every prompt file for the run has been saved and verified, generate images in batches by default.
|
||||
|
||||
Priority order:
|
||||
|
||||
1. Use the chosen backend's native batch / multi-task interface if it exists. Each task must keep its own prompt file, output path, aspect ratio, and direct reference images.
|
||||
2. If no native batch interface exists but the runtime can issue parallel tool calls, dispatch up to `generation_batch_size` images at a time. Default: `4`. An explicit user request in the current message, such as `--batch-size 4` or "并行4张一起生成", overrides EXTEND.md.
|
||||
3. If neither native batch nor parallel tool calls are available, generate sequentially.
|
||||
|
||||
Rules:
|
||||
|
||||
- Never start the first batch until all prompt files for that batch exist on disk.
|
||||
- Retry failed items once without regenerating successful items.
|
||||
- Do not use subagents merely to parallelize image rendering. Use subagents only for separate prompt iteration or creative exploration.
|
||||
|
||||
## Confirmation Policy
|
||||
|
||||
Default behavior: **confirm before generation**.
|
||||
|
||||
- Treat explicit skill invocation, a file path, matched signals/presets, and `EXTEND.md` defaults as **recommendation inputs only**. None of them authorizes skipping confirmation.
|
||||
- Do **not** start Step 4 or later until the user completes Step 3.
|
||||
- Skip confirmation only when the current request explicitly says to do so, for example: "直接生成", "不用确认", "跳过确认", "按默认出图", or equivalent wording.
|
||||
- If confirmation is skipped explicitly, state the assumed type / density / style / palette / language / backend in the next user-facing update before generating.
|
||||
|
||||
## Reference Images
|
||||
|
||||
Users may supply reference images via `--ref <files...>` or by providing file paths / pasting images in conversation. Refs guide style, palette, composition, or subject for specific illustrations.
|
||||
@@ -111,6 +145,8 @@ Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--a
|
||||
|
||||
### Step 3: Confirm Settings ⚠️
|
||||
|
||||
**Hard gate**: this step is mandatory per the [Confirmation Policy](#confirmation-policy) — Steps 4+ cannot start until the user confirms here (or explicitly opts out with "直接生成" / equivalent wording in the current request).
|
||||
|
||||
**ONE AskUserQuestion, max 4 Qs. Q1-Q2 REQUIRED. Q3 required unless preset chosen.**
|
||||
|
||||
| Q | Options |
|
||||
@@ -147,7 +183,7 @@ Full template: [references/workflow.md](references/workflow.md#step-4-generate-o
|
||||
4. LABELS **MUST** include article-specific data: actual numbers, terms, metrics, quotes
|
||||
5. **DO NOT** pass ad-hoc inline prompts to `--prompt` without saving prompt files first
|
||||
6. Select the backend via the `## Image Generation Tools` rule at the top: use whatever is available; if multiple, ask the user once. Do this once per session before any generation.
|
||||
7. **Execution strategy**: When multiple illustrations have saved prompt files and the task is now plain generation, prefer the chosen backend's batch interface (if it offers one) over spawning subagents. Use subagents only when each image still needs separate prompt iteration or creative exploration. If the backend has no batch interface, generate sequentially.
|
||||
7. **Execution strategy**: Generate in batches per the `## Batch Generation Policy`: backend native batch first, runtime parallel tool calls second, sequential only as fallback. Default batch size is 4 unless EXTEND.md or the current request overrides it.
|
||||
8. Process references (`direct`/`style`/`palette`) per prompt frontmatter
|
||||
9. Apply watermark if EXTEND.md enabled
|
||||
10. Generate from saved prompt files; retry once on failure
|
||||
@@ -207,3 +243,18 @@ When input is **pasted content** (no file path), always uses `illustrations/{top
|
||||
| [references/style-presets.md](references/style-presets.md) | Preset shortcuts (type + style + palette) |
|
||||
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
|
||||
| [references/config/first-time-setup.md](references/config/first-time-setup.md) | First-time setup |
|
||||
|
||||
## Changing Preferences
|
||||
|
||||
EXTEND.md lives at the first matching path listed in Step 1.5. Three ways to change it:
|
||||
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: `references/config/preferences-schema.md`.
|
||||
- **Reconfigure interactively** — delete EXTEND.md (or ask "reconfigure baoyu-article-illustrator preferences" / "重新配置"). The next run re-triggers first-time setup.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `generation_batch_size: 4` — default number of images to render concurrently when the runtime supports parallel generation calls.
|
||||
- `preferred_type: infographic`, `preferred_style: notion`, `preferred_palette: macaron`, `language: zh`.
|
||||
- `default_output_dir: imgs-subdir` — where to write generated images relative to the article.
|
||||
|
||||
@@ -61,8 +61,10 @@ Position defaults to bottom-right.
|
||||
header: "Style"
|
||||
question: "Default illustration style preference? Or type another style name or your custom style"
|
||||
options:
|
||||
- label: "None (Recommended)"
|
||||
description: "Auto-select based on content analysis"
|
||||
- label: "sketch-notes (Recommended)"
|
||||
description: "Warm cream paper, black hand-drawn lines, soft pastel blocks — educational infographic feel. Great default for most articles."
|
||||
- label: "None"
|
||||
description: "Auto-select based on content analysis (falls back to sketch-notes when no strong signal)"
|
||||
- label: "notion"
|
||||
description: "Minimalist hand-drawn line art"
|
||||
- label: "warm"
|
||||
@@ -126,13 +128,16 @@ preferred_style:
|
||||
description: ""
|
||||
default_output_dir: imgs-subdir # same-dir | imgs-subdir | illustrations-subdir | independent
|
||||
language: null
|
||||
preferred_image_backend: auto
|
||||
generation_batch_size: 4
|
||||
custom_styles: []
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend: auto` is the baked-in default — first-time setup does not ask about it. The `## Image Generation Tools` rule in SKILL.md then picks the runtime-native tool (Codex `imagegen`, Hermes `image_generate`, etc.) when available, and falls back to installed backends.
|
||||
|
||||
`generation_batch_size: 4` is the baked-in default for batch rendering. The current user request may override it for one run.
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or run setup again:
|
||||
- Delete EXTEND.md to trigger setup
|
||||
- Edit YAML frontmatter for quick changes
|
||||
- Full schema: `config/preferences-schema.md`
|
||||
See the `## Changing Preferences` section in `SKILL.md` for the canonical list of common edits (pin backend, change defaults, retrigger setup). Full schema: `preferences-schema.md`.
|
||||
|
||||
@@ -26,6 +26,10 @@ language: null # zh|en|ja|ko|auto
|
||||
|
||||
default_output_dir: null # same-dir|illustrations-subdir|independent
|
||||
|
||||
preferred_image_backend: auto # auto|ask|<backend-id>
|
||||
|
||||
generation_batch_size: 4 # 1-8, used when backend/runtime supports batch or parallel generation
|
||||
|
||||
custom_styles:
|
||||
- name: my-style
|
||||
description: "Style description"
|
||||
@@ -52,6 +56,8 @@ custom_styles:
|
||||
| `preferred_palette` | string | null | Palette override (macaron, warm, neon, or null) |
|
||||
| `language` | string | null | Output language (null = auto-detect) |
|
||||
| `default_output_dir` | enum | null | Output directory preference (null = ask each time) |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. Resolution logic is documented in `SKILL.md`'s `## Image Generation Tools` section. |
|
||||
| `generation_batch_size` | int | 4 | Number of images to dispatch per batch when the backend has native batch support or the runtime can issue parallel generation calls. Clamp invalid values to 1-8. Current user request overrides this value. |
|
||||
| `custom_styles` | array | [] | User-defined styles |
|
||||
|
||||
## Position Options
|
||||
@@ -113,6 +119,10 @@ preferred_style:
|
||||
|
||||
language: zh
|
||||
|
||||
preferred_image_backend: codex-imagegen
|
||||
|
||||
generation_batch_size: 4
|
||||
|
||||
custom_styles:
|
||||
- name: corporate
|
||||
description: "Professional B2B style"
|
||||
|
||||
@@ -139,6 +139,39 @@ STYLE: [style characteristics]
|
||||
ASPECT: 16:9
|
||||
```
|
||||
|
||||
**Infographic + sketch-notes + macaron palette** (default / `hand-drawn-edu` preset):
|
||||
```
|
||||
Single-page hand-drawn educational infographic in a clean presentation style.
|
||||
Warm cream paper background, black hand-drawn lines with slight wobble, soft
|
||||
pastel color blocks. Feels simple, friendly, and easy to understand at a glance.
|
||||
Diagram-style visuals ONLY — no realistic or photographic images.
|
||||
|
||||
PALETTE: macaron — soft pastel blocks on warm cream
|
||||
COLORS: Warm Cream background (#F5F0E8); Black (#1A1A1A) for ALL lines, text,
|
||||
arrows, and doodles; section fills in Light Blue (#A8D8EA), Mint Green
|
||||
(#B5E5CF), Lavender (#D5C6E0), Peach (#FFD5C2); Coral Red (#E8655A)
|
||||
sparingly for one or two emphasis points only.
|
||||
|
||||
LAYOUT (top → bottom):
|
||||
- TOP: Bold hand-lettered title, oversized, slightly wobbly, with an optional
|
||||
decorative underline or small doodle.
|
||||
- MIDDLE: 2–6 rounded-rectangle info boxes arranged in a clean grid, row, or
|
||||
radial pattern. Each box = one section, one pastel fill color, one
|
||||
simple icon or sketchy cartoon element, one short keyword/phrase.
|
||||
Hand-drawn arrows connect related zones.
|
||||
- BOTTOM: One short hand-lettered takeaway sentence summarizing the main idea.
|
||||
|
||||
ELEMENTS: Rounded info boxes with clear sectioning, wavy/straight hand-drawn
|
||||
arrows with small inline labels, simple icons and sketchy cartoon
|
||||
elements (stick figures, tools, objects), small doodle decorations
|
||||
(stars, sparkles, underlines, dots, asterisks) used sparingly.
|
||||
|
||||
STYLE: Minimal, well-organized, airy. Color fills don't completely fill
|
||||
outlines (slight "hand-painted" overshoot). ALL text hand-lettered —
|
||||
no computer fonts. Short labels and keywords only, never long
|
||||
paragraphs. Generous white space between sections.
|
||||
```
|
||||
|
||||
**Infographic + vector-illustration**:
|
||||
```
|
||||
Flat vector illustration infographic. Clean black outlines on all elements.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
`--preset X` expands to a type + style + optional palette combination. Users can override any dimension.
|
||||
|
||||
## Default Preset
|
||||
|
||||
When content analysis surfaces no strong signal (generic knowledge article, mixed-topic post, no clear data/comparison/narrative cue), recommend **`hand-drawn-edu`** as the primary option in Step 3 Q1. It is the warm, friendly educational-infographic default — safe for most articles and universally readable.
|
||||
|
||||
## By Category
|
||||
|
||||
### Technical & Engineering
|
||||
@@ -23,7 +27,9 @@
|
||||
| `process-flow` | `flowchart` | `notion` | — | Workflow documentation, onboarding flows |
|
||||
| `warm-knowledge` | `infographic` | `vector-illustration` | `warm` | Product showcases, team intros, feature cards, brand content |
|
||||
| `edu-visual` | `infographic` | `vector-illustration` | `macaron` | Knowledge summaries, concept explainers, educational articles |
|
||||
| `hand-drawn-edu` | `flowchart` | `sketch-notes` | `macaron` | Hand-drawn educational diagrams, process explainers, onboarding visuals |
|
||||
| `hand-drawn-edu` | `infographic` | `sketch-notes` | `macaron` | **Default preset.** Hand-drawn educational infographic — warm cream paper, black lines, pastel blocks. Great for single-page explainers, concept summaries, onboarding, general knowledge articles |
|
||||
| `hand-drawn-edu-flow` | `flowchart` | `sketch-notes` | `macaron` | Hand-drawn process explainer — step-by-step workflow in the same warm educational style |
|
||||
| `hand-drawn-edu-compare` | `comparison` | `sketch-notes` | `macaron` | Hand-drawn side-by-side comparison in the warm educational style |
|
||||
| `ink-notes-compare` | `comparison` | `ink-notes` | `mono-ink` | Before/After essays, Traditional vs New, OS-style comparisons, mindset-shift narratives |
|
||||
| `ink-notes-flow` | `flowchart` | `ink-notes` | `mono-ink` | Professional process explainers, workforce pipelines, hand-drawn technical walkthroughs |
|
||||
| `ink-notes-framework` | `framework` | `ink-notes` | `mono-ink` | System analogies, command-center diagrams, architecture-as-metaphor, tech manifestos |
|
||||
@@ -59,18 +65,19 @@ Use this table during Step 3 to recommend presets based on Step 2 content analys
|
||||
|
||||
| Content Type (Step 2) | Primary Preset | Alternatives |
|
||||
|------------------------|----------------|--------------|
|
||||
| Technical | `tech-explainer` | `system-design`, `architecture` |
|
||||
| Tutorial | `tutorial` | `process-flow`, `knowledge-base`, `edu-visual` |
|
||||
| **General / No strong signal** | `hand-drawn-edu` | `edu-visual`, `knowledge-base` |
|
||||
| Education / Knowledge | `hand-drawn-edu` | `edu-visual`, `knowledge-base`, `tutorial` |
|
||||
| Tutorial | `hand-drawn-edu-flow` | `tutorial`, `process-flow`, `hand-drawn-edu` |
|
||||
| SaaS / Product | `hand-drawn-edu` | `saas-guide`, `knowledge-base`, `process-flow`, `warm-knowledge` |
|
||||
| Technical | `tech-explainer` | `system-design`, `architecture`, `hand-drawn-edu` |
|
||||
| Methodology / Framework | `system-design` | `architecture`, `process-flow` |
|
||||
| Data / Metrics | `data-report` | `versus`, `tech-explainer` |
|
||||
| Comparison / Review | `versus` | `business-compare`, `editorial-poster`, `ink-notes-compare` |
|
||||
| Comparison / Review | `versus` | `business-compare`, `hand-drawn-edu-compare`, `editorial-poster`, `ink-notes-compare` |
|
||||
| Manifesto / Mindset shift / Professional visual note | `ink-notes-compare` | `ink-notes-framework`, `ink-notes-flow` |
|
||||
| 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`, `warm-knowledge` |
|
||||
| Education / Knowledge | `edu-visual` | `knowledge-base`, `tutorial`, `hand-drawn-edu` |
|
||||
|
||||
## Override Examples
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@ Simplified style tier for quick selection:
|
||||
|
||||
| Core Style | Maps To | Best For |
|
||||
|------------|---------|----------|
|
||||
| `hand-drawn` | sketch-notes | **Default.** Warm cream paper, black hand-drawn lines, pastel blocks — educational infographics, concept explainers, onboarding, general knowledge articles |
|
||||
| `vector` | vector-illustration | Knowledge articles, tutorials, tech content |
|
||||
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
|
||||
| `sci-fi` | blueprint | AI, frontier tech, system design |
|
||||
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual content |
|
||||
| `editorial` | editorial | Processes, data, journalism |
|
||||
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
|
||||
| `poster` | screen-print | Opinion, editorial, cultural, cinematic |
|
||||
|
||||
Use Core Styles for most cases. See full Style Gallery below for granular control.
|
||||
Use Core Styles for most cases. **When no strong content signal is detected, default to `hand-drawn` (→ sketch-notes).** See full Style Gallery below for granular control.
|
||||
|
||||
---
|
||||
|
||||
@@ -50,42 +50,45 @@ Full specifications: `references/styles/<style>.md`
|
||||
|
||||
## Type × Style Compatibility Matrix
|
||||
|
||||
| | vector-illustration | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific | screen-print |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| infographic | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| scene | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ | ✓✓ |
|
||||
| flowchart | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✗ |
|
||||
| comparison | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
| framework | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ | ✓ |
|
||||
| timeline | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
| | sketch-notes | vector-illustration | notion | warm | minimal | blueprint | watercolor | elegant | editorial | scientific | screen-print |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| infographic | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ |
|
||||
| scene | ✗ | ✓ | ✓ | ✓✓ | ✓ | ✗ | ✓✓ | ✓ | ✓ | ✗ | ✓✓ |
|
||||
| flowchart | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✗ | ✓ | ✓✓ | ✓ | ✗ |
|
||||
| comparison | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
| framework | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✗ | ✓✓ | ✓ | ✓✓ | ✓ |
|
||||
| timeline | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ |
|
||||
|
||||
✓✓ = highly recommended | ✓ = compatible | ✗ = not recommended
|
||||
|
||||
## Auto Selection by Type
|
||||
|
||||
When no content signal matches strongly, `sketch-notes` is the default primary for every diagrammatic type. Only override with another primary when the content analysis in Step 2 surfaces a clear signal (technical/data/narrative/opinion).
|
||||
|
||||
| Type | Primary Style | Secondary Styles |
|
||||
|------|---------------|------------------|
|
||||
| infographic | vector-illustration | notion, blueprint, editorial |
|
||||
| infographic | sketch-notes | vector-illustration, notion, blueprint, editorial |
|
||||
| scene | warm | watercolor, elegant |
|
||||
| flowchart | vector-illustration | notion, blueprint |
|
||||
| comparison | vector-illustration | notion, elegant |
|
||||
| framework | blueprint | vector-illustration, notion |
|
||||
| timeline | elegant | warm, editorial |
|
||||
| flowchart | sketch-notes | vector-illustration, notion, blueprint |
|
||||
| comparison | sketch-notes | vector-illustration, notion, elegant |
|
||||
| framework | sketch-notes | blueprint, vector-illustration, notion |
|
||||
| timeline | elegant | sketch-notes, warm, editorial |
|
||||
|
||||
## Auto Selection by Content Signals
|
||||
|
||||
| Content Signals | Recommended Type | Recommended Style |
|
||||
|-----------------|------------------|-------------------|
|
||||
| **(no strong signal / general article)** | **infographic** | **sketch-notes** |
|
||||
| Knowledge, concept, tutorial, learning, guide, onboarding | infographic | sketch-notes, vector-illustration, notion |
|
||||
| Productivity, SaaS, tool, app, software | infographic | sketch-notes, notion, vector-illustration |
|
||||
| How-to, steps, workflow, process, tutorial | flowchart | sketch-notes, vector-illustration, notion |
|
||||
| API, metrics, data, comparison, numbers | infographic | blueprint, vector-illustration |
|
||||
| Knowledge, concept, tutorial, learning, guide | infographic | vector-illustration, notion |
|
||||
| Tech, AI, programming, development, code | infographic | vector-illustration, blueprint |
|
||||
| How-to, steps, workflow, process, tutorial | flowchart | vector-illustration, notion |
|
||||
| Framework, model, architecture, principles | framework | blueprint, vector-illustration |
|
||||
| vs, pros/cons, before/after, alternatives | comparison | vector-illustration, notion |
|
||||
| Tech, AI, programming, development, code | infographic | vector-illustration, blueprint, sketch-notes |
|
||||
| Framework, model, architecture, principles | framework | blueprint, vector-illustration, sketch-notes |
|
||||
| vs, pros/cons, before/after, alternatives | comparison | vector-illustration, notion, sketch-notes |
|
||||
| Manifesto, mindset shift, workforce, OS, whiteboard, professional visual note | comparison / framework | ink-notes |
|
||||
| Story, emotion, journey, experience, personal | scene | warm, watercolor |
|
||||
| History, timeline, progress, evolution | timeline | elegant, warm |
|
||||
| Productivity, SaaS, tool, app, software | infographic | notion, vector-illustration |
|
||||
| Business, professional, strategy, corporate | framework | elegant |
|
||||
| Opinion, editorial, culture, philosophy, cinematic, dramatic, poster | scene | screen-print |
|
||||
| Biology, chemistry, medical, scientific | infographic | scientific |
|
||||
@@ -93,6 +96,15 @@ Full specifications: `references/styles/<style>.md`
|
||||
|
||||
## Style Characteristics by Type
|
||||
|
||||
### infographic + sketch-notes (default)
|
||||
- Warm cream paper background, black hand-drawn lines with slight wobble
|
||||
- 2–6 rounded pastel info boxes (light blue / mint / lavender / peach)
|
||||
- Bold hand-lettered title at the top
|
||||
- Short keyword labels, simple icons, small doodles (stars, underlines, sparkles)
|
||||
- One-line hand-lettered takeaway sentence at the bottom
|
||||
- Airy, minimal, diagram-style — never realistic
|
||||
- Perfect for single-page educational explainers and concept summaries
|
||||
|
||||
### infographic + vector-illustration
|
||||
- Clean flat vector shapes, bold geometric forms
|
||||
- Vibrant but harmonious color palette
|
||||
|
||||
@@ -1,56 +1,91 @@
|
||||
# sketch-notes
|
||||
|
||||
Soft hand-drawn illustration style with warm, educational feel
|
||||
Hand-drawn educational infographic style with warm cream paper, black hand-drawn lines, and soft pastel section blocks. Optimized for single-page visual explainers.
|
||||
|
||||
## Design Aesthetic
|
||||
|
||||
Hand-drawn feel with soft, relaxed brush strokes. Fresh, refined style with minimalist editorial approach. Emphasis on precision, clarity and intelligent elegance while prioritizing warmth, approachability and friendliness.
|
||||
Hand-drawn educational infographic in a clean presentation style. Feels like a visual explainer slide: simple, friendly, and easy to understand at a glance. Bold handwritten-style title at the top, clearly sectioned content in the middle with rounded boxes and small doodles, and one short takeaway sentence at the bottom. Neat, airy, and visually similar to a hand-drawn concept diagram — never realistic or photographic.
|
||||
|
||||
## Background
|
||||
|
||||
- Color: Warm Off-White (#FAF8F0)
|
||||
- Texture: Subtle paper grain, warm tone
|
||||
- Color: Warm Cream Paper (#F5F0E8) — preferred; fallback Warm Off-White (#FAF8F0)
|
||||
- Texture: Subtle warm paper grain, matte finish, no gloss
|
||||
|
||||
## Color Palette
|
||||
|
||||
Default sketch-notes palette is the **macaron** pastel set. Lines are always black; pastel blocks are used only as rounded card fills for information sections.
|
||||
|
||||
| Role | Color | Hex | Usage |
|
||||
|------|-------|-----|-------|
|
||||
| Background | Warm Off-White | #FAF8F0 | Primary background |
|
||||
| Primary Text | Deep Charcoal | #2C3E50 | Main elements |
|
||||
| Alt Text | Deep Brown | #4A4A4A | Secondary elements |
|
||||
| Accent 1 | Soft Orange | #F4A261 | Highlights, emphasis |
|
||||
| Accent 2 | Mustard Yellow | #E9C46A | Secondary highlights |
|
||||
| Accent 3 | Sage Green | #87A96B | Nature, growth concepts |
|
||||
| Accent 4 | Light Blue | #7EC8E3 | Tech, digital elements |
|
||||
| Accent 5 | Red Brown | #A0522D | Earthy elements |
|
||||
| Background | Warm Cream | #F5F0E8 | Paper background |
|
||||
| Primary Ink | Black | #1A1A1A | ALL outlines, text, arrows, doodles |
|
||||
| Block Blue | Light Blue | #A8D8EA | Info block fill (cool / tech) |
|
||||
| Block Mint | Mint Green | #B5E5CF | Info block fill (growth / positive) |
|
||||
| Block Lavender | Lavender | #D5C6E0 | Info block fill (concept / abstract) |
|
||||
| Block Peach | Peach | #FFD5C2 | Info block fill (warm / human) |
|
||||
| Accent | Coral Red | #E8655A | One or two emphasis points only |
|
||||
| Muted Text | Warm Gray | #6B6B6B | Small annotations |
|
||||
|
||||
Use **4 pastel block colors max** per image, one color per section. Black ink does all the structural line work.
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- Connection lines with hand-drawn wavy feel
|
||||
- Conceptual abstract icons illustrating ideas
|
||||
- Color fills don't completely fill outlines (hand-painted feel)
|
||||
- Simple geometric shapes with rounded corners
|
||||
- Arrows and pointers with sketchy style
|
||||
- Doodle decorations: stars, spirals, underlines
|
||||
- Bold hand-lettered title at the top (oversized, slightly wobbly)
|
||||
- Rounded-rectangle info boxes with clear sectioning (2–6 zones)
|
||||
- Short keyword labels inside boxes — never long paragraphs
|
||||
- Simple icons and sketchy cartoon elements (stick figures, tools, objects) to explain each idea
|
||||
- Hand-drawn arrows (straight, curved, or wavy) connecting related zones
|
||||
- Small doodle decorations: stars, sparkles, underlines, dots, asterisks — used sparingly for emphasis
|
||||
- Single-line hand-lettered takeaway sentence at the bottom
|
||||
- Color fills do not completely fill outlines (slight "hand-painted" overshoot/undershoot)
|
||||
- Generous white space between sections — airy, never crowded
|
||||
|
||||
## Layout Guidelines
|
||||
|
||||
Canonical single-page layout (16:9 or 4:3):
|
||||
|
||||
1. **Top (10–15%)** — Bold hand-lettered title, optionally with a small decorative underline or doodle.
|
||||
2. **Middle (70–80%)** — 2–6 rounded pastel info boxes arranged in a clear grid, row, or radial pattern. Each box = one section, one color, one icon, one keyword/phrase.
|
||||
3. **Bottom (10–15%)** — One short hand-lettered takeaway sentence summarizing the core insight.
|
||||
|
||||
Keep margins generous. Aim for breathing room around every element.
|
||||
|
||||
## Style Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Keep layouts open and well-structured
|
||||
- Emphasize information hierarchy
|
||||
- Use hand-drawn quality for all elements
|
||||
- Allow imperfection (slight wobbles add character)
|
||||
- Layer elements with subtle overlaps
|
||||
- Use warm cream paper background (no pure white)
|
||||
- Use black hand-drawn lines for ALL structural elements
|
||||
- Use soft pastel blocks (blue / mint / lavender / peach) for section fills
|
||||
- Keep text to short keywords and phrases only
|
||||
- Include a bold handwritten title at the top
|
||||
- Include a short takeaway sentence at the bottom
|
||||
- Use diagram-style visuals (icons, doodles, simple shapes)
|
||||
- Allow slight wobble — hand-drawn imperfection is the point
|
||||
- Maintain clear sectioning with rounded boxes
|
||||
|
||||
### Don't
|
||||
|
||||
- Use perfect geometric shapes
|
||||
- Create photorealistic elements
|
||||
- Overcrowd with too many elements
|
||||
- Use pure white backgrounds
|
||||
- Make it look computer-generated
|
||||
- Use pure white backgrounds (that's `ink-notes`' territory)
|
||||
- Render realistic or photographic images — this style is diagram-only
|
||||
- Fill zones with gradients, shadows, or digital effects
|
||||
- Use long paragraphs of text — keywords only
|
||||
- Use computer-generated / sans-serif body fonts — ALL text must be hand-lettered
|
||||
- Use more than 4 pastel block colors per image
|
||||
- Overcrowd the canvas — keep it airy and minimal
|
||||
- Use perfect geometric shapes — preserve the hand-drawn wobble
|
||||
|
||||
## Type Compatibility
|
||||
|
||||
| Type | Rating | Notes |
|
||||
|------|--------|-------|
|
||||
| infographic | ✓✓ | **Best fit** — single-page visual explainers, concept summaries, educational slides |
|
||||
| framework | ✓✓ | Labeled zones and connectors render well |
|
||||
| flowchart | ✓✓ | Rounded step boxes with wavy arrows |
|
||||
| comparison | ✓✓ | Two pastel blocks side by side; prefer `ink-notes` for strict Before/After contrasts |
|
||||
| timeline | ✓ | Hand-drawn horizontal arrow with milestone cards |
|
||||
| scene | ✗ | Not recommended — too diagrammatic |
|
||||
|
||||
## Best For
|
||||
|
||||
Educational content, knowledge sharing, technical explanations, tutorials, onboarding materials, friendly articles
|
||||
Educational content, knowledge sharing, concept explainers, tutorials, onboarding materials, product walkthroughs, single-page visual summaries, "how things work" posts, friendly technical articles
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
# Specify density
|
||||
/baoyu-article-illustrator path/to/article.md --density rich
|
||||
|
||||
# Generate up to 4 images in parallel after prompts are saved
|
||||
/baoyu-article-illustrator path/to/article.md --batch-size 4
|
||||
|
||||
# Direct content input (paste mode)
|
||||
/baoyu-article-illustrator
|
||||
[paste content]
|
||||
@@ -31,6 +34,7 @@
|
||||
| `--style <name>` | Visual style (see references/styles.md) |
|
||||
| `--preset <name>` | Shorthand for type + style combo (see [references/style-presets.md](references/style-presets.md)) |
|
||||
| `--density <level>` | Image count: minimal / balanced / rich |
|
||||
| `--batch-size <n>` | Temporary generation batch size for this run. Default: `generation_batch_size` from EXTEND.md, otherwise 4. Clamp to 1-8. |
|
||||
|
||||
## Input Modes
|
||||
|
||||
|
||||
@@ -176,6 +176,8 @@ Based on Step 2 content analysis, recommend a preset first (sets both type & sty
|
||||
- [Alternative preset] — [brief]
|
||||
- Or choose type manually: infographic / scene / flowchart / comparison / framework / timeline / mixed
|
||||
|
||||
**Default**: if Step 2 found no strong content signal, the recommended preset MUST be `hand-drawn-edu` (infographic + sketch-notes + macaron — warm cream paper, black hand-drawn lines, soft pastel blocks). This is the universal fallback.
|
||||
|
||||
**If user picks a preset → skip Q3** (type & style both resolved).
|
||||
**If user picks a type → Q3 is REQUIRED.**
|
||||
|
||||
@@ -203,13 +205,15 @@ If no `preferred_style` (present Core Styles first):
|
||||
|
||||
| Core Style | Maps To | Best For |
|
||||
|------------|---------|----------|
|
||||
| `hand-drawn` | sketch-notes | **Default.** Warm cream paper, black hand-drawn lines, pastel blocks — educational infographics, concept explainers, onboarding, general knowledge articles |
|
||||
| `minimal-flat` | notion | General, knowledge sharing, SaaS |
|
||||
| `sci-fi` | blueprint | AI, frontier tech, system design |
|
||||
| `hand-drawn` | sketch/warm | Relaxed, reflective, casual |
|
||||
| `editorial` | editorial | Processes, data, journalism |
|
||||
| `scene` | warm/watercolor | Narratives, emotional, lifestyle |
|
||||
| `poster` | screen-print | Opinion, editorial, cultural, cinematic |
|
||||
|
||||
**Default recommendation**: when Step 2 surfaces no strong content signal, recommend **`hand-drawn-edu`** preset (→ infographic + sketch-notes + macaron) as the primary option in Q1. When the user picks a type manually without a preferred_style, recommend `sketch-notes` first in Q3.
|
||||
|
||||
Style selection based on Type × Style compatibility matrix (styles.md).
|
||||
**In Step 5.1**, read `styles/<style>.md` for visual elements and rendering rules.
|
||||
|
||||
@@ -331,8 +335,11 @@ 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`.
|
||||
|
||||
**Execution choice**:
|
||||
- If multiple illustrations already have saved prompt files and the task is now plain generation, prefer the chosen backend's batch interface (if it offers one); otherwise generate sequentially
|
||||
- Use subagents only when each illustration still needs separate prompt rewriting, style exploration, or other per-image reasoning before generation
|
||||
- If multiple illustrations already have saved prompt files and the task is now plain generation, use batch generation by default.
|
||||
- Prefer the chosen backend's native batch / multi-task interface when available.
|
||||
- If the backend has no native batch interface but the runtime can issue parallel tool calls, dispatch up to `generation_batch_size` tasks at a time. Default: `4`. The current user request overrides EXTEND.md.
|
||||
- Generate sequentially only when neither backend batch nor runtime parallel calls are available.
|
||||
- Use subagents only when each illustration still needs separate prompt rewriting, style exploration, or other per-image reasoning before generation. Do not use subagents just to parallelize rendering.
|
||||
|
||||
**CRITICAL - References in Frontmatter**:
|
||||
- Only add `references` field if files ACTUALLY EXIST in `references/` directory
|
||||
@@ -341,7 +348,14 @@ Prompt Files:
|
||||
|
||||
### 5.2 Select Generation Skill
|
||||
|
||||
Check available skills. If multiple, ask user.
|
||||
Follow the `## Image Generation Tools` rule at the top of `SKILL.md`. Concretely:
|
||||
|
||||
- If `imagegen` is in your available-skills list (Codex), use it — invoke via the `Skill` tool with `skill: "imagegen"`.
|
||||
- Else if the EXTEND.md pin is available, use it.
|
||||
- Else if exactly one non-native backend is installed, use it.
|
||||
- Else, ask the user.
|
||||
|
||||
**Do not generate SVG, HTML, or any code-based vector as a substitute for the raster image.** If no raster backend can be resolved, ask the user how to proceed.
|
||||
|
||||
### 5.3 Process References ⚠️ REQUIRED if references saved in Step 1.0
|
||||
|
||||
@@ -383,12 +397,18 @@ Add: `Include a subtle watermark "[content]" at [position].`
|
||||
|
||||
### 5.5 Generate
|
||||
|
||||
1. For each illustration:
|
||||
- **Backup rule**: If image file exists, rename to `NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||
- If references with `direct` usage: include `--ref` parameter
|
||||
- Generate image
|
||||
2. After each: "Generated X/N"
|
||||
3. On failure: retry once, then log and continue
|
||||
1. Build a generation task list from saved prompt files:
|
||||
- `prompt_file`: `{output-dir}/prompts/NN-{type}-{slug}.md`
|
||||
- `output_file`: `{output-dir}/NN-{type}-{slug}.png`
|
||||
- `aspect_ratio`: from prompt frontmatter or prompt body
|
||||
- `refs`: only verified `direct` references from prompt frontmatter
|
||||
2. **Backup rule**: Before dispatching a task, if its output image already exists, rename it to `NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.{ext}`.
|
||||
3. Dispatch tasks in batches:
|
||||
- Native batch backend: send all eligible tasks, or chunks of `generation_batch_size` if the backend has a practical limit.
|
||||
- Runtime parallel calls: issue up to `generation_batch_size` image calls concurrently, then continue with the next chunk.
|
||||
- Sequential fallback: process one task at a time.
|
||||
4. After each completed task, record: "Generated X/N: filename".
|
||||
5. On failure: retry the failed task once from the same saved prompt file. Keep successful outputs and continue.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
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".
|
||||
version: 1.56.1
|
||||
description: Knowledge comic creator supporting multiple art styles and tones. Creates original educational comics with detailed panel layouts and batch-capable image generation. Use when user asks to create "知识漫画", "教育漫画", "biography comic", "tutorial comic", or "Logicomix-style comic".
|
||||
version: 1.57.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-comic
|
||||
@@ -27,16 +27,42 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The backend receives the prompt file (or its content); the file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
|
||||
## Batch Generation Policy
|
||||
|
||||
After every prompt file for the current generation group has been saved and verified, generate images in batches by default.
|
||||
|
||||
Priority order:
|
||||
|
||||
1. Use the chosen backend's native batch / multi-task interface if it exists. Each task must keep its own prompt file, output path, aspect ratio, session ID, and direct reference images.
|
||||
2. If no native batch interface exists but the runtime can issue parallel tool calls, dispatch up to `generation_batch_size` images at a time. Default: `4`. An explicit user request in the current message, such as `--batch-size 4` or "并行4张一起生成", overrides EXTEND.md.
|
||||
3. If neither native batch nor parallel tool calls are available, generate sequentially.
|
||||
|
||||
Rules:
|
||||
|
||||
- Honor workflow dependencies first: generate `characters/characters.png` before pages that use it as a reference.
|
||||
- Never start the first page batch until all selected page prompt files exist on disk.
|
||||
- Retry failed items once without regenerating successful items.
|
||||
- Do not use subagents merely to parallelize image rendering. Use subagents only for separate prompt iteration or creative exploration.
|
||||
|
||||
## Reference Images
|
||||
|
||||
Users may supply reference images to guide art style, palette, scene composition, or subject. This is **separate from** the auto-generated character sheet (Step 7.1) — both can coexist: user refs guide the look, the character sheet anchors recurring character identity.
|
||||
@@ -81,6 +107,7 @@ references:
|
||||
| `--aspect` | 3:4 (default, portrait), 4:3 (landscape), 16:9 (widescreen) | Page aspect ratio |
|
||||
| `--lang` | auto (default), zh, en, ja, etc. | Output language |
|
||||
| `--ref <files...>` | File paths | Reference images applied to every page for style / palette / scene guidance. See [Reference Images](#reference-images) above. |
|
||||
| `--batch-size <n>` | 1-8 | Temporary page generation batch size for this run. Default: `generation_batch_size` from EXTEND.md, otherwise 4. |
|
||||
|
||||
### Partial Workflow Options
|
||||
|
||||
@@ -228,6 +255,8 @@ Analyze → [Check Existing?] → [Confirm: Style + Reviews] → Storyboard →
|
||||
| Exists | Not supported | Prepend character descriptions to every prompt file |
|
||||
| Skipped | — | All descriptions inline in prompt |
|
||||
|
||||
**Execution strategy**: Generate the character sheet first when needed. Then build the selected page task list from saved prompt files and dispatch pages in batches per the `## Batch Generation Policy`: backend native batch first, runtime parallel tool calls second, sequential only as fallback. `--regenerate N` and `--images-only` apply the same batching rules to the selected existing prompts.
|
||||
|
||||
**Backup rule**: existing `prompts/…md` and `…png` files → rename with `-backup-YYYYMMDD-HHMMSS` suffix before regenerating. Aspect ratio from storyboard (default `3:4`; preset may override).
|
||||
|
||||
**`--ref` failure recovery**: compress sheet → retry → still fails → drop `--ref` and embed character descriptions in the prompt text.
|
||||
@@ -248,7 +277,7 @@ If EXTEND.md is not found, first-time setup is **blocking** — complete it befo
|
||||
| Found | Read, parse, display summary → continue |
|
||||
| Not found | ⛔ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → save EXTEND.md → continue |
|
||||
|
||||
**EXTEND.md supports**: watermark, preferred art/tone/layout, custom style definitions, character presets, language preference. Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md).
|
||||
**EXTEND.md supports**: watermark, preferred art/tone/layout, custom style definitions, character presets, language preference, preferred image backend, generation batch size. Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md).
|
||||
|
||||
## References
|
||||
|
||||
@@ -295,3 +324,17 @@ If EXTEND.md is not found, first-time setup is **blocking** — complete it befo
|
||||
- **Step 7.1 character sheet** - recommended for multi-page comics, optional for simple presets
|
||||
- **Step 7.2 character reference** - use `--ref` if sheet exists; compress/convert on failure; fall back to prompt-only
|
||||
- Watermark/language configured once in EXTEND.md
|
||||
|
||||
## Changing Preferences
|
||||
|
||||
EXTEND.md lives at `.baoyu-skills/baoyu-comic/EXTEND.md` (project) or `~/.baoyu-skills/baoyu-comic/EXTEND.md` (user). Three ways to change it:
|
||||
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: `references/config/preferences-schema.md`.
|
||||
- **Reconfigure interactively** — delete EXTEND.md (or ask "reconfigure baoyu-comic preferences" / "重新配置"). The next run re-triggers first-time setup.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `generation_batch_size: 4` — default number of page images to render concurrently when the backend/runtime supports batch or parallel generation.
|
||||
- `watermark.enabled: true`, `preferred_art`, `preferred_tone`, `preferred_layout`, `language` — shift the auto-selection defaults and cosmetic choices.
|
||||
|
||||
@@ -142,13 +142,16 @@ preferred_tone: [selected tone or null]
|
||||
preferred_layout: null
|
||||
preferred_aspect: null
|
||||
language: [selected or null]
|
||||
preferred_image_backend: auto
|
||||
generation_batch_size: 4
|
||||
character_presets: []
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend: auto` is the baked-in default — first-time setup does not ask about it. The `## Image Generation Tools` rule in SKILL.md then picks the runtime-native tool (Codex `imagegen`, Hermes `image_generate`, etc.) when available, and falls back to installed backends.
|
||||
|
||||
`generation_batch_size: 4` is the baked-in default for page batch rendering. The current user request may override it for one run.
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or run setup again:
|
||||
- Delete EXTEND.md to trigger setup
|
||||
- Edit YAML frontmatter for quick changes
|
||||
- Full schema: `config/preferences-schema.md`
|
||||
See the `## Changing Preferences` section in `SKILL.md` for the canonical list of common edits (pin backend, change defaults, retrigger setup). Full schema: `config/preferences-schema.md`.
|
||||
|
||||
@@ -23,6 +23,10 @@ preferred_aspect: null # 3:4|4:3|16:9
|
||||
|
||||
language: null # zh|en|ja|ko|auto
|
||||
|
||||
preferred_image_backend: auto # auto|ask|<backend-id>
|
||||
|
||||
generation_batch_size: 4 # 1-8, used when backend/runtime supports batch or parallel page generation
|
||||
|
||||
character_presets:
|
||||
- name: my-characters
|
||||
roles:
|
||||
@@ -46,6 +50,8 @@ character_presets:
|
||||
| `preferred_layout` | string | null | Layout preference or null |
|
||||
| `preferred_aspect` | string | null | Aspect ratio (3:4, 4:3, 16:9) |
|
||||
| `language` | string | null | Output language (null = auto-detect) |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. Resolution logic is documented in `SKILL.md`'s `## Image Generation Tools` section. |
|
||||
| `generation_batch_size` | int | 4 | Number of page images to dispatch per batch when the backend has native batch support or the runtime can issue parallel generation calls. Clamp invalid values to 1-8. Current user request overrides this value. |
|
||||
| `character_presets` | array | [] | Preset character roles for styles like ohmsha |
|
||||
|
||||
## Art Style Options
|
||||
@@ -122,6 +128,10 @@ preferred_aspect: "3:4"
|
||||
|
||||
language: zh
|
||||
|
||||
preferred_image_backend: codex-imagegen
|
||||
|
||||
generation_batch_size: 4
|
||||
|
||||
character_presets:
|
||||
- name: tech-tutorial
|
||||
roles:
|
||||
|
||||
@@ -488,12 +488,19 @@ When character sheet was skipped or `--ref` failed:
|
||||
- No `--ref` parameter needed
|
||||
- Rely on detailed text descriptions for character consistency
|
||||
|
||||
**For each page (cover + pages)**:
|
||||
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
|
||||
2. **Backup rule**: If image file exists, rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||
3. Generate image using Strategy A, B, or C
|
||||
4. Save to `NN-{cover|page}-[slug].png`
|
||||
5. Report progress after each generation: "Generated X/N: [page title]"
|
||||
**Page batch generation (cover + pages)**:
|
||||
1. Build a page task list from selected saved prompts:
|
||||
- `prompt_file`: `prompts/NN-{cover|page}-[slug].md`
|
||||
- `output_file`: `NN-{cover|page}-[slug].png`
|
||||
- `aspect_ratio`: from storyboard (default `3:4`; preset may override)
|
||||
- `refs`: character sheet and verified direct user refs when Strategy A is active
|
||||
2. **Backup rule**: Before dispatching a task, if its image file exists, rename it to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`.
|
||||
3. Dispatch tasks in batches:
|
||||
- Native batch backend: send all eligible page tasks, or chunks of `generation_batch_size` if the backend has a practical limit.
|
||||
- Runtime parallel calls: issue up to `generation_batch_size` image calls concurrently, then continue with the next chunk.
|
||||
- Sequential fallback: process one page at a time.
|
||||
4. After each completed task, report: "Generated X/N: [page title]".
|
||||
5. On failure, retry the failed task once from the same saved prompt file. Keep successful outputs and continue.
|
||||
|
||||
**Session Management**:
|
||||
If image generation skill supports `--sessionId`:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-cover-image
|
||||
description: Generates article cover images with 5 dimensions (type, palette, rendering, text, mood) combining 11 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
|
||||
version: 1.56.2
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-cover-image
|
||||
@@ -23,16 +23,34 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The backend receives the prompt file (or its content); the file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
|
||||
## Confirmation Policy
|
||||
|
||||
Default behavior: **confirm before generation**.
|
||||
|
||||
- Treat explicit skill invocation, a file path, matched keywords/presets, `EXTEND.md` defaults, and any documented auto-selection as **recommendation inputs only**. None of them authorizes skipping confirmation.
|
||||
- Do **not** start Step 3 or Step 4 until the user confirms the dimensions / aspect / language / backend choices.
|
||||
- Skip confirmation only when the current request explicitly says to do so, for example: `--quick`, "直接生成", "不用确认", "跳过确认", "按默认出图", or equivalent wording. `quick_mode: true` in `EXTEND.md` counts as a standing explicit opt-out — set it only when you want every run to skip Step 2.
|
||||
- If confirmation is skipped explicitly, state the assumed dimensions / aspect / language / backend in the next user-facing update before generating.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
@@ -164,6 +182,8 @@ See [reference-images.md](references/workflow/reference-images.md) for full deci
|
||||
|
||||
### Step 2: Confirm Options ⚠️
|
||||
|
||||
**Hard gate**: this step is mandatory per the [Confirmation Policy](#confirmation-policy) — Steps 3–4 cannot start until the user confirms here (or explicitly opts out with `--quick` / `quick_mode: true` / equivalent wording in the current request).
|
||||
|
||||
**MUST use `AskUserQuestion` tool** to present options as interactive selection — NOT plain text tables. Present up to 4 questions in a single `AskUserQuestion` call (Type, Palette, Rendering, Font + Settings). Each question shows the recommended option first with reason, followed by alternatives.
|
||||
|
||||
Full confirmation flow and question format: [references/workflow/confirm-options.md](references/workflow/confirm-options.md)
|
||||
@@ -228,13 +248,18 @@ Files:
|
||||
- **Characters**: Simplified silhouettes; NO realistic humans
|
||||
- **Title**: Use exact title from user/source; never invent
|
||||
|
||||
## Extension Support
|
||||
## Changing Preferences
|
||||
|
||||
Custom configurations via EXTEND.md. See **Step 0** for paths.
|
||||
EXTEND.md lives at the path noted in **Step 0**. Three ways to change it:
|
||||
|
||||
Supports: Watermark | Preferred dimensions | Default aspect/output | Quick mode | Custom palettes | Language
|
||||
|
||||
Schema: [references/config/preferences-schema.md](references/config/preferences-schema.md)
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: [references/config/preferences-schema.md](references/config/preferences-schema.md).
|
||||
- **Reconfigure interactively** — delete EXTEND.md (or ask "reconfigure baoyu-cover-image preferences" / "重新配置"). The next run re-triggers first-time setup.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `watermark.enabled: true`, `preferred_type`, `preferred_palette`, `preferred_rendering`, `default_aspect`, `quick_mode: true`, `language` — shift the auto-selection defaults and confirmation flow.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -188,15 +188,15 @@ default_aspect: [16:9/2.35:1/1:1/3:4]
|
||||
default_output_dir: [independent/same-dir/imgs-subdir]
|
||||
quick_mode: [true/false]
|
||||
language: null
|
||||
preferred_image_backend: auto
|
||||
custom_palettes: []
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend: auto` is the baked-in default — first-time setup does not ask about it. The `## Image Generation Tools` rule in SKILL.md then picks the runtime-native tool (Codex `imagegen`, Hermes `image_generate`, etc.) when available, and falls back to installed backends.
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or run setup again:
|
||||
- Delete EXTEND.md to trigger setup
|
||||
- Edit YAML frontmatter for quick changes
|
||||
- Full schema: `preferences-schema.md`
|
||||
See the `## Changing Preferences` section in `SKILL.md` for the canonical list of common edits (pin backend, change defaults, retrigger setup). Full schema: `preferences-schema.md`.
|
||||
|
||||
**EXTEND.md Supports**: Watermark | Preferred type | Preferred palette | Preferred rendering | Preferred text | Preferred mood | Default aspect ratio | Default output directory | Quick mode | Custom palette definitions | Language preference
|
||||
**EXTEND.md Supports**: Watermark | Preferred type | Preferred palette | Preferred rendering | Preferred text | Preferred mood | Default aspect ratio | Default output directory | Quick mode | Image backend preference | Custom palette definitions | Language preference
|
||||
|
||||
@@ -32,6 +32,8 @@ quick_mode: false # Skip confirmation when true
|
||||
|
||||
language: null # zh|en|ja|ko|auto (null = auto-detect)
|
||||
|
||||
preferred_image_backend: auto # auto|ask|<backend-id>
|
||||
|
||||
custom_palettes:
|
||||
- name: my-palette
|
||||
description: "Palette description"
|
||||
@@ -60,6 +62,7 @@ custom_palettes:
|
||||
| `default_aspect` | string | "2.35:1" | Default aspect ratio |
|
||||
| `quick_mode` | bool | false | Skip confirmation step |
|
||||
| `language` | string | null | Output language (null = auto-detect) |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. Resolution logic is documented in `SKILL.md`'s `## Image Generation Tools` section. |
|
||||
| `custom_palettes` | array | [] | User-defined palettes |
|
||||
|
||||
## Type Options
|
||||
@@ -187,6 +190,8 @@ quick_mode: true
|
||||
|
||||
language: en
|
||||
|
||||
preferred_image_backend: codex-imagegen
|
||||
|
||||
custom_palettes:
|
||||
- name: corporate-tech
|
||||
description: "Professional B2B tech palette"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { collect_generated_image_urls_from_response_parts } from "./client.ts";
|
||||
|
||||
test("response part fallback finds generated images when legacy generated markers are absent", () => {
|
||||
const generatedUrl = "https://lh3.googleusercontent.com/gg-dl/example-generated-image";
|
||||
const initialCandidate = ["rcid-1", ["image generated successfully"]];
|
||||
const imageCandidate = [
|
||||
"rcid-1",
|
||||
["image generated successfully"],
|
||||
{ nestedPayload: [{ media: generatedUrl }] },
|
||||
];
|
||||
const responseJson = [
|
||||
["wrb.fr", null, JSON.stringify([null, [], null, null, [initialCandidate]])],
|
||||
["wrb.fr", null, JSON.stringify([null, [], null, null, [imageCandidate]])],
|
||||
];
|
||||
|
||||
assert.equal(initialCandidate[12], undefined);
|
||||
assert.equal(
|
||||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(String(initialCandidate[1]?.[0])),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(collect_generated_image_urls_from_response_parts(responseJson, 0, 0), [generatedUrl]);
|
||||
});
|
||||
@@ -37,6 +37,8 @@ type InitOptions = {
|
||||
|
||||
type RequestKwargs = RequestInit & { timeout_ms?: number };
|
||||
|
||||
const GENERATED_IMAGE_URL_PREFIX = 'https://lh3.googleusercontent.com/gg-dl/';
|
||||
|
||||
function normalize_headers(h?: HeadersInit): Record<string, string> {
|
||||
if (!h) return {};
|
||||
if (Array.isArray(h)) return Object.fromEntries(h.map(([k, v]) => [k, v]));
|
||||
@@ -78,6 +80,59 @@ function collect_strings(root: unknown, accept: (s: string) => boolean, limit: n
|
||||
return out;
|
||||
}
|
||||
|
||||
function collect_generated_image_urls(root: unknown, limit: number = 4): string[] {
|
||||
return collect_strings(root, (s) => s.startsWith(GENERATED_IMAGE_URL_PREFIX), limit);
|
||||
}
|
||||
|
||||
function parse_response_part_body(part: unknown): unknown[] | null {
|
||||
if (!Array.isArray(part)) return null;
|
||||
const part_body = get_nested_value<string | null>(part, [2], null);
|
||||
if (!part_body) return null;
|
||||
try {
|
||||
const part_json = JSON.parse(part_body) as unknown;
|
||||
return Array.isArray(part_json) ? part_json : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function find_generated_image_part(
|
||||
response_json: unknown[],
|
||||
body_index: number,
|
||||
candidate_index: number,
|
||||
limit: number = 4,
|
||||
): { body: unknown[]; urls: string[] } | null {
|
||||
for (let part_index = body_index; part_index < response_json.length; part_index++) {
|
||||
const part_json = parse_response_part_body(response_json[part_index]);
|
||||
if (!part_json) continue;
|
||||
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
|
||||
if (!cand) continue;
|
||||
const urls = collect_generated_image_urls(cand, limit);
|
||||
if (urls.length > 0) return { body: part_json, urls };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function collect_generated_image_urls_from_response_parts(
|
||||
response_json: unknown[],
|
||||
body_index: number,
|
||||
candidate_index: number,
|
||||
limit: number = 4,
|
||||
): string[] {
|
||||
return find_generated_image_part(response_json, body_index, candidate_index, limit)?.urls ?? [];
|
||||
}
|
||||
|
||||
function push_generated_images(
|
||||
generated_images: GeneratedImage[],
|
||||
urls: string[],
|
||||
proxy: string | null,
|
||||
cookies: Record<string, string>,
|
||||
): void {
|
||||
for (const url of urls) {
|
||||
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', proxy, cookies));
|
||||
}
|
||||
}
|
||||
|
||||
export class GeminiClient extends GemMixin {
|
||||
public cookies: Record<string, string> = {};
|
||||
public proxy: string | null = null;
|
||||
@@ -404,24 +459,8 @@ export class GeminiClient extends GemMixin {
|
||||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
|
||||
|
||||
if (wants_generated) {
|
||||
let img_body: unknown[] | null = null;
|
||||
for (let part_index = body_index; part_index < (response_json as unknown[]).length; part_index++) {
|
||||
const part = (response_json as unknown[])[part_index];
|
||||
if (!Array.isArray(part)) continue;
|
||||
const part_body = get_nested_value<string | null>(part, [2], null);
|
||||
if (!part_body) continue;
|
||||
try {
|
||||
const part_json = JSON.parse(part_body) as unknown[];
|
||||
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
|
||||
if (!cand) continue;
|
||||
|
||||
const urls = collect_strings(cand, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 1);
|
||||
if (urls.length > 0) {
|
||||
img_body = part_json;
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const image_part = find_generated_image_part(response_json as unknown[], body_index, candidate_index, 1);
|
||||
const img_body = image_part?.body ?? null;
|
||||
|
||||
if (!img_body) {
|
||||
throw new ImageGenerationError(
|
||||
@@ -452,13 +491,22 @@ export class GeminiClient extends GemMixin {
|
||||
}
|
||||
|
||||
if (generated_images.length === 0) {
|
||||
const urls = collect_strings(img_candidate, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 4);
|
||||
for (const url of urls) {
|
||||
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', this.proxy, this.cookies));
|
||||
}
|
||||
push_generated_images(generated_images, collect_generated_image_urls(img_candidate), this.proxy, this.cookies);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: unconditionally scan all response parts for generated image URLs.
|
||||
// The `wants_generated` detection above relies on `candidate[12][7][0]` and an old
|
||||
// `googleusercontent.com/image_generation_content/` URL pattern, both of which no
|
||||
// longer appear in the current Gemini Web API response format.
|
||||
// When Gemini does generate images, their URLs now start with
|
||||
// `https://lh3.googleusercontent.com/gg-dl/` and are present somewhere in the
|
||||
// response parts — this fallback finds them so images are not silently dropped.
|
||||
if (generated_images.length === 0) {
|
||||
const urls = collect_generated_image_urls_from_response_parts(response_json as unknown[], body_index, candidate_index);
|
||||
push_generated_images(generated_images, urls, this.proxy, this.cookies);
|
||||
}
|
||||
|
||||
out.push(new Candidate({ rcid, text, thoughts, web_images, generated_images }));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-image-cards
|
||||
description: Generates infographic image card series with 12 visual styles, 8 layouts, and 3 color palettes. Breaks content into 1-10 cartoon-style image cards optimized for social media engagement. Use when user mentions "小红书图片", "小红书种草", "小绿书", "微信图文", "微信贴图", "image cards", "图片卡片", or wants social media infographic series.
|
||||
version: 1.56.1
|
||||
version: 1.57.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-image-cards
|
||||
@@ -23,14 +23,51 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
|
||||
## Batch Generation Policy
|
||||
|
||||
After every prompt file for the current generation group has been saved and verified, generate images in batches by default.
|
||||
|
||||
Priority order:
|
||||
|
||||
1. Use the chosen backend's native batch / multi-task interface if it exists. Each task must keep its own prompt file, output path, aspect ratio, session ID, and direct reference images.
|
||||
2. If no native batch interface exists but the runtime can issue parallel tool calls, dispatch up to `generation_batch_size` images at a time. Default: `4`. An explicit user request in the current message, such as `--batch-size 4` or "并行4张一起生成", overrides EXTEND.md.
|
||||
3. If neither native batch nor parallel tool calls are available, generate sequentially.
|
||||
|
||||
Rules:
|
||||
|
||||
- Honor the image-1 anchor chain: generate image 1 first, then batch images 2+ using image 1 as the reference.
|
||||
- Never start a batch until every selected prompt file for that batch exists on disk.
|
||||
- Retry failed items once without regenerating successful items.
|
||||
- Do not use subagents merely to parallelize image rendering. Use subagents only for separate prompt iteration or creative exploration.
|
||||
|
||||
## Confirmation Policy
|
||||
|
||||
Default behavior: **confirm before generation**.
|
||||
|
||||
- Treat explicit skill invocation, a file path, matched signals/presets, and `EXTEND.md` defaults as **recommendation inputs only**. None of them authorizes skipping confirmation.
|
||||
- Do **not** start Step 3 until the user completes Step 2.
|
||||
- Skip confirmation only when the current request explicitly says to do so, for example: `--yes`, "直接生成", "不用确认", "跳过确认", "按默认出图", or equivalent wording.
|
||||
- If confirmation is skipped explicitly, state the assumed strategy / style / layout / palette / count / backend in the next user-facing update before generating.
|
||||
|
||||
## Language
|
||||
|
||||
Respond in the user's language across questions, progress, errors, and completion summary. Keep technical tokens (style names, file paths, code) in English.
|
||||
@@ -44,6 +81,7 @@ Respond in the user's language across questions, progress, errors, and completio
|
||||
| `--palette <name>` | Color override: macaron / warm / neon |
|
||||
| `--preset <name>` | Style + layout + optional palette shorthand (see Presets below; per-preset prompt fragments in `references/style-presets.md`) |
|
||||
| `--ref <files...>` | Reference images applied to image 1 as the series anchor |
|
||||
| `--batch-size <n>` | Temporary generation batch size for this run. Default: `generation_batch_size` from EXTEND.md, otherwise 4. Clamp to 1-8. |
|
||||
| `--yes` | Non-interactive: skip all confirmations, use EXTEND.md or built-in defaults, auto-confirm recommended plan (Path A) |
|
||||
|
||||
## Dimensions
|
||||
@@ -279,7 +317,7 @@ Check these paths in order; first hit wins:
|
||||
- **Not found + interactive** → run first-time setup (see `references/config/first-time-setup.md`) and save before anything else. Do NOT analyze content or ask style questions until preferences exist — this keeps first-run behavior predictable.
|
||||
- **Not found + `--yes`** → skip setup, use built-in defaults (no watermark, style/layout auto-selected, language from content). Do not prompt, do not create EXTEND.md.
|
||||
|
||||
**EXTEND.md keys**: watermark, preferred style/layout, custom style definitions, language preference. Schema: `references/config/preferences-schema.md`.
|
||||
**EXTEND.md keys**: watermark, preferred style/layout, custom style definitions, language preference, preferred image backend, generation batch size. Schema: `references/config/preferences-schema.md`.
|
||||
|
||||
### Step 1: Analyze Content → `analysis.md`
|
||||
|
||||
@@ -291,6 +329,8 @@ Check these paths in order; first hit wins:
|
||||
|
||||
### Step 2: Smart Confirm ⚠️ REQUIRED
|
||||
|
||||
**Hard gate**: this step is mandatory per the [Confirmation Policy](#confirmation-policy) — Step 3 cannot start until the user confirms here (or explicitly opts out with `--yes` / equivalent wording in the current request).
|
||||
|
||||
Goal: present the auto-recommended plan and let the user confirm or adjust. Skip this step entirely under `--yes` — proceed with Path A using the analysis and any CLI overrides.
|
||||
|
||||
**Display summary** before asking:
|
||||
@@ -326,14 +366,13 @@ With confirmed outline + style + layout + palette:
|
||||
|
||||
**Visual consistency — image-1 anchor chain**: character / mascot / color rendering drifts between calls unless you anchor them. Generate image 1 (cover) first WITHOUT `--ref`, then pass image 1 as `--ref` to every subsequent image. This is the single most important consistency trick for this skill — don't skip it even if the backend also supports a session ID.
|
||||
|
||||
For each image (cover, content, ending):
|
||||
Generation flow:
|
||||
|
||||
1. Write the full prompt to `prompts/NN-{type}-{slug}.md` in the user's preferred language (backup rule applies).
|
||||
2. Generate:
|
||||
- **Image 1**: no `--ref` (establishes the anchor).
|
||||
- **Images 2+**: add `--ref <path-to-image-01.png>`.
|
||||
- Backup rule applies to the PNG files.
|
||||
3. Report progress after each image.
|
||||
1. Write the full prompt for every image to `prompts/NN-{type}-{slug}.md` in the user's preferred language (backup rule applies), then verify all selected prompt files exist.
|
||||
2. Generate **image 1** first without `--ref`; backup rule applies to the PNG file. This establishes the anchor.
|
||||
3. Build a task list for **images 2+** using image 1 as `--ref <path-to-image-01.png>`.
|
||||
4. Dispatch images 2+ in batches per the `## Batch Generation Policy`: backend native batch first, runtime parallel tool calls second, sequential only as fallback.
|
||||
5. Report progress after each completed image. On failure, retry only the failed item once from the same saved prompt file.
|
||||
|
||||
**Watermark** (if enabled in EXTEND.md): append to the generation prompt:
|
||||
|
||||
@@ -417,4 +456,17 @@ Always update the prompt file before regenerating — it's the source of truth a
|
||||
- For sensitive public figures, use stylized cartoon alternatives.
|
||||
- Smart Confirm (Step 2) is required; Detailed mode adds a second confirmation (2a + 2c).
|
||||
|
||||
Custom configurations via EXTEND.md. See Step 0 for paths and schema.
|
||||
## Changing Preferences
|
||||
|
||||
EXTEND.md lives at the first matching path listed in Step 0. Three ways to change it:
|
||||
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: `references/config/preferences-schema.md`.
|
||||
- **Reconfigure interactively** — delete EXTEND.md (or ask "reconfigure baoyu-image-cards preferences" / "重新配置"). The next run re-triggers first-time setup.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `generation_batch_size: 4` — default number of images to render concurrently when the backend/runtime supports batch or parallel generation.
|
||||
- `preferred_style: notion`, `preferred_layout: dense`, `preferred_palette: macaron`, `language: zh`.
|
||||
- `watermark.enabled: true` + `watermark.content: "@handle"` — add a watermark.
|
||||
|
||||
@@ -110,13 +110,16 @@ preferred_style:
|
||||
description: ""
|
||||
preferred_layout: null
|
||||
language: null
|
||||
preferred_image_backend: auto
|
||||
generation_batch_size: 4
|
||||
custom_styles: []
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend: auto` is the baked-in default — first-time setup does not ask about it. The `## Image Generation Tools` rule in SKILL.md then picks the runtime-native tool (Codex `imagegen`, Hermes `image_generate`, etc.) when available, and falls back to installed backends.
|
||||
|
||||
`generation_batch_size: 4` is the baked-in default for batch rendering. The current user request may override it for one run.
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or run setup again:
|
||||
- Delete EXTEND.md to trigger setup
|
||||
- Edit YAML frontmatter for quick changes
|
||||
- Full schema: `config/preferences-schema.md`
|
||||
See the `## Changing Preferences` section in `SKILL.md` for the canonical list of common edits (pin backend, change defaults, retrigger setup). Full schema: `preferences-schema.md`.
|
||||
|
||||
@@ -24,6 +24,10 @@ preferred_layout: null # sparse|balanced|dense|list|comparison|flow
|
||||
|
||||
language: null # zh|en|ja|ko|auto
|
||||
|
||||
preferred_image_backend: auto # auto|ask|<backend-id>
|
||||
|
||||
generation_batch_size: 4 # 1-8, used when backend/runtime supports batch or parallel generation
|
||||
|
||||
custom_styles:
|
||||
- name: my-style
|
||||
description: "Style description"
|
||||
@@ -49,6 +53,8 @@ custom_styles:
|
||||
| `preferred_style.description` | string | "" | Custom notes/override |
|
||||
| `preferred_layout` | string | null | Layout preference or null |
|
||||
| `language` | string | null | Output language (null = auto-detect) |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. Resolution logic is documented in `SKILL.md`'s `## Image Generation Tools` section. |
|
||||
| `generation_batch_size` | int | 4 | Number of images to dispatch per batch when the backend has native batch support or the runtime can issue parallel generation calls. Clamp invalid values to 1-8. Current user request overrides this value. |
|
||||
| `custom_styles` | array | [] | User-defined styles |
|
||||
|
||||
## Position Options
|
||||
@@ -104,6 +110,10 @@ preferred_layout: dense
|
||||
|
||||
language: zh
|
||||
|
||||
preferred_image_backend: codex-imagegen
|
||||
|
||||
generation_batch_size: 4
|
||||
|
||||
custom_styles:
|
||||
- name: corporate
|
||||
description: "Professional B2B style"
|
||||
|
||||
@@ -24,6 +24,6 @@ Read when the user picks `--provider minimax` or sets `default_model.minimax`. D
|
||||
|
||||
## Official References
|
||||
|
||||
- [Image Generation Guide](https://platform.minimax.io/docs/guides/image-generation)
|
||||
- [Text-to-Image API](https://platform.minimax.io/docs/api-reference/image-generation-t2i)
|
||||
- [Image-to-Image API](https://platform.minimax.io/docs/api-reference/image-generation-i2i)
|
||||
- [Image Generation Guide](https://platform.minimaxi.com/docs/guides/image-generation)
|
||||
- [Text-to-Image API](https://platform.minimaxi.com/docs/api-reference/image-generation-t2i)
|
||||
- [Image-to-Image API](https://platform.minimaxi.com/docs/api-reference/image-generation-i2i)
|
||||
|
||||
@@ -60,8 +60,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
};
|
||||
}
|
||||
|
||||
test("MiniMax URL builder normalizes /v1 suffixes", (t) => {
|
||||
useEnv(t, { MINIMAX_BASE_URL: "https://api.minimax.io" });
|
||||
test("MiniMax URL builder uses documented default and normalizes /v1 suffixes", (t) => {
|
||||
useEnv(t, { MINIMAX_BASE_URL: null });
|
||||
assert.equal(buildMinimaxUrl(), "https://api.minimaxi.com/v1/image_generation");
|
||||
|
||||
process.env.MINIMAX_BASE_URL = "https://api.minimax.io";
|
||||
assert.equal(buildMinimaxUrl(), "https://api.minimax.io/v1/image_generation");
|
||||
|
||||
process.env.MINIMAX_BASE_URL = "https://proxy.example.com/custom/v1/";
|
||||
|
||||
@@ -44,7 +44,7 @@ function getApiKey(): string | null {
|
||||
}
|
||||
|
||||
export function buildMinimaxUrl(): string {
|
||||
const base = (process.env.MINIMAX_BASE_URL || "https://api.minimax.io").replace(/\/+$/g, "");
|
||||
const base = (process.env.MINIMAX_BASE_URL || "https://api.minimaxi.com").replace(/\/+$/g, "");
|
||||
return base.endsWith("/v1") ? `${base}/image_generation` : `${base}/v1/image_generation`;
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export async function generateImage(
|
||||
): Promise<Uint8Array> {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error("MINIMAX_API_KEY is required. Get one from https://platform.minimax.io/");
|
||||
throw new Error("MINIMAX_API_KEY is required. Get one from https://platform.minimaxi.com/");
|
||||
}
|
||||
|
||||
const body = await buildRequestBody(prompt, model, args);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-imagine
|
||||
description: AI image generation with OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, Z.AI GLM-Image, MiniMax, Jimeng, Seedream 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.57.0
|
||||
description: AI image generation with OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope, Z.AI GLM-Image, MiniMax, Jimeng, Seedream 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.58.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-imagine
|
||||
@@ -13,7 +13,7 @@ metadata:
|
||||
|
||||
# Image Generation (AI SDK)
|
||||
|
||||
Official API-based image generation. Supports OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Z.AI GLM-Image, MiniMax, Jimeng (即梦), Seedream (豆包) and Replicate.
|
||||
Official API-based image generation. Supports OpenAI GPT Image 2, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Z.AI GLM-Image, MiniMax, Jimeng (即梦), Seedream (豆包) and Replicate.
|
||||
|
||||
## User Input Tools
|
||||
|
||||
@@ -68,6 +68,9 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref so
|
||||
# Specific provider
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider dashscope --model qwen-image-2.0-pro
|
||||
|
||||
# OpenAI GPT Image 2
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider openai --model gpt-image-2
|
||||
|
||||
# Batch mode
|
||||
${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4
|
||||
```
|
||||
@@ -84,11 +87,11 @@ ${BUN_X} {baseDir}/scripts/main.ts --batchfile batch.json --jobs 4
|
||||
| `--provider google\|openai\|azure\|openrouter\|dashscope\|zai\|minimax\|jimeng\|seedream\|replicate` | Force provider (default: auto-detect) |
|
||||
| `--model <id>`, `-m` | Model ID — see provider references for defaults and allowed values |
|
||||
| `--ar <ratio>` | Aspect ratio (`16:9`, `1:1`, `4:3`, …) |
|
||||
| `--size <WxH>` | Explicit size (e.g., `1024x1024`) |
|
||||
| `--size <WxH>` | Explicit size (e.g., `1024x1024`; for `gpt-image-2`, width/height must be multiples of 16, max edge 3840px, ratio no wider than 3:1) |
|
||||
| `--quality normal\|2k` | Quality preset (default: `2k`) |
|
||||
| `--imageSize 1K\|2K\|4K` | Image size for Google/OpenRouter (default: from quality) |
|
||||
| `--imageApiDialect openai-native\|ratio-metadata` | OpenAI-compatible endpoint dialect — use `ratio-metadata` for gateways that expect aspect-ratio `size` plus `metadata.resolution` |
|
||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate supported families, MiniMax subject-reference, Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, SeedEdit 3.0 |
|
||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate supported families, MiniMax subject-reference, Seedream 5.0/4.5/4.0, DashScope `wan2.7-image-pro`/`wan2.7-image`. Not supported by Jimeng, Seedream 3.0, SeedEdit 3.0, or any DashScope model outside the `wan2.7-image*` family |
|
||||
| `--n <count>` | Number of images. Replicate requires `--n 1` (single-output save semantics) |
|
||||
| `--json` | JSON output |
|
||||
|
||||
@@ -128,7 +131,9 @@ Priority (highest → lowest) applies to every provider:
|
||||
3. Env var `<PROVIDER>_IMAGE_MODEL`
|
||||
4. Built-in default
|
||||
|
||||
For Azure, `--model` / `default_model.azure` is the Azure deployment name. `AZURE_OPENAI_DEPLOYMENT` is the preferred env var; `AZURE_OPENAI_IMAGE_MODEL` is kept as a backward-compatible alias.
|
||||
For OpenAI, the built-in default is `gpt-image-2`. `gpt-image-1.5`, `gpt-image-1`, and GPT Image snapshots remain selectable with `--model` or `OPENAI_IMAGE_MODEL`.
|
||||
|
||||
For Azure, `--model` / `default_model.azure` is the Azure deployment name. `AZURE_OPENAI_DEPLOYMENT` is the preferred env var; `AZURE_OPENAI_IMAGE_MODEL` is kept as a backward-compatible alias. If your Azure deployment is named after the underlying model, use `gpt-image-2`; otherwise use the exact custom deployment name.
|
||||
|
||||
EXTEND.md overrides env vars: if EXTEND.md sets `default_model.google: "gemini-3-pro-image-preview"` and the env var sets `GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview`, EXTEND.md wins.
|
||||
|
||||
@@ -169,17 +174,19 @@ Each provider has its own quirks (model families, size rules, ref support, limit
|
||||
|
||||
| Preset | Google imageSize | OpenAI size | OpenRouter size | Replicate resolution | Use case |
|
||||
|--------|------------------|-------------|-----------------|----------------------|----------|
|
||||
| `normal` | 1K | 1024px | 1K | 1K | Quick previews |
|
||||
| `2k` (default) | 2K | 2048px | 2K | 2K | Covers, illustrations, infographics |
|
||||
| `normal` | 1K | 1024px target | 1K | 1K | Quick previews |
|
||||
| `2k` (default) | 2K | 2048px target | 2K | 2K | Covers, illustrations, infographics |
|
||||
|
||||
Google/OpenRouter `imageSize` can be overridden with `--imageSize 1K|2K|4K`.
|
||||
|
||||
For OpenAI native `gpt-image-2`, `normal` maps to `quality=medium` and a low-latency valid size near the requested aspect ratio; `2k` maps to `quality=high` and 2048px-class sizes such as `2048x2048`, `2048x1152`, or `1152x2048`. Use explicit `--size` for valid custom or 4K outputs, e.g. `3840x2160`.
|
||||
|
||||
## Aspect Ratios
|
||||
|
||||
Supported: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2.35:1`.
|
||||
|
||||
- Google multimodal: `imageConfig.aspectRatio`
|
||||
- OpenAI: closest supported size
|
||||
- OpenAI: `gpt-image-2` uses the closest valid custom size for the requested ratio; older GPT Image and DALL·E models use their closest supported fixed size
|
||||
- OpenRouter: `imageGenerationOptions.aspect_ratio`; if only `--size <WxH>` is given, the ratio is inferred
|
||||
- Replicate: behavior is model-specific — `google/nano-banana*` uses `aspect_ratio`, `bytedance/seedream-*` uses documented Replicate ratios, Wan 2.7 maps `--ar` to a concrete `size`
|
||||
- MiniMax: official `aspect_ratio` values; if `--size <WxH>` is given without `--ar`, sends `width`/`height` for `image-01`
|
||||
|
||||
@@ -46,7 +46,7 @@ options:
|
||||
- label: "Google (Recommended)"
|
||||
description: "Gemini multimodal - high quality, reference images, flexible sizes"
|
||||
- label: "OpenAI"
|
||||
description: "GPT Image - consistent quality, reliable output"
|
||||
description: "GPT Image 2 - latest OpenAI image model, reference-image workflows"
|
||||
- label: "Azure OpenAI"
|
||||
description: "Azure-hosted GPT Image deployments with resource-specific routing"
|
||||
- label: "OpenRouter"
|
||||
@@ -101,10 +101,12 @@ Only show if user selected Azure OpenAI.
|
||||
header: "Azure Deploy"
|
||||
question: "Default Azure image deployment name?"
|
||||
options:
|
||||
- label: "gpt-image-1.5 (Recommended)"
|
||||
description: "Best default if your Azure deployment uses the same name"
|
||||
- label: "gpt-image-1"
|
||||
- label: "gpt-image-2 (Recommended)"
|
||||
description: "Use if your Azure deployment uses the GPT Image 2 model name"
|
||||
- label: "gpt-image-1.5"
|
||||
description: "Previous GPT Image deployment name"
|
||||
- label: "gpt-image-1"
|
||||
description: "Earlier GPT Image deployment name"
|
||||
```
|
||||
|
||||
### Question 2d: Default MiniMax Model
|
||||
@@ -214,10 +216,12 @@ options:
|
||||
header: "OpenAI Model"
|
||||
question: "Choose a default OpenAI image generation model?"
|
||||
options:
|
||||
- label: "gpt-image-1.5 (Recommended)"
|
||||
description: "Latest GPT Image model, high quality"
|
||||
- label: "gpt-image-2 (Recommended)"
|
||||
description: "Latest GPT Image model, flexible sizes up to 4K, high-fidelity image inputs"
|
||||
- label: "gpt-image-1.5"
|
||||
description: "Previous GPT Image model"
|
||||
- label: "gpt-image-1"
|
||||
description: "Previous generation GPT Image model"
|
||||
description: "Earlier GPT Image model"
|
||||
```
|
||||
|
||||
### Azure Deployment Selection
|
||||
@@ -226,8 +230,10 @@ options:
|
||||
header: "Azure Deploy"
|
||||
question: "Choose a default Azure image deployment name?"
|
||||
options:
|
||||
- label: "gpt-image-1.5 (Recommended)"
|
||||
description: "Use when your Azure deployment name matches the GPT-image-1.5 model"
|
||||
- label: "gpt-image-2 (Recommended)"
|
||||
description: "Use when your Azure deployment name matches the GPT Image 2 model"
|
||||
- label: "gpt-image-1.5"
|
||||
description: "Use when your Azure deployment name matches the GPT Image 1.5 model"
|
||||
- label: "gpt-image-1"
|
||||
description: "Use when your Azure deployment name matches GPT-image-1"
|
||||
```
|
||||
@@ -265,6 +271,10 @@ options:
|
||||
description: "Legacy Qwen model with five fixed output sizes"
|
||||
- label: "qwen-image-plus"
|
||||
description: "Legacy Qwen model, same current capability as qwen-image"
|
||||
- label: "wan2.7-image-pro"
|
||||
description: "Wan 2.7 Pro — supports up to 4K text-to-image and reference-image editing"
|
||||
- label: "wan2.7-image"
|
||||
description: "Wan 2.7 base — faster generation, up to 2K, supports reference-image editing"
|
||||
- label: "z-image-turbo"
|
||||
description: "Legacy DashScope model for compatibility"
|
||||
- label: "z-image-ultra"
|
||||
@@ -275,6 +285,7 @@ Notes for DashScope setup:
|
||||
|
||||
- Prefer `qwen-image-2.0-pro` when the user needs custom `--size`, uncommon ratios like `21:9`, or strong Chinese/English text rendering.
|
||||
- `qwen-image-max` / `qwen-image-plus` / `qwen-image` only support five fixed sizes: `1664*928`, `1472*1104`, `1328*1328`, `1104*1472`, `928*1664`.
|
||||
- `wan2.7-image-pro` and `wan2.7-image` are the only DashScope models that accept `--ref`. Pick one of these when the user wants reference-image editing or multi-image fusion via DashScope.
|
||||
- In `baoyu-imagine`, `quality` is a compatibility preset. It is not a native DashScope parameter.
|
||||
|
||||
### Z.AI Model Selection
|
||||
|
||||
@@ -23,8 +23,8 @@ default_image_api_dialect: null # openai-native|ratio-metadata|null (OpenAI-com
|
||||
|
||||
default_model:
|
||||
google: null # e.g., "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"
|
||||
openai: null # e.g., "gpt-image-1.5", "gpt-image-1"
|
||||
azure: null # Azure deployment name, e.g., "gpt-image-1.5" or "image-prod"
|
||||
openai: null # e.g., "gpt-image-2", "gpt-image-1.5", "gpt-image-1"
|
||||
azure: null # Azure deployment name, e.g., "gpt-image-2" or "image-prod"
|
||||
openrouter: null # e.g., "google/gemini-3.1-flash-image-preview"
|
||||
dashscope: null # e.g., "qwen-image-2.0-pro"
|
||||
zai: null # e.g., "glm-image"
|
||||
@@ -106,8 +106,8 @@ default_image_size: 2K
|
||||
default_image_api_dialect: null
|
||||
default_model:
|
||||
google: "gemini-3-pro-image-preview"
|
||||
openai: "gpt-image-1.5"
|
||||
azure: "gpt-image-1.5"
|
||||
openai: "gpt-image-2"
|
||||
azure: "gpt-image-2"
|
||||
openrouter: "google/gemini-3.1-flash-image-preview"
|
||||
dashscope: "qwen-image-2.0-pro"
|
||||
zai: "glm-image"
|
||||
|
||||
@@ -17,6 +17,17 @@ Read when the user picks `--provider dashscope`, sets `default_model.dashscope`,
|
||||
- Default is `1664*928`
|
||||
- `qwen-image` currently has the same capability as `qwen-image-plus`
|
||||
|
||||
**`wan2.7-image*`** — multimodal Wan 2.7 family. Members: `wan2.7-image-pro`, `wan2.7-image`.
|
||||
|
||||
- Free-form `size` in `宽*高` format, plus aspect-ratio inference
|
||||
- `wan2.7-image-pro` text-to-image (no `--ref`): total pixels in `[768*768, 4096*4096]`, ratio in `[1:8, 8:1]`
|
||||
- `wan2.7-image-pro` with reference images and `wan2.7-image` (all scenarios): total pixels in `[768*768, 2048*2048]`, ratio in `[1:8, 8:1]`
|
||||
- Default: `1024*1024` (`--quality normal`) or `2048*2048` (`--quality 2k`); 4K requires explicit `--size`
|
||||
- Supports up to 9 reference images in `--ref` (image editing / multi-image fusion)
|
||||
- Reference images are sent inline as base64 (or passed through if the path is an `http(s)://` URL)
|
||||
- API does NOT use `prompt_extend`; the skill omits it for this family
|
||||
- The Wan 2.7 API defaults `n` to **4** in non-collage mode and bills per generated image. baoyu-imagine forces `n: 1` and rejects `--n > 1` to avoid silently paying for and discarding extra images.
|
||||
|
||||
**Legacy** — `z-image-turbo`, `z-image-ultra`, `wanx-v1`. Only use when the user explicitly asks for legacy behavior.
|
||||
|
||||
## Size Resolution
|
||||
@@ -24,7 +35,8 @@ Read when the user picks `--provider dashscope`, sets `default_model.dashscope`,
|
||||
- `--size` wins over `--ar`
|
||||
- For `qwen-image-2.0*`: prefer explicit `--size`; otherwise infer from `--ar` using the recommended table below
|
||||
- For `qwen-image-max/plus/image`: only use the five fixed sizes; if the requested ratio doesn't fit, switch to `qwen-image-2.0-pro`
|
||||
- `--quality` is a baoyu-imagine preset, not an official DashScope field. The mapping of `normal`/`2k` onto the `qwen-image-2.0*` table is an implementation choice, not an API guarantee
|
||||
- For `wan2.7-image*`: explicit `--size` is validated against the per-mode pixel/ratio limits; otherwise the size is derived from `--ar` and `--quality` (`normal` ≈ 1K, `2k` ≈ 2K). To request 4K with `wan2.7-image-pro` text-to-image, pass `--size` explicitly (e.g. `4096*4096`, `3840*2160`)
|
||||
- `--quality` is a baoyu-imagine preset, not an official DashScope field. The mapping of `normal`/`2k` onto the `qwen-image-2.0*` and `wan2.7-image*` tables is an implementation choice, not an API guarantee
|
||||
|
||||
### Recommended `qwen-image-2.0*` sizes
|
||||
|
||||
@@ -39,12 +51,19 @@ Read when the user picks `--provider dashscope`, sets `default_model.dashscope`,
|
||||
| `16:9` | `1280*720` | `1920*1080` |
|
||||
| `21:9` | `1344*576` | `2048*872` |
|
||||
|
||||
## Reference Images
|
||||
|
||||
- Only `wan2.7-image-pro` and `wan2.7-image` accept `--ref`. Other DashScope models (qwen-image-2.0*, qwen-image-max/plus/image, legacy) reject `--ref` and the user is steered to a different provider/model.
|
||||
- Up to 9 reference images per request. Local files are inlined as base64 data URLs; `http(s)://` URLs are forwarded as-is.
|
||||
- Supplying any `--ref` automatically clamps the wan2.7-image-pro pixel ceiling from 4K to 2K (the API only supports 4K for pure text-to-image with no image input).
|
||||
|
||||
## Not Exposed
|
||||
|
||||
DashScope APIs also support `negative_prompt`, `prompt_extend`, and `watermark`, but `baoyu-imagine` does not expose them as CLI flags today.
|
||||
DashScope APIs also support `negative_prompt`, `prompt_extend`, `watermark`, `thinking_mode`, `seed`, `bbox_list`, `enable_sequential`, and `color_palette`. `baoyu-imagine` does not expose them as CLI flags today; the wan2.7 family relies on the API defaults (e.g. `thinking_mode=true`). The skill always sends `n=1` for wan2.7 — if you want grid/collage mode you currently need to call the API directly.
|
||||
|
||||
## Official References
|
||||
|
||||
- [Qwen-Image API](https://help.aliyun.com/zh/model-studio/qwen-image-api)
|
||||
- [Text-to-image guide](https://help.aliyun.com/zh/model-studio/text-to-image)
|
||||
- [Qwen-Image Edit API](https://help.aliyun.com/zh/model-studio/qwen-image-edit-api)
|
||||
- [Wan 2.7 image generation & editing API](https://help.aliyun.com/zh/model-studio/wan-image-generation-and-editing-api-reference)
|
||||
|
||||
@@ -24,6 +24,6 @@ Read when the user picks `--provider minimax` or sets `default_model.minimax`. D
|
||||
|
||||
## Official References
|
||||
|
||||
- [Image Generation Guide](https://platform.minimax.io/docs/guides/image-generation)
|
||||
- [Text-to-Image API](https://platform.minimax.io/docs/api-reference/image-generation-t2i)
|
||||
- [Image-to-Image API](https://platform.minimax.io/docs/api-reference/image-generation-i2i)
|
||||
- [Image Generation Guide](https://platform.minimaxi.com/docs/guides/image-generation)
|
||||
- [Text-to-Image API](https://platform.minimaxi.com/docs/api-reference/image-generation-t2i)
|
||||
- [Image-to-Image API](https://platform.minimaxi.com/docs/api-reference/image-generation-i2i)
|
||||
|
||||
@@ -25,10 +25,13 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref so
|
||||
|
||||
```bash
|
||||
# OpenAI
|
||||
${BUN_X} {baseDir}/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 --model gpt-image-2
|
||||
|
||||
# Azure OpenAI (model = deployment name)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider azure --model gpt-image-1.5
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider azure --model gpt-image-2
|
||||
|
||||
# OpenAI GPT Image 2 custom 4K size
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cinematic landscape" --image out.png --provider openai --model gpt-image-2 --size 3840x2160
|
||||
|
||||
# Google with explicit model
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
||||
@@ -48,6 +51,12 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "为咖啡品牌设计一张 21:9
|
||||
# DashScope legacy fixed-size
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "一张电影感海报" --image out.png --provider dashscope --model qwen-image-max --size 1664x928
|
||||
|
||||
# DashScope Wan 2.7 Image Pro (4K text-to-image)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "一间有着精致窗户的花店" --image out.png --provider dashscope --model wan2.7-image-pro --size 4096x4096
|
||||
|
||||
# DashScope Wan 2.7 Image with reference image (multi-image fusion)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "把图2的涂鸦喷绘在图1的汽车上" --image out.png --provider dashscope --model wan2.7-image-pro --ref car.webp paint.webp
|
||||
|
||||
# Z.AI GLM-image
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
parseArgs,
|
||||
parseOpenAIImageApiDialect,
|
||||
parseSimpleYaml,
|
||||
validateReferenceImages,
|
||||
} from "./main.ts";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
@@ -123,6 +124,15 @@ test("parseArgs falls back to positional prompt and rejects invalid provider", (
|
||||
);
|
||||
});
|
||||
|
||||
test("validateReferenceImages can skip remote URLs for providers that support them", async () => {
|
||||
await validateReferenceImages(["https://example.com/ref.png"], { allowRemoteUrls: true });
|
||||
|
||||
await assert.rejects(
|
||||
() => validateReferenceImages(["https://example.com/ref.png"]),
|
||||
/Reference image not found/,
|
||||
);
|
||||
});
|
||||
|
||||
test("parseSimpleYaml parses nested defaults and provider limits", () => {
|
||||
const yaml = `
|
||||
version: 2
|
||||
@@ -133,7 +143,7 @@ default_image_size: 2K
|
||||
default_image_api_dialect: ratio-metadata
|
||||
default_model:
|
||||
google: gemini-3-pro-image-preview
|
||||
openai: gpt-image-1.5
|
||||
openai: gpt-image-2
|
||||
zai: glm-image
|
||||
azure: image-prod
|
||||
minimax: image-01
|
||||
@@ -165,7 +175,7 @@ batch:
|
||||
assert.equal(config.default_image_size, "2K");
|
||||
assert.equal(config.default_image_api_dialect, "ratio-metadata");
|
||||
assert.equal(config.default_model?.google, "gemini-3-pro-image-preview");
|
||||
assert.equal(config.default_model?.openai, "gpt-image-1.5");
|
||||
assert.equal(config.default_model?.openai, "gpt-image-2");
|
||||
assert.equal(config.default_model?.zai, "glm-image");
|
||||
assert.equal(config.default_model?.azure, "image-prod");
|
||||
assert.equal(config.default_model?.minimax, "image-01");
|
||||
@@ -308,7 +318,7 @@ test("detectProvider rejects non-ref-capable providers and prefers Google first
|
||||
() =>
|
||||
detectProvider(
|
||||
makeArgs({
|
||||
provider: "dashscope",
|
||||
provider: "zai",
|
||||
referenceImages: ["ref.png"],
|
||||
}),
|
||||
),
|
||||
@@ -426,6 +436,33 @@ test("detectProvider infers Seedream from model id and allows Seedream reference
|
||||
);
|
||||
});
|
||||
|
||||
test("detectProvider allows DashScope reference-image workflows when explicitly chosen for wan2.7 models", (t) => {
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
OPENAI_API_KEY: null,
|
||||
AZURE_OPENAI_API_KEY: null,
|
||||
AZURE_OPENAI_BASE_URL: null,
|
||||
OPENROUTER_API_KEY: null,
|
||||
DASHSCOPE_API_KEY: "dashscope-key",
|
||||
MINIMAX_API_KEY: null,
|
||||
REPLICATE_API_TOKEN: null,
|
||||
JIMENG_ACCESS_KEY_ID: null,
|
||||
JIMENG_SECRET_ACCESS_KEY: null,
|
||||
ARK_API_KEY: null,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
detectProvider(
|
||||
makeArgs({
|
||||
provider: "dashscope",
|
||||
model: "wan2.7-image-pro",
|
||||
referenceImages: ["ref.png"],
|
||||
}),
|
||||
),
|
||||
"dashscope",
|
||||
);
|
||||
});
|
||||
|
||||
test("detectProvider selects MiniMax when only MiniMax credentials are configured or the model id matches", (t) => {
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
@@ -504,7 +541,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
|
||||
id: "hero",
|
||||
promptFiles: ["prompts/hero.md"],
|
||||
image: "out/hero",
|
||||
ref: ["refs/hero.png"],
|
||||
ref: ["refs/hero.png", "https://example.com/ref.png"],
|
||||
ar: "16:9",
|
||||
},
|
||||
],
|
||||
@@ -533,6 +570,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
|
||||
assert.equal(taskArgs.imagePath, path.join(loaded.batchDir, "out/hero"));
|
||||
assert.deepEqual(taskArgs.referenceImages, [
|
||||
path.join(loaded.batchDir, "refs/hero.png"),
|
||||
"https://example.com/ref.png",
|
||||
]);
|
||||
assert.equal(taskArgs.provider, "replicate");
|
||||
assert.equal(taskArgs.aspectRatio, "16:9");
|
||||
@@ -557,5 +595,29 @@ test("path normalization, worker count, and retry classification follow expected
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7 image models accept at most 9 reference images. Received 10."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7 image models in baoyu-imagine support exactly one output image per request."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isRetryableGenerationError(
|
||||
new Error("DashScope wan2.7-image requires total pixels between 768*768 and 2048*2048."),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(isRetryableGenerationError(new Error("socket hang up")), true);
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ Options:
|
||||
--quality normal|2k Quality preset (default: 2k)
|
||||
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
|
||||
--imageApiDialect <id> OpenAI-compatible image dialect: openai-native|ratio-metadata
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, or Seedream 4.0/4.5/5.0)
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate supported families, MiniMax, Seedream 4.0/4.5/5.0, or DashScope wan2.7-image*)
|
||||
--n <count> Number of images for the current task (default: 1; Replicate currently requires 1)
|
||||
--json JSON output
|
||||
-h, --help Show help
|
||||
@@ -124,7 +124,7 @@ Environment variables:
|
||||
JIMENG_ACCESS_KEY_ID Jimeng Access Key ID
|
||||
JIMENG_SECRET_ACCESS_KEY Jimeng Secret Access Key
|
||||
ARK_API_KEY Seedream/Ark API key
|
||||
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-1.5)
|
||||
OPENAI_IMAGE_MODEL Default OpenAI model (gpt-image-2)
|
||||
OPENROUTER_IMAGE_MODEL Default OpenRouter model (google/gemini-3.1-flash-image-preview)
|
||||
GOOGLE_IMAGE_MODEL Default Google model (gemini-3-pro-image-preview)
|
||||
DASHSCOPE_IMAGE_MODEL Default DashScope model (qwen-image-2.0-pro)
|
||||
@@ -151,7 +151,7 @@ Environment variables:
|
||||
AZURE_OPENAI_BASE_URL Azure OpenAI resource or deployment endpoint
|
||||
AZURE_OPENAI_DEPLOYMENT Default Azure deployment name
|
||||
AZURE_API_VERSION Azure API version (default: 2025-04-01-preview)
|
||||
AZURE_OPENAI_IMAGE_MODEL Backward-compatible Azure deployment/model alias (defaults to gpt-image-1.5)
|
||||
AZURE_OPENAI_IMAGE_MODEL Backward-compatible Azure deployment/model alias (defaults to gpt-image-2)
|
||||
SEEDREAM_BASE_URL Custom Seedream endpoint
|
||||
BAOYU_IMAGE_GEN_MAX_WORKERS Override batch worker cap
|
||||
BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY Override provider concurrency
|
||||
@@ -698,10 +698,11 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
args.provider !== "openrouter" &&
|
||||
args.provider !== "replicate" &&
|
||||
args.provider !== "seedream" &&
|
||||
args.provider !== "minimax"
|
||||
args.provider !== "minimax" &&
|
||||
args.provider !== "dashscope"
|
||||
) {
|
||||
throw new Error(
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, --provider dashscope with a wan2.7 image model, --provider seedream for supported Seedream models, or --provider minimax for MiniMax subject-reference workflows."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -775,8 +776,24 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateReferenceImages(referenceImages: string[]): Promise<void> {
|
||||
export type ReferenceImageValidationOptions = {
|
||||
allowRemoteUrls?: boolean;
|
||||
};
|
||||
|
||||
function isRemoteReferenceImage(refPath: string): boolean {
|
||||
return /^https?:\/\//i.test(refPath);
|
||||
}
|
||||
|
||||
function shouldAllowRemoteReferenceImages(provider: Provider | null): boolean {
|
||||
return provider === "dashscope";
|
||||
}
|
||||
|
||||
export async function validateReferenceImages(
|
||||
referenceImages: string[],
|
||||
options: ReferenceImageValidationOptions = {},
|
||||
): Promise<void> {
|
||||
for (const refPath of referenceImages) {
|
||||
if (options.allowRemoteUrls && isRemoteReferenceImage(refPath)) continue;
|
||||
const fullPath = path.resolve(refPath);
|
||||
try {
|
||||
await access(fullPath);
|
||||
@@ -803,6 +820,11 @@ export function isRetryableGenerationError(error: unknown): boolean {
|
||||
"API error (404)",
|
||||
"temporarily disabled",
|
||||
"supports saving exactly one image",
|
||||
"supports only",
|
||||
"support exactly one output image",
|
||||
"support aspect ratios in",
|
||||
"requires total pixels between",
|
||||
"accept at most",
|
||||
];
|
||||
return !nonRetryableMarkers.some((marker) => msg.includes(marker));
|
||||
}
|
||||
@@ -858,7 +880,11 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConf
|
||||
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);
|
||||
if (args.referenceImages.length > 0) {
|
||||
await validateReferenceImages(args.referenceImages, {
|
||||
allowRemoteUrls: shouldAllowRemoteReferenceImages(args.provider),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = detectProvider(args);
|
||||
const providerModule = await loadProviderModule(provider);
|
||||
@@ -907,6 +933,10 @@ export function resolveBatchPath(batchDir: string, filePath: string): string {
|
||||
return path.isAbsolute(filePath) ? filePath : path.resolve(batchDir, filePath);
|
||||
}
|
||||
|
||||
function resolveBatchReferencePath(batchDir: string, filePath: string): string {
|
||||
return isRemoteReferenceImage(filePath) ? filePath : resolveBatchPath(batchDir, filePath);
|
||||
}
|
||||
|
||||
export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir: string): CliArgs {
|
||||
return {
|
||||
...baseArgs,
|
||||
@@ -922,7 +952,7 @@ export function createTaskArgs(baseArgs: CliArgs, task: BatchTaskInput, batchDir
|
||||
imageSize: task.imageSize ?? baseArgs.imageSize ?? null,
|
||||
imageSizeSource: task.imageSize != null ? "task" : (baseArgs.imageSizeSource ?? null),
|
||||
imageApiDialect: task.imageApiDialect ?? baseArgs.imageApiDialect ?? null,
|
||||
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchPath(batchDir, filePath)) : [],
|
||||
referenceImages: task.ref ? task.ref.map((filePath) => resolveBatchReferencePath(batchDir, filePath)) : [],
|
||||
n: task.n ?? baseArgs.n,
|
||||
batchFile: null,
|
||||
jobs: baseArgs.jobs,
|
||||
@@ -946,7 +976,11 @@ async function prepareBatchTasks(
|
||||
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);
|
||||
if (taskArgs.referenceImages.length > 0) {
|
||||
await validateReferenceImages(taskArgs.referenceImages, {
|
||||
allowRemoteUrls: shouldAllowRemoteReferenceImages(taskArgs.provider),
|
||||
});
|
||||
}
|
||||
|
||||
const provider = detectProvider(taskArgs);
|
||||
const providerModule = await loadProviderModule(provider);
|
||||
|
||||
@@ -46,7 +46,7 @@ export function getDefaultModel(): string {
|
||||
}
|
||||
}
|
||||
|
||||
return process.env.AZURE_OPENAI_IMAGE_MODEL || "gpt-image-1.5";
|
||||
return process.env.AZURE_OPENAI_IMAGE_MODEL || "gpt-image-2";
|
||||
}
|
||||
|
||||
function getEndpoint(): AzureEndpoint {
|
||||
|
||||
@@ -2,15 +2,42 @@ import assert from "node:assert/strict";
|
||||
import test, { type TestContext } from "node:test";
|
||||
|
||||
import {
|
||||
generateImage,
|
||||
getDefaultModel,
|
||||
getModelFamily,
|
||||
getQwen2SizeFromAspectRatio,
|
||||
getSizeFromAspectRatio,
|
||||
getWan27SizeFromAspectRatio,
|
||||
normalizeSize,
|
||||
parseAspectRatio,
|
||||
parseSize,
|
||||
resolveSizeForModel,
|
||||
} from "./dashscope.ts";
|
||||
import type { CliArgs } from "../types.ts";
|
||||
|
||||
function makeCliArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
prompt: null,
|
||||
promptFiles: [],
|
||||
imagePath: null,
|
||||
provider: "dashscope",
|
||||
model: null,
|
||||
aspectRatio: null,
|
||||
aspectRatioSource: null,
|
||||
size: null,
|
||||
quality: "2k",
|
||||
imageSize: null,
|
||||
imageSizeSource: null,
|
||||
imageApiDialect: null,
|
||||
referenceImages: [],
|
||||
n: 1,
|
||||
batchFile: null,
|
||||
jobs: null,
|
||||
json: false,
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function useEnv(
|
||||
t: TestContext,
|
||||
@@ -51,9 +78,11 @@ test("DashScope aspect-ratio parsing accepts numeric ratios only", () => {
|
||||
assert.equal(parseAspectRatio("-1:2"), null);
|
||||
});
|
||||
|
||||
test("DashScope model family routing distinguishes qwen-2.0, fixed-size qwen, and legacy models", () => {
|
||||
test("DashScope model family routing distinguishes qwen-2.0, fixed-size qwen, wan2.7, and legacy models", () => {
|
||||
assert.equal(getModelFamily("qwen-image-2.0-pro"), "qwen2");
|
||||
assert.equal(getModelFamily("qwen-image"), "qwenFixed");
|
||||
assert.equal(getModelFamily("wan2.7-image"), "wan27");
|
||||
assert.equal(getModelFamily("wan2.7-image-pro"), "wan27");
|
||||
assert.equal(getModelFamily("z-image-turbo"), "legacy");
|
||||
assert.equal(getModelFamily("wanx-v1"), "legacy");
|
||||
});
|
||||
@@ -146,3 +175,218 @@ test("DashScope size normalization converts WxH into provider format", () => {
|
||||
assert.equal(normalizeSize("1024x1024"), "1024*1024");
|
||||
assert.equal(normalizeSize("2048*1152"), "2048*1152");
|
||||
});
|
||||
|
||||
test("Wan 2.7 derives sizes that match the requested ratio at the chosen pixel budget", () => {
|
||||
const square2k = getWan27SizeFromAspectRatio(null, "2k", 2048 * 2048);
|
||||
const parsedSquare = parseSize(square2k);
|
||||
assert.ok(parsedSquare);
|
||||
assert.equal(parsedSquare.width, parsedSquare.height);
|
||||
assert.ok(parsedSquare.width * parsedSquare.height <= 2048 * 2048);
|
||||
|
||||
const widescreen = getWan27SizeFromAspectRatio("16:9", "2k", 2048 * 2048);
|
||||
const parsedWide = parseSize(widescreen);
|
||||
assert.ok(parsedWide);
|
||||
assert.ok(Math.abs(parsedWide.width / parsedWide.height - 16 / 9) < 0.05);
|
||||
assert.ok(parsedWide.width * parsedWide.height <= 2048 * 2048);
|
||||
|
||||
const pro4k = getWan27SizeFromAspectRatio("16:9", "2k", 4096 * 4096);
|
||||
const parsed4k = parseSize(pro4k);
|
||||
assert.ok(parsed4k);
|
||||
assert.ok(parsed4k.width * parsed4k.height > 2048 * 2048);
|
||||
assert.ok(parsed4k.width * parsed4k.height <= 4096 * 4096);
|
||||
});
|
||||
|
||||
test("Wan 2.7 rejects aspect ratios outside the [1:8, 8:1] range", () => {
|
||||
assert.throws(
|
||||
() => getWan27SizeFromAspectRatio("9:1", "2k", 2048 * 2048),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
assert.throws(
|
||||
() => getWan27SizeFromAspectRatio("1:9", "normal", 2048 * 2048),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Wan 2.7 derived sizes stay inside the boundary ratio limits after rounding", () => {
|
||||
for (const ar of ["8:1", "1:8"]) {
|
||||
const size = getWan27SizeFromAspectRatio(ar, "2k", 2048 * 2048);
|
||||
const parsed = parseSize(size);
|
||||
assert.ok(parsed);
|
||||
const ratio = parsed.width / parsed.height;
|
||||
assert.ok(ratio >= 1 / 8);
|
||||
assert.ok(ratio <= 8);
|
||||
assert.ok(parsed.width * parsed.height <= 2048 * 2048);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveSizeForModel routes wan2.7-image to the 2K-capped derivation", () => {
|
||||
const size = resolveSizeForModel("wan2.7-image", {
|
||||
size: null,
|
||||
aspectRatio: "16:9",
|
||||
quality: "2k",
|
||||
});
|
||||
const parsed = parseSize(size);
|
||||
assert.ok(parsed);
|
||||
assert.ok(parsed.width * parsed.height <= 2048 * 2048);
|
||||
assert.ok(Math.abs(parsed.width / parsed.height - 16 / 9) < 0.05);
|
||||
});
|
||||
|
||||
test("resolveSizeForModel allows wan2.7-image-pro 4K only when there are no reference images", () => {
|
||||
assert.equal(
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "4096*4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
"4096*4096",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "4096*4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
const proWithRef = resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: null,
|
||||
aspectRatio: "1:1",
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
});
|
||||
const parsedRef = parseSize(proWithRef);
|
||||
assert.ok(parsedRef);
|
||||
assert.ok(parsedRef.width * parsedRef.height <= 2048 * 2048);
|
||||
});
|
||||
|
||||
test("Wan 2.7 request body forces n=1 and omits prompt_extend / negative_prompt", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedBody: any = null;
|
||||
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
||||
capturedBody = JSON.parse(String(init?.body));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: [{ image: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
await generateImage("hello", "wan2.7-image-pro", makeCliArgs({ aspectRatio: "1:1" }));
|
||||
|
||||
assert.equal(capturedBody.model, "wan2.7-image-pro");
|
||||
assert.deepEqual(Object.keys(capturedBody.parameters).sort(), ["n", "size", "watermark"]);
|
||||
assert.equal(capturedBody.parameters.n, 1);
|
||||
assert.equal(capturedBody.parameters.watermark, false);
|
||||
assert.equal(typeof capturedBody.parameters.size, "string");
|
||||
assert.ok(!("prompt_extend" in capturedBody.parameters));
|
||||
assert.ok(!("negative_prompt" in capturedBody.parameters));
|
||||
|
||||
assert.deepEqual(capturedBody.input.messages[0].content, [{ text: "hello" }]);
|
||||
});
|
||||
|
||||
test("Wan 2.7 request body forwards remote reference image URLs", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedBody: any = null;
|
||||
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
||||
capturedBody = JSON.parse(String(init?.body));
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
output: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: [{ image: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
await generateImage(
|
||||
"combine these",
|
||||
"wan2.7-image-pro",
|
||||
makeCliArgs({ referenceImages: ["https://example.com/ref.png"] }),
|
||||
);
|
||||
|
||||
assert.deepEqual(capturedBody.input.messages[0].content, [
|
||||
{ image: "https://example.com/ref.png" },
|
||||
{ text: "combine these" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("Wan 2.7 rejects --n > 1 to prevent silent multi-image billing", async (t) => {
|
||||
useEnv(t, { DASHSCOPE_API_KEY: "fake-key" });
|
||||
|
||||
await assert.rejects(
|
||||
() => generateImage("hi", "wan2.7-image-pro", makeCliArgs({ n: 2 })),
|
||||
/support exactly one output image/,
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSizeForModel validates explicit wan2.7 sizes by pixel budget and ratio", () => {
|
||||
assert.equal(
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3840x2160",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
"3840*2160",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3840x2160",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
referenceImages: ["a.png"],
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image", {
|
||||
size: "4096x4096",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
/total pixels between 768\*768 and 2048\*2048/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveSizeForModel("wan2.7-image-pro", {
|
||||
size: "3072*256",
|
||||
aspectRatio: null,
|
||||
quality: "2k",
|
||||
}),
|
||||
/1:8, 8:1/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import path from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type { CliArgs, Quality } from "../types";
|
||||
|
||||
type DashScopeModelFamily = "qwen2" | "qwenFixed" | "legacy";
|
||||
type DashScopeModelFamily = "qwen2" | "qwenFixed" | "wan27" | "legacy";
|
||||
|
||||
type DashScopeModelSpec = {
|
||||
family: DashScopeModelFamily;
|
||||
@@ -19,6 +21,16 @@ const QWEN_2_TARGET_PIXELS: Record<Quality, number> = {
|
||||
"2k": 1536 * 1536,
|
||||
};
|
||||
|
||||
const MIN_WAN27_TOTAL_PIXELS = 768 * 768;
|
||||
const MAX_WAN27_PRO_T2I_PIXELS = 4096 * 4096;
|
||||
const MAX_WAN27_GENERAL_PIXELS = 2048 * 2048;
|
||||
const WAN27_MAX_REFERENCE_IMAGES = 9;
|
||||
|
||||
const WAN27_TARGET_PIXELS: Record<Quality, number> = {
|
||||
normal: 1024 * 1024,
|
||||
"2k": 2048 * 2048,
|
||||
};
|
||||
|
||||
const QWEN_2_RECOMMENDED: Record<string, Record<Quality, string>> = {
|
||||
"1:1": { normal: "1024*1024", "2k": "1536*1536" },
|
||||
"2:3": { normal: "768*1152", "2k": "1024*1536" },
|
||||
@@ -73,6 +85,11 @@ const QWEN_FIXED_SPEC: DashScopeModelSpec = {
|
||||
defaultSize: QWEN_FIXED_SIZES_BY_RATIO["16:9"],
|
||||
};
|
||||
|
||||
const WAN27_SPEC: DashScopeModelSpec = {
|
||||
family: "wan27",
|
||||
defaultSize: "2048*2048",
|
||||
};
|
||||
|
||||
const LEGACY_SPEC: DashScopeModelSpec = {
|
||||
family: "legacy",
|
||||
defaultSize: "1536*1536",
|
||||
@@ -88,12 +105,31 @@ const MODEL_SPEC_ALIASES: Record<string, DashScopeModelSpec> = {
|
||||
"qwen-image-plus": QWEN_FIXED_SPEC,
|
||||
"qwen-image-plus-2026-01-09": QWEN_FIXED_SPEC,
|
||||
"qwen-image": QWEN_FIXED_SPEC,
|
||||
"wan2.7-image-pro": WAN27_SPEC,
|
||||
"wan2.7-image": WAN27_SPEC,
|
||||
};
|
||||
|
||||
export function getDefaultModel(): string {
|
||||
return process.env.DASHSCOPE_IMAGE_MODEL || DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
function getReferenceImageMime(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
||||
if (ext === ".webp") return "image/webp";
|
||||
if (ext === ".bmp") return "image/bmp";
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
async function loadReferenceImage(refPath: string): Promise<string> {
|
||||
if (/^https?:\/\//i.test(refPath)) {
|
||||
return refPath;
|
||||
}
|
||||
const fullPath = path.resolve(refPath);
|
||||
const bytes = await readFile(fullPath);
|
||||
return `data:${getReferenceImageMime(fullPath)};base64,${bytes.toString("base64")}`;
|
||||
}
|
||||
|
||||
function getApiKey(): string | null {
|
||||
return process.env.DASHSCOPE_API_KEY || null;
|
||||
}
|
||||
@@ -173,6 +209,10 @@ function roundToStep(value: number): number {
|
||||
return Math.max(SIZE_STEP, Math.round(value / SIZE_STEP) * SIZE_STEP);
|
||||
}
|
||||
|
||||
function floorToStep(value: number): number {
|
||||
return Math.max(SIZE_STEP, Math.floor(value / SIZE_STEP) * SIZE_STEP);
|
||||
}
|
||||
|
||||
function fitToPixelBudget(
|
||||
width: number,
|
||||
height: number,
|
||||
@@ -220,6 +260,21 @@ function fitToPixelBudget(
|
||||
return { width: roundedWidth, height: roundedHeight };
|
||||
}
|
||||
|
||||
function clampWan27DerivedSizeToRatioBounds(
|
||||
size: { width: number; height: number },
|
||||
): { width: number; height: number } {
|
||||
let { width, height } = size;
|
||||
const ratio = width / height;
|
||||
|
||||
if (ratio > 8) {
|
||||
width = floorToStep(height * 8);
|
||||
} else if (ratio < 1 / 8) {
|
||||
height = floorToStep(width * 8);
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
export function getSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
|
||||
const normalizedQuality = normalizeQuality(quality);
|
||||
const sizes = normalizedQuality === "2k" ? LEGACY_STANDARD_SIZES_2K : LEGACY_STANDARD_SIZES;
|
||||
@@ -276,6 +331,77 @@ export function getQwen2SizeFromAspectRatio(ar: string | null, quality: CliArgs[
|
||||
return formatSize(fitted.width, fitted.height);
|
||||
}
|
||||
|
||||
function isWan27ProModel(model: string): boolean {
|
||||
return model.trim().toLowerCase() === "wan2.7-image-pro";
|
||||
}
|
||||
|
||||
function getWan27MaxPixels(model: string, hasReferenceImages: boolean): number {
|
||||
if (isWan27ProModel(model) && !hasReferenceImages) {
|
||||
return MAX_WAN27_PRO_T2I_PIXELS;
|
||||
}
|
||||
return MAX_WAN27_GENERAL_PIXELS;
|
||||
}
|
||||
|
||||
export function getWan27SizeFromAspectRatio(
|
||||
ar: string | null,
|
||||
quality: CliArgs["quality"],
|
||||
maxPixels: number,
|
||||
): string {
|
||||
const normalizedQuality = normalizeQuality(quality);
|
||||
const targetPixels = Math.min(WAN27_TARGET_PIXELS[normalizedQuality], maxPixels);
|
||||
|
||||
if (!ar) {
|
||||
const side = roundToStep(Math.sqrt(targetPixels));
|
||||
return formatSize(side, side);
|
||||
}
|
||||
|
||||
const parsed = parseAspectRatio(ar);
|
||||
if (!parsed) {
|
||||
const side = roundToStep(Math.sqrt(targetPixels));
|
||||
return formatSize(side, side);
|
||||
}
|
||||
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (ratio < 1 / 8 || ratio > 8) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. Received "${ar}".`
|
||||
);
|
||||
}
|
||||
|
||||
const rawWidth = Math.sqrt(targetPixels * ratio);
|
||||
const rawHeight = Math.sqrt(targetPixels / ratio);
|
||||
const fitted = fitToPixelBudget(
|
||||
rawWidth,
|
||||
rawHeight,
|
||||
MIN_WAN27_TOTAL_PIXELS,
|
||||
maxPixels,
|
||||
);
|
||||
const bounded = clampWan27DerivedSizeToRatioBounds(fitted);
|
||||
|
||||
return formatSize(bounded.width, bounded.height);
|
||||
}
|
||||
|
||||
function validateWan27Size(size: string, maxPixels: number, model: string): string {
|
||||
const normalized = normalizeSize(size);
|
||||
const parsed = validateSizeFormat(normalized);
|
||||
const totalPixels = parsed.width * parsed.height;
|
||||
if (totalPixels < MIN_WAN27_TOTAL_PIXELS || totalPixels > maxPixels) {
|
||||
const limit = maxPixels === MAX_WAN27_PRO_T2I_PIXELS ? "4096*4096" : "2048*2048";
|
||||
throw new Error(
|
||||
`DashScope ${model} requires total pixels between 768*768 and ${limit} ` +
|
||||
`for the current request. Received ${normalized} (${totalPixels} pixels).`
|
||||
);
|
||||
}
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (ratio < 1 / 8 || ratio > 8) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. ` +
|
||||
`Received ${normalized} (ratio ${ratio.toFixed(3)}).`
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getQwenFixedSizeFromAspectRatio(ar: string | null, quality: CliArgs["quality"]): string {
|
||||
if (quality === "normal") {
|
||||
console.warn(
|
||||
@@ -331,9 +457,16 @@ function validateQwenFixedSize(size: string): string {
|
||||
|
||||
export function resolveSizeForModel(
|
||||
model: string,
|
||||
args: Pick<CliArgs, "size" | "aspectRatio" | "quality">,
|
||||
args: Pick<CliArgs, "size" | "aspectRatio" | "quality"> & { referenceImages?: string[] },
|
||||
): string {
|
||||
const spec = getModelSpec(model);
|
||||
const referenceCount = args.referenceImages?.length ?? 0;
|
||||
|
||||
if (spec.family === "wan27") {
|
||||
const maxPixels = getWan27MaxPixels(model, referenceCount > 0);
|
||||
if (args.size) return validateWan27Size(args.size, maxPixels, model);
|
||||
return getWan27SizeFromAspectRatio(args.aspectRatio, args.quality, maxPixels);
|
||||
}
|
||||
|
||||
if (args.size) {
|
||||
if (spec.family === "qwen2") return validateQwen2Size(args.size);
|
||||
@@ -357,6 +490,14 @@ function buildParameters(
|
||||
family: DashScopeModelFamily,
|
||||
size: string,
|
||||
): Record<string, unknown> {
|
||||
if (family === "wan27") {
|
||||
return {
|
||||
size,
|
||||
n: 1,
|
||||
watermark: false,
|
||||
};
|
||||
}
|
||||
|
||||
const parameters: Record<string, unknown> = {
|
||||
prompt_extend: false,
|
||||
size,
|
||||
@@ -419,23 +560,44 @@ export async function generateImage(
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) throw new Error("DASHSCOPE_API_KEY is required");
|
||||
|
||||
if (args.referenceImages.length > 0) {
|
||||
const spec = getModelSpec(model);
|
||||
|
||||
if (args.referenceImages.length > 0 && spec.family !== "wan27") {
|
||||
throw new Error(
|
||||
"Reference images are not supported with DashScope provider in baoyu-imagine. Use --provider google with a Gemini multimodal model."
|
||||
"Reference images are not supported with this DashScope model. Use a wan2.7 image model (--model wan2.7-image-pro or wan2.7-image), or switch to --provider google with a Gemini multimodal model."
|
||||
);
|
||||
}
|
||||
|
||||
if (args.referenceImages.length > WAN27_MAX_REFERENCE_IMAGES) {
|
||||
throw new Error(
|
||||
`DashScope wan2.7 image models accept at most ${WAN27_MAX_REFERENCE_IMAGES} reference images. Received ${args.referenceImages.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (spec.family === "wan27" && args.n !== 1) {
|
||||
throw new Error(
|
||||
"DashScope wan2.7 image models in baoyu-imagine support exactly one output image per request (extra images would be billed but discarded). Remove --n or use --n 1."
|
||||
);
|
||||
}
|
||||
|
||||
const spec = getModelSpec(model);
|
||||
const size = resolveSizeForModel(model, args);
|
||||
const url = `${getBaseUrl()}/api/v1/services/aigc/multimodal-generation/generation`;
|
||||
|
||||
const content: Array<Record<string, unknown>> = [];
|
||||
if (spec.family === "wan27" && args.referenceImages.length > 0) {
|
||||
for (const refPath of args.referenceImages) {
|
||||
content.push({ image: await loadReferenceImage(refPath) });
|
||||
}
|
||||
}
|
||||
content.push({ text: prompt });
|
||||
|
||||
const body = {
|
||||
model,
|
||||
input: {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ text: prompt }],
|
||||
content,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -61,8 +61,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
};
|
||||
}
|
||||
|
||||
test("MiniMax URL builder normalizes /v1 suffixes", (t) => {
|
||||
useEnv(t, { MINIMAX_BASE_URL: "https://api.minimax.io" });
|
||||
test("MiniMax URL builder uses documented default and normalizes /v1 suffixes", (t) => {
|
||||
useEnv(t, { MINIMAX_BASE_URL: null });
|
||||
assert.equal(buildMinimaxUrl(), "https://api.minimaxi.com/v1/image_generation");
|
||||
|
||||
process.env.MINIMAX_BASE_URL = "https://api.minimax.io";
|
||||
assert.equal(buildMinimaxUrl(), "https://api.minimax.io/v1/image_generation");
|
||||
|
||||
process.env.MINIMAX_BASE_URL = "https://proxy.example.com/custom/v1/";
|
||||
|
||||
@@ -44,7 +44,7 @@ function getApiKey(): string | null {
|
||||
}
|
||||
|
||||
export function buildMinimaxUrl(): string {
|
||||
const base = (process.env.MINIMAX_BASE_URL || "https://api.minimax.io").replace(/\/+$/g, "");
|
||||
const base = (process.env.MINIMAX_BASE_URL || "https://api.minimaxi.com").replace(/\/+$/g, "");
|
||||
return base.endsWith("/v1") ? `${base}/image_generation` : `${base}/v1/image_generation`;
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export async function generateImage(
|
||||
): Promise<Uint8Array> {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error("MINIMAX_API_KEY is required. Get one from https://platform.minimax.io/");
|
||||
throw new Error("MINIMAX_API_KEY is required. Get one from https://platform.minimaxi.com/");
|
||||
}
|
||||
|
||||
const body = await buildRequestBody(prompt, model, args);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { CliArgs } from "../types.ts";
|
||||
import {
|
||||
buildOpenAIGenerationsBody,
|
||||
extractImageFromResponse,
|
||||
getDefaultModel,
|
||||
getOpenAIAspectRatio,
|
||||
getOpenAIImageApiDialect,
|
||||
getOpenAIResolution,
|
||||
@@ -13,9 +15,33 @@ import {
|
||||
inferAspectRatioFromSize,
|
||||
inferResolutionFromSize,
|
||||
parseAspectRatio,
|
||||
validateArgs,
|
||||
} from "./openai.ts";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
prompt: null,
|
||||
promptFiles: [],
|
||||
imagePath: null,
|
||||
provider: null,
|
||||
model: null,
|
||||
aspectRatio: null,
|
||||
size: null,
|
||||
quality: "2k",
|
||||
imageSize: null,
|
||||
imageApiDialect: null,
|
||||
referenceImages: [],
|
||||
n: 1,
|
||||
batchFile: null,
|
||||
jobs: null,
|
||||
json: false,
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("OpenAI aspect-ratio parsing and size selection match model families", () => {
|
||||
assert.equal(getDefaultModel(), "gpt-image-2");
|
||||
assert.deepEqual(parseAspectRatio("16:9"), { width: 16, height: 9 });
|
||||
assert.equal(parseAspectRatio("wide"), null);
|
||||
assert.equal(parseAspectRatio("0:1"), null);
|
||||
@@ -25,6 +51,10 @@ test("OpenAI aspect-ratio parsing and size selection match model families", () =
|
||||
assert.equal(getOpenAISize("dall-e-2", "16:9", "2k"), "1024x1024");
|
||||
assert.equal(getOpenAISize("gpt-image-1.5", "16:9", "2k"), "1536x1024");
|
||||
assert.equal(getOpenAISize("gpt-image-1.5", "4:3", "2k"), "1024x1024");
|
||||
assert.equal(getOpenAISize("gpt-image-2", "16:9", "2k"), "2048x1152");
|
||||
assert.equal(getOpenAISize("gpt-image-2", "9:16", "2k"), "1152x2048");
|
||||
assert.equal(getOpenAISize("gpt-image-2", "4:3", "2k"), "2048x1536");
|
||||
assert.equal(getOpenAISize("gpt-image-2", "2.35:1", "normal"), "1248x528");
|
||||
assert.equal(inferAspectRatioFromSize("1536x1024"), "3:2");
|
||||
assert.equal(inferResolutionFromSize("1536x1024"), "2K");
|
||||
assert.equal(getOpenAIAspectRatio({ aspectRatio: null, size: "2048x1152" }), "16:9");
|
||||
@@ -37,7 +67,7 @@ test("OpenAI aspect-ratio parsing and size selection match model families", () =
|
||||
|
||||
test("OpenAI generations body switches between native and ratio-metadata dialects", () => {
|
||||
assert.deepEqual(
|
||||
buildOpenAIGenerationsBody("Draw a skyline", "gpt-image-1.5", {
|
||||
buildOpenAIGenerationsBody("Draw a skyline", "gpt-image-2", {
|
||||
aspectRatio: "16:9",
|
||||
size: null,
|
||||
quality: "2k",
|
||||
@@ -45,9 +75,10 @@ test("OpenAI generations body switches between native and ratio-metadata dialect
|
||||
imageApiDialect: null,
|
||||
}),
|
||||
{
|
||||
model: "gpt-image-1.5",
|
||||
model: "gpt-image-2",
|
||||
prompt: "Draw a skyline",
|
||||
size: "1536x1024",
|
||||
size: "2048x1152",
|
||||
quality: "high",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -90,6 +121,28 @@ test("OpenAI generations body switches between native and ratio-metadata dialect
|
||||
);
|
||||
});
|
||||
|
||||
test("OpenAI validates gpt-image-2 custom size constraints", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
validateArgs("gpt-image-2", makeArgs({ size: "3840x2160" })),
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
validateArgs("gpt-image-2-2026-04-21", makeArgs({ aspectRatio: "2.35:1" })),
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => validateArgs("gpt-image-2", makeArgs({ size: "1024x576" })),
|
||||
/total pixels/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateArgs("gpt-image-2", makeArgs({ size: "1025x1024" })),
|
||||
/multiples of 16px/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateArgs("gpt-image-2", makeArgs({ aspectRatio: "4:1" })),
|
||||
/must not exceed 3:1/,
|
||||
);
|
||||
});
|
||||
|
||||
test("OpenAI mime-type detection covers supported reference image extensions", () => {
|
||||
assert.equal(getMimeType("frame.png"), "image/png");
|
||||
assert.equal(getMimeType("frame.jpg"), "image/jpeg");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
|
||||
import type { CliArgs, OpenAIImageApiDialect } from "../types";
|
||||
|
||||
export function getDefaultModel(): string {
|
||||
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-1.5";
|
||||
return process.env.OPENAI_IMAGE_MODEL || "gpt-image-2";
|
||||
}
|
||||
|
||||
type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };
|
||||
@@ -25,6 +25,55 @@ type SizeMapping = {
|
||||
|
||||
type OpenAIGenerationsBody = Record<string, unknown>;
|
||||
|
||||
function isGptImageModel(model: string): boolean {
|
||||
return model.includes("gpt-image");
|
||||
}
|
||||
|
||||
function isGptImage2Model(model: string): boolean {
|
||||
return model.includes("gpt-image-2");
|
||||
}
|
||||
|
||||
function roundToMultiple(value: number, multiple: number): number {
|
||||
return Math.max(multiple, Math.round(value / multiple) * multiple);
|
||||
}
|
||||
|
||||
function buildGptImage2SizeFromAspectRatio(
|
||||
ar: string | null,
|
||||
quality: CliArgs["quality"],
|
||||
): string {
|
||||
const parsed = ar ? parseAspectRatio(ar) : null;
|
||||
const ratio = parsed ? parsed.width / parsed.height : 1;
|
||||
|
||||
if (!parsed || Math.abs(ratio - 1) < 0.1) {
|
||||
const edge = quality === "2k" ? 2048 : 1024;
|
||||
return `${edge}x${edge}`;
|
||||
}
|
||||
|
||||
const targetLongEdge = quality === "2k" ? 2048 : 1024;
|
||||
let width: number;
|
||||
let height: number;
|
||||
|
||||
if (ratio > 1) {
|
||||
width = targetLongEdge;
|
||||
height = roundToMultiple(width / ratio, 16);
|
||||
} else {
|
||||
height = targetLongEdge;
|
||||
width = roundToMultiple(height * ratio, 16);
|
||||
}
|
||||
|
||||
while (width * height < 655_360) {
|
||||
if (ratio > 1) {
|
||||
width += 16;
|
||||
height = roundToMultiple(width / ratio, 16);
|
||||
} else {
|
||||
height += 16;
|
||||
width = roundToMultiple(height * ratio, 16);
|
||||
}
|
||||
}
|
||||
|
||||
return `${width}x${height}`;
|
||||
}
|
||||
|
||||
export function getOpenAISize(
|
||||
model: string,
|
||||
ar: string | null,
|
||||
@@ -37,6 +86,10 @@ export function getOpenAISize(
|
||||
return "1024x1024";
|
||||
}
|
||||
|
||||
if (isGptImage2Model(model)) {
|
||||
return buildGptImage2SizeFromAspectRatio(ar, quality);
|
||||
}
|
||||
|
||||
const sizes: SizeMapping = isDalle3
|
||||
? {
|
||||
square: "1024x1024",
|
||||
@@ -127,6 +180,18 @@ export function getOpenAIResolution(
|
||||
return args.quality === "normal" ? "1K" : "2K";
|
||||
}
|
||||
|
||||
function getOpenAIQuality(model: string, quality: CliArgs["quality"]): "standard" | "hd" | "medium" | "high" | null {
|
||||
if (model.includes("dall-e-3")) {
|
||||
return quality === "2k" ? "hd" : "standard";
|
||||
}
|
||||
|
||||
if (isGptImageModel(model)) {
|
||||
return quality === "2k" ? "high" : "medium";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getOrientationFromAspectRatio(ar: string): "landscape" | "portrait" | null {
|
||||
const parsed = parseAspectRatio(ar);
|
||||
if (!parsed) return null;
|
||||
@@ -163,13 +228,53 @@ export function buildOpenAIGenerationsBody(
|
||||
size: args.size || getOpenAISize(model, args.aspectRatio, args.quality),
|
||||
};
|
||||
|
||||
if (model.includes("dall-e-3")) {
|
||||
body.quality = args.quality === "2k" ? "hd" : "standard";
|
||||
const quality = getOpenAIQuality(model, args.quality);
|
||||
if (quality) {
|
||||
body.quality = quality;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export function validateArgs(model: string, args: CliArgs): void {
|
||||
if (!isGptImage2Model(model)) return;
|
||||
|
||||
if (args.aspectRatio && !args.size) {
|
||||
const parsed = parseAspectRatio(args.aspectRatio);
|
||||
if (!parsed) {
|
||||
throw new Error(`Invalid gpt-image-2 aspect ratio: ${args.aspectRatio}`);
|
||||
}
|
||||
const ratio = parsed.width / parsed.height;
|
||||
if (Math.max(ratio, 1 / ratio) > 3) {
|
||||
throw new Error("gpt-image-2 aspect ratio must not exceed 3:1.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.size) return;
|
||||
|
||||
const parsedSize = parsePixelSize(args.size);
|
||||
if (!parsedSize) {
|
||||
throw new Error(`Invalid gpt-image-2 --size: ${args.size}. Expected <width>x<height>.`);
|
||||
}
|
||||
|
||||
const { width, height } = parsedSize;
|
||||
const totalPixels = width * height;
|
||||
const ratio = Math.max(width, height) / Math.min(width, height);
|
||||
|
||||
if (Math.max(width, height) > 3840) {
|
||||
throw new Error("gpt-image-2 --size maximum edge length must be 3840px or less.");
|
||||
}
|
||||
if (width % 16 !== 0 || height % 16 !== 0) {
|
||||
throw new Error("gpt-image-2 --size width and height must both be multiples of 16px.");
|
||||
}
|
||||
if (ratio > 3) {
|
||||
throw new Error("gpt-image-2 --size long edge to short edge ratio must not exceed 3:1.");
|
||||
}
|
||||
if (totalPixels < 655_360 || totalPixels > 8_294_400) {
|
||||
throw new Error("gpt-image-2 --size total pixels must be between 655,360 and 8,294,400.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateImage(
|
||||
prompt: string,
|
||||
model: string,
|
||||
@@ -198,7 +303,7 @@ export async function generateImage(
|
||||
}
|
||||
if (model.includes("dall-e-2") || model.includes("dall-e-3")) {
|
||||
throw new Error(
|
||||
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-1.5 (or another gpt-image model)."
|
||||
"Reference images with OpenAI in this skill require GPT Image models. Use --model gpt-image-2 (or another gpt-image model)."
|
||||
);
|
||||
}
|
||||
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
|
||||
@@ -283,8 +388,9 @@ async function generateWithOpenAIEdits(
|
||||
form.append("prompt", prompt);
|
||||
form.append("size", size);
|
||||
|
||||
if (model.includes("gpt-image")) {
|
||||
form.append("quality", quality === "2k" ? "high" : "medium");
|
||||
const openAIQuality = getOpenAIQuality(model, quality);
|
||||
if (openAIQuality && openAIQuality !== "standard" && openAIQuality !== "hd") {
|
||||
form.append("quality", openAIQuality);
|
||||
}
|
||||
|
||||
for (const refPath of referenceImages) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-infographic
|
||||
description: Generate professional infographics with 21 layout types and 21 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
|
||||
description: Generate professional infographics with 21 layout types and 22 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.58.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-infographic
|
||||
@@ -23,11 +23,20 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The backend receives the prompt file (or its content); the file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
@@ -64,14 +73,24 @@ references:
|
||||
- If `usage: direct` AND the chosen backend accepts reference images (e.g., `baoyu-imagine` via `--ref`) → pass the file via the backend's ref parameter
|
||||
- Otherwise → embed extracted `style`/`palette` traits in the prompt text
|
||||
|
||||
## Confirmation Policy
|
||||
|
||||
Default behavior: **confirm before generation**.
|
||||
|
||||
- Treat explicit skill invocation, a file path, a matched keyword shortcut, `EXTEND.md` defaults, and the documented default combination as **recommendation inputs only**. None of them authorizes skipping confirmation.
|
||||
- Do **not** start Step 5 or Step 6 until the user confirms the combination/aspect/language/backend choices.
|
||||
- Skip confirmation only when the current request explicitly says to do so, for example: `--no-confirm`, "直接生成", "不用确认", "跳过确认", "按默认出图", or equivalent wording.
|
||||
- If confirmation is skipped explicitly, state the assumed combination/aspect/language/backend in the next user-facing update before generating.
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Values |
|
||||
|--------|--------|
|
||||
| `--layout` | 21 options (see Layout Gallery), default: bento-grid |
|
||||
| `--style` | 21 options (see Style Gallery), default: craft-handmade |
|
||||
| `--style` | 22 options (see Style Gallery), default: craft-handmade |
|
||||
| `--aspect` | Named: landscape (16:9), portrait (9:16), square (1:1). Custom: any W:H ratio (e.g., 3:4, 4:3, 2.35:1) |
|
||||
| `--lang` | en, zh, ja, etc. |
|
||||
| `--no-confirm` | Skip Step 4 only when the user explicitly requests direct generation without confirmation |
|
||||
| `--ref <files...>` | Reference images (file paths) for style / palette / composition / subject guidance |
|
||||
|
||||
## Layout Gallery (21)
|
||||
@@ -102,7 +121,7 @@ references:
|
||||
|
||||
Full definitions live at `references/layouts/<layout>.md`.
|
||||
|
||||
## Style Gallery (21)
|
||||
## Style Gallery (22)
|
||||
|
||||
| Style | Description |
|
||||
|-------|-------------|
|
||||
@@ -127,6 +146,7 @@ Full definitions live at `references/layouts/<layout>.md`.
|
||||
| `morandi-journal` | Hand-drawn doodle, warm Morandi tones |
|
||||
| `retro-pop-grid` | 1970s retro pop art, Swiss grid, thick outlines |
|
||||
| `hand-drawn-edu` | Macaron pastels, hand-drawn wobble, stick figures |
|
||||
| `retro-popup-pop` | Retro popup collage, vintage UI, thick outlines, flat pop colors |
|
||||
|
||||
Full definitions live at `references/styles/<style>.md`.
|
||||
|
||||
@@ -149,18 +169,19 @@ Full definitions live at `references/styles/<style>.md`.
|
||||
| Product Guide | `dense-modules` + `morandi-journal` |
|
||||
| Technical Guide | `dense-modules` + `pop-laboratory` |
|
||||
| Trendy Guide | `dense-modules` + `retro-pop-grid` |
|
||||
| Retro Pop Guide | `dense-modules` + `retro-popup-pop` |
|
||||
| Educational Diagram | `hub-spoke` + `hand-drawn-edu` |
|
||||
| Process Tutorial | `linear-progression` + `hand-drawn-edu` |
|
||||
|
||||
Default combination: `bento-grid` + `craft-handmade`.
|
||||
Default combination: `bento-grid` + `craft-handmade` (fallback recommendation only — per the [Confirmation Policy](#confirmation-policy), defaults never bypass Step 4).
|
||||
|
||||
## Keyword Shortcuts
|
||||
|
||||
When the user's input contains these keywords, auto-select the layout and promote the listed styles to the top of Step 3 recommendations. Skip content-based layout inference for matched keywords. Append any `Prompt Notes` to the Step 5 prompt.
|
||||
When the user's input contains these keywords, use the mapped layout as the leading Step 3 recommendation and promote the listed styles to the top of the Step 3 list. Skip content-based layout inference for matched keywords. Append any `Prompt Notes` to the Step 5 prompt.
|
||||
|
||||
| User Keyword | Layout | Recommended Styles | Default Aspect | Prompt Notes |
|
||||
|--------------|--------|--------------------|----------------|--------------|
|
||||
| 高密度信息大图 / high-density-info | `dense-modules` | `morandi-journal`, `pop-laboratory`, `retro-pop-grid` | portrait | — |
|
||||
| 高密度信息大图 / high-density-info | `dense-modules` | `morandi-journal`, `pop-laboratory`, `retro-pop-grid`, `retro-popup-pop` | portrait | — |
|
||||
| 信息图 / infographic | `bento-grid` | `craft-handmade` | landscape | Minimalist: clean canvas, ample whitespace, no complex background textures. Simple cartoon elements and icons only. |
|
||||
|
||||
## Output Structure
|
||||
@@ -201,7 +222,7 @@ Check EXTEND.md in priority order — the first one found wins:
|
||||
| Found | Read, parse, display a one-line summary |
|
||||
| Not found | Ask the user with `AskUserQuestion` (see `references/config/first-time-setup.md`) |
|
||||
|
||||
**EXTEND.md supports**: preferred layout/style, default aspect ratio, custom style definitions, language preference.
|
||||
**EXTEND.md supports**: preferred layout/style, default aspect ratio, language preference, preferred image backend, custom style definitions.
|
||||
|
||||
Schema: `references/config/preferences-schema.md`
|
||||
|
||||
@@ -231,7 +252,7 @@ See `references/structured-content-template.md` for detailed format.
|
||||
|
||||
### Step 3: Recommend Combinations
|
||||
|
||||
**3.1 Check Keyword Shortcuts first**: If user input matches a keyword from the **Keyword Shortcuts** table, auto-select the associated layout and prioritize associated styles as top recommendations. Skip content-based layout inference.
|
||||
**3.1 Check Keyword Shortcuts first**: If user input matches a keyword from the **Keyword Shortcuts** table, use the associated layout as the leading recommendation and prioritize associated styles as top recommendations. Skip content-based layout inference.
|
||||
|
||||
**3.2 Otherwise**, recommend 3-5 layout×style combinations based on:
|
||||
- Data structure → matching layout
|
||||
@@ -241,6 +262,8 @@ See `references/structured-content-template.md` for detailed format.
|
||||
|
||||
### Step 4: Confirm Options
|
||||
|
||||
**Hard gate**: this step is mandatory per the [Confirmation Policy](#confirmation-policy) — Steps 5–6 cannot start until the user confirms here (or explicitly opts out with `--no-confirm` / equivalent in the current request).
|
||||
|
||||
Ask the user to confirm the questions below following the [User Input Tools](#user-input-tools) rule at the top of this file (batch into one call if the runtime supports multiple questions; otherwise ask one at a time in priority order).
|
||||
|
||||
| Priority | Question | When | Options |
|
||||
@@ -248,6 +271,7 @@ Ask the user to confirm the questions below following the [User Input Tools](#us
|
||||
| 1 | **Combination** | Always | 3+ layout×style combos with rationale |
|
||||
| 2 | **Aspect** | Always | Named presets (landscape/portrait/square) or custom W:H ratio (e.g., 3:4, 4:3, 2.35:1) |
|
||||
| 3 | **Language** | Only if source ≠ user language | Language for text content |
|
||||
| 4 | **Image Backend** | Only if step 3 of the `## Image Generation Tools` rule needs to ask (no runtime-native tool AND multiple non-native backends, OR `preferred_image_backend: ask`) | Available backends |
|
||||
|
||||
### Step 5: Generate Prompt → `prompts/infographic.md`
|
||||
|
||||
@@ -266,7 +290,7 @@ Combine:
|
||||
|
||||
### Step 6: Generate Image
|
||||
|
||||
1. Select the backend via the `## Image Generation Tools` rule at the top: use whatever is available; if multiple, ask the user once. Do this once per session.
|
||||
1. Resolve the backend per the `## Image Generation Tools` rule at the top of this file.
|
||||
2. Ensure the full final prompt is persisted at `prompts/infographic.md` (already written in Step 5) BEFORE invoking the backend — the file is the reproducibility record.
|
||||
3. **Check for existing file**: Before generating, check if `infographic.png` exists
|
||||
- If exists: Rename to `infographic-backup-YYYYMMDD-HHMMSS.png`
|
||||
@@ -275,7 +299,7 @@ Combine:
|
||||
|
||||
### Step 7: Output Summary
|
||||
|
||||
Report: topic, layout, style, aspect, language, output path, files created.
|
||||
Report: topic, layout, style, aspect, language, image backend, output path, files created.
|
||||
|
||||
## References
|
||||
|
||||
@@ -285,6 +309,15 @@ Report: topic, layout, style, aspect, language, output path, files created.
|
||||
- `references/layouts/<layout>.md` - 21 layout definitions
|
||||
- `references/styles/<style>.md` - 21 style definitions
|
||||
|
||||
## Extension Support
|
||||
## Changing Preferences
|
||||
|
||||
Custom configurations via EXTEND.md. See **Step 1.1** for paths and supported options.
|
||||
EXTEND.md lives at the first matching path in Step 1.1. Three ways to change it:
|
||||
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: `references/config/preferences-schema.md`.
|
||||
- **Reconfigure interactively** — delete EXTEND.md (or ask "reconfigure baoyu-infographic preferences" / "重新配置"). The next run re-triggers first-time setup.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `preferred_layout: dense-modules`, `preferred_style: morandi-journal`, `preferred_aspect: portrait`, `language: zh` — shift the Step-3 recommendations and Step-4 defaults (per [Confirmation Policy](#confirmation-policy), these never bypass Step 4).
|
||||
|
||||
@@ -39,7 +39,7 @@ Approach content analysis as a **world-class instructional designer**:
|
||||
| **System/Structure** | Components, architecture, anatomy | structural-breakdown, bento-grid | technical-schematic, ikea-manual |
|
||||
| **Journey/Narrative** | Stories, user flows, milestones | winding-roadmap, story-mountain | storybook-watercolor, comic-strip |
|
||||
| **Overview/Summary** | Multiple topics, feature highlights | bento-grid, periodic-table, dense-modules | chalkboard, bold-graphic |
|
||||
| **Product/Buying Guide** | Multi-dimension comparisons, specs, pitfalls | dense-modules | morandi-journal, pop-laboratory, retro-pop-grid |
|
||||
| **Product/Buying Guide** | Multi-dimension comparisons, specs, pitfalls | dense-modules | morandi-journal, pop-laboratory, retro-pop-grid, retro-popup-pop |
|
||||
|
||||
### 2. Learning Objective Identification
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ description: First-time setup flow for baoyu-infographic preferences
|
||||
|
||||
## Overview
|
||||
|
||||
When no EXTEND.md is found, guide the user through preference setup before generating any infographic.
|
||||
When no EXTEND.md is found, guide the user through preference setup before generating any infographic. Saved preferences shift Step-3 recommendations and Step-4 defaults only — they never bypass Step 4 confirmation (see the `## Confirmation Policy` section in SKILL.md).
|
||||
|
||||
**⛔ BLOCKING OPERATION**: This setup MUST complete before ANY other workflow steps. Do NOT:
|
||||
- Ask about source content or topic
|
||||
@@ -141,13 +141,13 @@ preferred_layout: [selected layout or null]
|
||||
preferred_style: [selected style or null]
|
||||
preferred_aspect: [landscape|portrait|square|null]
|
||||
language: [selected language or null]
|
||||
preferred_image_backend: auto
|
||||
custom_styles: []
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend: auto` is the baked-in default — first-time setup never asks about it. The `## Image Generation Tools` rule in SKILL.md then picks the runtime-native tool (Codex `imagegen`, Hermes `image_generate`, etc.) when one is available, and falls back to installed backends like `baoyu-imagine`.
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or trigger setup again:
|
||||
- Delete EXTEND.md to re-trigger setup
|
||||
- Edit YAML frontmatter for quick changes
|
||||
- Full schema: `references/config/preferences-schema.md`
|
||||
See the `## Changing Preferences` section in `SKILL.md` for the canonical list of common edits (pin backend, change layout/style defaults, retrigger setup). Full schema: `references/config/preferences-schema.md`.
|
||||
|
||||
@@ -17,6 +17,8 @@ preferred_aspect: null # landscape|portrait|square|null (custom W:H also acc
|
||||
|
||||
language: null # zh|en|ja|ko|null (null = auto-detect from source)
|
||||
|
||||
preferred_image_backend: auto # auto|ask|<backend-id>
|
||||
|
||||
custom_styles: # extra style definitions merged with the 21 built-ins
|
||||
- name: my-brand
|
||||
description: "Short description shown in Step 3 recommendations"
|
||||
@@ -33,8 +35,21 @@ custom_styles: # extra style definitions merged with the 21 built-ins
|
||||
| `preferred_style` | string\|null | null | Pre-selected style — surfaces as the top recommendation in Step 3 |
|
||||
| `preferred_aspect` | string\|null | null | Default aspect for Step 4 (named preset or W:H string) |
|
||||
| `language` | string\|null | null | Output language (null = auto-detect from source content) |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. |
|
||||
| `custom_styles` | array | [] | Additional styles available alongside the 21 built-ins |
|
||||
|
||||
Backend resolution logic is documented in the `## Image Generation Tools` section of `SKILL.md`. This doc only defines the field.
|
||||
|
||||
All fields in this schema are defaults only — they shape Step-3 recommendations and Step-4 defaults but never bypass Step 4 confirmation (see the `## Confirmation Policy` section in SKILL.md).
|
||||
|
||||
Example backend ids:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `codex-imagegen` | Codex built-in `imagegen` tool |
|
||||
| `baoyu-imagine` | `baoyu-imagine` skill / script backend |
|
||||
| `image_generate` | Generic runtime image tool such as Hermes |
|
||||
|
||||
## Layout Options
|
||||
|
||||
See the **Layout Gallery (21)** table in `SKILL.md` for the canonical list. Common picks:
|
||||
@@ -87,6 +102,8 @@ language: zh
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend` is omitted above; absence is treated as `auto`.
|
||||
|
||||
## Example: Full Preferences
|
||||
|
||||
```yaml
|
||||
@@ -99,6 +116,8 @@ preferred_aspect: portrait
|
||||
|
||||
language: zh
|
||||
|
||||
preferred_image_backend: codex-imagegen
|
||||
|
||||
custom_styles:
|
||||
- name: my-brand
|
||||
description: "Brand-aligned warm pastel infographic"
|
||||
|
||||
@@ -68,5 +68,6 @@ High-density modular layout with 6-7 typed information modules packed with concr
|
||||
- `pop-laboratory`: Technical precision with coordinate markers and blueprint grid
|
||||
- `morandi-journal`: Hand-drawn warmth with doodle illustrations and organic frames
|
||||
- `retro-pop-grid`: 1970s pop art with strict grid cells and bold contrast
|
||||
- `retro-popup-pop`: Vintage desktop popups with chunky pixel UI for retro-tech dense guides
|
||||
- `corporate-memphis`: Clean business feel for product comparisons
|
||||
- `technical-schematic`: Engineering precision for technical product guides
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# retro-popup-pop
|
||||
|
||||
Retro pixel popup × pop-art collage — content rendered as a stack of 80/90s desktop dialog windows with thick black outlines, flat color fills, and bright cyan or vintage cream backgrounds.
|
||||
|
||||
## Color Palette
|
||||
|
||||
- Background: Bright cyan (#12B8DE) primary canvas, vintage cream (#F5F0E6) alternate
|
||||
- Window fills: Vintage cream (#F5F0E6) and pure white (#FFFFFF)
|
||||
- Primary text and outlines: Pure black (#000000)
|
||||
- Reverse text: Pure white on solid black title bars
|
||||
- Accent fills (small areas only): Salmon pink, sky blue, mustard yellow, mint green — muted retro tones
|
||||
|
||||
## Visual Elements
|
||||
|
||||
- 80s/90s desktop popup windows with title bars, close buttons (×), and chunky borders
|
||||
- Multiple windows tiled or lightly overlapping with offset solid-black drop shadows for depth
|
||||
- ERROR / ALERT / WARNING dialogs to highlight misconceptions, common pitfalls, risks
|
||||
- File-window vignettes labeled with retro filenames (PROBLEMS.EXE, METHOD.PNG, BURNOUT.PSD, FOCUS_SCAN, SYSTEM_ALERT, IDEAS_MISSING)
|
||||
- Progress bars for completion, difficulty, conversion rate, or trend
|
||||
- Pixel-style chunky icons: folders, magnifiers, gears, exclamation marks, floppy disks, hourglasses
|
||||
- Action buttons with thick black borders and short pop-style copy: OK, CANCEL, FIX IT, SCAN, PLAY, RETRY, SAVE
|
||||
- Comparison data rendered as windowed lists or small tabular dialogs
|
||||
- Uniform thick black outlines on every window, button, icon, and divider
|
||||
- Pure 2D flat color fills throughout — no gradients, no glow, no glass
|
||||
|
||||
## Typography
|
||||
|
||||
- Headers: Pixel / dot-matrix / chunky bitmap display fonts, large and high-contrast
|
||||
- Body: Retro monospace or system-style sans-serif, high legibility
|
||||
- Title bars: Reverse white on solid black, all-caps preferred
|
||||
- Decorative all-caps English allowed for filenames, status strings, button labels (PROBLEMS.EXE, OK, CANCEL); body content text remains in the confirmed output language
|
||||
|
||||
## Style Enforcement
|
||||
|
||||
- Every information chunk must live inside a window, dialog, button, or progress bar — no floating elements
|
||||
- Uniform thick black outlines everywhere; offset solid-black drop shadows for stacked-window depth
|
||||
- Limit accent palette to ~4 colors per composition; let cyan or cream dominate the canvas
|
||||
- Maintain low-fi pixel/popup feel; humorous popup copy welcome, polished modern UI prohibited
|
||||
|
||||
## Avoid
|
||||
|
||||
- Modern UI styles: glassmorphism, neumorphism, soft shadows, blurs, gradients
|
||||
- 3D rendering, photorealism, ray-traced shadows
|
||||
- Hand-drawn wobble, watercolor washes, or sketch textures
|
||||
- Smooth anti-aliased curves on icons (prefer chunky pixel/stair-step edges)
|
||||
- Pure black backgrounds — keep cyan or cream as the canvas
|
||||
|
||||
## Best For
|
||||
|
||||
High-density "干货" knowledge guides, common-pitfall and debunking posts, knowledge-pop content for design-savvy or developer audiences, Xiaohongshu-style retro-tech posts, dense-modules portrait infographics.
|
||||
@@ -245,7 +245,7 @@ Files created:
|
||||
|-------|-----|
|
||||
| Missing API credentials | Follow guided setup in Step 2 |
|
||||
| Access token error | Verify credentials valid and not expired |
|
||||
| Not logged in (browser) | First run opens browser — scan QR to log in |
|
||||
| Not logged in (browser) | First run opens browser — scan QR to log in. Set `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` to receive the QR image via Telegram |
|
||||
| Chrome not found | Set `WECHAT_BROWSER_CHROME_PATH` |
|
||||
| Title/summary missing | Use auto-generation or provide manually |
|
||||
| No cover image | Add frontmatter cover or place `imgs/cover.png` in article directory |
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const clipboardScript = fs.readFileSync(path.join(__dirname, "copy-to-clipboard.ts"), "utf8");
|
||||
|
||||
test("macOS image clipboard copy avoids Swift AppKit JIT", () => {
|
||||
assert.match(clipboardScript, /copyImageMacWithOsascript/);
|
||||
assert.doesNotMatch(clipboardScript, /await runCommand\('swift', \[swiftPath, 'image', imagePath\]\)/);
|
||||
});
|
||||
|
||||
test("macOS image clipboard copy converts WebP to PNG before AppleScript", () => {
|
||||
assert.match(clipboardScript, /convertWebpMacToPng/);
|
||||
assert.match(clipboardScript, /path\.extname\(imagePath\)\.toLowerCase\(\) === '\.webp'/);
|
||||
assert.match(clipboardScript, /await copyImageMacWithOsascript\(pngPath\)/);
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import decodeWebp, { init as initWebpDecode } from '@jsquash/webp/decode.js';
|
||||
import { Jimp, JimpMime } from 'jimp';
|
||||
|
||||
const SUPPORTED_IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
|
||||
|
||||
@@ -186,12 +189,73 @@ default:
|
||||
`;
|
||||
}
|
||||
|
||||
async function copyImageMac(imagePath: string): Promise<void> {
|
||||
await withTempDir('copy-to-clipboard-', async (tempDir) => {
|
||||
const swiftPath = path.join(tempDir, 'clipboard.swift');
|
||||
await writeFile(swiftPath, getMacSwiftClipboardSource(), 'utf8');
|
||||
await runCommand('swift', [swiftPath, 'image', imagePath]);
|
||||
function escapeAppleScriptString(value: string): string {
|
||||
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function getAppleScriptImageType(imagePath: string): string {
|
||||
const ext = path.extname(imagePath).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.png':
|
||||
return '«class PNGf»';
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
return 'JPEG picture';
|
||||
case '.gif':
|
||||
return 'GIF picture';
|
||||
default:
|
||||
throw new Error(`macOS clipboard image copy supports PNG, JPEG, and GIF via AppleScript; convert ${ext || 'this file'} to PNG first.`);
|
||||
}
|
||||
}
|
||||
|
||||
let webpDecoderReady: Promise<void> | undefined;
|
||||
|
||||
async function ensureWebpDecoder(): Promise<void> {
|
||||
if (!webpDecoderReady) {
|
||||
webpDecoderReady = (async () => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const wasmPath = path.resolve(__dirname, 'node_modules/@jsquash/webp/codec/dec/webp_dec.wasm');
|
||||
const wasmModule = await WebAssembly.compile(await readFile(wasmPath));
|
||||
await initWebpDecode(wasmModule, {});
|
||||
})();
|
||||
}
|
||||
|
||||
await webpDecoderReady;
|
||||
}
|
||||
|
||||
async function convertWebpMacToPng(webpPath: string, tempDir: string): Promise<string> {
|
||||
await ensureWebpDecoder();
|
||||
const decoded = await decodeWebp(await readFile(webpPath));
|
||||
const image = new Jimp({
|
||||
data: Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength),
|
||||
width: decoded.width,
|
||||
height: decoded.height,
|
||||
});
|
||||
const pngPath = path.join(tempDir, `${path.basename(webpPath, path.extname(webpPath))}.png`);
|
||||
await writeFile(pngPath, await image.getBuffer(JimpMime.png));
|
||||
return pngPath;
|
||||
}
|
||||
|
||||
async function copyImageMacWithOsascript(imagePath: string): Promise<void> {
|
||||
const imageType = getAppleScriptImageType(imagePath);
|
||||
const escapedPath = escapeAppleScriptString(imagePath);
|
||||
await runCommand('osascript', [
|
||||
'-e',
|
||||
`set the clipboard to (read (POSIX file "${escapedPath}") as ${imageType})`,
|
||||
]);
|
||||
}
|
||||
|
||||
async function copyImageMac(imagePath: string): Promise<void> {
|
||||
if (path.extname(imagePath).toLowerCase() === '.webp') {
|
||||
await withTempDir('copy-to-clipboard-', async (tempDir) => {
|
||||
const pngPath = await convertWebpMacToPng(imagePath, tempDir);
|
||||
await copyImageMacWithOsascript(pngPath);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await copyImageMacWithOsascript(imagePath);
|
||||
}
|
||||
|
||||
async function copyHtmlMac(htmlFilePath: string): Promise<void> {
|
||||
@@ -377,4 +441,3 @@ await main().catch((err) => {
|
||||
console.error(`Error: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const articleScript = fs.readFileSync(path.join(__dirname, "wechat-article.ts"), "utf8");
|
||||
|
||||
test("browser article paste uses CDP-targeted paste instead of global macOS keystrokes", () => {
|
||||
assert.equal(
|
||||
articleScript.includes('tell application "System Events" to keystroke "v" using command down'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("browser article publishing verifies the title before saving drafts", () => {
|
||||
assert.match(articleScript, /verifyTitleUnchangedBeforeSave/);
|
||||
assert.match(articleScript, /Title was modified during paste/);
|
||||
});
|
||||
|
||||
test("browser article body insertion does not paste HTML through the active form field", () => {
|
||||
assert.match(articleScript, /insertHtmlIntoEditorFromFile/);
|
||||
assert.doesNotMatch(articleScript, /await copyHtmlFromBrowser\(cdp, effectiveHtmlFile, contentImages\)/);
|
||||
});
|
||||
|
||||
test("browser article body operations target the body ProseMirror, not the title ProseMirror", () => {
|
||||
assert.match(articleScript, /BODY_EDITOR_SELECTOR = '\.rich_media_content \.ProseMirror'/);
|
||||
assert.doesNotMatch(articleScript, /clickElement\(session, '\\.ProseMirror'\)/);
|
||||
});
|
||||
|
||||
test("browser article reuses the selected account Chrome profile before launching", () => {
|
||||
assert.match(articleScript, /await findExistingChromeDebugPort\(profileDir\)/);
|
||||
});
|
||||
|
||||
test("browser article inserts inline images through WeChat local upload instead of clipboard paste", () => {
|
||||
assert.match(articleScript, /uploadImageThroughFileInput/);
|
||||
assert.match(articleScript, /DOM\.setFileInputFiles/);
|
||||
assert.doesNotMatch(articleScript, /await copyImageToClipboard\(img\.localPath\)/);
|
||||
});
|
||||
|
||||
test("browser article uploads original images before fallback processing", () => {
|
||||
const rawUploadIndex = articleScript.indexOf("await uploadImagePathThroughFileInput(session, absolutePath, beforeCount)");
|
||||
const fallbackIndex = articleScript.indexOf("const fallback = await prepareFallbackWechatBodyImageUpload(absolutePath)");
|
||||
|
||||
assert.notEqual(rawUploadIndex, -1);
|
||||
assert.notEqual(fallbackIndex, -1);
|
||||
assert.ok(rawUploadIndex < fallbackIndex);
|
||||
assert.match(articleScript, /prepareWechatBodyImageUpload/);
|
||||
assert.match(articleScript, /Raw image upload failed, retrying with fallback processing/);
|
||||
});
|
||||
|
||||
test("browser article waits for a saved draft appmsgid before reporting success", () => {
|
||||
assert.match(articleScript, /waitForDraftSaved/);
|
||||
assert.match(articleScript, /appmsgid/);
|
||||
assert.doesNotMatch(articleScript, /Waiting for save confirmation/);
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, getAccountProfileDir, type ChromeSession, type CdpConnection } from './cdp.ts';
|
||||
import { loadWechatExtendConfig, resolveAccount } from './wechat-extend-config.ts';
|
||||
import { prepareWechatBodyImageUpload } from './wechat-image-processor.ts';
|
||||
|
||||
const WECHAT_URL = 'https://mp.weixin.qq.com/';
|
||||
const BODY_EDITOR_SELECTOR = '.rich_media_content .ProseMirror';
|
||||
|
||||
interface ImageInfo {
|
||||
placeholder: string;
|
||||
@@ -31,7 +34,98 @@ interface ArticleOptions {
|
||||
cdpPort?: number;
|
||||
}
|
||||
|
||||
async function sendQrToTelegram(session: ChromeSession): Promise<void> {
|
||||
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||
if (!botToken || !chatId) return;
|
||||
|
||||
// Wait for QR to render before extracting
|
||||
await sleep(2000);
|
||||
|
||||
try {
|
||||
// Try to extract QR image from DOM first (avoids full-page screenshot noise)
|
||||
const domResult = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||
expression: `
|
||||
(function() {
|
||||
const selectors = [
|
||||
'.login__type__container__scan img',
|
||||
'.login_img img',
|
||||
'#login_container img',
|
||||
'.qrcode img',
|
||||
'img[src*="qrcode"]',
|
||||
'img[src*="login"]',
|
||||
];
|
||||
for (const sel of selectors) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el?.src && !el.src.startsWith('data:,')) return el.src.startsWith('data:') ? el.src : 'url:' + el.src;
|
||||
}
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (canvas) try { return canvas.toDataURL('image/png'); } catch {}
|
||||
return '';
|
||||
})()
|
||||
`,
|
||||
returnByValue: true,
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
const raw = (domResult.result.value as string) ?? '';
|
||||
let imgBuffer: Buffer;
|
||||
|
||||
if (raw.startsWith('data:image')) {
|
||||
imgBuffer = Buffer.from(raw.split(',')[1] ?? '', 'base64');
|
||||
} else if (raw.startsWith('url:')) {
|
||||
// Fetch inside Chrome to carry WeChat session cookies
|
||||
const imgUrl = raw.slice(4);
|
||||
const inBrowserFetch = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||
expression: `
|
||||
(async () => {
|
||||
const resp = await fetch(${JSON.stringify(imgUrl)}, { credentials: 'include' });
|
||||
const buf = await resp.arrayBuffer();
|
||||
const bytes = new Uint8Array(buf);
|
||||
let b = '';
|
||||
for (let i = 0; i < bytes.length; i++) b += String.fromCharCode(bytes[i]);
|
||||
return btoa(b);
|
||||
})()
|
||||
`,
|
||||
returnByValue: true,
|
||||
awaitPromise: true,
|
||||
}, { sessionId: session.sessionId });
|
||||
imgBuffer = Buffer.from((inBrowserFetch.result.value as string) ?? '', 'base64');
|
||||
} else {
|
||||
// Fallback: viewport screenshot (smaller than full-page; QR is usually in viewport)
|
||||
const screenshotResp = await session.cdp.send<{ data: string }>(
|
||||
'Page.captureScreenshot', { format: 'png', captureBeyondViewport: false }, { sessionId: session.sessionId }
|
||||
);
|
||||
imgBuffer = Buffer.from(screenshotResp.data ?? '', 'base64');
|
||||
}
|
||||
|
||||
const boundary = `tgboundary${Date.now()}`;
|
||||
const parts: Buffer[] = [
|
||||
Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n${chatId}\r\n`),
|
||||
Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\nWeChat QR code — please scan to log in\r\n`),
|
||||
Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="photo"; filename="qr.png"\r\nContent-Type: image/png\r\n\r\n`),
|
||||
imgBuffer,
|
||||
Buffer.from(`\r\n--${boundary}--\r\n`),
|
||||
];
|
||||
const tgResp = await fetch(`https://api.telegram.org/bot${botToken}/sendPhoto`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
|
||||
body: Buffer.concat(parts),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
const tgJson = await tgResp.json() as { ok: boolean; description?: string };
|
||||
if (tgJson.ok) {
|
||||
console.log('[wechat] QR code sent to Telegram.');
|
||||
} else {
|
||||
console.error('[wechat] Telegram send failed:', tgJson.description);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[wechat] Failed to send QR to Telegram:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promise<boolean> {
|
||||
// Notify via Telegram if configured (no-op when env vars absent)
|
||||
await sendQrToTelegram(session);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const url = await evaluate<string>(session, 'window.location.href');
|
||||
@@ -105,27 +199,27 @@ async function pasteInEditor(session: ChromeSession): Promise<void> {
|
||||
}
|
||||
|
||||
async function sendCopy(cdp?: CdpConnection, sessionId?: string): Promise<void> {
|
||||
if (process.platform === 'darwin') {
|
||||
if (cdp && sessionId) {
|
||||
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'c', code: 'KeyC', modifiers, windowsVirtualKeyCode: 67 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'c', code: 'KeyC', modifiers, windowsVirtualKeyCode: 67 }, { sessionId });
|
||||
} else if (process.platform === 'darwin') {
|
||||
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "c" using command down']);
|
||||
} else if (process.platform === 'linux') {
|
||||
spawnSync('xdotool', ['key', 'ctrl+c']);
|
||||
} else if (cdp && sessionId) {
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'c', code: 'KeyC', modifiers: 2, windowsVirtualKeyCode: 67 }, { sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void> {
|
||||
if (process.platform === 'darwin') {
|
||||
spawnSync('osascript', ['-e', 'tell application "System Events" to keystroke "v" using command down']);
|
||||
} else if (process.platform === 'linux') {
|
||||
spawnSync('xdotool', ['key', 'ctrl+v']);
|
||||
} else if (cdp && sessionId) {
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers: 2, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
if (!cdp || !sessionId) {
|
||||
throw new Error('Targeted paste requires a Chrome DevTools session');
|
||||
}
|
||||
|
||||
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
await sleep(50);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'v', code: 'KeyV', modifiers, windowsVirtualKeyCode: 86 }, { sessionId });
|
||||
}
|
||||
|
||||
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string, contentImages: ImageInfo[] = []): Promise<void> {
|
||||
@@ -180,6 +274,10 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string, con
|
||||
}, { sessionId });
|
||||
await sleep(300);
|
||||
|
||||
console.log('[wechat] Activating HTML tab for copy...');
|
||||
await cdp.send('Target.activateTarget', { targetId });
|
||||
await sleep(300);
|
||||
|
||||
console.log('[wechat] Copying content...');
|
||||
await sendCopy(cdp, sessionId);
|
||||
await sleep(1000);
|
||||
@@ -189,11 +287,92 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string, con
|
||||
}
|
||||
|
||||
async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void> {
|
||||
console.log('[wechat] Activating editor tab for paste...');
|
||||
if (session.targetId) {
|
||||
await session.cdp.send('Target.activateTarget', { targetId: session.targetId });
|
||||
await sleep(300);
|
||||
}
|
||||
console.log('[wechat] Pasting content...');
|
||||
await sendPaste(session.cdp, session.sessionId);
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
async function insertHtmlIntoEditorFromFile(
|
||||
session: ChromeSession,
|
||||
htmlFilePath: string,
|
||||
contentImages: ImageInfo[] = [],
|
||||
): Promise<void> {
|
||||
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
|
||||
const html = fs.readFileSync(absolutePath, 'utf8');
|
||||
const replacements = contentImages.map(img => ({ placeholder: img.placeholder, localPath: img.localPath }));
|
||||
|
||||
const result = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', {
|
||||
expression: `
|
||||
(function() {
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return JSON.stringify({ ok: false, reason: 'editor-missing' });
|
||||
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = ${JSON.stringify(html)};
|
||||
const replacements = ${JSON.stringify(replacements)};
|
||||
|
||||
for (const img of Array.from(template.content.querySelectorAll('img'))) {
|
||||
const src = img.getAttribute('src') || '';
|
||||
const localPath = img.getAttribute('data-local-path') || '';
|
||||
const replacement = replacements.find((item) => item.placeholder === src || item.localPath === localPath);
|
||||
if (replacement) {
|
||||
img.replaceWith(document.createTextNode(replacement.placeholder));
|
||||
}
|
||||
}
|
||||
|
||||
const output = template.content.querySelector('#output');
|
||||
const wrapper = document.createElement('div');
|
||||
if (output) {
|
||||
wrapper.innerHTML = output.innerHTML;
|
||||
} else {
|
||||
wrapper.appendChild(template.content.cloneNode(true));
|
||||
}
|
||||
|
||||
editor.focus();
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(editor);
|
||||
range.deleteContents();
|
||||
range.collapse(true);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
const inserted = document.execCommand('insertHTML', false, wrapper.innerHTML);
|
||||
editor.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
inputType: 'insertHTML',
|
||||
data: wrapper.innerText || ''
|
||||
}));
|
||||
|
||||
return JSON.stringify({
|
||||
ok: inserted || (editor.innerText || '').trim().length > 0,
|
||||
textLength: (editor.innerText || '').trim().length
|
||||
});
|
||||
})()
|
||||
`,
|
||||
returnByValue: true,
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
const parsed = JSON.parse(result.result.value || '{}') as { ok?: boolean; reason?: string; textLength?: number };
|
||||
if (!parsed.ok) {
|
||||
throw new Error(`Failed to insert HTML into body editor${parsed.reason ? `: ${parsed.reason}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyTitleUnchangedBeforeSave(session: ChromeSession, expectedTitle: string): Promise<void> {
|
||||
if (!expectedTitle) return;
|
||||
|
||||
const actualTitle = await evaluate<string>(session, `document.querySelector('#title')?.value || ''`);
|
||||
if (actualTitle !== expectedTitle) {
|
||||
throw new Error(`Title was modified during paste. Expected: "${expectedTitle}", got: "${actualTitle}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareEditorPasteTarget(
|
||||
session: ChromeSession,
|
||||
context: string,
|
||||
@@ -203,19 +382,24 @@ async function prepareEditorPasteTarget(
|
||||
await sleep(100);
|
||||
|
||||
if (options.clickEditor) {
|
||||
await clickElement(session, '.ProseMirror');
|
||||
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
const ready = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
|
||||
const active = document.activeElement;
|
||||
const selection = window.getSelection();
|
||||
const selectionInEditor = !!selection && selection.rangeCount > 0 && !!selection.anchorNode && editor.contains(selection.anchorNode);
|
||||
const focusInEditor = !!active && (active === editor || editor.contains(active));
|
||||
const activeIsUnsafeInput = !!active && (
|
||||
active.matches?.('#title, #author, #js_description') ||
|
||||
((active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') && !editor.contains(active))
|
||||
);
|
||||
if (activeIsUnsafeInput) return false;
|
||||
if (selectionInEditor || focusInEditor) return true;
|
||||
|
||||
if (${JSON.stringify(Boolean(options.clickEditor))}) {
|
||||
@@ -338,7 +522,7 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
||||
const result = await session.cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||
expression: `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
|
||||
const placeholder = ${JSON.stringify(placeholder)};
|
||||
@@ -356,6 +540,7 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
||||
// Exact match if next char is not a digit
|
||||
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
editor.focus();
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(node, idx);
|
||||
@@ -386,7 +571,7 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
|
||||
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
|
||||
const removed = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
|
||||
const sel = window.getSelection();
|
||||
@@ -448,6 +633,188 @@ async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<b
|
||||
return removed;
|
||||
}
|
||||
|
||||
async function getBodyImageCount(session: ChromeSession): Promise<number> {
|
||||
return await evaluate<number>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return 0;
|
||||
return Array.from(editor.querySelectorAll('img')).filter((img) => !img.classList.contains('ProseMirror-separator')).length;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async function waitForBodyImageCount(session: ChromeSession, minimumCount: number, timeoutMs = 45_000): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const count = await getBodyImageCount(session);
|
||||
if (count >= minimumCount) return true;
|
||||
await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function inferImageContentType(imagePath: string): string {
|
||||
const ext = path.extname(imagePath).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
return 'image/jpeg';
|
||||
case '.png':
|
||||
return 'image/png';
|
||||
case '.gif':
|
||||
return 'image/gif';
|
||||
case '.webp':
|
||||
return 'image/webp';
|
||||
case '.bmp':
|
||||
return 'image/bmp';
|
||||
case '.svg':
|
||||
return 'image/svg+xml';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadImagePathThroughFileInput(
|
||||
session: ChromeSession,
|
||||
absolutePath: string,
|
||||
beforeCount: number,
|
||||
): Promise<void> {
|
||||
const documentNode = await session.cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {
|
||||
depth: -1,
|
||||
pierce: true,
|
||||
}, { sessionId: session.sessionId });
|
||||
const inputNode = await session.cdp.send<{ nodeId: number }>('DOM.querySelector', {
|
||||
nodeId: documentNode.root.nodeId,
|
||||
selector: 'input[type="file"][accept*="image"]',
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
if (!inputNode.nodeId) throw new Error('WeChat local image upload input not found');
|
||||
|
||||
await session.cdp.send('DOM.setFileInputFiles', {
|
||||
nodeId: inputNode.nodeId,
|
||||
files: [absolutePath],
|
||||
}, { sessionId: session.sessionId });
|
||||
|
||||
const inserted = await waitForBodyImageCount(session, beforeCount + 1);
|
||||
if (!inserted) {
|
||||
const afterCount = await getBodyImageCount(session);
|
||||
throw new Error(`Image upload did not insert into editor: ${path.basename(absolutePath)} (${beforeCount} -> ${afterCount})`);
|
||||
}
|
||||
}
|
||||
|
||||
interface FallbackUploadImage {
|
||||
uploadPath: string;
|
||||
wasProcessed: boolean;
|
||||
processingNotes: string[];
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
async function prepareFallbackWechatBodyImageUpload(absolutePath: string): Promise<FallbackUploadImage> {
|
||||
const buffer = fs.readFileSync(absolutePath);
|
||||
const prepared = await prepareWechatBodyImageUpload({
|
||||
buffer,
|
||||
filename: path.basename(absolutePath),
|
||||
contentType: inferImageContentType(absolutePath),
|
||||
fileExt: path.extname(absolutePath).toLowerCase(),
|
||||
fileSize: buffer.length,
|
||||
});
|
||||
|
||||
if (!prepared.wasProcessed) {
|
||||
return {
|
||||
uploadPath: absolutePath,
|
||||
wasProcessed: false,
|
||||
processingNotes: [],
|
||||
cleanup: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-body-image-'));
|
||||
const uploadPath = path.join(tempDir, prepared.filename);
|
||||
fs.writeFileSync(uploadPath, prepared.buffer);
|
||||
|
||||
return {
|
||||
uploadPath,
|
||||
wasProcessed: true,
|
||||
processingNotes: prepared.processingNotes,
|
||||
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadImageThroughFileInput(session: ChromeSession, imagePath: string): Promise<void> {
|
||||
const absolutePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(process.cwd(), imagePath);
|
||||
if (!fs.existsSync(absolutePath)) throw new Error(`Image file not found: ${absolutePath}`);
|
||||
|
||||
const beforeCount = await getBodyImageCount(session);
|
||||
try {
|
||||
await uploadImagePathThroughFileInput(session, absolutePath, beforeCount);
|
||||
return;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message.includes('local image upload input not found')) throw err;
|
||||
|
||||
const currentCount = await getBodyImageCount(session);
|
||||
if (currentCount > beforeCount) return;
|
||||
|
||||
console.warn(`[wechat] Raw image upload failed, retrying with fallback processing: ${message}`);
|
||||
const fallback = await prepareFallbackWechatBodyImageUpload(absolutePath);
|
||||
const notes = fallback.processingNotes.length > 0 ? ` (${fallback.processingNotes.join('; ')})` : '';
|
||||
console.log(`[wechat] Retrying image upload with ${fallback.wasProcessed ? 'processed' : 'original'} file: ${path.basename(fallback.uploadPath)}${notes}`);
|
||||
|
||||
try {
|
||||
await uploadImagePathThroughFileInput(session, fallback.uploadPath, currentCount);
|
||||
} finally {
|
||||
fallback.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DraftSaveStatus {
|
||||
appmsgid: string;
|
||||
isLoading: boolean;
|
||||
submitText: string;
|
||||
url: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
async function getDraftSaveStatus(session: ChromeSession): Promise<DraftSaveStatus> {
|
||||
const raw = await evaluate<string>(session, `
|
||||
(function() {
|
||||
const submit = document.querySelector('#js_submit');
|
||||
const button = submit?.querySelector('button');
|
||||
const url = location.href;
|
||||
const appmsgid = new URL(url).searchParams.get('appmsgid') || '';
|
||||
const messages = Array.from(document.querySelectorAll('.weui-desktop-toast, .weui-desktop-toptips, .js_tips'))
|
||||
.map((el) => (el.innerText || el.textContent || '').trim())
|
||||
.filter(Boolean);
|
||||
return JSON.stringify({
|
||||
appmsgid,
|
||||
isLoading: !!submit?.classList.contains('btn_loading') || !!button?.disabled,
|
||||
submitText: (submit?.innerText || '').trim(),
|
||||
url,
|
||||
messages
|
||||
});
|
||||
})()
|
||||
`);
|
||||
return JSON.parse(raw || '{}') as DraftSaveStatus;
|
||||
}
|
||||
|
||||
async function waitForDraftSaved(session: ChromeSession, timeoutMs = 60_000): Promise<string> {
|
||||
const start = Date.now();
|
||||
let lastStatus: DraftSaveStatus | null = null;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
lastStatus = await getDraftSaveStatus(session);
|
||||
if (lastStatus.appmsgid && !lastStatus.isLoading) return lastStatus.appmsgid;
|
||||
|
||||
const relevantFailure = lastStatus.messages.find((message) => /保存.*失败|草稿.*失败|save.*fail/i.test(message));
|
||||
if (relevantFailure) throw new Error(`Draft save failed: ${relevantFailure}`);
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
throw new Error(`Draft save did not complete${lastStatus ? `: ${JSON.stringify(lastStatus)}` : ''}`);
|
||||
}
|
||||
|
||||
export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
const { title, content, htmlFile, markdownFile, theme, color, citeStatus = true, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
||||
let { contentImages = [] } = options;
|
||||
@@ -491,7 +858,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
let chrome: ReturnType<typeof import('node:child_process').spawn> | null = null;
|
||||
|
||||
// Try connecting to existing Chrome: explicit port > auto-detect > launch new
|
||||
const portToTry = cdpPort ?? await findExistingChromeDebugPort();
|
||||
const portToTry = cdpPort ?? await findExistingChromeDebugPort(profileDir);
|
||||
if (portToTry) {
|
||||
const existing = await tryConnectExisting(portToTry);
|
||||
if (existing) {
|
||||
@@ -548,7 +915,8 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
|
||||
const url = await evaluate<string>(session, 'window.location.href');
|
||||
if (!url.includes('/cgi-bin/')) {
|
||||
console.log('[wechat] Not logged in. Please scan QR code...');
|
||||
const hasTelegram = !!(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID);
|
||||
console.log(`[wechat] Not logged in. Please scan QR code...${hasTelegram ? ' (sending to Telegram)' : ''}`);
|
||||
const loggedIn = await waitForLogin(session);
|
||||
if (!loggedIn) throw new Error('Login timeout');
|
||||
}
|
||||
@@ -579,7 +947,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
console.log('[wechat] Waiting for editor to load...');
|
||||
const editorLoaded = await waitForElement(session, '#title', 30_000);
|
||||
if (!editorLoaded) throw new Error('Editor did not load (#title not found)');
|
||||
await waitForElement(session, '.ProseMirror', 15_000);
|
||||
await waitForElement(session, BODY_EDITOR_SELECTOR, 15_000);
|
||||
await sleep(2000);
|
||||
|
||||
if (effectiveTitle) {
|
||||
@@ -604,25 +972,23 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
}
|
||||
|
||||
console.log('[wechat] Clicking on editor...');
|
||||
await clickElement(session, '.ProseMirror');
|
||||
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||
await sleep(1000);
|
||||
|
||||
console.log('[wechat] Ensuring editor focus...');
|
||||
await clickElement(session, '.ProseMirror');
|
||||
await clickElement(session, BODY_EDITOR_SELECTOR);
|
||||
await sleep(500);
|
||||
|
||||
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
|
||||
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
|
||||
await copyHtmlFromBrowser(cdp, effectiveHtmlFile, contentImages);
|
||||
await sleep(500);
|
||||
console.log(`[wechat] Inserting HTML content from: ${effectiveHtmlFile}`);
|
||||
await prepareEditorPasteTarget(session, 'body content paste', { clickEditor: true });
|
||||
console.log('[wechat] Pasting into editor...');
|
||||
await pasteFromClipboardInEditor(session);
|
||||
await insertHtmlIntoEditorFromFile(session, effectiveHtmlFile, contentImages);
|
||||
await sleep(3000);
|
||||
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||
|
||||
const editorHasContent = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
const text = editor.innerText?.trim() || '';
|
||||
return text.length > 0;
|
||||
@@ -648,18 +1014,15 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
console.log(`[wechat] Copying image: ${path.basename(img.localPath)}`);
|
||||
await copyImageToClipboard(img.localPath);
|
||||
await sleep(300);
|
||||
|
||||
console.log('[wechat] Deleting placeholder with Backspace...');
|
||||
await pressDeleteKey(session);
|
||||
await sleep(200);
|
||||
|
||||
console.log('[wechat] Pasting image...');
|
||||
await prepareEditorPasteTarget(session, 'inline image paste');
|
||||
await pasteFromClipboardInEditor(session);
|
||||
await sleep(3000);
|
||||
console.log(`[wechat] Uploading image: ${path.basename(img.localPath)}`);
|
||||
await prepareEditorPasteTarget(session, 'inline image upload');
|
||||
await uploadImageThroughFileInput(session, img.localPath);
|
||||
await sleep(1000);
|
||||
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||
await removeExtraEmptyLineAfterImage(session);
|
||||
}
|
||||
console.log('[wechat] All images inserted.');
|
||||
@@ -667,12 +1030,10 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
} else if (content) {
|
||||
for (const img of images) {
|
||||
if (fs.existsSync(img)) {
|
||||
console.log(`[wechat] Pasting image: ${img}`);
|
||||
await copyImageToClipboard(img);
|
||||
await sleep(500);
|
||||
await prepareEditorPasteTarget(session, 'leading image paste');
|
||||
await pasteInEditor(session);
|
||||
await sleep(2000);
|
||||
console.log(`[wechat] Uploading image: ${img}`);
|
||||
await prepareEditorPasteTarget(session, 'leading image upload');
|
||||
await uploadImageThroughFileInput(session, img);
|
||||
await sleep(1000);
|
||||
await removeExtraEmptyLineAfterImage(session);
|
||||
}
|
||||
}
|
||||
@@ -684,7 +1045,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
|
||||
const editorHasContent = await evaluate<boolean>(session, `
|
||||
(function() {
|
||||
const editor = document.querySelector('.ProseMirror');
|
||||
const editor = document.querySelector(${JSON.stringify(BODY_EDITOR_SELECTOR)});
|
||||
if (!editor) return false;
|
||||
const text = editor.innerText?.trim() || '';
|
||||
return text.length > 0;
|
||||
@@ -721,17 +1082,12 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await verifyTitleUnchangedBeforeSave(session, effectiveTitle);
|
||||
|
||||
console.log('[wechat] Saving as draft...');
|
||||
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
||||
await sleep(3000);
|
||||
|
||||
const saved = await evaluate<boolean>(session, `!!document.querySelector('.weui-desktop-toast')`);
|
||||
if (saved) {
|
||||
console.log('[wechat] Draft saved successfully!');
|
||||
} else {
|
||||
console.log('[wechat] Waiting for save confirmation...');
|
||||
await sleep(5000);
|
||||
}
|
||||
const appmsgid = await waitForDraftSaved(session);
|
||||
console.log(`[wechat] Draft saved successfully! appmsgid: ${appmsgid}`);
|
||||
|
||||
console.log('[wechat] Done. Browser window left open.');
|
||||
} finally {
|
||||
|
||||
+145
-10
@@ -1,7 +1,7 @@
|
||||
---
|
||||
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".
|
||||
version: 1.56.1
|
||||
description: Posts content and articles to X (Twitter). Supports regular posts with images/videos and X Articles (long-form Markdown). In Codex, honor explicit requests for the Codex Chrome plugin/@chrome by using the Chrome Extension workflow; otherwise use Chrome Computer Use when available and fall back to real Chrome CDP scripts only when allowed. Use when user asks to "post to X", "tweet", "publish to Twitter", or "share on X".
|
||||
version: 1.57.2
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-post-to-x
|
||||
@@ -13,7 +13,12 @@ metadata:
|
||||
|
||||
# Post to X (Twitter)
|
||||
|
||||
Posts text, images, videos, and long-form articles to X via real Chrome browser (bypasses anti-bot detection).
|
||||
Posts text, images, videos, and long-form articles to X via a real Chrome browser.
|
||||
|
||||
In Codex, do not conflate these browser paths:
|
||||
- **Codex Chrome plugin / `@chrome` / Chrome Extension**: use the bundled `chrome:Chrome` skill and its Node REPL browser client. This is required whenever the user says "Codex Chrome plugin", "Codex 自带的 Chrome 插件", `@chrome`, or similar.
|
||||
- **Chrome Computer Use**: use `mcp__computer_use__.*` against the visible Google Chrome UI only when the user asks for Computer Use or no Chrome-plugin preference is stated and Computer Use is available.
|
||||
- **CDP script mode**: use only as a fallback when the selected mode is unavailable or the user explicitly asks for CDP/script mode.
|
||||
|
||||
## Script Directory
|
||||
|
||||
@@ -28,15 +33,67 @@ Posts text, images, videos, and long-form articles to X via real Chrome browser
|
||||
**Script Reference**:
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/x-browser.ts` | Regular posts (text + images) |
|
||||
| `scripts/x-video.ts` | Video posts (text + video) |
|
||||
| `scripts/x-quote.ts` | Quote tweet with comment |
|
||||
| `scripts/x-article.ts` | Long-form article publishing (Markdown) |
|
||||
| `scripts/x-browser.ts` | Regular posts (text + images), CDP fallback |
|
||||
| `scripts/x-video.ts` | Video posts (text + video), CDP fallback |
|
||||
| `scripts/x-quote.ts` | Quote tweet with comment, CDP fallback |
|
||||
| `scripts/x-article.ts` | Long-form article publishing (Markdown), CDP fallback |
|
||||
| `scripts/md-to-html.ts` | Markdown → HTML conversion |
|
||||
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
|
||||
| `scripts/paste-from-clipboard.ts` | Send real paste keystroke |
|
||||
| `scripts/check-paste-permissions.ts` | Verify environment & permissions |
|
||||
|
||||
## Execution Mode Selection (Required)
|
||||
|
||||
Choose exactly one mode before interacting with X:
|
||||
|
||||
1. If the user explicitly asks for the Codex Chrome plugin, `@chrome`, the Chrome extension, or "Codex 自带的 Chrome 插件", use **Codex Chrome Plugin Mode**. Do not call Computer Use first.
|
||||
2. If the user explicitly asks for Chrome Computer Use, use **Chrome Computer Use Mode**. Do not fall back to CDP, Playwright, the in-app Browser, or the Chrome plugin without telling the user and getting approval.
|
||||
3. If the user explicitly asks for CDP/script mode, use **CDP Script Mode**.
|
||||
4. Otherwise, prefer **Chrome Computer Use Mode**. For Markdown **X Articles with local content images**, use the tested X editor flow: insert each body image from the toolbar (`Insert` -> `Media` -> dialog icon button `Add photos or video`) at its placeholder, then delete the placeholder text. Use CDP Script Mode only when the selected browser-control mode is unavailable or the UI upload/selection flow is unreliable.
|
||||
|
||||
Never use the in-app Browser for X publishing workflows.
|
||||
|
||||
## Codex Chrome Plugin Mode
|
||||
|
||||
Use this mode whenever the user requests the Codex Chrome plugin, `@chrome`, or the Chrome Extension path. This uses the user's real Chrome profile and X login through the bundled Chrome plugin, not Computer Use and not CDP.
|
||||
|
||||
**Setup**
|
||||
1. Load the `chrome:Chrome` skill before browser work.
|
||||
2. Use `tool_search` for `node_repl js` if the Node REPL `js` tool is not already visible.
|
||||
3. Initialize the Chrome browser client exactly as the Chrome skill specifies, then run a lightweight call such as `browser.user.openTabs()` to verify the extension connection.
|
||||
4. If the first lightweight call fails, wait 2 seconds and retry once. If it still fails, follow the Chrome skill's extension checks and recovery steps. If checks pass but communication still fails, ask the user before opening a new Chrome window. Do not switch to Computer Use or CDP silently.
|
||||
|
||||
**General rules**
|
||||
- Use the Chrome plugin's `browser.tabs.*`, `tab.playwright.*`, `tab.cua.*`, and file chooser APIs for X UI actions.
|
||||
- Shell commands are allowed for Markdown preprocessing and rich-HTML clipboard preparation. For X Article body images, do not rely on image clipboard paste; use the editor's `Insert` -> `Media` upload flow.
|
||||
- If a file upload fails with `Not allowed`, tell the user: `To enable file upload, go to chrome://extensions in Chrome, click Details under the Codex extension, and enable "Allow access to file URLs." See https://developers.openai.com/codex/app/chrome-extension#upload-files for details.`
|
||||
- If the Chrome plugin reports `native pipe is closed`, retry the lightweight browser call once after 2 seconds, then run the Chrome skill health checks. If Chrome is running, the extension is enabled, and the native host manifest is correct, ask permission to open a new Chrome window and retry. Do not keep sending browser actions through the broken pipe.
|
||||
- Never click `Publish`, `Post`, or any externally visible submit action without explicit final confirmation from the user in the current conversation.
|
||||
|
||||
**X Articles**
|
||||
1. Convert Markdown and keep the image map:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/x-article-body.html > /tmp/x-article.json
|
||||
```
|
||||
2. Read the JSON output for `title`, `coverImage`, and `contentImages` (`placeholder` → `localPath`).
|
||||
3. Open or create the article draft at `https://x.com/compose/articles`.
|
||||
4. Upload the cover with the Chrome plugin file chooser flow. If upload is blocked by extension permissions, stop and report the exact permission fix above.
|
||||
5. Fill the title, then copy rich HTML:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/x-article-body.html
|
||||
```
|
||||
6. Paste into the article body with a real paste keystroke through the Chrome plugin. On macOS use `Meta+V`.
|
||||
7. Verify the editor text contains the article body and `XIMGPH_` placeholders. Do not rely on `tab.clipboard.readText()` as proof of the system clipboard after shell clipboard writes; on macOS verify with `pbpaste` if needed.
|
||||
8. For each `contentImages` item in placeholder order:
|
||||
- Locate the visible placeholder text (`XIMGPH_N`) and click it to place the caret there.
|
||||
- Open the toolbar menu `Insert` -> `Media`.
|
||||
- In the modal, click the icon button with `aria-label="Add photos or video"`; do not click the text/dropzone or hidden file input.
|
||||
- Use the file chooser to upload that image's `localPath`.
|
||||
- After the image appears, if `XIMGPH_N` remains above it, select exactly that placeholder and press `Delete` first. Use `Backspace` only if `Delete` fails and the selected text is confirmed to be exactly the placeholder.
|
||||
- Verify the placeholder count for that `XIMGPH_N` is `0`.
|
||||
9. Open Preview and verify title, cover, body, links, and images.
|
||||
10. Ask for explicit confirmation before clicking `Publish`.
|
||||
|
||||
## Preferences (EXTEND.md)
|
||||
|
||||
Check EXTEND.md in priority order — the first one found wins:
|
||||
@@ -86,6 +143,73 @@ Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystrok
|
||||
|
||||
---
|
||||
|
||||
## Chrome Computer Use Mode
|
||||
|
||||
Use this mode when the user explicitly asks for Chrome Computer Use, or when no Chrome-plugin preference is stated and Codex can control `Google Chrome` with Computer Use. This uses the user's existing Chrome window, cookies, login, extensions, and X session.
|
||||
|
||||
**General rules**:
|
||||
- Start each assistant turn that controls Chrome by calling `get_app_state` for `Google Chrome`.
|
||||
- Prefer element-index actions when available; use coordinates only for editor text selection or drag selection.
|
||||
- Do not use the in-app Browser, the Chrome plugin, Playwright, or CDP for X UI actions in this mode unless the user approves a mode change.
|
||||
- Never click `Publish`, `Post`, or any externally visible submit action without an explicit final confirmation from the user in the current conversation.
|
||||
|
||||
**Regular posts**:
|
||||
1. Open or navigate Chrome to `https://x.com/compose/post`.
|
||||
2. Type the post text into the composer using Computer Use.
|
||||
3. For each image, run:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /absolute/path/to/image.png
|
||||
```
|
||||
4. Paste with Computer Use (`super+v` on macOS, `control+v` on Windows/Linux), then wait until X finishes uploading media.
|
||||
5. Ask for confirmation before clicking `Post`.
|
||||
|
||||
**Video posts**:
|
||||
1. Open or navigate Chrome to `https://x.com/compose/post`.
|
||||
2. Type the post text into the composer.
|
||||
3. Use the visible media upload/file picker UI to attach the video.
|
||||
4. Wait for upload and processing to complete.
|
||||
5. Ask for confirmation before clicking `Post`.
|
||||
|
||||
**Quote tweets**:
|
||||
1. Open the tweet URL in Chrome.
|
||||
2. Use the visible quote/repost UI to choose Quote.
|
||||
3. Type the comment.
|
||||
4. Ask for confirmation before clicking `Post`.
|
||||
|
||||
**X Articles**:
|
||||
1. Convert Markdown and keep the image map:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/x-article-body.html > /tmp/x-article.json
|
||||
```
|
||||
2. Read the JSON output for `title`, `coverImage`, and `contentImages` (`placeholder` → `localPath`).
|
||||
3. In Chrome, open `https://x.com/compose/articles`, create or open the draft, upload the cover if present, and fill the title.
|
||||
4. Copy rich HTML to the clipboard:
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/x-article-body.html
|
||||
```
|
||||
5. Paste into the article body with Computer Use.
|
||||
6. For each `contentImages` entry in placeholder order:
|
||||
- Locate the exact visible placeholder text such as `XIMGPH_3` and click it to set the insertion point.
|
||||
- Open the toolbar `Insert` dropdown, choose `Media`, then click the modal's icon button labeled `Add photos or video`.
|
||||
- Use the native file picker to choose the image's `localPath`.
|
||||
- Wait until the image block appears and any upload activity is finished.
|
||||
- If the placeholder remains above the inserted image, reselect exactly that placeholder text and press `Delete` first. Use `Backspace` only if `Delete` fails and the selected text is confirmed to be exactly the placeholder.
|
||||
7. Verify no `XIMGPH_` placeholders remain and the expected images appear.
|
||||
8. Open Preview and verify title, cover, body, links, and images.
|
||||
9. Ask for explicit confirmation before clicking `Publish`.
|
||||
|
||||
If Computer Use selection, toolbar upload, or file-picker control becomes unreliable, stop and report the blocker instead of switching to the Chrome plugin or CDP silently.
|
||||
|
||||
---
|
||||
|
||||
## CDP Script Mode (Fallback)
|
||||
|
||||
Use the script sections below only when the selected browser-control mode is unavailable, unreliable, or explicitly not requested. These scripts launch or reuse a real Chrome instance via CDP and keep the browser open for review.
|
||||
|
||||
Do not use CDP Script Mode when the user explicitly requires the Codex Chrome plugin or Chrome Computer Use unless the user approves the fallback after you explain the blocker.
|
||||
|
||||
---
|
||||
|
||||
## Post Type Selection
|
||||
|
||||
Unless the user explicitly specifies the post type:
|
||||
@@ -107,6 +231,8 @@ ${BUN_X} {baseDir}/scripts/x-browser.ts "Hello!" --image ./photo.png
|
||||
|
||||
**Note**: Script opens browser with content filled in. User reviews and publishes manually.
|
||||
|
||||
**Codex mode note**: If the user explicitly requested the Codex Chrome plugin, use **Codex Chrome Plugin Mode**. Otherwise, if Chrome Computer Use is enabled, use **Chrome Computer Use Mode** instead of running `x-browser.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Video Posts
|
||||
@@ -126,6 +252,8 @@ ${BUN_X} {baseDir}/scripts/x-video.ts "Check this out!" --video ./clip.mp4
|
||||
|
||||
**Note**: Script opens browser with content filled in. User reviews and publishes manually.
|
||||
|
||||
**Codex mode note**: If the user explicitly requested the Codex Chrome plugin, use **Codex Chrome Plugin Mode**. Otherwise, if Chrome Computer Use is enabled, use **Chrome Computer Use Mode** instead of running `x-video.ts`.
|
||||
|
||||
**Limits**: Regular 140s max, Premium 60min. Processing: 30-60s.
|
||||
|
||||
---
|
||||
@@ -147,6 +275,8 @@ ${BUN_X} {baseDir}/scripts/x-quote.ts https://x.com/user/status/123 "Great insig
|
||||
|
||||
**Note**: Script opens browser with content filled in. User reviews and publishes manually.
|
||||
|
||||
**Codex mode note**: If the user explicitly requested the Codex Chrome plugin, use **Codex Chrome Plugin Mode**. Otherwise, if Chrome Computer Use is enabled, use **Chrome Computer Use Mode** instead of running `x-quote.ts`.
|
||||
|
||||
---
|
||||
|
||||
## X Articles
|
||||
@@ -167,7 +297,11 @@ ${BUN_X} {baseDir}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||
|
||||
**Frontmatter**: `title`, `cover_image` supported in YAML front matter.
|
||||
|
||||
**Note**: Script opens browser with article filled in. User reviews and publishes manually.
|
||||
**Codex mode note**: If the user explicitly requested the Codex Chrome plugin, follow **Codex Chrome Plugin Mode** above. If the user explicitly requested Chrome Computer Use, follow **Chrome Computer Use Mode**. Otherwise, prefer Chrome Computer Use; for Markdown articles with local content images, use the toolbar `Insert` -> `Media` image-upload workflow before falling back to `x-article.ts` in **CDP Script Mode**.
|
||||
|
||||
**CDP fallback note**: The script opens browser with article filled in. User reviews and publishes manually unless `--submit` is used.
|
||||
|
||||
**Publish safety**: Do not use `--submit` or click `Publish` unless the user explicitly confirms the final public publish action.
|
||||
|
||||
**Post-Composition Check**: The script automatically verifies after all images are inserted:
|
||||
- Remaining `XIMGPH_` placeholders in editor content
|
||||
@@ -181,7 +315,7 @@ If the check fails (warnings in output), alert the user with the specific issues
|
||||
|
||||
### Chrome debug port not ready
|
||||
|
||||
If a script fails with `Chrome debug port not ready` or `Unable to connect`, kill existing Chrome CDP instances first, then retry:
|
||||
CDP fallback only: if a script fails with `Chrome debug port not ready` or `Unable to connect`, kill existing Chrome CDP instances first, then retry:
|
||||
|
||||
```bash
|
||||
pkill -f "Chrome.*remote-debugging-port" 2>/dev/null; pkill -f "Chromium.*remote-debugging-port" 2>/dev/null; sleep 2
|
||||
@@ -192,7 +326,8 @@ pkill -f "Chrome.*remote-debugging-port" 2>/dev/null; pkill -f "Chromium.*remote
|
||||
## Notes
|
||||
|
||||
- First run: manual login required (session persists)
|
||||
- All scripts only fill content into the browser, user must review and publish manually
|
||||
- In Codex Chrome Plugin Mode and Chrome Computer Use Mode, use the user's existing Chrome session and do not launch a separate CDP profile
|
||||
- CDP scripts only fill content into the browser by default; user must review and publish manually unless `--submit` is explicitly used
|
||||
- Cross-platform: macOS, Linux, Windows
|
||||
|
||||
## Extension Support
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
Publish Markdown articles to X Articles editor with rich text formatting and images.
|
||||
|
||||
## Mode Selection
|
||||
|
||||
In Codex, choose the browser-control mode from the user's wording:
|
||||
|
||||
1. If the user says "Codex Chrome plugin", "Codex 自带的 Chrome 插件", `@chrome`, or Chrome Extension, use **Codex Chrome Plugin Workflow**. Do not try Computer Use first.
|
||||
2. If the user explicitly asks for Chrome Computer Use, use **Computer Use Workflow**.
|
||||
3. If the user explicitly asks for CDP/script mode, use **CDP Script Workflow**.
|
||||
4. Otherwise, use Computer Use when available; if unavailable or blocked, use CDP Script Workflow.
|
||||
|
||||
Never use the in-app Browser for X Article publishing. Never switch away from an explicitly requested mode without explaining the blocker and getting approval.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- X Premium subscription (required for Articles)
|
||||
@@ -10,6 +21,42 @@ Publish Markdown articles to X Articles editor with rich text formatting and ima
|
||||
|
||||
## Usage
|
||||
|
||||
### Codex Chrome Plugin (When Requested)
|
||||
|
||||
Use the `chrome:Chrome` skill and its Node REPL browser client. Verify the connection with a lightweight call such as `browser.user.openTabs()`. If it fails, wait 2 seconds and retry once, then follow the Chrome skill's health checks.
|
||||
|
||||
Prepare the article HTML and image map:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/x-article-body.html > /tmp/x-article.json
|
||||
```
|
||||
|
||||
Copy generated HTML as rich text:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/x-article-body.html
|
||||
```
|
||||
|
||||
Use the Chrome plugin's tab, Playwright-wrapper, CUA, clipboard, and file chooser APIs for all X UI operations. If upload fails with `Not allowed`, stop and tell the user to enable file URL access for the Codex Chrome Extension in `chrome://extensions` → Details.
|
||||
|
||||
### Chrome Computer Use
|
||||
|
||||
Prepare the article HTML and image map:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/md-to-html.ts article.md --save-html /tmp/x-article-body.html > /tmp/x-article.json
|
||||
```
|
||||
|
||||
Copy the generated HTML as rich text:
|
||||
|
||||
```bash
|
||||
${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/x-article-body.html
|
||||
```
|
||||
|
||||
Then use Codex Computer Use against `Google Chrome` for all X UI operations.
|
||||
|
||||
### CDP Script Fallback
|
||||
|
||||
```bash
|
||||
# Publish markdown article (preview mode)
|
||||
${BUN_X} {baseDir}/scripts/x-article.ts article.md
|
||||
@@ -21,6 +68,8 @@ ${BUN_X} {baseDir}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||
${BUN_X} {baseDir}/scripts/x-article.ts article.md --submit
|
||||
```
|
||||
|
||||
Do not use `--submit` unless the user has explicitly confirmed the final public publish action.
|
||||
|
||||
## Markdown Format
|
||||
|
||||
```markdown
|
||||
@@ -118,7 +167,63 @@ JSON output:
|
||||
| `1. item` | `<ol><li>` |
|
||||
| `` | Image placeholder |
|
||||
|
||||
## Workflow
|
||||
## Codex Chrome Plugin Workflow
|
||||
|
||||
1. **Load Chrome skill**: use `chrome:Chrome`, not Computer Use.
|
||||
2. **Connect**: initialize the Chrome plugin browser client and verify with `browser.user.openTabs()`.
|
||||
3. **Parse Markdown**: run `md-to-html.ts --save-html /tmp/x-article-body.html > /tmp/x-article.json`.
|
||||
4. **Read the map**: use `/tmp/x-article.json` for `title`, `coverImage`, and `contentImages`.
|
||||
5. **Open X Articles**: open or claim a Chrome tab for `https://x.com/compose/articles`.
|
||||
6. **Create Draft**: click the create/write button if needed, or open the target draft.
|
||||
7. **Upload Cover**: use the Chrome plugin file chooser flow. If file upload returns `Not allowed`, report the Chrome Extension file-access fix and stop.
|
||||
8. **Fill Title**: fill the title field.
|
||||
9. **Paste Content**:
|
||||
- Run `copy-to-clipboard.ts html --file /tmp/x-article-body.html`.
|
||||
- Click the article body.
|
||||
- Press `Meta+V` on macOS or `Control+V` on Windows/Linux through the Chrome plugin.
|
||||
- Verify the article body appeared and contains `XIMGPH_` placeholders. On macOS, use `pbpaste` to verify shell-written system clipboard contents if paste is suspicious; `tab.clipboard.readText()` may not reflect the system clipboard after shell writes.
|
||||
10. **Insert Images**: for each `contentImages` item in placeholder order:
|
||||
- Locate the exact visible placeholder text (`XIMGPH_N`) and click it to put the insertion point there.
|
||||
- Open the editor toolbar dropdown `Insert` and choose `Media`.
|
||||
- In the `Insert` modal, click the icon button with `aria-label="Add photos or video"`; do not click the "Choose a file or drag it here" text/dropzone or hidden file input.
|
||||
- Use the Chrome plugin file chooser flow to upload that image's `localPath`.
|
||||
- Wait until the image block appears. If `XIMGPH_N` remains above the image, select exactly that placeholder and press `Delete` first; use `Backspace` only if `Delete` fails and the selected text is confirmed to be exactly the placeholder.
|
||||
- Verify that placeholder's count is `0` before continuing.
|
||||
11. **Verify**:
|
||||
- Inspect the editor for `XIMGPH_` residue.
|
||||
- Confirm the expected number of image blocks is visible.
|
||||
- Open Preview and verify title, cover, body, links, and images.
|
||||
12. **Publish Safety**: ask the user for explicit final confirmation before clicking `Publish`.
|
||||
|
||||
If the Chrome plugin reports `native pipe is closed`, retry one lightweight browser call after 2 seconds, then run the Chrome skill health checks. If Chrome, the extension, and native host are healthy, ask the user before opening a new Chrome window and retrying.
|
||||
|
||||
## Computer Use Workflow
|
||||
|
||||
1. **Detect Computer Use**: call `get_app_state` for `Google Chrome`; use `tool_search` first if the tools are not visible.
|
||||
2. **Parse Markdown**: run `md-to-html.ts --save-html /tmp/x-article-body.html > /tmp/x-article.json`.
|
||||
3. **Read the map**: use `/tmp/x-article.json` for `title`, `coverImage`, and `contentImages`.
|
||||
4. **Open X Articles**: use Chrome Computer Use to navigate to `https://x.com/compose/articles`.
|
||||
5. **Create Draft**: click the create/write button if needed, or open the target draft.
|
||||
6. **Upload Cover**: if `coverImage` exists, use Chrome's visible upload/file picker UI. If the file picker cannot be operated reliably, stop and ask for help rather than switching to CDP silently.
|
||||
7. **Fill Title**: type the title into the title field.
|
||||
8. **Paste Content**:
|
||||
- Run `copy-to-clipboard.ts html --file /tmp/x-article-body.html`.
|
||||
- Click the article body.
|
||||
- Press `super+v` on macOS or `control+v` on Windows/Linux with Computer Use.
|
||||
9. **Insert Images**: for each `contentImages` item in placeholder order:
|
||||
- Locate the exact visible placeholder text (`XIMGPH_N`) and click it to put the insertion point there.
|
||||
- Open the editor toolbar dropdown `Insert`, choose `Media`, then click the icon button with `aria-label="Add photos or video"` inside the modal.
|
||||
- Use the native file picker to choose that image's `localPath`.
|
||||
- Wait until the image block appears and upload activity is complete.
|
||||
- If `XIMGPH_N` remains above the inserted image, reselect exactly that placeholder text and press `Delete` first; use `Backspace` only if `Delete` fails and the Computer Use state confirms the selected text is exactly the placeholder.
|
||||
- Confirm that placeholder is gone before continuing.
|
||||
10. **Verify**:
|
||||
- Inspect the Computer Use state for `XIMGPH_` residue.
|
||||
- Confirm the expected number of image blocks is visible.
|
||||
- Open Preview and verify title, cover, body, links, and images.
|
||||
11. **Publish Safety**: ask the user for explicit final confirmation before clicking `Publish`.
|
||||
|
||||
## CDP Script Workflow (Fallback)
|
||||
|
||||
1. **Parse Markdown**: Extract title, cover, content images, generate HTML
|
||||
2. **Launch Chrome**: Real browser with CDP, persistent login
|
||||
@@ -127,17 +232,18 @@ JSON output:
|
||||
5. **Upload Cover**: Use file input for cover image
|
||||
6. **Fill Title**: Type title into title field
|
||||
7. **Paste Content**: Copy HTML to clipboard, paste into editor
|
||||
8. **Insert Images**: For each placeholder (reverse order):
|
||||
- Find placeholder text in editor
|
||||
- Select the placeholder
|
||||
- Copy image to clipboard
|
||||
- Paste to replace selection
|
||||
8. **Insert Images**: For each placeholder in placeholder order:
|
||||
- Find and click the placeholder text in the editor
|
||||
- Use `Insert` -> `Media`
|
||||
- Click the modal's icon button labeled `Add photos or video`
|
||||
- Upload the matching image file
|
||||
- Delete the leftover placeholder text with `Delete` after the image appears
|
||||
9. **Post-Composition Check** (automatic):
|
||||
- Scan editor for remaining `XIMGPH_` placeholders
|
||||
- Compare expected vs actual image count
|
||||
- Warn if issues found
|
||||
10. **Review**: Browser stays open for 60s preview
|
||||
11. **Publish**: Only with `--submit` flag
|
||||
11. **Publish**: Only with `--submit` flag and explicit user confirmation
|
||||
|
||||
## Example Session
|
||||
|
||||
@@ -145,23 +251,27 @@ JSON output:
|
||||
User: /post-to-x article ./blog/my-post.md --cover ./thumbnail.png
|
||||
|
||||
Claude:
|
||||
1. Parses markdown: title="My Post", 3 content images
|
||||
2. Launches Chrome with CDP
|
||||
3. Navigates to x.com/compose/articles
|
||||
4. Clicks create button
|
||||
1. Detects that the user requested the Codex Chrome plugin
|
||||
2. Parses markdown: title="My Post", 3 content images
|
||||
3. Saves `/tmp/x-article-body.html` and `/tmp/x-article.json`
|
||||
4. Uses the Chrome plugin to open X Articles and create a draft
|
||||
5. Uploads thumbnail.png as cover
|
||||
6. Fills title "My Post"
|
||||
7. Pastes HTML content
|
||||
7. Pastes HTML content with a real Chrome paste
|
||||
8. Inserts 3 images at placeholder positions
|
||||
9. Reports: "Article composed. Review and use --submit to publish."
|
||||
9. Opens Preview and asks before publishing
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No create button**: Ensure X Premium subscription is active
|
||||
- **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; use `Insert` -> `Media` -> modal icon button `Add photos or video`, not image clipboard paste, the dropzone text, or the hidden file input.
|
||||
- **Content not pasting**: Check HTML clipboard: `${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
||||
- **Chrome plugin `native pipe is closed`**: retry once after 2 seconds, then run Chrome skill checks; ask before opening a new Chrome window if checks pass.
|
||||
- **Chrome plugin upload `Not allowed`**: enable file URL access for the Codex Chrome Extension in `chrome://extensions` → Details.
|
||||
- **Computer Use unavailable**: Use the CDP fallback script, unless the user explicitly required Chrome Computer Use.
|
||||
- **Placeholder remains after upload**: Select only the placeholder text and press `Delete` after upload completes. Use `Backspace` only if `Delete` fails and the selection is exactly the placeholder.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -172,7 +282,21 @@ Claude:
|
||||
- Downloads remote images locally
|
||||
- Returns structured JSON
|
||||
|
||||
2. `x-article.ts` publishes via CDP:
|
||||
2. The Codex Chrome plugin publishes through the user's real Chrome session when explicitly requested:
|
||||
- Uses the user's active Chrome profile and logged-in X session
|
||||
- Uses the Chrome Extension browser client rather than Computer Use or CDP
|
||||
- Uses `copy-to-clipboard.ts` for rich HTML body paste
|
||||
- Inserts body images through X's toolbar `Insert` -> `Media` modal and its `Add photos or video` icon button
|
||||
- Keeps the final publish click under user confirmation
|
||||
|
||||
3. Chrome Computer Use publishes through the user's visible Chrome UI:
|
||||
- Uses the user's active Chrome profile and logged-in X session
|
||||
- Uses `copy-to-clipboard.ts` for rich HTML body paste
|
||||
- Inserts body images through X's toolbar `Insert` -> `Media` modal and its `Add photos or video` icon button
|
||||
- Uses real keystrokes (`super+v`/`control+v`) through Codex Computer Use
|
||||
- Keeps the final publish click under user confirmation
|
||||
|
||||
4. `x-article.ts` publishes via CDP as a fallback:
|
||||
- Launches real Chrome (bypasses detection)
|
||||
- Uses persistent profile (saved login)
|
||||
- Navigates and fills editor via DOM manipulation
|
||||
|
||||
@@ -6,6 +6,17 @@ Detailed documentation for posting text and images to X.
|
||||
|
||||
If you prefer step-by-step control:
|
||||
|
||||
### Step 0: Prefer Chrome Computer Use in Codex
|
||||
|
||||
When running inside Codex, first detect whether Chrome Computer Use is enabled:
|
||||
|
||||
1. If Computer Use tools are already visible, call `get_app_state` for `Google Chrome`.
|
||||
2. If not, use `tool_search` for `computer-use get_app_state click press_key drag scroll Google Chrome`, then call `get_app_state`.
|
||||
3. If `get_app_state` succeeds, use the user's real Chrome with Computer Use for all X UI actions.
|
||||
4. Use CDP scripts only when Computer Use is unavailable or explicitly requested.
|
||||
|
||||
If the user explicitly asks for Chrome Computer Use, do not use Playwright, the in-app Browser, or CDP without approval.
|
||||
|
||||
### Step 1: Copy Image to Clipboard
|
||||
|
||||
```bash
|
||||
@@ -25,27 +36,15 @@ ${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retri
|
||||
${BUN_X} {baseDir}/scripts/paste-from-clipboard.ts --delay 200
|
||||
```
|
||||
|
||||
### Step 3: Use Playwright MCP (if Chrome session available)
|
||||
### Step 3: Use Chrome Computer Use (Preferred)
|
||||
|
||||
```bash
|
||||
# Navigate
|
||||
mcp__playwright__browser_navigate url="https://x.com/compose/post"
|
||||
|
||||
# Get element refs
|
||||
mcp__playwright__browser_snapshot
|
||||
|
||||
# Type text
|
||||
mcp__playwright__browser_click element="editor" ref="<ref>"
|
||||
mcp__playwright__browser_type element="editor" ref="<ref>" text="Your content"
|
||||
|
||||
# Paste image (after copying to clipboard)
|
||||
mcp__playwright__browser_press_key key="Meta+v" # macOS
|
||||
# or
|
||||
mcp__playwright__browser_press_key key="Control+v" # Windows/Linux
|
||||
|
||||
# Screenshot to verify
|
||||
mcp__playwright__browser_take_screenshot filename="preview.png"
|
||||
```
|
||||
1. Use `get_app_state` for `Google Chrome`.
|
||||
2. Navigate Chrome to `https://x.com/compose/post` if needed.
|
||||
3. Click the composer and type the post text.
|
||||
4. Copy each image to the clipboard with `copy-to-clipboard.ts image <path>`.
|
||||
5. Press `super+v` on macOS or `control+v` on Windows/Linux with Computer Use.
|
||||
6. Wait until X finishes media upload.
|
||||
7. Ask for explicit confirmation before clicking `Post`.
|
||||
|
||||
## Image Support
|
||||
|
||||
@@ -59,12 +58,12 @@ mcp__playwright__browser_take_screenshot filename="preview.png"
|
||||
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
||||
|
||||
Claude:
|
||||
1. Runs: ${BUN_X} {baseDir}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
||||
2. Chrome opens with X compose page
|
||||
3. Text is typed into editor
|
||||
4. Image is copied to clipboard and pasted
|
||||
5. Browser stays open 30s for preview
|
||||
6. Reports: "Post composed. Use --submit to post."
|
||||
1. Detects Chrome Computer Use
|
||||
2. Opens X compose in the user's real Chrome
|
||||
3. Types text into editor
|
||||
4. Copies image to clipboard and pastes with Computer Use
|
||||
5. Waits for upload and verifies the preview
|
||||
6. Asks before clicking Post
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -80,7 +79,13 @@ Claude:
|
||||
|
||||
## How It Works
|
||||
|
||||
The `x-browser.ts` script uses Chrome DevTools Protocol (CDP) to:
|
||||
In Chrome Computer Use mode:
|
||||
1. Codex controls the user's visible Google Chrome window
|
||||
2. Text is typed through the real UI
|
||||
3. Images are copied to the system clipboard and pasted with real keystrokes
|
||||
4. The user confirms before the final public post
|
||||
|
||||
The `x-browser.ts` script is the CDP fallback. It uses Chrome DevTools Protocol (CDP) to:
|
||||
1. Launch real Chrome (not Playwright) with `--disable-blink-features=AutomationControlled`
|
||||
2. Use persistent profile directory for saved login sessions
|
||||
3. Interact with X via CDP commands (Runtime.evaluate, Input.dispatchKeyEvent)
|
||||
|
||||
@@ -5,6 +5,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import frontMatter from 'front-matter';
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
@@ -458,7 +459,9 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch((err) => {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main().catch((err) => {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||
|
||||
// Check if we're on the articles list page (has Write button)
|
||||
console.log('[x-article] Looking for Write button...');
|
||||
const writeButtonFound = await waitForElement('[data-testid="empty_state_button_text"]', 10_000);
|
||||
const writeButtonFound = await waitForElement('[data-testid="empty_state_button_text"]', 30_000);
|
||||
|
||||
if (writeButtonFound) {
|
||||
console.log('[x-article] Clicking Write button...');
|
||||
@@ -172,6 +172,40 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||
expression: `document.querySelector('[data-testid="empty_state_button_text"]')?.click()`,
|
||||
}, { sessionId });
|
||||
await sleep(2000);
|
||||
} else {
|
||||
console.log('[x-article] Write button not found, looking for create button...');
|
||||
const createClicked = await cdp.send<{ result: { value: boolean } }>('Runtime.evaluate', {
|
||||
expression: `(() => {
|
||||
const selectors = [
|
||||
'button[aria-label="create"]',
|
||||
'button[aria-label="Create"]',
|
||||
'button[aria-label*="create" i]',
|
||||
'button[aria-label*="write" i]',
|
||||
'a[href="/compose/articles"]'
|
||||
];
|
||||
for (const sel of selectors) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el instanceof HTMLElement) {
|
||||
el.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const buttons = [...document.querySelectorAll('button')];
|
||||
const byText = buttons.find((el) => /^(create|write)$/i.test((el.textContent || '').trim()));
|
||||
if (byText instanceof HTMLElement) {
|
||||
byText.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})()`,
|
||||
returnByValue: true,
|
||||
}, { sessionId });
|
||||
if (createClicked.result.value) {
|
||||
console.log('[x-article] Create button clicked');
|
||||
await sleep(2000);
|
||||
} else {
|
||||
console.log('[x-article] Create button not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for editor (title textarea)
|
||||
|
||||
@@ -218,6 +218,7 @@ async function launchChromeOnce(
|
||||
|
||||
try {
|
||||
await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
|
||||
chrome.unref();
|
||||
return { chrome, port };
|
||||
} catch (error) {
|
||||
killChrome(chrome);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
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".
|
||||
version: 1.56.1
|
||||
version: 1.57.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-slide-deck
|
||||
@@ -27,14 +27,51 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-slide-[slug].md`) BEFORE invoking any backend. The file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
|
||||
## Batch Generation Policy
|
||||
|
||||
After every prompt file for the current generation group has been saved and verified, generate slide images in batches by default.
|
||||
|
||||
Priority order:
|
||||
|
||||
1. Use the chosen backend's native batch / multi-task interface if it exists. Each task must keep its own prompt file, output path, aspect ratio, session ID, and direct reference images.
|
||||
2. If no native batch interface exists but the runtime can issue parallel tool calls, dispatch up to `generation_batch_size` slide images at a time. Default: `4`. An explicit user request in the current message, such as `--batch-size 4` or "并行4张一起生成", overrides EXTEND.md.
|
||||
3. If neither native batch nor parallel tool calls are available, generate sequentially.
|
||||
|
||||
Rules:
|
||||
|
||||
- Never start the first batch until all selected slide prompt files exist on disk.
|
||||
- Retry failed items once without regenerating successful items.
|
||||
- Do not use subagents merely to parallelize image rendering. Use subagents only for separate prompt iteration or creative exploration.
|
||||
- Merge PPTX/PDF only after all selected slide images are generated.
|
||||
|
||||
## Confirmation Policy
|
||||
|
||||
Default behavior: **confirm before generation**.
|
||||
|
||||
- Treat explicit skill invocation, a file path, matched signals/presets, and `EXTEND.md` defaults as **recommendation inputs only**. None of them authorizes skipping confirmation.
|
||||
- Do **not** start Step 3 or later until the user completes Step 2.
|
||||
- Skip confirmation only when the current request explicitly says to do so, for example: "直接生成", "不用确认", "跳过确认", "按默认出幻灯片", or equivalent wording.
|
||||
- If confirmation is skipped explicitly, state the assumed style / audience / slide-count / language / backend in the next user-facing update before generating.
|
||||
|
||||
## Language
|
||||
|
||||
Respond in the user's language across questions, progress reports, error messages, and the completion summary. Keep technical tokens (style names, file paths, code) in English.
|
||||
@@ -57,6 +94,7 @@ Respond in the user's language across questions, progress reports, error message
|
||||
| `--lang <code>` | Output language (en, zh, ja, ...) |
|
||||
| `--slides <N>` | Target slide count (8-25 recommended, max 30) |
|
||||
| `--ref <files...>` | Reference images applied per slide (style / palette / composition / subject) |
|
||||
| `--batch-size <n>` | Temporary slide image generation batch size for this run. Default: `generation_batch_size` from EXTEND.md, otherwise 4. Clamp to 1-8. |
|
||||
| `--outline-only` | Stop after outline |
|
||||
| `--prompts-only` | Stop after prompts (skip image generation) |
|
||||
| `--images-only` | Skip to Step 7; requires existing `prompts/` |
|
||||
@@ -203,7 +241,7 @@ Copy this checklist and check off items as you complete them:
|
||||
| `${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-slide-deck/EXTEND.md` | XDG |
|
||||
| `$HOME/.baoyu-skills/baoyu-slide-deck/EXTEND.md` | User home |
|
||||
|
||||
If found, read, parse, and print a summary (style / audience / language / review). If not, proceed with defaults — first-time setup is not blocking for this skill. Schema: `references/config/preferences-schema.md`.
|
||||
If found, read, parse, and print a summary (style / audience / language / review / generation batch size). If not, proceed with defaults — first-time setup is not blocking for this skill. Schema: `references/config/preferences-schema.md`.
|
||||
|
||||
**1.2 Analyze content** — follow `references/analysis-framework.md`: classify content, detect language, note signals for style selection, estimate slide count from length (see the **Slide Count Heuristic** in Style System above), generate topic slug. Save source as `source.md` (honor backup rule if one exists).
|
||||
|
||||
@@ -213,6 +251,8 @@ Save findings to `analysis.md`: topic, audience, signals, recommended style and
|
||||
|
||||
### Step 2: Confirmation ⚠️ REQUIRED
|
||||
|
||||
**Hard gate**: this step is mandatory per the [Confirmation Policy](#confirmation-policy) — Steps 3+ cannot start until the user confirms here (or explicitly opts out with "直接生成" / equivalent wording in the current request).
|
||||
|
||||
**Round 1 (always)** — batch five questions in one `AskUserQuestion` call: style, audience, slide count, review-outline?, review-prompts?. Verbatim options in `references/confirmation.md`.
|
||||
|
||||
Summary displayed before the questions:
|
||||
@@ -257,7 +297,8 @@ Display the prompts index (`# | Filename | Slide Title`) and ask: proceed / edit
|
||||
1. Resolve the image backend via the Image Generation Tools rule at the top — ask once if multiple are installed.
|
||||
2. Confirm every `prompts/NN-slide-{slug}.md` exists (hard requirement; prompt files are the reproducibility record regardless of backend).
|
||||
3. Session ID: `slides-{topic-slug}-{timestamp}` — pass to the backend only if it supports sessions.
|
||||
4. For each slide: generate sequentially, reusing the session ID. Backup rule applies to PNG files. Report progress as `Generated X/N`. Auto-retry once on failure before reporting an error.
|
||||
4. Build a task list for selected slides with each slide's prompt file, output PNG path, aspect ratio, session ID, and verified direct references.
|
||||
5. Dispatch slide images in batches per the `## Batch Generation Policy`: backend native batch first, runtime parallel tool calls second, sequential only as fallback. Backup rule applies to PNG files before dispatch. Report progress as `Generated X/N`. Retry only failed items once before reporting an error.
|
||||
|
||||
`--regenerate N` jumps to this step for the named slides only. `--images-only` starts here with existing prompts.
|
||||
|
||||
@@ -320,4 +361,15 @@ See `references/modification-guide.md` for full details.
|
||||
- For sensitive public figures, prefer stylized alternatives to avoid likeness issues.
|
||||
- Maintain visual consistency via the session ID when the backend supports it.
|
||||
|
||||
Custom configurations via EXTEND.md. See Step 1.1 for paths and schema.
|
||||
## Changing Preferences
|
||||
|
||||
EXTEND.md lives at the first matching path listed in Step 1.1. Two ways to change it:
|
||||
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: `references/config/preferences-schema.md`.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `generation_batch_size: 4` — default number of slide images to render concurrently when the backend/runtime supports batch or parallel generation.
|
||||
- `preferred_style: blueprint`, `preferred_audience: experts`, `language: zh`.
|
||||
|
||||
@@ -12,6 +12,8 @@ style: blueprint # Preset name OR "custom"
|
||||
audience: general # beginners | intermediate | experts | executives | general
|
||||
language: auto # auto | en | zh | ja | etc.
|
||||
review: true # true = review outline before generation
|
||||
preferred_image_backend: auto # auto | ask | <backend-id>
|
||||
generation_batch_size: 4 # 1-8, used when backend/runtime supports batch or parallel slide generation
|
||||
|
||||
## Custom Dimensions (only when style: custom)
|
||||
dimensions:
|
||||
@@ -40,6 +42,8 @@ custom_styles:
|
||||
| `audience` | string | `general` | Default target audience |
|
||||
| `language` | string | `auto` | Output language (auto = detect from input) |
|
||||
| `review` | boolean | `true` | Show outline review before generation |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. Resolution logic is documented in `SKILL.md`'s `## Image Generation Tools` section. |
|
||||
| `generation_batch_size` | int | 4 | Number of slide images to dispatch per batch when the backend has native batch support or the runtime can issue parallel generation calls. Clamp invalid values to 1-8. Current user request overrides this value. |
|
||||
|
||||
### Custom Dimensions
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# baoyu-wechat-summary preferences
|
||||
#
|
||||
# Copy this file to one of:
|
||||
# .baoyu-skills/baoyu-wechat-summary/EXTEND.md (project-local, takes precedence)
|
||||
# $XDG_CONFIG_HOME/baoyu-skills/baoyu-wechat-summary/EXTEND.md (XDG, falls back to ~/.config)
|
||||
# ~/.baoyu-skills/baoyu-wechat-summary/EXTEND.md (per-user home)
|
||||
#
|
||||
# First match wins. Keys are case-insensitive. Use `key: value` or `key=value`.
|
||||
# Blank lines and lines starting with `#` are ignored.
|
||||
|
||||
# REQUIRED — your own wxid. Used to recognize your own messages so they show up
|
||||
# as `self_display` (below) in digests instead of the raw wxid string.
|
||||
# Find via: wx contacts --query <你的昵称> --json (look for chat_type=private, your own row)
|
||||
self_wxid: wxid_xxxxxxxxxxxx
|
||||
|
||||
# REQUIRED — the display name to substitute for your wxid in digest text.
|
||||
self_display: 宝玉
|
||||
|
||||
# OPTIONAL — which version(s) to generate when the user doesn't explicitly say.
|
||||
# Values: normal | roast | both
|
||||
# Default: normal
|
||||
default_version: normal
|
||||
|
||||
# OPTIONAL — default time range when the user gives none and there's no prior
|
||||
# digest to incrementally extend from. Forms: `Nd` (days), `Nh` (hours), or a
|
||||
# fixed `YYYY-MM-DD` (treated as both --since and --until).
|
||||
# Default: 24h (i.e., today)
|
||||
# default_time_range: 7d
|
||||
|
||||
# OPTIONAL — override the root directory where digest folders live. Default
|
||||
# resolves to `{project_root}/wechat`. Useful if you want a shared archive
|
||||
# outside the current project.
|
||||
# data_root: ~/Documents/wechat-digests
|
||||
@@ -0,0 +1,463 @@
|
||||
---
|
||||
name: baoyu-wechat-summary
|
||||
description: Summarizes WeChat group chat highlights into a structured digest using the local wx-cli binary (https://github.com/jackwener/wx-cli). Generates a normal digest by default; a roast (毒舌) version is opt-in. Maintains per-group history (history.json + history-digests.jsonl) and per-user profiles across runs, with privacy guardrails baked in. Use when the user asks to "总结群聊", "群聊精华", "群聊摘要", "summarize group chat", "group chat digest", mentions a WeChat group name with a time range, says "帮我看看 XX 群最近聊了什么", "XX 群有什么值得看的", or asks to "回溯画像" / "初始化画像" / "backfill profiles". Adds the roast version when the user says "毒舌版", "roast 版", "再来个毒舌的", or similar.
|
||||
version: 0.1.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-wechat-summary
|
||||
requires:
|
||||
anyBins:
|
||||
- wx
|
||||
---
|
||||
|
||||
# WeChat Group Summary
|
||||
|
||||
群聊精华提取专家。把零散的微信群聊记录提炼成结构化、可读性强的简报,并维护跨次运行的群聊历史与群友画像。底层依赖外部 [wx-cli](https://github.com/jackwener/wx-cli) 二进制(`wx` 命令),不打包脚本。
|
||||
|
||||
> **⚠️ Sandbox restriction**
|
||||
>
|
||||
> wx-cli reads from `~/.wx-cli/` (config, cache, daemon socket) and from WeChat's data directory (`~/Library/Containers/com.tencent.xinWeChat/` on macOS). Both paths are outside Claude Code's default sandbox. Every `wx` command in this skill needs to run with `dangerouslyDisableSandbox: true` from the start — don't waste a sandbox attempt first. The user can use `/sandbox` to view/edit restrictions.
|
||||
|
||||
## User Input Tools
|
||||
|
||||
When this skill prompts the user, follow this tool-selection rule (priority order):
|
||||
|
||||
1. **Prefer built-in user-input tools** exposed by the current agent runtime — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent.
|
||||
2. **Fallback**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question.
|
||||
3. **Batching**: if the tool supports multiple questions per call, combine all applicable questions into a single call; if only single-question, ask them one at a time in priority order.
|
||||
|
||||
Concrete `AskUserQuestion` references below are examples — substitute the local equivalent in other runtimes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before invoking the workflow, verify the environment. Run these checks in order; stop at the first failure and surface the exact next command the user needs.
|
||||
|
||||
1. **wx-cli installed** — run `wx --version`. If missing, tell the user to install it themselves (`npm install -g @jackwener/wx-cli` or use one of the alternatives at https://github.com/jackwener/wx-cli). **Do NOT auto-install** — this repo forbids piped/silent installs.
|
||||
2. **`~/.wx-cli` directory owned by the current user** — `sudo wx init` historically chowned this directory to root, which breaks every subsequent non-sudo `wx` call. Check:
|
||||
```bash
|
||||
ls -la ~/.wx-cli/ 2>/dev/null | head -5
|
||||
```
|
||||
If the directory exists but the owner is `root` (or anything other than `$(whoami)`), tell the user to repair it themselves:
|
||||
```bash
|
||||
sudo chown -R $(whoami) ~/.wx-cli
|
||||
sudo rm -f ~/.wx-cli/daemon.pid ~/.wx-cli/daemon.sock
|
||||
wx daemon start
|
||||
```
|
||||
The skill should NOT run `sudo` on the user's behalf.
|
||||
3. **wx-cli initialized** — `wx sessions` should return data. If it fails with "no keys" / "init required", instruct the user to run `wx init` while WeChat is running (on macOS, `codesign --force --deep --sign - /Applications/WeChat.app` first). Prefer non-sudo init; only fall back to `sudo wx init` if the user's wx-cli version requires it — and warn them that they'll need step 2's chown after.
|
||||
4. **WeChat 4.x running and logged in** — required for the daemon to find data files.
|
||||
|
||||
## Preferences (EXTEND.md)
|
||||
|
||||
Check EXTEND.md in priority order — the first one found wins:
|
||||
|
||||
| Priority | Path | Scope |
|
||||
|----------|------|-------|
|
||||
| 1 | `.baoyu-skills/baoyu-wechat-summary/EXTEND.md` (relative to project root) | Project |
|
||||
| 2 | `${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-wechat-summary/EXTEND.md` | XDG |
|
||||
| 3 | `$HOME/.baoyu-skills/baoyu-wechat-summary/EXTEND.md` | User home |
|
||||
|
||||
| Result | Action |
|
||||
|--------|--------|
|
||||
| Found | Read, parse, apply. On first use in session, briefly remind: "Using preferences from [path]. Edit it to change defaults." |
|
||||
| Not found | **MUST** run first-time setup (BLOCKING) before generating any digest — do NOT silently use defaults. |
|
||||
|
||||
### Supported keys
|
||||
|
||||
EXTEND.md is plain text with `key: value` or `key=value` lines, `#` for comments, case-insensitive keys.
|
||||
|
||||
| Key | Type | Default | Purpose |
|
||||
|-----|------|---------|---------|
|
||||
| `self_wxid` | string | (required) | The owning account's wxid. Messages whose `from_wxid` matches this are attributed to the user. |
|
||||
| `self_display` | string | (required) | Display name to substitute for the user's own messages in digest text. |
|
||||
| `default_version` | `normal` / `roast` / `both` | `normal` | Which version(s) to generate when the user doesn't say otherwise. |
|
||||
| `default_time_range` | string (e.g. `7d`, `24h`, `1d`) | (none) | Default range when the user omits time and there's no incremental anchor. |
|
||||
| `data_root` | path | `{project_root}/wechat` | Override where digest folders live. |
|
||||
|
||||
A starter template lives at [EXTEND.md.example](EXTEND.md.example).
|
||||
|
||||
### First-Time Setup (BLOCKING)
|
||||
|
||||
If no EXTEND.md is found, do NOT silently proceed.
|
||||
|
||||
**Step A — Try to auto-discover `self_wxid` and `self_display` first.** Run (in order, stop at the first that succeeds):
|
||||
|
||||
```bash
|
||||
# 1. If wx-cli exposes a whoami, use it
|
||||
wx whoami --json 2>/dev/null
|
||||
|
||||
# 2. Otherwise, find self-sent messages in recent sessions
|
||||
wx sessions --json --limit 20 2>/dev/null
|
||||
```
|
||||
|
||||
For option 2, scan the sessions for any private/group thread the user has sent into and read one of their own `from_wxid` / `from_nickname` pairs. If you can confidently pre-fill both values, use them as defaults in the question below; otherwise leave the fields blank for the user to fill in.
|
||||
|
||||
**Step B — Confirm with one `AskUserQuestion` call (batched), pre-filling whatever auto-discovery found:**
|
||||
|
||||
- `self_wxid` (e.g., `wxid_abc123`) — fall-back hint: the user can find it with `wx contacts --query "<own nickname>"`, or by inspecting any of their own sent messages in `wx sessions --json`
|
||||
- `self_display` (e.g., `宝玉`) — how they want their messages attributed
|
||||
- `default_version` — pick one of `normal` / `roast` / `both`
|
||||
- `data_root` — where digest folders live. Default: `{project_root}/wechat`. Enter a custom absolute path (e.g. `~/Documents/wechat-digests`) or leave blank for default.
|
||||
- Save location — pick one of project / XDG / home
|
||||
|
||||
Write EXTEND.md to the chosen path. If the user provided a non-default `data_root`, include it as an uncommented line; otherwise omit it (the default applies automatically). Confirm "Preferences saved to [path]. Edit it any time to change defaults.", then continue with the digest workflow.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Parse the user's request
|
||||
|
||||
Extract:
|
||||
|
||||
- **Group name** (or partial name for fuzzy matching)
|
||||
- **Time range** — interpret flexibly:
|
||||
- "最近 1 天" / "今天" / "last 24 hours" → 1 day
|
||||
- "最近 3 天" → 3 days
|
||||
- "最近 7 天" / "这周" → 7 days
|
||||
- "最近 30 天" / "最近一个月" → 30 days
|
||||
- "某天" (e.g. "3 月 5 号") → that specific date
|
||||
- "某天到某天" (e.g. "3 月 1 号到 3 月 5 号") → date range
|
||||
- "从上次开始" / "继续" / "接着上次" / "since last" → **incremental mode**: read `history.json` for this group, use `last_digest.last_message_time` as the start
|
||||
- No time specified → **incremental mode**. If no `history.json` exists yet, fall back to `default_time_range` from EXTEND.md if set, else last 24 hours.
|
||||
- **Version(s) to generate**:
|
||||
- Start from `default_version` in EXTEND.md.
|
||||
- User request overrides: keywords "毒舌"/"roast"/"挑衅"/"再来个毒的"/"sass" → force `include_roast=true`. Keywords "只要正经的"/"normal only"/"不要毒舌" → force `include_normal=true, include_roast=false`. "都来一份"/"两个版本都要"/"both" → both.
|
||||
- At least one of `include_normal`/`include_roast` must end up true.
|
||||
|
||||
Convert relative ranges into absolute `--since YYYY-MM-DD --until YYYY-MM-DD` pairs using today's local date.
|
||||
|
||||
### Step 2: Find the group + resolve folder path
|
||||
|
||||
```bash
|
||||
wx contacts --query "<group_name>" --json
|
||||
```
|
||||
|
||||
Filter for entries whose `username` ends in `@chatroom`. If multiple groups match, use `AskUserQuestion` to disambiguate. If none match, fall back to `wx sessions --json` and search there before asking the user.
|
||||
|
||||
Once resolved, compute the folder path:
|
||||
|
||||
```
|
||||
{data_root}/{group_id}-{sanitized_group_name}/
|
||||
```
|
||||
|
||||
where `data_root` is from EXTEND.md (default `{project_root}/wechat`).
|
||||
|
||||
**Sanitize the group name** — replace any of `/ \ : * ? " < > | NUL` and control characters with `_`. Trim trailing dots and whitespace. Don't strip emoji or Chinese characters.
|
||||
|
||||
**Group-rename detection**: list existing folders under `{data_root}/` and find any folder whose name starts with `{group_id}-`. If one exists but the suffix differs (group was renamed), rename the existing folder to the new `{group_id}-{sanitized_new_name}` form. If a target with the new name already exists (rare), keep both and prefer the existing one for this run.
|
||||
|
||||
### Step 3: Fetch messages
|
||||
|
||||
For small batches (single-day digest, typically < 200 messages), pipe JSON into the agent directly:
|
||||
|
||||
```bash
|
||||
wx history "<group_name_or_id>" --since YYYY-MM-DD --until YYYY-MM-DD -n 5000 --json
|
||||
```
|
||||
|
||||
For **large batches** (weekly / monthly digests, > 200 messages), redirect to `$TMPDIR` first so the raw payload never sits in conversation context:
|
||||
|
||||
```bash
|
||||
wx history "<group_name_or_id>" --since YYYY-MM-DD --until YYYY-MM-DD -n 5000 --json > "$TMPDIR/wx-messages.json"
|
||||
wc -c "$TMPDIR/wx-messages.json"
|
||||
jq 'length' "$TMPDIR/wx-messages.json"
|
||||
```
|
||||
|
||||
Then read the file in slices via `Read` with `offset` + `limit`, or process with `jq` queries (e.g. `jq '.[0:200]'`, `jq '[.[] | {id, from_nickname, timestamp, content: (.content | .[0:50])}]'` for a lightweight skeleton pass). Reading all 500+ messages at once will burn token budget unnecessarily.
|
||||
|
||||
Notes:
|
||||
|
||||
- `--since` is inclusive; `--until` is interpreted as a date (the whole day). If the user asked for "today only", set both to today.
|
||||
- `-n 5000` is a defensive cap; for very active groups, raise it and re-fetch.
|
||||
- Filter the returned messages by their `timestamp` to be safe (some daemons may return adjacent days).
|
||||
- **Range splitting**: for ranges > 7 days OR > 500 messages, prefer generating per-3-day digests and then a meta-summary over forcing one giant digest — the categorization quality degrades sharply past a week's worth of unrelated topics.
|
||||
|
||||
**Incremental mode**: after the fetch, drop any message whose `timestamp` is `<=` the `last_message_time` from `history.json`. If zero messages remain, tell the user "上次摘要后没有新消息,已跳过生成" and exit.
|
||||
|
||||
### Step 3.5: Parse the message schema
|
||||
|
||||
`wx history --json` returns an array of message objects. Use the fields that are present; tolerate missing fields:
|
||||
|
||||
- **`id` / `msg_id` / `local_id`** — message identifier (use whichever wx-cli emits). Reference IDs in working notes as anchors when building the skeleton.
|
||||
- **`from_wxid`** — stable sender identifier
|
||||
- **`from_nickname`** — display name (may be the group remark or original nickname)
|
||||
- **`content`** — text payload. Examples:
|
||||
- Plain text → use as-is
|
||||
- `[图片]` → opaque placeholder; see image handling below
|
||||
- `[表情]` → emoji/sticker; skip in body unless surrounded by discussion
|
||||
- `[视频]` / `[文件]` → media reference; skip unless discussed
|
||||
- `[链接] <title>` or `[链接/文件] <title>` → shared article; the title IS the information — quote it and credit the sharer
|
||||
- `[系统] ... revokemsg` → revoked; exclude from digest and from leaderboard
|
||||
- **`timestamp`** — convert to `MM-DD HH:MM` for display (and use full ISO for `generated_at`)
|
||||
- **`chat_type`** — sanity-check `group`
|
||||
- **Quote/reply** — try `quote_id`, `reply_to`, `quoted_msg_id`, or any nested `quote` object. If present, use it as strong attribution. If absent, fall back to context but flag the inferred link as uncertain.
|
||||
|
||||
### Step 3.6: Resolve self + ambiguous nicknames
|
||||
|
||||
- Substitute `self_display` for every message whose `from_wxid` matches `self_wxid` (from EXTEND.md). Apply this in the leaderboard, portraits, and body text. The user MUST appear under their real display name and count toward stats — never skip them.
|
||||
- Scan all unique senders for ambiguous handles: ≤2 characters, common programming words (`nil`, `null`, `test`, `admin`, `user`, `undefined`), single emoji, or otherwise low-information. For each, run `wx contacts --query "<nick>" --json --limit 5` and pick a meaningful name in this priority: remark > nickname > wxid. Apply the substitution everywhere in the digest.
|
||||
|
||||
### Step 3.7: Load user profiles
|
||||
|
||||
For each unique sender appearing in this batch:
|
||||
|
||||
- Look in `{folder}/profiles/{wxid}-*.md` by `wxid` prefix match. Read the matched file if found.
|
||||
- If `include_roast`, **also** look in `{folder}/profiles-roast/{wxid}-*.md` for the roast pass.
|
||||
|
||||
Compile a condensed **profile context block** as internal working memory — do NOT write it into the final digest. Example shape:
|
||||
|
||||
```
|
||||
== 群友历史画像(来自 profiles/)==
|
||||
K. H:空中直播员 / 生活百科全书。常见话题:旅行、金融、美食。经典金句:"要不要买moderna"。
|
||||
可可苏玛:...
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Only load profiles for users active in this batch — never preload everyone.
|
||||
- Profile is **background**, not template. Current messages are still the primary source.
|
||||
- Use historical labels for **continuity** ("又双叒叕化身空中直播员") or **contrast** ("一向省钱的 XX 今天居然...").
|
||||
- **Strict separation**: normal pass reads only `profiles/`, roast pass reads only `profiles-roast/`. Never cross-load.
|
||||
|
||||
See [references/profiles.md](references/profiles.md) for the full file format.
|
||||
|
||||
### Step 3.8: Detect existing in-chat digests (optional)
|
||||
|
||||
Some users (e.g., the original 宝玉 workflow) post digests directly into the group as messages. If we don't notice these, the new digest will re-cover the same ground.
|
||||
|
||||
Scan the fetched messages for signals of a prior in-chat digest:
|
||||
|
||||
- `from_wxid == self_wxid` AND
|
||||
- `content` contains `群聊精华` OR `消息统计:` OR `📊 消息统计` OR a leaderboard pattern (e.g. `^\d+\. .+: \d+ 条`), AND
|
||||
- `content` length > 1500 chars.
|
||||
|
||||
If a match is found:
|
||||
|
||||
1. Extract the digest's covered date or range from the title line (e.g., `xxx 群聊精华 · 2026-05-12` or `... · 2026-05-10 ~ 2026-05-12`).
|
||||
2. Surface the finding to the user via `AskUserQuestion`:
|
||||
- "Detected an in-chat digest by you covering {范围}. Use {范围 end + 1} as the start instead of `history.json`?"
|
||||
- Options: `Yes, skip up to {end of detected range}` / `No, use history.json` / `No, cover everything in the requested range`.
|
||||
3. Apply the chosen anchor.
|
||||
|
||||
This is a heuristic — when uncertain (multiple matches, malformed title), default to `history.json` and tell the user what was skipped.
|
||||
|
||||
Generate the digest in three rounds so nothing slips through. The methodology stays here in SKILL.md; the content/style rules live in [references/output-formats.md](references/output-formats.md) — read that file in Round 2 before drafting.
|
||||
|
||||
#### Round 1 — Build the skeleton
|
||||
|
||||
Read every message in order. **Skip image fetching/decoding** in this round. List every distinct discussion topic. Bias toward over-listing — trim in Round 3.
|
||||
|
||||
Internal working format (not written to the final file):
|
||||
|
||||
```
|
||||
== 话题清单(共 N 条消息)==
|
||||
1. [HH:MM-HH:MM] 话题名称(参与者:A, B, C)— 一句话概括(锚点 id:54052, 54055, 54063)
|
||||
2. [HH:MM-HH:MM] 话题名称(参与者:D, E)— 一句话概括(锚点 id:54100-54112)
|
||||
...
|
||||
|
||||
== 可能需要图片上下文的话题 ==
|
||||
- 话题 3:锚点 id=49661(图片是讨论主体)
|
||||
|
||||
== 发言统计 ==
|
||||
1. XXX — N 条 2. YYY — N 条 ...
|
||||
```
|
||||
|
||||
Topic principles:
|
||||
|
||||
- Topic-switch signals: time gap > 30 min, participant change, content jump.
|
||||
- 2+ participants OR substantive content qualifies as a topic; pure emoji-banter does not.
|
||||
- **Strict attribution**: each topic must record "who said what". Don't fuse adjacent messages from different senders just because they're close in time — when minutes apart or interleaved with others, split into separate topics. Prefer two topics over one wrongly-merged topic.
|
||||
- **Carry anchor IDs**: list the key message IDs for each topic. In Round 2, jump back to these IDs in the raw messages and verify content, don't guess from context. If `quote_id` / `reply_to` is present, use the ID chain — that's the most reliable attribution.
|
||||
|
||||
**Flag-for-images criteria** (any one triggers): an explicit comment on an image (`看发型是X?`, `这是谁?`, `笑死`), multiple people piling onto the same image without saying what it is, an image as the core information (晒单/截图/资料), an explanatory line right after an image (`gpt-image-2`, `太可怕了`), or cross-sender ambiguity (B says "这个看着像 X" but the previous image is from A).
|
||||
|
||||
#### Round 2 — Flesh out + write the digest
|
||||
|
||||
For each topic in the skeleton, jump back to its anchor IDs and expand into full content with quotes and clear attribution. Then write the digest file.
|
||||
|
||||
**Image handling** (limited — wx-cli does not decode chat images):
|
||||
|
||||
For each flagged topic, check whether a description file already exists at `{folder}/imgs/{message_id}.txt`. If yes, read it (one-line plain text) and weave its content into the topic. If no, treat the image as opaque (`[图片]`) and write around it — describe what the surrounding messages tell us, but don't invent visual content.
|
||||
|
||||
The `imgs/` directory exists as an **extension point**: a user (or a future wx-cli capability) can drop `{message_id}.txt` files with one-line descriptions, and the skill will pick them up. The skill itself does NOT generate these files in this version.
|
||||
|
||||
**Use the profile context block** (from Step 3.7):
|
||||
|
||||
- Echo continuity for matching behavior ("又双叒叕直播飞行体验")
|
||||
- Highlight contrast for departures ("一向话少的 XX 今天突然爆发")
|
||||
- Callback past quotes ("继上次'要不要买 moderna'之后,这次又...")
|
||||
- Don't sacrifice current material to force a callback.
|
||||
|
||||
**Roast pass — profile usage extras** (only when generating the roast version):
|
||||
|
||||
- 历史槽点可做 callback joke
|
||||
- Running gag 可以升级和迭代
|
||||
- 历史毒舌语录可以引用或翻新
|
||||
- 但当期素材优先,不要为了 callback 硬凑
|
||||
|
||||
**Writing order**: write the body categories first, then the opening overview based on the finished body (so the hook is accurate).
|
||||
|
||||
Detailed structure, voice, formatting rules, and content guidelines are in [references/output-formats.md](references/output-formats.md). Load that file now if not already loaded.
|
||||
|
||||
#### Round 3 — Audit
|
||||
|
||||
Walk the Round 1 skeleton against the finished digest. Check:
|
||||
|
||||
- Any listed topic missing from the digest?
|
||||
- Quotes, names, product/tool names preserved verbatim?
|
||||
- Categorization makes sense — is anything in the wrong bucket?
|
||||
|
||||
Fix in place. When clean, confirm and proceed.
|
||||
|
||||
### Step 7: Save the digest file(s)
|
||||
|
||||
If `include_normal`:
|
||||
|
||||
- Single date → `{folder}/YYYY-MM-DD.md`
|
||||
- Date range → `{folder}/YYYY-MM-DD_YYYY-MM-DD.md`
|
||||
- Overwrite if the same date/range already exists.
|
||||
|
||||
If `include_roast`:
|
||||
|
||||
- Same naming, but with `-roast` suffix: `YYYY-MM-DD-roast.md` or `YYYY-MM-DD_YYYY-MM-DD-roast.md`.
|
||||
|
||||
Both versions share the same statistics (message count, leaderboard) and the same underlying skeleton.
|
||||
|
||||
### Step 8: Save history (two files)
|
||||
|
||||
Maintain two files in the group folder:
|
||||
|
||||
#### `history.json` — single record, fast read
|
||||
|
||||
Always reflects only the most recent normal digest. Overwrite on each run when `include_normal=true`.
|
||||
|
||||
```json
|
||||
{
|
||||
"group_id": "12345678901@chatroom",
|
||||
"group_name": "相亲相爱一家人",
|
||||
"folder": "12345678901@chatroom-相亲相爱一家人",
|
||||
"last_digest": {
|
||||
"file": "2026-03-12.md",
|
||||
"date_range": "2026-03-12",
|
||||
"generated_at": "2026-03-12T10:30:00+08:00",
|
||||
"message_count": 150,
|
||||
"last_message_time": "03-12 18:45"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `group_name` updates on every run (handles renames).
|
||||
- `folder` records the current folder basename for cross-reference.
|
||||
- `last_message_time` is the timestamp of the most recent message included, in `MM-DD HH:MM` — used by incremental mode.
|
||||
- Roast-only runs do NOT touch this file.
|
||||
|
||||
#### `history-digests.jsonl` — append-only archive
|
||||
|
||||
One JSON object per line, same shape as `last_digest`. Every normal-version run appends one line (in chronological order). Used by backfill and historical lookups. Never read for incremental mode (which only needs the latest).
|
||||
|
||||
```jsonl
|
||||
{"file":"2026-03-10.md","date_range":"2026-03-10","generated_at":"2026-03-10T09:00:00+08:00","message_count":420,"last_message_time":"03-10 22:30"}
|
||||
{"file":"2026-03-11.md","date_range":"2026-03-11","generated_at":"2026-03-11T09:05:00+08:00","message_count":312,"last_message_time":"03-11 23:10"}
|
||||
{"file":"2026-03-12.md","date_range":"2026-03-12","generated_at":"2026-03-12T10:30:00+08:00","message_count":150,"last_message_time":"03-12 18:45"}
|
||||
```
|
||||
|
||||
If a normal digest with the same `file` name is regenerated, append a new line anyway (the JSONL is a strict log; readers can dedupe by `file` if they need to).
|
||||
|
||||
### Step 8.5: Update user profiles
|
||||
|
||||
For each user with 3+ messages in this batch who appeared in the 群友画像 section:
|
||||
|
||||
- If `include_normal`, update `{folder}/profiles/{wxid}-{nickname}.md`.
|
||||
- If `include_roast`, update `{folder}/profiles-roast/{wxid}-{nickname}.md`.
|
||||
|
||||
Counts, frontmatter updates, append-only rules for quotes and events, and privacy guardrails are detailed in [references/profiles.md](references/profiles.md). Load that file when running this step.
|
||||
|
||||
### Completion checklist
|
||||
|
||||
Profile updates are easy to forget once the digest is on disk. Before reporting the run as "done", verify every applicable file:
|
||||
|
||||
- [ ] `{folder}/YYYY-MM-DD.md` written (if `include_normal`)
|
||||
- [ ] `{folder}/YYYY-MM-DD-roast.md` written (if `include_roast`)
|
||||
- [ ] `{folder}/history.json` overwritten with the new `last_digest` (if `include_normal`)
|
||||
- [ ] `{folder}/history-digests.jsonl` appended one line (if `include_normal`)
|
||||
- [ ] `{folder}/profiles/{wxid}-*.md` updated for every user with 3+ messages (if `include_normal`)
|
||||
- [ ] `{folder}/profiles-roast/{wxid}-*.md` updated for every user with 3+ messages (if `include_roast`)
|
||||
|
||||
If any item is unchecked, finish it before declaring success. Don't ship a digest with a stale `history.json` — incremental mode depends on it.
|
||||
|
||||
### Step 9: Backfill (user-triggered)
|
||||
|
||||
When the user says "回溯画像" / "初始化画像" / "backfill profiles":
|
||||
|
||||
1. Confirm the target group (if not specified, ask which one).
|
||||
2. List all digest files in `{folder}/` and `history-digests.jsonl`.
|
||||
3. Read existing digests in batches of 10–15 to avoid context blowup.
|
||||
4. For users appearing in 3+ digests, seed profile files using their leaderboard counts, portrait paragraphs, and quoted lines from the historical digests.
|
||||
5. Write to `profiles/` (and `profiles-roast/` if any `-roast.md` files exist).
|
||||
6. Report back: how many profiles were created, how many users covered.
|
||||
|
||||
Full procedure in [references/profiles.md](references/profiles.md).
|
||||
|
||||
## Storage layout
|
||||
|
||||
```
|
||||
{data_root}/ # default: {project_root}/wechat/
|
||||
└── {group_id}-{group_name}/ # e.g. 12345678901@chatroom-相亲相爱一家人/
|
||||
├── history.json # last digest pointer (fast)
|
||||
├── history-digests.jsonl # append-only archive
|
||||
├── 2026-03-12.md # normal digest, single date
|
||||
├── 2026-03-12-roast.md # roast digest (only if generated)
|
||||
├── 2026-03-10_2026-03-12.md # normal digest, date range
|
||||
├── profiles/ # normal user profiles
|
||||
│ ├── onlytiancai-胡浩🐸.md
|
||||
│ └── ...
|
||||
├── profiles-roast/ # roast user profiles (only if any roast generated)
|
||||
│ ├── onlytiancai-胡浩🐸.md
|
||||
│ └── ...
|
||||
└── imgs/ # optional image-description files
|
||||
├── 49661.txt # one-line plain text description
|
||||
└── ...
|
||||
```
|
||||
|
||||
## wx-cli quick reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `wx --version` | Sanity-check that wx-cli is installed |
|
||||
| `wx sessions --json` | List recent sessions; useful for verifying init and finding the user's own wxid |
|
||||
| `wx contacts --query "<name>" --json` | Fuzzy-match contacts/groups by display name, remark, or wxid |
|
||||
| `wx history "<group>" --since DATE --until DATE -n N --json` | Pull a group's messages within a date range as JSON |
|
||||
| `wx members "<group>" --json` | List a group's members (rarely needed; mostly for completeness) |
|
||||
| `wx stats "<group>" --since DATE` | wx-cli's built-in stats; we compute our own from `wx history` JSON so the format matches our digest |
|
||||
| `wx daemon status` / `wx daemon stop` / `wx daemon logs --follow` | Daemon lifecycle (troubleshooting) |
|
||||
|
||||
All `wx` commands accept `--json` for machine-readable output. Default output is YAML — only use it for human eyeballing during debugging.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
When a `wx` command fails, diagnose by the symptom, not by retrying blindly. Common patterns:
|
||||
|
||||
| Symptom | Cause | Fix (tell the user to run these — do NOT run `sudo` for them) |
|
||||
|---------|-------|----------------------------------------------------------------|
|
||||
| `Operation not permitted` / `Access denied to ~/.wx-cli` | Sandbox is on | Re-run the command with `dangerouslyDisableSandbox: true`. Persistent fix: `/sandbox` to allow `~/.wx-cli` and the WeChat data dir. |
|
||||
| `无法写入 /Users/<u>/.wx-cli` / `Permission denied` | `~/.wx-cli` is owned by root (legacy `sudo wx init`) | `sudo chown -R $(whoami) ~/.wx-cli && sudo rm -f ~/.wx-cli/daemon.{pid,sock} && wx daemon start` |
|
||||
| `wx history` hangs / times out / returns nothing | Daemon is stuck | `wx daemon stop && rm -f ~/.wx-cli/daemon.{pid,sock} && wx daemon start`, then retry |
|
||||
| `no keys` / `init required` after the daemon was working | Keys went stale (WeChat restart, version upgrade) | Make sure WeChat is running, then `wx init --force` (non-sudo first; only `sudo` if your wx-cli version requires it) |
|
||||
| `wx contacts` returns zero rows for a group you know exists | Group is folded into 折叠群 or the daemon hasn't indexed it yet | `wx sessions --json` and search there; if missing, run `wx daemon stop && wx daemon start` and retry |
|
||||
| Messages returned but `--since` / `--until` window looks wrong | Date string not in `YYYY-MM-DD` format, or off-by-one timezone | Confirm the dates are local-time `YYYY-MM-DD`. Re-filter the JSON by `timestamp` locally as a belt-and-suspenders step. |
|
||||
| Empty result for a chat that should have activity | `-n` cap too low for a noisy group | Raise `-n` (e.g. to 20000) and re-fetch |
|
||||
|
||||
**Recovery order when nothing makes sense:**
|
||||
|
||||
1. Is WeChat running?
|
||||
2. Is `~/.wx-cli` owned by `$(whoami)`?
|
||||
3. Is the daemon healthy? (`wx daemon status`)
|
||||
4. Restart the daemon (`wx daemon stop && wx daemon start`)
|
||||
5. Last resort: `wx init --force` (while WeChat is running)
|
||||
|
||||
Never auto-retry inside the skill — every failure should produce a clear diagnostic plus the exact command the user needs to run.
|
||||
|
||||
## Notes and limitations
|
||||
|
||||
- **Image content is opaque**. wx-cli does not decode chat images. The skill respects an `imgs/{message_id}.txt` extension point but does not auto-populate it. When a topic depends heavily on an image with no description file, the digest should say so honestly rather than invent visual content.
|
||||
- **Reply attribution is best-effort**. If wx-cli's output exposes a quote/reply field, use it. Otherwise fall back to context and flag uncertain inferences in working notes.
|
||||
- **Local time only**. Date parsing uses the agent's local time zone. Cross-time-zone group members may show timestamps that don't match their wall clock. Per the format rules, never use timestamps to infer sleep or location.
|
||||
- **wx-cli reinit**. If `wx history` suddenly returns nothing after a WeChat restart, the keys may be stale. Tell the user to run `sudo wx init --force` (while WeChat is running) and retry.
|
||||
@@ -0,0 +1,273 @@
|
||||
# Output formats — normal & roast digest
|
||||
|
||||
This reference defines the two digest variants the skill produces: the **normal** version (default, sober summary) and the **roast** version (毒舌,sarcastic critique, opt-in). Load this file during Step 4 (skeleton) and keep it open through Step 6 (audit).
|
||||
|
||||
Both versions share the same overall layout and writing rules; the differences are tone, the leaderboard annotations, the portraits, and the footer. Write the normal version first when both are requested — it's the anchor for incremental mode and the source of truth for the profile updates.
|
||||
|
||||
---
|
||||
|
||||
## 1. Normal version
|
||||
|
||||
### 1.1 Five-part structure
|
||||
|
||||
```
|
||||
[Title line]
|
||||
[📊 Stats block + Top 10 leaderboard]
|
||||
[Opening summary — 1-2 paragraphs of prose]
|
||||
[群友画像 — one entry per active user (3+ msgs)]
|
||||
[Categorized body — 3-6 self-named sections per day]
|
||||
[Optional pain-point section]
|
||||
[Fixed footer]
|
||||
```
|
||||
|
||||
### 1.2 Title line
|
||||
|
||||
- Single line, no markdown heading.
|
||||
- Form: `{群名} 群聊精华 · {日期或日期区间}`
|
||||
- Date single day: `2026-03-12`. Date range: `2026-03-12 ~ 2026-03-15`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
相亲相爱一家人 群聊精华 · 2026-03-12
|
||||
```
|
||||
|
||||
### 1.3 Statistics block
|
||||
|
||||
- Starts with `📊 消息统计: 共 N 条消息`.
|
||||
- Followed by a leaderboard, top 10 senders by message count, one per line.
|
||||
- Form per line: `{排名}. {昵称}: {消息数} 条`
|
||||
- Counting rules:
|
||||
- Include images, emojis, links, voice transcripts — anything that occupies a chat row is one message.
|
||||
- Exclude system messages and revoked messages (`[系统]`, `revokemsg`).
|
||||
- For the `self_wxid` user, substitute `self_display` from EXTEND.md before counting/displaying.
|
||||
- Resolve ambiguous nicknames (per SKILL.md Step 3.6) before tallying so the same person isn't double-counted.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
📊 消息统计: 共 387 条消息
|
||||
1. 蛙总: 92 条
|
||||
2. 老王: 58 条
|
||||
3. 阿喵: 41 条
|
||||
...
|
||||
```
|
||||
|
||||
### 1.4 Opening summary
|
||||
|
||||
- 1-2 paragraphs, plain prose, no headings, no bullets.
|
||||
- Hook the reader: lead with the most distinctive thread of the day (a heated debate, a surprising announcement, a market move someone reacted to).
|
||||
- Reference 2-4 of the day's category titles in the prose so the reader knows what's coming.
|
||||
- Mention 1-2 specific people only if their contribution is central; otherwise stay topic-focused.
|
||||
- No timestamps, no message counts (those live in the stats block).
|
||||
|
||||
### 1.5 群友画像 section
|
||||
|
||||
- Heading line: `群友画像`
|
||||
- One entry per user with 3+ messages this batch.
|
||||
- Order: by message count, descending.
|
||||
- Entry header: `{昵称}({角色标签})` — the role tag is your one-line read on this person *today*. Examples: `做空美股的乐子人`, `深夜技术指导`, `论坛级吐槽担当`.
|
||||
- Body: 2-5 bullets with `•` prefix. Each bullet states one observation. Quote evidence inline where natural.
|
||||
- Continuity: if you loaded a prior profile in Step 3.7, carry forward the established tags/observations that still apply, and call out *change* explicitly (`今天罕见地没提空头`, `从昨天的乐观转向今天的焦虑`).
|
||||
- Don't invent backstory — only what's in the messages or the prior profile.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
群友画像
|
||||
|
||||
蛙总(做空美股的乐子人)
|
||||
• 全天反复提"做空 SPY",被群友提醒已连续三周看错方向
|
||||
• 难得正面回应技术问题:"我那个脚本是用 Bun 跑的,慢得跟蜗牛似的"
|
||||
• 临近收盘转为沉默,与昨日大放厥词的状态对比明显
|
||||
```
|
||||
|
||||
### 1.6 Categorized body
|
||||
|
||||
- 3-6 self-named categories per day.
|
||||
- Each category is a thematic bucket — name it for the *topic*, not generic ("讨论"、"闲聊" are forbidden labels).
|
||||
- Category header: `{emoji} {标题}` — one emoji prefix, then a short noun phrase.
|
||||
- Suggested emoji: 🛠 工具/技术,📦 产品发布,📰 新闻/市场,💬 观点辩论,😄 笑料/段子,📚 学习分享,💸 钱与消费,🍜 生活日常。
|
||||
- Body inside each category: prose with embedded quotes. Use `•` bullets when listing 3+ parallel items; otherwise paragraphs.
|
||||
- Attribution: name the speaker on first mention in a thread (`蛙总说他...`). For follow-on lines in the same thread, attribution can be implicit if the chain is short and clear.
|
||||
- Quotes: use 「」 for direct quotes. Quote when the wording is vivid, surprising, or characteristic; paraphrase otherwise.
|
||||
- Merge: a multi-person discussion is one entry, not a list of one-line replies.
|
||||
- Links: preserve the full URL inline. Article titles stay verbatim.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
🛠 Claude Code 4.7 实测
|
||||
|
||||
蛙总下午把 4.7 装上后第一反应是「比 4.6 慢一倍」,老王跟着复现,怀疑是 Opus 默认配置导致。阿喵贴了官方文档 https://docs.claude.com/.../opus-4-7 ,提到可以切回 Sonnet 4.6 跑速测,三人最终结论:复杂任务 4.7 强,日常用 4.6 更顺手。
|
||||
```
|
||||
|
||||
### 1.7 Pain-point section (optional)
|
||||
|
||||
- Include only when the day's chat contains at least one concrete unresolved or partially-resolved problem.
|
||||
- Heading: `今日待解决问题` or `本周悬而未决`.
|
||||
- One entry per problem. Format:
|
||||
```
|
||||
问题:<一句话描述>
|
||||
提出者:<昵称>
|
||||
背景:<1-2 句来龙去脉>
|
||||
状态:<✅ 已解决 / ⚠️ 部分解决 / ❌ 仍未解决>
|
||||
方案:<若有人提了方案,写在这;否则写"暂无方案">
|
||||
```
|
||||
- Skip the section entirely if there are no genuine pain points — don't pad with trivial questions.
|
||||
|
||||
### 1.8 Footer
|
||||
|
||||
Fixed line, last in file:
|
||||
|
||||
```
|
||||
本简报由 AI 自动生成
|
||||
```
|
||||
|
||||
No date, no signature, no version number.
|
||||
|
||||
---
|
||||
|
||||
## 2. Roast version (毒舌版)
|
||||
|
||||
Roast 版基于普通版的话题骨架和素材,用毒舌、尖锐、挑衅的风格重写。整体结构与普通版相同(统计区块、开头概览、群友画像、正文分类、结尾),但风格完全不同。痛点部分省略。仅当 `include_roast=true` 时生成。标题加 "毒舌版" 后缀。
|
||||
|
||||
风格要求:
|
||||
- 你是一位以尖锐和挑衅风格著称的专业评论员
|
||||
- 对每个群友的行为、言论进行犀利点评,不怕让人尴尬
|
||||
- 发言排行旁给每个人加一句毒舌备注(括号内)
|
||||
- 群友画像改为「不留情面版」,放大每个人的槽点和矛盾之处
|
||||
- 开头概览用更戏谑的口吻,突出荒诞和讽刺
|
||||
- 正文话题标题可以改得更损
|
||||
- 引用原话时配上辛辣点评
|
||||
- 结尾改为:本简报由一个没有感情的 AI 自动生成,如有冒犯,概不负责
|
||||
|
||||
注意:毒舌但不恶毒,调侃但不人身攻击。目标是让群友看了会笑,而不是生气。具体红线:
|
||||
- 只嘲讽群里的公开行为,不碰外貌、体重、健康、家庭、私人关系
|
||||
- 不用时间戳推断作息或时区(服务器时间不等于本地时间)
|
||||
- 不做医学/心理诊断类玩笑(「这位需要看医生」「典型 ADHD」)
|
||||
- 不揣测对方未主动公开的身份属性(性取向、宗教、政治立场)
|
||||
- 嘲讽观点本身,不嘲讽发言的权利(「这个观点错得离谱」可以,「连这都不懂还敢发言」不行)
|
||||
- 如果某人本期没有槽点(3+ 条但都很中性),给一句温和调侃即可,不要硬凑
|
||||
|
||||
**写作顺序:** 先放开写最狠的版本,写完再回头检查红线。不要边写边自我审查,那样只会写出温吞水。
|
||||
|
||||
---
|
||||
|
||||
## 3. Common formatting rules (both versions)
|
||||
|
||||
- **No markdown.** No `**bold**`, no `# headings`, no `*italic*`, no `[link](url)` syntax. Headings are plain text on their own line.
|
||||
- **Bullets use `•`.** Not `-`, not `*`, not `1.` for prose-style bullets.
|
||||
- **Numbered lists** (`1.`, `2.`) are reserved for the leaderboard.
|
||||
- **Subcategory hints** within a body block are plain text with no symbol prefix.
|
||||
- **Links preserved verbatim.** Paste the full URL inline. Don't shorten, don't hide behind text.
|
||||
- **One emoji per category title.** Don't stack 🛠💬 etc.
|
||||
- **Pain-point statuses** use ✅⚠️❌ verbatim.
|
||||
- **Quotes use 「」.** Single quotes for nested.
|
||||
- **Names verbatim.** Don't abbreviate `蛙总` to `蛙`, don't translate Chinese names, don't anonymize.
|
||||
|
||||
---
|
||||
|
||||
## 4. Common content rules (both versions)
|
||||
|
||||
- **Filter only pure noise.** Cut: lone emoji reactions, "好的"/"收到"/"哈哈哈" with no follow-on, duplicate forwards.
|
||||
- **Keep gossip, anecdotes, signature moments.** These are the highlight reel — the whole point of the digest.
|
||||
- **Plain language.** Preserve vivid expressions and idiosyncratic phrasings — that's what makes the speaker recognizable.
|
||||
- **Keep real names.** Both for traceability and so the digest is useful as memory.
|
||||
- **Tool, product, URL names complete.** `Claude Code 4.7`, not `CC`. `https://github.com/...`, not `GitHub 上那个项目`.
|
||||
- **Merge, don't list.** A 30-message debate becomes one paragraph, not 30 bullet points.
|
||||
- **Direct-quote deep observations.** When someone says something striking, quote it verbatim with 「」 rather than paraphrase.
|
||||
- **Shared articles → title + sharer.** `阿喵分享了《一个 Rust 工程师的反思》` — include the title and who shared.
|
||||
- **No timestamp-based sleep/timezone inference.** (Repeated here because it applies to both versions, not just roast — never say `凌晨 3 点还在线` in either.)
|
||||
- **No fabricated facts.** Every claim must be supported by an actual message in the batch (or in a loaded profile). If you're tempted to "add color," stop.
|
||||
|
||||
---
|
||||
|
||||
## 5. Output skeleton — quick reference
|
||||
|
||||
When you forget the structure mid-write, this is the skeleton:
|
||||
|
||||
### Normal
|
||||
|
||||
```
|
||||
{群名} 群聊精华 · {日期}
|
||||
|
||||
📊 消息统计: 共 N 条消息
|
||||
1. {昵称}: N 条
|
||||
2. {昵称}: N 条
|
||||
...
|
||||
10. {昵称}: N 条
|
||||
|
||||
{开篇 1-2 段,无标题,直入主题}
|
||||
|
||||
群友画像
|
||||
|
||||
{昵称}({角色标签})
|
||||
• {观察 1}
|
||||
• {观察 2}
|
||||
• {观察 3}
|
||||
|
||||
{昵称}({角色标签})
|
||||
• {观察 1}
|
||||
• {观察 2}
|
||||
|
||||
🛠 {分类标题 1}
|
||||
|
||||
{该分类下的整理过的讨论 / 段落 / 引用}
|
||||
|
||||
📦 {分类标题 2}
|
||||
|
||||
{...}
|
||||
|
||||
今日待解决问题(可选,没有就不写)
|
||||
|
||||
问题: {一句话}
|
||||
提出者: {昵称}
|
||||
背景: {1-2 句}
|
||||
状态: ⚠️ 部分解决
|
||||
方案: {若有}
|
||||
|
||||
本简报由 AI 自动生成
|
||||
```
|
||||
|
||||
### Roast
|
||||
|
||||
```
|
||||
{群名} 群聊精华 · {日期} · 毒舌版
|
||||
|
||||
📊 消息统计: 共 N 条消息
|
||||
1. {昵称}: N 条 ({毒舌评语})
|
||||
2. {昵称}: N 条 ({毒舌评语})
|
||||
...
|
||||
|
||||
{毒舌开篇 1-2 段}
|
||||
|
||||
群友画像
|
||||
|
||||
{昵称}({放大的角色标签})
|
||||
• {毒舌观察 1}
|
||||
• {毒舌观察 2}
|
||||
|
||||
🛠 {更大声的分类标题}
|
||||
|
||||
{保留真实引用的毒舌叙述}
|
||||
|
||||
本简报由一个没有感情的 AI 自动生成,如有冒犯,概不负责
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Self-check before saving
|
||||
|
||||
Before writing the digest file, mentally walk through:
|
||||
|
||||
1. Stats block accurate? Counts match the filtered message set?
|
||||
2. Top 10 names resolved (self_display substituted, ambiguous nicknames disambiguated)?
|
||||
3. Opening hooks at least one real category title?
|
||||
4. Every active user (3+ msgs) has a 画像 entry?
|
||||
5. Every category has a topic-named title (not "讨论")?
|
||||
6. Every quote uses 「」 and is traceable to a real message?
|
||||
7. Links inline and complete?
|
||||
8. No markdown bold/heading/link syntax leaked through?
|
||||
9. (Roast only) Every roast bullet would pass the §2 红线 audit?
|
||||
10. Footer line exact match?
|
||||
@@ -0,0 +1,273 @@
|
||||
# Profiles — user portrait files
|
||||
|
||||
This reference defines the per-user profile system. Profiles let the digest carry forward observations across many days so the 群友画像 section in each new digest can show continuity (`蛙总今天罕见地没提空头`) instead of starting from scratch.
|
||||
|
||||
Two parallel profile directories live alongside each group's digests:
|
||||
|
||||
- `profiles/` — observations sourced from the **normal** version of the digest.
|
||||
- `profiles-roast/` — observations sourced from the **roast** version.
|
||||
|
||||
They are kept strictly separate. The normal-version generation reads only `profiles/`; the roast-version generation reads only `profiles-roast/`. This prevents roast snark from contaminating the sober summary and vice versa.
|
||||
|
||||
Load this file during Step 3.7 (load profiles for active users), Step 8.5 (update profiles after digest is written), and Step 9 (backfill).
|
||||
|
||||
---
|
||||
|
||||
## 1. File format
|
||||
|
||||
### 1.1 Path & naming
|
||||
|
||||
- Normal: `wechat/{group_id}-{group_name}/profiles/{wxid}-{nickname}.md`
|
||||
- Roast: `wechat/{group_id}-{group_name}/profiles-roast/{wxid}-{nickname}.md`
|
||||
|
||||
The **stable** identifier is the `wxid` prefix. The `-{nickname}` suffix is for human browsability — if it changes, rename the file.
|
||||
|
||||
Filename sanitization: replace `/`, `\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`, NUL, and control characters with `_`. Trim trailing dots and whitespace. Cap total filename length at 200 chars (rare nicknames can be very long).
|
||||
|
||||
### 1.2 Frontmatter
|
||||
|
||||
YAML frontmatter at the top of every profile file:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: "<current display name>"
|
||||
wxid: "<wxid>"
|
||||
aliases: ["<old nickname>", "<even older nickname>"]
|
||||
first_seen: "YYYY-MM-DD"
|
||||
last_seen: "YYYY-MM-DD"
|
||||
total_messages: N
|
||||
digest_appearances: N
|
||||
avg_messages_per_digest: N.N
|
||||
---
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `name`: the most recent display name from `from_nickname` (or `self_display` for the owning user).
|
||||
- `wxid`: stable; never changes once written.
|
||||
- `aliases`: append-only; every prior display name we've seen. Don't include the current `name` in this list.
|
||||
- `first_seen` / `last_seen`: dates of first/most-recent digest appearance, YYYY-MM-DD.
|
||||
- `total_messages`: cumulative count across all digests this profile has been updated from.
|
||||
- `digest_appearances`: how many digest files this user has 3+ messages in.
|
||||
- `avg_messages_per_digest`: `total_messages / digest_appearances`, one decimal.
|
||||
|
||||
### 1.3 Free-form body — normal profile
|
||||
|
||||
Section headers are plain text on their own line. Order is fixed.
|
||||
|
||||
```
|
||||
角色标签
|
||||
|
||||
• {4-6 短语标签}
|
||||
|
||||
关注领域
|
||||
|
||||
• {领域 1}
|
||||
• {领域 2}
|
||||
|
||||
发言风格
|
||||
|
||||
{1-3 句描述,可以多段}
|
||||
|
||||
互动模式
|
||||
|
||||
• {与某某的互动模式}
|
||||
• {另一种互动模式}
|
||||
|
||||
经典金句
|
||||
|
||||
• [YYYY-MM-DD] 「{直接引用}」
|
||||
• [YYYY-MM-DD] 「{直接引用}」
|
||||
|
||||
标志性事件
|
||||
|
||||
• [YYYY-MM-DD] {事件描述}
|
||||
• [YYYY-MM-DD] {事件描述}
|
||||
```
|
||||
|
||||
### 1.4 Free-form body — roast profile
|
||||
|
||||
Same plain-text section header style, different sections.
|
||||
|
||||
```
|
||||
人设标签
|
||||
|
||||
• {4-6 放大版标签}
|
||||
|
||||
核心槽点
|
||||
|
||||
• {可吐槽点 1}
|
||||
• {可吐槽点 2}
|
||||
|
||||
毒舌语录库
|
||||
|
||||
• [YYYY-MM-DD] 「{该用户说过的话} — {简短毒舌点评}」
|
||||
• [YYYY-MM-DD] 「{...}」
|
||||
|
||||
经典翻车现场
|
||||
|
||||
• [YYYY-MM-DD] {翻车描述 + 引用 / 证据}
|
||||
• [YYYY-MM-DD] {...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Update rules
|
||||
|
||||
Rules differ per section. Append-only sections must never lose history; mergeable sections may be rewritten as understanding sharpens.
|
||||
|
||||
### 2.1 Normal profile
|
||||
|
||||
| Section | Update mode | Notes |
|
||||
|---------|-------------|-------|
|
||||
| 角色标签 | **Merge** | Cap 4-6 tags. Can replace less representative tags with stronger ones. Always keep the most consistently-supported tag. |
|
||||
| 关注领域 | **Merge dedupe** | Add new domains; dedupe by meaning, not exact string. |
|
||||
| 发言风格 | **Refine** | Only update when a clearly new pattern emerges. Avoid rewriting on every digest. |
|
||||
| 互动模式 | **Merge** | Add new modes; can refine existing ones with more detail. |
|
||||
| 经典金句 | **Append-only** | Never delete. No cap. Each entry must be dated and quoted verbatim. |
|
||||
| 标志性事件 | **Append-only** | Never delete. No cap. Each entry dated. |
|
||||
|
||||
### 2.2 Roast profile
|
||||
|
||||
| Section | Update mode | Notes |
|
||||
|---------|-------------|-------|
|
||||
| 人设标签 | **Merge** | Cap 4-6. Can sharpen tags as patterns repeat. |
|
||||
| 核心槽点 | **Append-only** | Never delete; recurring 槽点 build up here. |
|
||||
| 毒舌语录库 | **Append-only** | Never delete. No cap. Each entry dated, with both the quote and the roast comment. |
|
||||
| 经典翻车现场 | **Append-only** | Never delete. No cap. Each entry dated. |
|
||||
|
||||
### 2.3 Frontmatter on every update
|
||||
|
||||
- Update `name` if current display name differs from the recorded one. Push the old name onto `aliases` if not already there.
|
||||
- If `name` changed, also rename the file from `{wxid}-{old_nickname}.md` to `{wxid}-{new_nickname}.md`.
|
||||
- Update `last_seen` to the current digest's end date.
|
||||
- Increment `total_messages` by this batch's message count for this user.
|
||||
- Increment `digest_appearances` by 1.
|
||||
- Recompute `avg_messages_per_digest`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Step 8.5 — Update procedure
|
||||
|
||||
Run after the digest file(s) are written. Iterate over every user with 3+ messages in this batch.
|
||||
|
||||
1. **Look up the profile.**
|
||||
- Scan `profiles/` (or `profiles-roast/` for the roast pass) for a file whose name starts with `{wxid}-`.
|
||||
- If found: open it.
|
||||
- If not found: create a new file using the frontmatter template. `first_seen = last_seen = current digest end date`, `total_messages = this batch's count`, `digest_appearances = 1`.
|
||||
|
||||
2. **Resolve wxid for new users.** When a new user appears, you already know their `wxid` from the wx-cli message data — use it directly. If for some reason only the nickname is known, run `wx contacts --query "{nickname}" --json` to resolve; if multiple matches, prefer the one currently in the group (cross-check `wx members <group>` if needed).
|
||||
|
||||
3. **Update frontmatter.** Per §2.3.
|
||||
|
||||
4. **Update body sections.**
|
||||
- For mergeable sections (角色标签,关注领域,发言风格,互动模式 / roast: 人设标签): read the existing content, integrate new observations from this batch, rewrite the section.
|
||||
- For append-only sections (经典金句,标志性事件 / roast: 毒舌语录库,经典翻车现场,核心槽点): append new entries, each dated and verbatim. Never edit or remove prior entries.
|
||||
|
||||
5. **Write back.** Overwrite the file.
|
||||
|
||||
6. **Source separation.** Pass running for the normal digest writes only to `profiles/`. Pass running for the roast digest writes only to `profiles-roast/`. Even if both versions are generated in the same skill invocation, run two separate update passes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Step 9 — Backfill procedure
|
||||
|
||||
Triggered when the user says `回溯画像`, `初始化画像`, `backfill profiles`, or similar. This builds initial profiles from already-written digest files without re-fetching from wx-cli.
|
||||
|
||||
1. **List inputs.**
|
||||
- List every `*.md` digest file under `wechat/{group_id}-{group_name}/` (top level, not inside `profiles/` or `profiles-roast/`).
|
||||
- Partition by filename suffix: `*-roast.md` → roast pass, all others → normal pass.
|
||||
- Optionally also read `history-digests.jsonl` for fast metadata lookup (date, message count) before opening individual files.
|
||||
|
||||
2. **Decide whether to run roast backfill.** Only run the roast pass if at least one `*-roast.md` file exists.
|
||||
|
||||
3. **Process in batches of 10-15 digest files.** Reading all of them at once will blow context. For each batch:
|
||||
- Read the digests.
|
||||
- For each user appearing in the leaderboard or 群友画像 across the batch, accumulate:
|
||||
- Message counts per digest (from the stats block).
|
||||
- Role tags and observations (from the 群友画像 section).
|
||||
- Quotes (from inline 「」 in the body).
|
||||
- Dated events (from category bodies — when the digest mentions specific incidents).
|
||||
- Resolve wxid for each accumulated user via `wx contacts --query "{nickname}" --json` if not already cached. Cache the wxid↔nickname mapping for the rest of the backfill.
|
||||
|
||||
4. **Threshold.** Generate a profile file only for users appearing in **3 or more** digests in the corpus. Below that, skip (probably one-time visitors).
|
||||
|
||||
5. **Write profile files.**
|
||||
- For the normal pass, write to `profiles/{wxid}-{nickname}.md`.
|
||||
- For the roast pass, write to `profiles-roast/{wxid}-{nickname}.md`.
|
||||
- Use the most recent nickname as the filename suffix. Push older nicknames into `aliases`.
|
||||
- Sort 经典金句,标志性事件,毒舌语录库,经典翻车现场 entries chronologically by date.
|
||||
- No cap on the size of append-only sections during backfill — let history flow in.
|
||||
|
||||
6. **Compute frontmatter.**
|
||||
- `first_seen` = earliest digest date the user appeared in.
|
||||
- `last_seen` = latest digest date the user appeared in.
|
||||
- `total_messages` = sum of per-digest counts.
|
||||
- `digest_appearances` = number of digests the user crossed the 3-message threshold in.
|
||||
|
||||
7. **Report.** After both passes complete, print a short summary:
|
||||
- `Backfilled {N} normal profiles from {M} digests.`
|
||||
- `Backfilled {K} roast profiles from {L} roast digests.` (only if roast pass ran)
|
||||
- List any users skipped due to wxid resolution failures so the user can fix manually.
|
||||
|
||||
8. **Re-running backfill is safe.** If the user runs backfill twice, treat existing profile files as the prior state and merge — same rules as Step 8.5 updates. Don't blow away existing append-only entries.
|
||||
|
||||
---
|
||||
|
||||
## 5. Privacy guardrails
|
||||
|
||||
These apply to both normal and roast profiles, with an extra layer for roast.
|
||||
|
||||
### 5.1 Forbidden (write neither in normal nor roast)
|
||||
|
||||
- **Real-world full names** when only a nickname was used in the group. If the person introduced themselves with `我叫王二`, `王二` is on the table; `王晓明` inferred from another channel is not.
|
||||
- **Phone numbers, emails, ID numbers, home addresses, employer addresses, exact birth dates** — even if mentioned in the group, don't lift them into profile files.
|
||||
- **Health, medical, psychological information.** Even self-disclosed (`我最近有点抑郁`) — don't bake it into a permanent profile.
|
||||
- **Private romantic / family details** unless openly group-discussed by the person themselves. A passing mention by another member doesn't count.
|
||||
- **Embarrassing private failures.** Public ones (a take that aged badly in front of the group) are fair game; private ones (a job rejection mentioned briefly) are not.
|
||||
- **Sleep / timezone inference from timestamps.** Server time ≠ recipient's local time, and it implies surveillance.
|
||||
|
||||
### 5.2 Allowed
|
||||
|
||||
- **Public group behavior** — what they said, how they argued, what they shared.
|
||||
- **Direct quotes** of things said in the group (these are already public to the group).
|
||||
- **Interest areas, hobbies, tool preferences** as expressed in group discussion.
|
||||
- **Interaction patterns** with other group members.
|
||||
- **Publicly mentioned consumption** (`蛙总今天又分享了买了什么书`) — fine if they themselves mentioned it.
|
||||
- **Publicly shared travel / life anecdotes** they told the group.
|
||||
|
||||
### 5.3 Roast-only extras
|
||||
|
||||
In addition to §5.1, the roast profile must **not** include:
|
||||
|
||||
- **Anything about appearance, weight, body, looks.**
|
||||
- **Anything about family members** (their kids, parents, partners) — only the person themselves.
|
||||
- **Mental-health speculation**, even as a joke. No `这位需要看医生`, no `典型 ADHD`.
|
||||
- **Identity-based roasts.** No mocking of orientation, religion, ethnicity, nationality, gender.
|
||||
|
||||
The roast may mock:
|
||||
|
||||
- Stupid takes, contradictions, factual errors.
|
||||
- Repetitive behavior (`第 47 次预测见顶`).
|
||||
- Self-undermining moments (`昨天说 X,今天说 not X`).
|
||||
- Performative flexes that didn't land.
|
||||
|
||||
The rule of thumb: **roast the take, not the person.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Reading profiles during digest generation (Step 3.7)
|
||||
|
||||
When loading profile context for a fresh digest:
|
||||
|
||||
1. Iterate over users active in this batch (3+ messages).
|
||||
2. For the normal pass, read `profiles/{wxid}-*.md` for each. Skip if missing.
|
||||
3. If the current run also generates the roast version, **separately** read `profiles-roast/{wxid}-*.md` during the roast generation pass.
|
||||
4. Compile a condensed working-memory block:
|
||||
- The user's current `name` and `aliases` (so you can recognize them under different names).
|
||||
- 角色标签 / 人设标签 (so you can carry forward or contrast).
|
||||
- The 3-5 most recent 经典金句 / 毒舌语录 entries (so you can detect callbacks and repeats).
|
||||
- The 3-5 most recent 标志性事件 / 翻车现场 entries (so you can spot recurring themes).
|
||||
5. Don't dump the entire profile into the digest — the profile is *context*, the digest is *today*.
|
||||
|
||||
If a profile contradicts what you see in today's batch (e.g., the profile says `从不主动发起话题`, but today they started three threads), call that out explicitly in the day's 群友画像 — that's the kind of contrast that makes the digest interesting.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: baoyu-xhs-images
|
||||
description: "[Deprecated: use baoyu-image-cards] Generates Xiaohongshu (Little Red Book) image card series with 12 visual styles, 8 layouts, and 3 color palettes. Breaks content into 1-10 cartoon-style image cards optimized for XHS engagement. Use when user mentions \"小红书图片\", \"XHS images\", \"RedNote infographics\", \"小红书种草\", \"小绿书\", \"微信图文\", \"微信贴图\", or wants social media infographic series for Chinese platforms."
|
||||
version: 1.56.1
|
||||
version: 1.57.0
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-xhs-images
|
||||
@@ -24,14 +24,51 @@ Concrete `AskUserQuestion` references below are examples — substitute the loca
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
When this skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-imagine`, or any equivalent the user has installed.
|
||||
- **If multiple are available**, ask the user **once** at the start which to use (batch with any other initial questions).
|
||||
- **If none are available**, tell the user and ask how to proceed.
|
||||
1. **Current-request override** — if the user names a specific backend in the current message, use it.
|
||||
2. **Saved preference** — if `EXTEND.md` sets `preferred_image_backend` to a backend available right now, use it.
|
||||
3. **Auto-select** (when the preference is `auto`, unset, or the pinned backend isn't available):
|
||||
- **Codex (`imagegen`)** — first, inspect your available-skills / tool inventory. If a skill named `imagegen` is listed, you are running inside Codex and MUST use it: invoke via the `Skill` tool with `skill: "imagegen"`, passing the saved prompt file's content (plus output path and aspect ratio per Codex `imagegen`'s own args). Codex `imagegen` is the official raster backend in that runtime and outranks any non-native skill (e.g., `baoyu-imagine`) unless the user has explicitly pinned a different `preferred_image_backend`.
|
||||
- **Other runtime-native tools** — if the runtime exposes a different native image tool (e.g., Hermes `image_generate`), use it the same way.
|
||||
- Otherwise, if exactly one non-native backend is installed (e.g., `baoyu-imagine`), use it.
|
||||
- Otherwise (multiple non-native backends with no runtime-native tool), ask the user once — batch with any other initial questions.
|
||||
4. **If none are available**, tell the user and ask how to proceed.
|
||||
|
||||
**⛔ Never substitute SVG, HTML, canvas, or other code-based rendering for raster image generation.** Codex `imagegen`'s own description says it should be used "when the output should be a bitmap asset rather than repo-native code or vector." If you cannot resolve a raster backend via step 3, fall through to step 4 and ask the user — do **not** silently emit SVG, write inline `<svg>` markup, or produce HTML/CSS art as a substitute. This applies even if the article/section seems "diagram-like": the consumer skill calling this rule has already decided that a raster image is what it needs.
|
||||
|
||||
Setting `preferred_image_backend: ask` forces the step-3 prompt every run regardless of available backends. Users change the pinned backend via the `## Changing Preferences` section below.
|
||||
|
||||
**Prompt file requirement (hard)**: write each image's full, final prompt to a standalone file under `prompts/` (naming: `NN-{type}-[slug].md`) BEFORE invoking any backend. The file is the reproducibility record and lets you switch backends without regenerating prompts.
|
||||
|
||||
Concrete tool names (`imagegen`, `image_generate`, `baoyu-imagine`) above are examples — substitute the local equivalents under the same rule.
|
||||
|
||||
## Batch Generation Policy
|
||||
|
||||
After every prompt file for the current generation group has been saved and verified, generate images in batches by default.
|
||||
|
||||
Priority order:
|
||||
|
||||
1. Use the chosen backend's native batch / multi-task interface if it exists. Each task must keep its own prompt file, output path, aspect ratio, session ID, and direct reference images.
|
||||
2. If no native batch interface exists but the runtime can issue parallel tool calls, dispatch up to `generation_batch_size` images at a time. Default: `4`. An explicit user request in the current message, such as `--batch-size 4` or "并行4张一起生成", overrides EXTEND.md.
|
||||
3. If neither native batch nor parallel tool calls are available, generate sequentially.
|
||||
|
||||
Rules:
|
||||
|
||||
- Honor the image-1 anchor chain: generate image 1 first, then batch images 2+ using image 1 as the reference.
|
||||
- Never start a batch until every selected prompt file for that batch exists on disk.
|
||||
- Retry failed items once without regenerating successful items.
|
||||
- Do not use subagents merely to parallelize image rendering. Use subagents only for separate prompt iteration or creative exploration.
|
||||
|
||||
## Confirmation Policy
|
||||
|
||||
Default behavior: **confirm before generation**.
|
||||
|
||||
- Treat explicit skill invocation, a file path, matched signals/presets, and `EXTEND.md` defaults as **recommendation inputs only**. None of them authorizes skipping confirmation.
|
||||
- Do **not** start Step 3 until the user completes Step 2.
|
||||
- Skip confirmation only when the current request explicitly says to do so, for example: `--yes`, "直接生成", "不用确认", "跳过确认", "按默认出图", or equivalent wording.
|
||||
- If confirmation is skipped explicitly, state the assumed strategy / style / layout / palette / count / backend in the next user-facing update before generating.
|
||||
|
||||
## Language
|
||||
|
||||
Respond in the user's language across questions, progress, errors, and completion summary. Keep technical tokens (style names, file paths, code) in English.
|
||||
@@ -45,6 +82,7 @@ Respond in the user's language across questions, progress, errors, and completio
|
||||
| `--palette <name>` | Color override: macaron / warm / neon |
|
||||
| `--preset <name>` | Style + layout + optional palette shorthand (see Presets below; per-preset prompt fragments in `references/style-presets.md`) |
|
||||
| `--ref <files...>` | Reference images applied to image 1 as the series anchor |
|
||||
| `--batch-size <n>` | Temporary generation batch size for this run. Default: `generation_batch_size` from EXTEND.md, otherwise 4. Clamp to 1-8. |
|
||||
| `--yes` | Non-interactive: skip all confirmations, use EXTEND.md or built-in defaults, auto-confirm recommended plan (Path A) |
|
||||
|
||||
## Dimensions
|
||||
@@ -280,7 +318,7 @@ Check these paths in order; first hit wins:
|
||||
- **Not found + interactive** → run first-time setup (see `references/config/first-time-setup.md`) and save before anything else. Do NOT analyze content or ask style questions until preferences exist — this keeps first-run behavior predictable.
|
||||
- **Not found + `--yes`** → skip setup, use built-in defaults (no watermark, style/layout auto-selected, language from content). Do not prompt, do not create EXTEND.md.
|
||||
|
||||
**EXTEND.md keys**: watermark, preferred style/layout, custom style definitions, language preference. Schema: `references/config/preferences-schema.md`.
|
||||
**EXTEND.md keys**: watermark, preferred style/layout, custom style definitions, language preference, preferred image backend, generation batch size. Schema: `references/config/preferences-schema.md`.
|
||||
|
||||
### Step 1: Analyze Content → `analysis.md`
|
||||
|
||||
@@ -292,6 +330,8 @@ Check these paths in order; first hit wins:
|
||||
|
||||
### Step 2: Smart Confirm ⚠️ REQUIRED
|
||||
|
||||
**Hard gate**: this step is mandatory per the [Confirmation Policy](#confirmation-policy) — Step 3 cannot start until the user confirms here (or explicitly opts out with `--yes` / equivalent wording in the current request).
|
||||
|
||||
Goal: present the auto-recommended plan and let the user confirm or adjust. Skip this step entirely under `--yes` — proceed with Path A using the analysis and any CLI overrides.
|
||||
|
||||
**Display summary** before asking:
|
||||
@@ -327,14 +367,13 @@ With confirmed outline + style + layout + palette:
|
||||
|
||||
**Visual consistency — image-1 anchor chain**: character / mascot / color rendering drifts between calls unless you anchor them. Generate image 1 (cover) first WITHOUT `--ref`, then pass image 1 as `--ref` to every subsequent image. This is the single most important consistency trick for this skill — don't skip it even if the backend also supports a session ID.
|
||||
|
||||
For each image (cover, content, ending):
|
||||
Generation flow:
|
||||
|
||||
1. Write the full prompt to `prompts/NN-{type}-{slug}.md` in the user's preferred language (backup rule applies).
|
||||
2. Generate:
|
||||
- **Image 1**: no `--ref` (establishes the anchor).
|
||||
- **Images 2+**: add `--ref <path-to-image-01.png>`.
|
||||
- Backup rule applies to the PNG files.
|
||||
3. Report progress after each image.
|
||||
1. Write the full prompt for every image to `prompts/NN-{type}-{slug}.md` in the user's preferred language (backup rule applies), then verify all selected prompt files exist.
|
||||
2. Generate **image 1** first without `--ref`; backup rule applies to the PNG file. This establishes the anchor.
|
||||
3. Build a task list for **images 2+** using image 1 as `--ref <path-to-image-01.png>`.
|
||||
4. Dispatch images 2+ in batches per the `## Batch Generation Policy`: backend native batch first, runtime parallel tool calls second, sequential only as fallback.
|
||||
5. Report progress after each completed image. On failure, retry only the failed item once from the same saved prompt file.
|
||||
|
||||
**Watermark** (if enabled in EXTEND.md): append to the generation prompt:
|
||||
|
||||
@@ -418,4 +457,17 @@ Always update the prompt file before regenerating — it's the source of truth a
|
||||
- For sensitive public figures, use stylized cartoon alternatives.
|
||||
- Smart Confirm (Step 2) is required; Detailed mode adds a second confirmation (2a + 2c).
|
||||
|
||||
Custom configurations via EXTEND.md. See Step 0 for paths and schema.
|
||||
## Changing Preferences
|
||||
|
||||
EXTEND.md lives at the first matching path listed in Step 0. Three ways to change it:
|
||||
|
||||
- **Edit directly** — open EXTEND.md and change fields. Full schema: `references/config/preferences-schema.md`.
|
||||
- **Reconfigure interactively** — delete EXTEND.md (or ask "reconfigure baoyu-xhs-images preferences" / "重新配置"). The next run re-triggers first-time setup.
|
||||
- **Common one-line edits**:
|
||||
- `preferred_image_backend: auto` — default; runtime-native tool wins, falls back to the only installed backend, asks only if multiple non-native are present.
|
||||
- `preferred_image_backend: codex-imagegen` — pin to Codex's built-in.
|
||||
- `preferred_image_backend: baoyu-imagine` — pin to the baoyu-imagine skill.
|
||||
- `preferred_image_backend: ask` — confirm backend every run.
|
||||
- `generation_batch_size: 4` — default number of images to render concurrently when the backend/runtime supports batch or parallel generation.
|
||||
- `preferred_style: notion`, `preferred_layout: dense`, `preferred_palette: macaron`, `language: zh`.
|
||||
- `watermark.enabled: true` + `watermark.content: "@handle"` — add a watermark.
|
||||
|
||||
@@ -110,13 +110,16 @@ preferred_style:
|
||||
description: ""
|
||||
preferred_layout: null
|
||||
language: null
|
||||
preferred_image_backend: auto
|
||||
generation_batch_size: 4
|
||||
custom_styles: []
|
||||
---
|
||||
```
|
||||
|
||||
`preferred_image_backend: auto` is the baked-in default — first-time setup does not ask about it. The `## Image Generation Tools` rule in SKILL.md then picks the runtime-native tool (Codex `imagegen`, Hermes `image_generate`, etc.) when available, and falls back to installed backends.
|
||||
|
||||
`generation_batch_size: 4` is the baked-in default for batch rendering. The current user request may override it for one run.
|
||||
|
||||
## Modifying Preferences Later
|
||||
|
||||
Users can edit EXTEND.md directly or run setup again:
|
||||
- Delete EXTEND.md to trigger setup
|
||||
- Edit YAML frontmatter for quick changes
|
||||
- Full schema: `config/preferences-schema.md`
|
||||
See the `## Changing Preferences` section in `SKILL.md` for the canonical list of common edits (pin backend, change defaults, retrigger setup). Full schema: `preferences-schema.md`.
|
||||
|
||||
@@ -24,6 +24,10 @@ preferred_layout: null # sparse|balanced|dense|list|comparison|flow
|
||||
|
||||
language: null # zh|en|ja|ko|auto
|
||||
|
||||
preferred_image_backend: auto # auto|ask|<backend-id>
|
||||
|
||||
generation_batch_size: 4 # 1-8, used when backend/runtime supports batch or parallel generation
|
||||
|
||||
custom_styles:
|
||||
- name: my-style
|
||||
description: "Style description"
|
||||
@@ -49,6 +53,8 @@ custom_styles:
|
||||
| `preferred_style.description` | string | "" | Custom notes/override |
|
||||
| `preferred_layout` | string | null | Layout preference or null |
|
||||
| `language` | string | null | Output language (null = auto-detect) |
|
||||
| `preferred_image_backend` | string | `auto` | Image backend selection. `auto` = prefer runtime-native tool, fall back to the only installed backend, ask if multiple non-native are present. `ask` = always confirm on every run. `<backend-id>` (e.g., `codex-imagegen`, `baoyu-imagine`, `image_generate`) = pin this backend when available; fall back to `auto` when it isn't. Absent = `auto`. Resolution logic is documented in `SKILL.md`'s `## Image Generation Tools` section. |
|
||||
| `generation_batch_size` | int | 4 | Number of images to dispatch per batch when the backend has native batch support or the runtime can issue parallel generation calls. Clamp invalid values to 1-8. Current user request overrides this value. |
|
||||
| `custom_styles` | array | [] | User-defined styles |
|
||||
|
||||
## Position Options
|
||||
@@ -104,6 +110,10 @@ preferred_layout: dense
|
||||
|
||||
language: zh
|
||||
|
||||
preferred_image_backend: codex-imagegen
|
||||
|
||||
generation_batch_size: 4
|
||||
|
||||
custom_styles:
|
||||
- name: corporate
|
||||
description: "Professional B2B style"
|
||||
|
||||
Reference in New Issue
Block a user