mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-10 13:11:30 +00:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3cbce9ce3 | |||
| bc303345f4 | |||
| 17d7d6105c | |||
| 50c6c72944 | |||
| ad1755fa23 | |||
| 3cc1225b18 | |||
| 460bd087c6 | |||
| daf0fb7bec | |||
| adbfa3036b | |||
| 6026b619f0 | |||
| 1406a85331 | |||
| dac5867f8a | |||
| 49829e7f98 | |||
| df16bd5d1a | |||
| 9596d39e7b | |||
| 156f8627c2 | |||
| e0b861c148 | |||
| 3b29f3c57c | |||
| f8fb457f36 | |||
| 174e472a39 | |||
| 8b99fa7af0 | |||
| 6699b802c0 | |||
| edcdc19ac5 | |||
| e98fa33bc2 | |||
| 5d2a39c636 | |||
| db58bdee8c | |||
| 38cc497748 | |||
| 0f95d12a09 | |||
| 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 | |||
| f8e434d2a0 | |||
| bb66585c00 | |||
| d6442e1ab4 | |||
| 2f09f48726 | |||
| fe26c2b639 | |||
| d7f623158c | |||
| 2c800c670a | |||
| 5b20f9a746 | |||
| 9977ff520c | |||
| 03bd68d3a4 | |||
| 39792f4360 | |||
| 25b8a7f73d | |||
| e13092ba2a | |||
| acbcf19ba2 | |||
| dcd0f81433 | |||
| c938396efc | |||
| 1ba68c9a9c | |||
| b3f5c0a8aa | |||
| 9c45ede0c7 | |||
| 30d2ac98ce | |||
| bfdd64bd4e | |||
| 434d4857da | |||
| 990fea4f7b | |||
| 517ff566a1 | |||
| 4c9af7d92f | |||
| 46c4859d48 | |||
| f3f886217b | |||
| 4d465d55d0 | |||
| 11d80eeaa9 | |||
| 58ba4579ef | |||
| 67a45a57a0 | |||
| 0b8ac256f4 | |||
| eaa0f1aa11 | |||
| ec5f4ffcc9 | |||
| d206e1674d | |||
| b62ad26098 | |||
| 31b2929d1c | |||
| 7c2253dd3e | |||
| 8be98c5afe | |||
| fe1a854bed |
@@ -6,14 +6,14 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.97.0"
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "baoyu-skills",
|
||||
"description": "Content generation, AI backends, and utility tools for daily work efficiency",
|
||||
"source": "./",
|
||||
"strict": true,
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./skills/baoyu-article-illustrator",
|
||||
"./skills/baoyu-comic",
|
||||
@@ -21,8 +21,10 @@
|
||||
"./skills/baoyu-cover-image",
|
||||
"./skills/baoyu-danger-gemini-web",
|
||||
"./skills/baoyu-danger-x-to-markdown",
|
||||
"./skills/baoyu-diagram",
|
||||
"./skills/baoyu-electron-extract",
|
||||
"./skills/baoyu-format-markdown",
|
||||
"./skills/baoyu-imagine",
|
||||
"./skills/baoyu-image-gen",
|
||||
"./skills/baoyu-infographic",
|
||||
"./skills/baoyu-markdown-to-html",
|
||||
"./skills/baoyu-post-to-weibo",
|
||||
@@ -31,7 +33,8 @@
|
||||
"./skills/baoyu-slide-deck",
|
||||
"./skills/baoyu-translate",
|
||||
"./skills/baoyu-url-to-markdown",
|
||||
"./skills/baoyu-image-cards",
|
||||
"./skills/baoyu-wechat-summary",
|
||||
"./skills/baoyu-xhs-images",
|
||||
"./skills/baoyu-youtube-transcript"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
---
|
||||
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
|
||||
|
||||
Universal release workflow supporting any project type with multi-language changelog.
|
||||
|
||||
## 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.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Just run `/release-skills` - auto-detects your project configuration.
|
||||
@@ -29,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
|
||||
|
||||
@@ -47,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**:
|
||||
|
||||
@@ -300,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**:
|
||||
|
||||
@@ -315,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
|
||||
@@ -325,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:
|
||||
@@ -339,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:
|
||||
|
||||
@@ -357,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
|
||||
@@ -370,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.
|
||||
@@ -381,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:
|
||||
@@ -488,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
|
||||
@@ -496,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.
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ set -eu
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
node scripts/sync-shared-skill-packages.mjs --repo-root "$REPO_ROOT" --enforce-clean
|
||||
node scripts/verify-shared-package-deps.mjs --repo-root "$REPO_ROOT"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: codex-imagegen tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'scripts/codex-imagegen/**'
|
||||
- 'scripts/codex-imagegen.sh'
|
||||
- '.github/workflows/codex-imagegen-tests.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'scripts/codex-imagegen/**'
|
||||
- 'scripts/codex-imagegen.sh'
|
||||
- '.github/workflows/codex-imagegen-tests.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Show Bun version
|
||||
run: bun --version
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: scripts/codex-imagegen
|
||||
run: bun test
|
||||
|
||||
- name: Bundle smoke test (catches import/syntax errors)
|
||||
run: bun build --target=node scripts/codex-imagegen/main.ts --outfile /tmp/main-build.js
|
||||
|
||||
- name: Help output smoke test
|
||||
run: bun scripts/codex-imagegen/main.ts --help
|
||||
@@ -11,6 +11,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -18,8 +20,16 @@ jobs:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Verify skill release commits
|
||||
run: npm run verify:skill-release-commits
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install baoyu-post-to-wechat script dependencies
|
||||
run: |
|
||||
cd skills/baoyu-post-to-wechat/scripts
|
||||
npm install --no-audit --no-fund --ignore-scripts
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
@@ -83,6 +83,10 @@ out
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Shared package builds are published to npm.
|
||||
!packages/*/dist/
|
||||
!packages/*/dist/**
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
@@ -154,6 +158,7 @@ illustrations/
|
||||
comic/
|
||||
translate/
|
||||
posts/
|
||||
diagram/
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
@@ -168,3 +173,6 @@ posts/
|
||||
.worktrees/
|
||||
youtube-transcript/
|
||||
.omx/
|
||||
.codex-tmp/
|
||||
outputs/
|
||||
wechat/
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@ release:
|
||||
target_globs:
|
||||
- skills/*
|
||||
hooks:
|
||||
prepare_artifact: node scripts/sync-shared-skill-packages.mjs --repo-root "{project_root}" --target "{target}"
|
||||
prepare_artifact: node scripts/verify-shared-package-deps.mjs --repo-root "{project_root}" --target "{target}"
|
||||
publish_artifact: node scripts/publish-skill.mjs --skill-dir "{target}" --version "{version}" --changelog-file "{release_notes_file}" --dry-run "{dry_run}"
|
||||
|
||||
+292
-1
@@ -2,6 +2,297 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 2.0.0 - 2026-05-24
|
||||
|
||||
### Breaking
|
||||
- Removed `baoyu-imagine` skill. All functionality (providers, scripts, references) now lives under `baoyu-image-gen`. The skill is registered in `marketplace.json` under the new name and its `homepage` URL has changed to `#baoyu-image-gen`.
|
||||
- Removed `baoyu-image-cards` skill. All functionality (styles, layouts, palettes, presets) now lives under `baoyu-xhs-images`. The skill is registered in `marketplace.json` under the new name.
|
||||
- Cross-skill `## Image Generation Tools` examples in `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-infographic`, and `baoyu-slide-deck` now reference `baoyu-image-gen` instead of `baoyu-imagine`.
|
||||
|
||||
### Migration
|
||||
- Existing `~/.baoyu-skills/baoyu-imagine/EXTEND.md` and `.baoyu-skills/baoyu-imagine/EXTEND.md` configs are auto-renamed to `…/baoyu-image-gen/EXTEND.md` on first run via the legacy-path resolver in `scripts/main.ts`.
|
||||
- Users invoking the skill via slash command should switch from `/baoyu-imagine ...` to `/baoyu-image-gen ...` and from any `baoyu-image-cards` reference to `baoyu-xhs-images`.
|
||||
|
||||
## 1.119.0 - 2026-05-24
|
||||
|
||||
### Features
|
||||
- `baoyu-electron-extract`: new skill that extracts resources and JavaScript from any installed Electron app's `app.asar`. Restores original sources from embedded `sourcesContent` in `.js.map` files when present (TypeScript/JSX included) or formats minified JS/CSS with Prettier in place when not. Source-map paths are resolved relative to each `.js.map` file first, so bundler-relative entries like `../../src/main.ts` restore to readable paths under `restored/` instead of hashed placeholders. Always skips `node_modules` and `webpack/runtime/*` entries. Auto-discovers apps on macOS (`/Applications`, `~/Applications`) and Windows (`%LOCALAPPDATA%\Programs`, `%PROGRAMFILES%`, `%PROGRAMFILES(X86)%`, `%APPDATA%`); other platforms can pass `--asar <path>` explicitly. Safety: refuses to write to `/`, the user home, or the current working directory, and refuses non-empty existing output dirs without `--force`
|
||||
|
||||
## 1.118.0 - 2026-05-21
|
||||
|
||||
### Features
|
||||
- `codex-imagegen`: new image-generation backend for non-Codex runtimes (e.g., Claude Code) — spawns `codex exec --json --sandbox danger-full-access` and delegates to Codex CLI's built-in `image_gen` tool, so no `OPENAI_API_KEY` is required. Ships with idempotency cache, file-lock concurrency control, JSONL event-stream parsing, PNG magic-byte validation, and exponential-backoff retries (by @yelban, #158)
|
||||
- `baoyu-cover-image`: wire `SKILL.md` to call the `codex-imagegen` wrapper when `preferred_image_backend: codex-imagegen` is set, with `--timeout` documented for slow networks
|
||||
|
||||
### Refactor
|
||||
- `codex-imagegen`: enforce `--prompt` / `--prompt-file` mutual exclusion in code (was docs-only)
|
||||
- `codex-imagegen`: replace `(opts as any).__promptFile` hack with a typed `promptFile` field on `CliOptions`
|
||||
- `codex-imagegen`: replace inline `cp|mv ... generated_images` regex with the shared `findCpToTarget` helper
|
||||
- `codex-imagegen`: propagate `attempts` on error responses (previously hardcoded to `0`)
|
||||
- `codex-imagegen`: drop dead `parseFinalJson()` + matching test (wrapper ignores agent-reported JSON in favor of disk verification)
|
||||
|
||||
### Security
|
||||
- `codex-imagegen`: reject `--image` / `--ref` paths containing shell metacharacters before interpolating them into the agent instruction sent to `codex exec --sandbox danger-full-access`
|
||||
|
||||
### Credits
|
||||
- `codex-imagegen` backend contributed by @yelban (#158)
|
||||
|
||||
## 1.117.5 - 2026-05-21
|
||||
|
||||
### Credits
|
||||
- `baoyu-post-to-wechat`: remote API publishing update credited to Dame5211 <1079825614@qq.com>
|
||||
|
||||
## 1.117.4 - 2026-05-21
|
||||
|
||||
### Features
|
||||
- `baoyu-post-to-wechat`: add remote API publishing via an SSH SOCKS5 tunnel
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-wechat`: make remote API publishing work under Bun and strictly validate remote publish config
|
||||
|
||||
### CI
|
||||
- Install `baoyu-post-to-wechat` script dependencies before running tests
|
||||
|
||||
## 1.117.3 - 2026-05-20
|
||||
|
||||
### Features
|
||||
- CI: add skill release commit validation — commits touching `skills/<name>/**` must use Conventional Commit subjects; SKILL.md version validated during publish/sync
|
||||
|
||||
### Fixes
|
||||
- `baoyu-diagram`: add version field to SKILL.md
|
||||
- `baoyu-post-to-wechat`: sync SKILL.md version
|
||||
|
||||
### Documentation
|
||||
- `baoyu-wechat-summary`: restructure profile fields — split `aliases` into `group_nicknames` (user's own prior names) and `aliases` (nicknames from other members), add `tags` for cross-cutting attributes
|
||||
|
||||
## 1.117.2 - 2026-05-17
|
||||
|
||||
### Documentation
|
||||
- `baoyu-cover-image`: ban programmatic text repair on generated bitmaps — disallow ImageMagick / Pillow / Canvas / SVG / HTML overlays to cover, rewrite, or replace title/subtitle text; regenerate from a corrected prompt or switch to a lower-text or no-title variant instead
|
||||
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-image-cards`, `baoyu-xhs-images`, `baoyu-infographic`, `baoyu-slide-deck`: sync the same text-repair ban with skill-specific text categories (labels/captions, dialogue/sound effects, titles/body/tags, headings/data values, slide titles/bullets)
|
||||
|
||||
## 1.117.1 - 2026-05-16
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-wechat`: fix WeChat browser article publishing (by @zhangga)
|
||||
- `baoyu-post-to-wechat`: fix image upload fallback and WebP clipboard copy on macOS
|
||||
|
||||
## 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
|
||||
- `baoyu-url-to-markdown`: vendor the `baoyu-fetch` runtime into `scripts/lib` and run it through a local `scripts/baoyu-fetch` CLI so published skill installs are self-contained
|
||||
|
||||
### Fixes
|
||||
- `baoyu-fetch`: extract playable X/Twitter video MP4 variants for single posts and X Articles, choosing the highest-bitrate MP4 and rendering article videos as `[video](...)`
|
||||
- `sync-clawhub`: publish from the shared release file list so extensionless CLI entrypoints, `bun.lock`, and vendored `scripts/lib` files are uploaded
|
||||
|
||||
### Maintenance
|
||||
- Upgrade `defuddle` to 0.17.0 and `jsdom` to 29.0.2; override `@xmldom/xmldom` to 0.8.13 to keep the Defuddle dependency chain vulnerability-free
|
||||
|
||||
## 1.108.0 - 2026-04-19
|
||||
|
||||
### Refactor
|
||||
- Refactor skills into focused reference files for better maintainability
|
||||
- Use npm packages for shared skill code across skills
|
||||
|
||||
## 1.107.0 - 2026-04-15
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add SVG-to-PNG @2x conversion script — auto-converts generated SVG diagrams to @2x PNG using Sharp; consolidate reference files and add `{baseDir}` path resolution for portable skill loading
|
||||
|
||||
### Fixes
|
||||
- `claude-plugin`: allow inline marketplace manifest (#130)
|
||||
|
||||
## 1.106.0 - 2026-04-14
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add architecture enrichment rules — automatically expand architecture diagrams with multiple client types, per-service tech stacks, database tiers, message buses, and color-coded categories; add full structural layout patterns, architecture-specific pitfalls, network topology templates, and layout math for complex diagrams
|
||||
|
||||
## 1.105.0 - 2026-04-13
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: unify to analyze→confirm→generate workflow — remove single/multi mode split; skill now analyzes any input material, recommends diagram types and splitting strategy, confirms once, then generates all diagrams
|
||||
|
||||
## 1.104.0 - 2026-04-13
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add Mermaid sketch step (6d-0) before SVG generation — write a Mermaid code block as structural intent; add Mermaid–SVG consistency check in step 6f
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-wechat`: verify editor focus before paste and type operations to prevent silent paste failures
|
||||
|
||||
## 1.103.1 - 2026-04-13
|
||||
|
||||
### Fixes
|
||||
- `baoyu-markdown-to-html`: decode HTML entities and strip tags from article summary
|
||||
- `baoyu-post-to-weibo`: decode HTML entities and strip tags from article summary
|
||||
|
||||
## 1.103.0 - 2026-04-12
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add multi-diagram mode — analyze article content and generate multiple diagrams at identified positions; new `--density` option (`minimal`, `balanced`, `per-section`, `rich`) and `--mode` option (`single`, `multi`, `auto`); auto-detects mode from input (file path → multi, short topic → single); inserts diagram image links into article; output structure `diagram/{article-slug}/NN-{type}-{slug}/`
|
||||
|
||||
### Fixes
|
||||
- `baoyu-article-illustrator`: prevent color names and hex codes from appearing as visible text in generated images — add semantic constraint to all palette references and prompt construction rules
|
||||
- `baoyu-cover-image`: prevent color names and hex codes from appearing as visible text in generated images — add constraint to all palette references and prompt template
|
||||
- `baoyu-image-cards`: prevent color names from appearing as visible text in generated images
|
||||
- `baoyu-post-to-wechat`: decode HTML entities and strip HTML tags from article summary before using as WeChat article digest
|
||||
|
||||
## 1.102.0 - 2026-04-12
|
||||
|
||||
### Features
|
||||
- `baoyu-imagine`: add OpenAI-compatible image API dialect — new `--imageApiDialect` flag, `OPENAI_IMAGE_API_DIALECT` env var, and `default_image_api_dialect` config for gateways that expect aspect-ratio `size` plus `metadata.resolution` instead of pixel `size`
|
||||
|
||||
## 1.101.0 - 2026-04-12
|
||||
|
||||
### Features
|
||||
- `baoyu-imagine`: improve Replicate provider compatibility — route models through family-specific input builders and validators (nano-banana, Seedream 4.5, Seedream 5 Lite, Wan 2.7 Image); update default model to `google/nano-banana-2`; fix Seedream 4.5 custom size encoding to use width/height schema; fix aspect-ratio default inheritance for unsupported Replicate models; block multi-output requests before they reach the API (by @justnode)
|
||||
|
||||
## 1.100.0 - 2026-04-12
|
||||
|
||||
### Features
|
||||
- `baoyu-imagine`: add Z.AI GLM-Image provider — supports `glm-image` and `cogview-4-250304` models via the Z.AI sync image API; configure with `ZAI_API_KEY` (or `BIGMODEL_API_KEY` for backward compatibility)
|
||||
|
||||
## 1.99.1 - 2026-04-11
|
||||
|
||||
### Fixes
|
||||
- `baoyu-article-illustrator`: omit `model` field from batch tasks when `--model` is not specified, letting `baoyu-imagine` resolve the default from env/config
|
||||
|
||||
## 1.99.0 - 2026-04-10
|
||||
|
||||
### Features
|
||||
- `baoyu-diagram`: add new skill for generating publication-ready SVG diagrams — flowcharts, structural/architecture diagrams, and illustrative intuition diagrams. Claude writes real SVG code directly following a cohesive design system; output is a single self-contained `.svg` file with embedded styles and auto dark-mode, ready to embed in articles, WeChat posts, slides, and docs
|
||||
|
||||
## 1.98.0 - 2026-04-10
|
||||
|
||||
### Features
|
||||
- `baoyu-xhs-images`: Restore as active skill (remove deprecated warning)
|
||||
- `baoyu-xhs-images`: Add `sketch-notes` style — hand-drawn educational infographic with macaron pastels, wobble lines, and warm cream background
|
||||
- `baoyu-xhs-images`: Add palette system (`macaron`, `warm`, `neon`) as optional `--palette` color override dimension
|
||||
- `baoyu-xhs-images`: Add 3 new presets: `hand-drawn-edu`, `sketch-card`, `sketch-summary`
|
||||
|
||||
## 1.97.1 - 2026-04-09
|
||||
|
||||
### Fixes
|
||||
- `baoyu-image-cards`: rename palette color roles from "Zone N" to "Block Color" to prevent AI rendering labels as visible text in images
|
||||
|
||||
## 1.97.0 - 2026-04-09
|
||||
|
||||
### Features
|
||||
@@ -549,7 +840,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
|
||||
|
||||
|
||||
+294
-3
@@ -2,6 +2,297 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 2.0.0 - 2026-05-24
|
||||
|
||||
### 破坏性变更
|
||||
- 移除 `baoyu-imagine` skill。所有功能(providers、脚本、references)合并入 `baoyu-image-gen`,`marketplace.json` 改用新名称注册,`homepage` 链接更新为 `#baoyu-image-gen`
|
||||
- 移除 `baoyu-image-cards` skill。所有功能(样式、布局、配色、预设)合并入 `baoyu-xhs-images`,`marketplace.json` 改用新名称注册
|
||||
- 其它 skill(`baoyu-article-illustrator`、`baoyu-comic`、`baoyu-cover-image`、`baoyu-infographic`、`baoyu-slide-deck`)中 `## Image Generation Tools` 示例统一改用 `baoyu-image-gen`,不再引用 `baoyu-imagine`
|
||||
|
||||
### 迁移说明
|
||||
- 旧的 `~/.baoyu-skills/baoyu-imagine/EXTEND.md` 与 `.baoyu-skills/baoyu-imagine/EXTEND.md` 配置文件会被 `scripts/main.ts` 中的 legacy-path 解析器自动重命名到 `…/baoyu-image-gen/EXTEND.md`,首次运行时迁移
|
||||
- 通过斜杠命令调用时,请将 `/baoyu-imagine ...` 改为 `/baoyu-image-gen ...`;将所有 `baoyu-image-cards` 改为 `baoyu-xhs-images`
|
||||
|
||||
## 1.119.0 - 2026-05-24
|
||||
|
||||
### 新功能
|
||||
- `baoyu-electron-extract`:新增 skill,可从任意已安装的 Electron 应用的 `app.asar` 中提取资源和 JavaScript。`.js.map` 文件内嵌 `sourcesContent` 时还原原始源码(含 TypeScript/JSX),否则用 Prettier 原地美化压缩后的 JS/CSS。source-map 路径先相对各 `.js.map` 文件解析,因此 `../../src/main.ts` 这类打包器相对路径会还原为 `restored/` 下的可读路径,而不是哈希占位符。始终跳过 `node_modules` 和 `webpack/runtime/*` 条目。macOS 下自动从 `/Applications` 和 `~/Applications` 发现应用,Windows 下从 `%LOCALAPPDATA%\Programs`、`%PROGRAMFILES%`、`%PROGRAMFILES(X86)%`、`%APPDATA%` 发现;其他平台请用 `--asar <path>` 显式指定。安全:拒绝写入 `/`、用户主目录或当前工作目录,未加 `--force` 时拒绝写入非空的已有输出目录
|
||||
|
||||
## 1.118.0 - 2026-05-21
|
||||
|
||||
### 新功能
|
||||
- `codex-imagegen`:新增面向非 Codex 运行时(如 Claude Code)的图像生成后端 —— 通过 `codex exec --json --sandbox danger-full-access` 调用 Codex CLI 内置的 `image_gen` 工具,无需 `OPENAI_API_KEY`。内置幂等缓存、文件锁并发控制、JSONL 事件流解析、PNG 魔术字节校验和指数退避重试 (by @yelban, #158)
|
||||
- `baoyu-cover-image`:在 `SKILL.md` 中接入 `codex-imagegen` 包装脚本(当 `preferred_image_backend: codex-imagegen` 时生效),并补充慢网络下的 `--timeout` 参数说明
|
||||
|
||||
### 重构
|
||||
- `codex-imagegen`:在代码中强制校验 `--prompt` 与 `--prompt-file` 互斥(此前仅在文档说明)
|
||||
- `codex-imagegen`:将 `(opts as any).__promptFile` 这一 hack 改为 `CliOptions` 上类型化的 `promptFile` 字段
|
||||
- `codex-imagegen`:用复用的 `findCpToTarget` 辅助函数替换内联的 `cp|mv ... generated_images` 正则
|
||||
- `codex-imagegen`:错误返回时正确透传 `attempts`(此前硬编码为 `0`)
|
||||
- `codex-imagegen`:删除无用的 `parseFinalJson()` 函数及对应测试(包装脚本以磁盘校验为准,不再依赖 agent 自报 JSON)
|
||||
|
||||
### 安全
|
||||
- `codex-imagegen`:在拼入发往 `codex exec --sandbox danger-full-access` 的 agent 指令前,拒绝包含 shell 元字符的 `--image` / `--ref` 路径
|
||||
|
||||
### 致谢
|
||||
- `codex-imagegen` 后端由 @yelban 贡献 (#158)
|
||||
|
||||
## 1.117.5 - 2026-05-21
|
||||
|
||||
### 致谢
|
||||
- `baoyu-post-to-wechat`:远程 API 发布更新感谢 Dame5211 <1079825614@qq.com>
|
||||
|
||||
## 1.117.4 - 2026-05-21
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-wechat`:新增通过 SSH SOCKS5 隧道进行远程 API 发布
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-wechat`:修复远程 API 发布在 Bun 下的运行问题,并严格校验远程发布配置
|
||||
|
||||
### CI
|
||||
- 测试前安装 `baoyu-post-to-wechat` 脚本依赖
|
||||
|
||||
## 1.117.3 - 2026-05-20
|
||||
|
||||
### 新功能
|
||||
- CI:新增 skill 发布提交校验 —— 涉及 `skills/<name>/**` 的提交必须使用 Conventional Commit 格式;发布/同步时校验 SKILL.md 版本一致性
|
||||
|
||||
### 修复
|
||||
- `baoyu-diagram`:为 SKILL.md 添加 version 字段
|
||||
- `baoyu-post-to-wechat`:同步 SKILL.md 版本
|
||||
|
||||
### 文档
|
||||
- `baoyu-wechat-summary`:重构 profile 字段 —— 将 `aliases` 拆分为 `group_nicknames`(用户历史群名)和 `aliases`(其他成员对用户的称呼),新增 `tags` 字段存储横向属性
|
||||
|
||||
## 1.117.2 - 2026-05-17
|
||||
|
||||
### 文档
|
||||
- `baoyu-cover-image`:禁止用代码修补已生成的位图文字 —— 不再使用 ImageMagick / Pillow / Canvas / SVG / HTML 叠层覆盖、重写或替换标题/副标题文字,文字异常时应改 prompt 重新生成或换用少字/无标题版本
|
||||
- `baoyu-article-illustrator`、`baoyu-comic`、`baoyu-image-cards`、`baoyu-xhs-images`、`baoyu-infographic`、`baoyu-slide-deck`:同步上述文字修补禁令,各自针对该 skill 的文字类别(标签/说明、对白/拟声词、标题/正文/标签、标题/数据、幻灯片标题/要点)
|
||||
|
||||
## 1.117.1 - 2026-05-16
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-wechat`:修复微信浏览器文章发布问题 (by @zhangga)
|
||||
- `baoyu-post-to-wechat`:修复图片上传回退逻辑及 macOS WebP 剪贴板复制
|
||||
|
||||
## 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
|
||||
|
||||
### 新功能
|
||||
- `baoyu-url-to-markdown`:将 `baoyu-fetch` 运行时代码 vendored 到 `scripts/lib`,并通过本地 `scripts/baoyu-fetch` CLI 调用,使发布后的技能安装不再依赖 `baoyu-fetch` npm 包
|
||||
|
||||
### 修复
|
||||
- `baoyu-fetch`:修复 X/Twitter 单条内容与 X Article 的视频解析,提取可播放的最高码率 MP4,并将文章视频渲染为 `[video](...)`
|
||||
- `sync-clawhub`:改用共享 release 文件清单发布,确保无扩展名 CLI 入口、`bun.lock` 和 vendored `scripts/lib` 文件都会被上传
|
||||
|
||||
### 维护
|
||||
- 将 `defuddle` 升级到 0.17.0、`jsdom` 升级到 29.0.2,并通过 override 将 `@xmldom/xmldom` 固定到 0.8.13,清除 Defuddle 依赖链上的漏洞提示
|
||||
|
||||
## 1.108.0 - 2026-04-19
|
||||
|
||||
### 重构
|
||||
- 将技能文档拆分为聚焦的参考文件,提升可维护性
|
||||
- 将多技能共享代码迁移至 npm 包管理
|
||||
|
||||
## 1.107.0 - 2026-04-15
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增 SVG 转 @2x PNG 转换脚本 —— 使用 Sharp 自动将生成的 SVG 图表转为 @2x PNG;精简合并参考文件,新增 `{baseDir}` 路径解析以支持可移植的技能加载
|
||||
|
||||
### 修复
|
||||
- `claude-plugin`:支持内联 marketplace manifest (#130)
|
||||
|
||||
## 1.106.0 - 2026-04-14
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增架构图丰富化规则 —— 自动扩展架构图,补充多客户端类型、各服务技术栈、数据库分层、消息总线和分色分类;新增完整结构布局模式、架构专用陷阱提示、网络拓扑模板和复杂图表布局计算
|
||||
|
||||
## 1.105.0 - 2026-04-13
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:统一为分析→确认→生成工作流 —— 移除单图/多图模式区分;技能现在分析任意输入素材,推荐图表类型和拆分策略,一次确认后批量生成所有图表
|
||||
|
||||
## 1.104.0 - 2026-04-13
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增 Mermaid 草图步骤(6d-0),在生成 SVG 前先写 Mermaid 代码块作为结构意图;在步骤 6f 新增 Mermaid–SVG 一致性检查
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-wechat`:在粘贴和输入操作前校验编辑器焦点,避免粘贴静默失败
|
||||
|
||||
## 1.103.1 - 2026-04-13
|
||||
|
||||
### 修复
|
||||
- `baoyu-markdown-to-html`:修复文章摘要中 HTML 实体未解码及 HTML 标签未剥离的问题
|
||||
- `baoyu-post-to-weibo`:修复文章摘要中 HTML 实体未解码及 HTML 标签未剥离的问题
|
||||
|
||||
## 1.103.0 - 2026-04-12
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增多图模式 —— 分析文章内容,在识别出的位置批量生成图表;新增 `--density` 参数(`minimal`、`balanced`、`per-section`、`rich`)和 `--mode` 参数(`single`、`multi`、`auto`);根据输入自动判断模式(文件路径→多图,短主题→单图);自动在文章中插入图表链接;输出目录结构 `diagram/{article-slug}/NN-{type}-{slug}/`
|
||||
|
||||
### 修复
|
||||
- `baoyu-article-illustrator`:修复生成图像中出现颜色名称和色值文字的问题 —— 在所有调色板参考文件和提示构建规则中添加语义约束
|
||||
- `baoyu-cover-image`:修复生成图像中出现颜色名称和色值文字的问题 —— 在所有调色板参考文件和提示模板中添加约束
|
||||
- `baoyu-image-cards`:修复生成图像中出现颜色名称文字的问题
|
||||
- `baoyu-post-to-wechat`:修复文章摘要中 HTML 实体未解码及 HTML 标签未剥离的问题,避免微信文章摘要显示乱码
|
||||
|
||||
## 1.102.0 - 2026-04-12
|
||||
|
||||
### 新功能
|
||||
- `baoyu-imagine`:新增 OpenAI 兼容图像 API 方言支持 —— 新增 `--imageApiDialect` 参数、`OPENAI_IMAGE_API_DIALECT` 环境变量及 `default_image_api_dialect` 配置项,用于对接期望宽高比格式 `size` 加 `metadata.resolution` 的兼容网关
|
||||
|
||||
## 1.101.0 - 2026-04-12
|
||||
|
||||
### 新功能
|
||||
- `baoyu-imagine`:改进 Replicate 服务商兼容性 —— 针对不同模型系列(nano-banana、Seedream 4.5、Seedream 5 Lite、Wan 2.7 Image)实现专属输入构建器和验证器;将默认模型更新为 `google/nano-banana-2`;修复 Seedream 4.5 自定义尺寸编码(改用 width/height schema);修复不支持的 Replicate 模型的宽高比默认值继承问题;在请求到达 API 前拦截多图请求 (by @justnode)
|
||||
|
||||
## 1.100.0 - 2026-04-12
|
||||
|
||||
### 新功能
|
||||
- `baoyu-imagine`:新增 Z.AI GLM-Image 服务商支持,支持 `glm-image` 和 `cogview-4-250304` 模型,通过 Z.AI 同步图像 API 调用;配置 `ZAI_API_KEY`(或 `BIGMODEL_API_KEY` 向后兼容)
|
||||
|
||||
## 1.99.1 - 2026-04-11
|
||||
|
||||
### 修复
|
||||
- `baoyu-article-illustrator`:未指定 `--model` 时,批处理任务中不再写入 `model` 字段,改由 `baoyu-imagine` 从环境变量或配置中解析默认值
|
||||
|
||||
## 1.99.0 - 2026-04-10
|
||||
|
||||
### 新功能
|
||||
- `baoyu-diagram`:新增技能,用于生成可直接发布的 SVG 图表 —— 包括流程图、架构/结构图、示意图(直觉图解)。Claude 直接输出符合统一设计规范的真实 SVG 代码,产物是单个自包含的 `.svg` 文件,内嵌样式并自动支持深色模式,可直接嵌入文章、微信公众号、幻灯片和文档中
|
||||
|
||||
## 1.98.0 - 2026-04-10
|
||||
|
||||
### 新功能
|
||||
- `baoyu-xhs-images`:恢复为正式技能(移除废弃警告)
|
||||
- `baoyu-xhs-images`:新增 `sketch-notes` 风格 —— 手绘教育信息图,马卡龙配色,波动线条,暖奶油背景
|
||||
- `baoyu-xhs-images`:新增配色系统(`macaron`、`warm`、`neon`),支持 `--palette` 参数覆盖风格默认颜色
|
||||
- `baoyu-xhs-images`:新增 3 个预设:`hand-drawn-edu`、`sketch-card`、`sketch-summary`
|
||||
|
||||
## 1.97.1 - 2026-04-09
|
||||
|
||||
### 修复
|
||||
- `baoyu-image-cards`:将配色方案中 "Zone N" 角色名改为 "Block Color",防止 AI 将标签文字渲染到图片中
|
||||
|
||||
## 1.97.0 - 2026-04-09
|
||||
|
||||
### 新功能
|
||||
@@ -505,7 +796,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`,提升上传可靠性
|
||||
|
||||
### 修复
|
||||
@@ -968,7 +1259,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`(氛围强度级别)。
|
||||
@@ -988,7 +1279,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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.97.0**.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **2.0.0**.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -31,7 +31,7 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
|
||||
|
||||
- **Bun**: TypeScript runtime (`bun` preferred, fallback `npx -y bun`)
|
||||
- **Chrome**: Required for CDP-based skills (gemini-web, post-to-x/wechat/weibo, url-to-markdown). All CDP skills share a single profile, override via `BAOYU_CHROME_PROFILE_DIR` env var. Platform paths: [docs/chrome-profile.md](docs/chrome-profile.md)
|
||||
- **Image generation APIs**: `baoyu-imagine` requires API key (OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, or Replicate) configured in EXTEND.md
|
||||
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, or Replicate) configured in EXTEND.md
|
||||
- **Gemini Web auth**: Browser cookies (first run opens Chrome for login, `--login` to refresh)
|
||||
|
||||
## Security
|
||||
@@ -46,16 +46,41 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| **Load project skills first** | Project skills override system/user-level skills with same name |
|
||||
| **Default image generation** | Use `skills/baoyu-imagine/SKILL.md` unless user specifies otherwise |
|
||||
| **Default image generation** | Use whatever image backend is available in the current runtime; if multiple are available, ask the user which to use. See `## Image Generation Tools` below. |
|
||||
|
||||
Priority: project `skills/` → `$HOME/.baoyu-skills/` → system-level.
|
||||
|
||||
## Deprecated Skills
|
||||
## Skill Self-Containment
|
||||
|
||||
| Skill | Note |
|
||||
|-------|------|
|
||||
| `baoyu-image-gen` | Migrated to `baoyu-imagine`. Do NOT add to `.claude-plugin/marketplace.json`. Do NOT update README for this skill. |
|
||||
| `baoyu-xhs-images` | Migrated to `baoyu-image-cards`. Do NOT add to `.claude-plugin/marketplace.json`. Do NOT update README for this skill. |
|
||||
Each skill under `skills/` (and `.claude/skills/`) is distributed and consumed independently — the folder may be extracted, copied into another project, or loaded without the rest of this repo. Therefore:
|
||||
|
||||
- **Never link from `SKILL.md` or its `references/` to files outside the skill's own directory.** This includes `docs/`, sibling skills, and the repo root. Relative paths like `../../docs/foo.md` break when the skill is used standalone.
|
||||
- **Inline any shared convention** (e.g., user-input rules, image-generation backend selection) directly in the skill rather than referencing an out-of-skill doc.
|
||||
- Shared docs under `docs/` exist for **repo-author guidance only** — they may be referenced from `CLAUDE.md` and `docs/creating-skills.md`, but NOT from any `SKILL.md`. This applies to `docs/user-input-tools.md`, `docs/image-generation-tools.md`, `docs/image-generation.md`, and any other `docs/` file.
|
||||
|
||||
## User Input Tools
|
||||
|
||||
Skills that prompt users for choices MUST declare the tool-selection convention **inline** in exactly one place per `SKILL.md` — a `## User Input Tools` section near the top. Do NOT link out to [docs/user-input-tools.md](docs/user-input-tools.md); that doc is the author-side canonical source — copy its body into each SKILL.md. Concrete `AskUserQuestion` mentions elsewhere in a skill are treated as examples — other runtimes substitute their local equivalent under the rule.
|
||||
|
||||
## Image Generation Tools
|
||||
|
||||
Skills that render images MUST declare the backend-selection convention **inline** in exactly one place per `SKILL.md` — a `## Image Generation Tools` section near the top (after `## User Input Tools`). Do NOT link out to [docs/image-generation-tools.md](docs/image-generation-tools.md); that doc is the author-side canonical source — copy its body into each SKILL.md. Concrete tool names (`imagegen`, `image_generate`, `baoyu-image-gen`) elsewhere in a skill are treated as examples — other runtimes substitute their local equivalent under the rule. The rule is stateless: use whatever backend is available; if multiple, ask the user once; if none, ask how to proceed. Every rendered image's full prompt must be written to a standalone `prompts/NN-*.md` file before any backend is invoked. Backend skills (`baoyu-image-gen`, `baoyu-danger-gemini-web`) are exempt — they render directly rather than selecting a backend.
|
||||
|
||||
### `codex-imagegen` Backend
|
||||
|
||||
A backend for non-Codex runtimes (e.g., Claude Code) that generates images by spawning `codex exec --json --sandbox danger-full-access` and delegating to Codex CLI's built-in `image_gen` tool. Uses the user's Codex subscription — no `OPENAI_API_KEY` required.
|
||||
|
||||
Invoke via:
|
||||
|
||||
```bash
|
||||
./scripts/codex-imagegen.sh \
|
||||
--image <output.png> \
|
||||
--prompt-file prompts/01-cover.md \
|
||||
--aspect 16:9 \
|
||||
--cache-dir ~/.cache/baoyu-codex-imagegen
|
||||
```
|
||||
|
||||
Stdout emits a single JSON line: `{"status":"ok","path":...,"bytes":N,...}`. On failure, `{"status":"error","error_kind":...}`. Skills route here by setting `preferred_image_backend: codex-imagegen` in EXTEND.md. Full reference: [docs/codex-imagegen-backend.md](docs/codex-imagegen-backend.md).
|
||||
|
||||
## Release Process
|
||||
|
||||
@@ -77,7 +102,9 @@ All skills MUST use `baoyu-` prefix. Details: [docs/creating-skills.md](docs/cre
|
||||
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Image generation guidelines | [docs/image-generation.md](docs/image-generation.md) |
|
||||
| Image generation output guidelines | [docs/image-generation.md](docs/image-generation.md) |
|
||||
| Image generation backend selection | [docs/image-generation-tools.md](docs/image-generation-tools.md) |
|
||||
| User input tool convention | [docs/user-input-tools.md](docs/user-input-tools.md) |
|
||||
| Chrome profile platform paths | [docs/chrome-profile.md](docs/chrome-profile.md) |
|
||||
| Comic style maintenance | [docs/comic-style-maintenance.md](docs/comic-style-maintenance.md) |
|
||||
| ClawHub/OpenClaw publishing | [docs/publishing.md](docs/publishing.md) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,8 @@ Skills shared by Baoyu for improving daily work efficiency with Claude Code.
|
||||
|
||||
## Installation
|
||||
|
||||
> **Tip**: This repository contains 20+ skills. Install only the ones you actually need — bulk-installing every skill adds unnecessary context overhead for your AI agent on every run.
|
||||
|
||||
### Quick Install (Recommended)
|
||||
|
||||
```bash
|
||||
@@ -32,7 +34,7 @@ This repository now supports publishing each `skills/baoyu-*` directory as an in
|
||||
ClawHub installs skills individually, not as one marketplace bundle. After publishing, users can install specific skills such as:
|
||||
|
||||
```bash
|
||||
clawhub install baoyu-imagine
|
||||
clawhub install baoyu-image-gen
|
||||
clawhub install baoyu-markdown-to-html
|
||||
```
|
||||
|
||||
@@ -40,7 +42,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 +66,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 +82,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**
|
||||
@@ -97,32 +99,32 @@ Skills are organized into three categories:
|
||||
|
||||
Content generation and publishing skills.
|
||||
|
||||
#### baoyu-image-cards
|
||||
#### baoyu-xhs-images
|
||||
|
||||
Image card series generator. Breaks down content into 1-10 cartoon-style image cards with **Style × Layout** system and optional palette override.
|
||||
Xiaohongshu image card series generator. Breaks down content into 1-10 cartoon-style image cards with **Style × Layout** system and optional palette override.
|
||||
|
||||
```bash
|
||||
# Auto-select style and layout
|
||||
/baoyu-image-cards posts/ai-future/article.md
|
||||
/baoyu-xhs-images posts/ai-future/article.md
|
||||
|
||||
# Specify style
|
||||
/baoyu-image-cards posts/ai-future/article.md --style notion
|
||||
/baoyu-xhs-images posts/ai-future/article.md --style notion
|
||||
|
||||
# Specify layout
|
||||
/baoyu-image-cards posts/ai-future/article.md --layout dense
|
||||
/baoyu-xhs-images posts/ai-future/article.md --layout dense
|
||||
|
||||
# Combine style and layout
|
||||
/baoyu-image-cards posts/ai-future/article.md --style notion --layout list
|
||||
/baoyu-xhs-images posts/ai-future/article.md --style notion --layout list
|
||||
|
||||
# Override palette
|
||||
/baoyu-image-cards posts/ai-future/article.md --style notion --palette macaron
|
||||
/baoyu-xhs-images posts/ai-future/article.md --style notion --palette macaron
|
||||
|
||||
# Direct content input
|
||||
/baoyu-image-cards 今日星座运势
|
||||
/baoyu-xhs-images 今日星座运势
|
||||
|
||||
# Non-interactive (skip all confirmations, for scheduled tasks)
|
||||
/baoyu-image-cards posts/ai-future/article.md --yes
|
||||
/baoyu-image-cards posts/ai-future/article.md --yes --preset knowledge-card
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
|
||||
```
|
||||
|
||||
**Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard`, `study-notes`, `screen-print`, `sketch-notes`
|
||||
@@ -272,6 +274,43 @@ Generate professional infographics with 21 layout types and 21 visual styles. An
|
||||
|  |  | |
|
||||
| knolling | lego-brick | |
|
||||
|
||||
#### baoyu-diagram
|
||||
|
||||
Generate publication-ready SVG diagrams from source material — flowcharts, sequence/protocol diagrams, structural/architecture diagrams, and illustrative intuition diagrams. Analyzes input material to recommend diagram type(s) and splitting strategy, confirms the plan once, then generates all diagrams. Claude writes real SVG code directly following a cohesive design system. Output is self-contained `.svg` files with embedded styles and auto dark-mode.
|
||||
|
||||
```bash
|
||||
# Topic string — skill analyzes and proposes a plan
|
||||
/baoyu-diagram "how JWT authentication works"
|
||||
/baoyu-diagram "Kubernetes architecture" --type structural
|
||||
/baoyu-diagram "OAuth 2.0 flow" --type sequence
|
||||
|
||||
# File path — skill reads, analyzes, and proposes a plan
|
||||
/baoyu-diagram path/to/article.md
|
||||
|
||||
# Language and output path
|
||||
/baoyu-diagram "微服务架构" --lang zh
|
||||
/baoyu-diagram "build pipeline" --out docs/build-pipeline.svg
|
||||
```
|
||||
|
||||
**Options**:
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--type <name>` | `flowchart`, `sequence`, `structural`, `illustrative`, `class`, `auto` (default). Skips type recommendation. |
|
||||
| `--lang <code>` | Output language (en, zh, ja, ...) |
|
||||
| `--out <path>` | Output file path. Generates exactly one diagram focused on the most important aspect. |
|
||||
|
||||
**Diagram types**:
|
||||
|
||||
| Type | Reader need | Verbs that trigger it |
|
||||
|------|-------------|------------------------|
|
||||
| `flowchart` | Walk me through the steps in order | walk through, steps, process, lifecycle, workflow, state machine |
|
||||
| `sequence` | Who talks to whom, in what order | protocol, handshake, auth flow, OAuth, TCP, request/response |
|
||||
| `structural` | Show me what's inside what, how it's organised | architecture, components, topology, layout, what's inside |
|
||||
| `illustrative` | Give me the intuition — draw the mechanism | how does X work, explain X, intuition for, why does X do Y |
|
||||
| `class` | What are the types and how are they related | class diagram, UML, inheritance, interface, schema |
|
||||
|
||||
Not an image-generation skill — no LLM image model is called. Claude writes the SVG by hand with hand-computed layout math, so every diagram honors the design system. Embedded `<style>` block with `@media (prefers-color-scheme: dark)` means the same file renders correctly in both light and dark mode anywhere it's embedded.
|
||||
|
||||
#### baoyu-cover-image
|
||||
|
||||
Generate cover images for articles with 5 dimensions: Type × Palette × Rendering × Text × Mood. Combines 11 color palettes with 7 rendering styles for 77 unique combinations.
|
||||
@@ -542,7 +581,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
|
||||
@@ -575,8 +614,9 @@ Post content to WeChat Official Account (微信公众号). Two modes available:
|
||||
|
||||
| Method | Speed | Requirements |
|
||||
|--------|-------|--------------|
|
||||
| API (Recommended) | Fast | API credentials |
|
||||
| API (Recommended) | Fast | API credentials (local IP allowlisted in WeChat) |
|
||||
| Browser | Slow | Chrome, login session |
|
||||
| Remote API | Fast | API credentials + SSH-reachable server whose IP is on WeChat's allowlist |
|
||||
|
||||
**API Configuration** (for faster publishing):
|
||||
|
||||
@@ -594,6 +634,17 @@ To obtain credentials:
|
||||
|
||||
**Browser Method** (no API setup needed): Requires Google Chrome. First run opens browser for QR code login (session preserved).
|
||||
|
||||
**Remote API Method** (for when WeChat's IP allowlist excludes your local machine): tunnels WeChat API calls through an SSH SOCKS5 dynamic port forward to a server whose IP is on the allowlist. No files are written to the remote host and `AppSecret` never leaves the local process. Add to your EXTEND.md:
|
||||
|
||||
```yaml
|
||||
# Optional: only set when WeChat's IP allowlist excludes your local machine
|
||||
remote_publish_host: server.example.com
|
||||
remote_publish_user: deploy
|
||||
remote_publish_identity_file: ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Then publish with `--remote` (or set `default_publish_method: remote-api`). Authentication is SSH key only; only the typed `remote_publish_*` keys are honored.
|
||||
|
||||
**Multi-Account Support**: Manage multiple WeChat Official Accounts via `EXTEND.md`:
|
||||
|
||||
```bash
|
||||
@@ -675,58 +726,67 @@ Post content to Weibo (微博). Supports regular posts with text, images, and vi
|
||||
|
||||
AI-powered generation backends.
|
||||
|
||||
#### baoyu-imagine
|
||||
#### baoyu-image-gen
|
||||
|
||||
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)
|
||||
/baoyu-imagine --prompt "A cute cat" --image cat.png
|
||||
/baoyu-image-gen --prompt "A cute cat" --image cat.png
|
||||
|
||||
# With aspect ratio
|
||||
/baoyu-imagine --prompt "A landscape" --image landscape.png --ar 16:9
|
||||
/baoyu-image-gen --prompt "A landscape" --image landscape.png --ar 16:9
|
||||
|
||||
# High quality (2k)
|
||||
/baoyu-imagine --prompt "A banner" --image banner.png --quality 2k
|
||||
/baoyu-image-gen --prompt "A banner" --image banner.png --quality 2k
|
||||
|
||||
# Specific provider
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider openai
|
||||
/baoyu-image-gen --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-image-gen --prompt "A cat" --image cat.png --provider azure --model gpt-image-2
|
||||
|
||||
# OpenRouter
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider openrouter
|
||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openrouter
|
||||
|
||||
# OpenRouter with reference images
|
||||
/baoyu-imagine --prompt "Make it blue" --image out.png --provider openrouter --model google/gemini-3.1-flash-image-preview --ref source.png
|
||||
/baoyu-image-gen --prompt "Make it blue" --image out.png --provider openrouter --model google/gemini-3.1-flash-image-preview --ref source.png
|
||||
|
||||
# DashScope (Aliyun Tongyi Wanxiang)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
||||
|
||||
# DashScope with custom size
|
||||
/baoyu-imagine --prompt "为咖啡品牌设计一张 21:9 横幅海报,包含清晰中文标题" --image banner.png --provider dashscope --model qwen-image-2.0-pro --size 2048x872
|
||||
/baoyu-image-gen --prompt "为咖啡品牌设计一张 21:9 横幅海报,包含清晰中文标题" --image banner.png --provider dashscope --model qwen-image-2.0-pro --size 2048x872
|
||||
|
||||
# Z.AI GLM-Image
|
||||
/baoyu-image-gen --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
|
||||
|
||||
# MiniMax
|
||||
/baoyu-imagine --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
|
||||
/baoyu-image-gen --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
|
||||
|
||||
# MiniMax with subject reference
|
||||
/baoyu-imagine --prompt "A girl stands by the library window, cinematic lighting" --image out.jpg --provider minimax --model image-01 --ref portrait.png --ar 16:9
|
||||
/baoyu-image-gen --prompt "A girl stands by the library window, cinematic lighting" --image out.jpg --provider minimax --model image-01 --ref portrait.png --ar 16:9
|
||||
|
||||
# Replicate
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider replicate
|
||||
# Replicate (default: google/nano-banana-2)
|
||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider replicate
|
||||
|
||||
# Replicate Seedream 4.5
|
||||
/baoyu-image-gen --prompt "A studio portrait" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
|
||||
|
||||
# Replicate Wan 2.7 Image Pro
|
||||
/baoyu-image-gen --prompt "A concept frame" --image frame.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
|
||||
|
||||
# Jimeng (即梦)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider jimeng
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider jimeng
|
||||
|
||||
# Seedream (豆包)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||
|
||||
# With reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, MiniMax, or Seedream 5.0/4.5/4.0)
|
||||
/baoyu-imagine --prompt "Make it blue" --image out.png --ref source.png
|
||||
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
||||
|
||||
# Batch mode
|
||||
/baoyu-imagine --batchfile batch.json --jobs 4 --json
|
||||
/baoyu-image-gen --batchfile batch.json --jobs 4 --json
|
||||
```
|
||||
|
||||
**Options**:
|
||||
@@ -737,14 +797,15 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `--image` | Output image path (required) |
|
||||
| `--batchfile` | JSON batch file for multi-image generation |
|
||||
| `--jobs` | Worker count for batch mode |
|
||||
| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `minimax`, `jimeng`, `seedream`, or `replicate` |
|
||||
| `--model`, `-m` | Model ID or deployment name. Azure uses deployment name; OpenRouter uses full model IDs; MiniMax uses `image-01` / `image-01-live` |
|
||||
| `--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 |
|
||||
| `--ref` | Reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, MiniMax, or Seedream 5.0/4.5/4.0) |
|
||||
| `--n` | Number of images per request |
|
||||
| `--imageApiDialect` | `openai-native` or `ratio-metadata` for OpenAI-compatible gateways |
|
||||
| `--ref` | Reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate supported families, MiniMax, or Seedream 5.0/4.5/4.0) |
|
||||
| `--n` | Number of images per request (`replicate` currently requires `--n 1`) |
|
||||
| `--json` | JSON output |
|
||||
|
||||
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
|
||||
@@ -756,22 +817,27 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `GOOGLE_API_KEY` | Google API key | - |
|
||||
| `GEMINI_API_KEY` | Alias for `GOOGLE_API_KEY` | - |
|
||||
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
|
||||
| `ZAI_API_KEY` | Z.AI API key | - |
|
||||
| `BIGMODEL_API_KEY` | Backward-compatible alias for Z.AI API key | - |
|
||||
| `MINIMAX_API_KEY` | MiniMax API key | - |
|
||||
| `REPLICATE_API_TOKEN` | Replicate API token | - |
|
||||
| `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` |
|
||||
| `ZAI_IMAGE_MODEL` | Z.AI model | `glm-image` |
|
||||
| `BIGMODEL_IMAGE_MODEL` | Backward-compatible alias for Z.AI model | `glm-image` |
|
||||
| `MINIMAX_IMAGE_MODEL` | MiniMax model | `image-01` |
|
||||
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-pro` |
|
||||
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-2` |
|
||||
| `JIMENG_IMAGE_MODEL` | Jimeng model | `jimeng_t2i_v40` |
|
||||
| `SEEDREAM_IMAGE_MODEL` | Seedream model | `doubao-seedream-5-0-260128` |
|
||||
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
|
||||
| `OPENAI_IMAGE_API_DIALECT` | OpenAI-compatible image API dialect (`openai-native` or `ratio-metadata`) | `openai-native` |
|
||||
| `OPENAI_IMAGE_USE_CHAT` | Use `/chat/completions` for OpenAI image generation | `false` |
|
||||
| `AZURE_OPENAI_BASE_URL` | Azure resource or deployment endpoint | - |
|
||||
| `AZURE_API_VERSION` | Azure image API version | `2025-04-01-preview` |
|
||||
@@ -780,7 +846,9 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `OPENROUTER_TITLE` | Optional app name for OpenRouter attribution | - |
|
||||
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
|
||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
|
||||
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint | `https://api.minimax.io` |
|
||||
| `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.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` |
|
||||
@@ -792,16 +860,20 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
**Provider Notes**:
|
||||
- Azure OpenAI: `--model` means Azure deployment name, not the underlying model family.
|
||||
- DashScope: `qwen-image-2.0-pro` is the recommended default for custom `--size`, `21:9`, and strong Chinese/English text rendering.
|
||||
- Z.AI: `glm-image` is recommended for posters, diagrams, and text-heavy Chinese/English images. Reference images are not supported.
|
||||
- MiniMax: `image-01` supports documented custom `width` / `height`; `image-01-live` is lower latency and works best with `--ar`.
|
||||
- MiniMax reference images are sent as `subject_reference`; the current API is specialized toward character / portrait consistency.
|
||||
- Jimeng does not support reference images.
|
||||
- Seedream reference images are supported by Seedream 5.0 / 4.5 / 4.0, not Seedream 3.0.
|
||||
- Replicate defaults to `google/nano-banana-2`. `baoyu-image-gen` only enables Replicate advanced options for `google/nano-banana*`, `bytedance/seedream-4.5`, `bytedance/seedream-5-lite`, `wan-video/wan-2.7-image`, and `wan-video/wan-2.7-image-pro`.
|
||||
- Replicate currently saves exactly one output image per request. `--n > 1` is blocked locally instead of silently dropping extra results.
|
||||
- Replicate model behavior is family-specific: nano-banana uses `--quality` / `--ar`, Seedream uses validated `--size` / `--ar`, and Wan uses validated `--size` (with `--ar` converted locally to a concrete size).
|
||||
|
||||
**Provider Auto-Selection**:
|
||||
1. If `--provider` is specified → use it
|
||||
2. If `--ref` is provided and no provider is specified → try Google, then OpenAI, Azure, OpenRouter, Replicate, Seedream, and finally MiniMax
|
||||
3. If only one API key is available → use that provider
|
||||
4. If multiple providers are available → default to Google
|
||||
4. If multiple providers are available → default to Google, then OpenAI, Azure, OpenRouter, DashScope, Z.AI, MiniMax, Replicate, Jimeng, Seedream
|
||||
|
||||
#### baoyu-danger-gemini-web
|
||||
|
||||
@@ -1054,12 +1126,79 @@ 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
|
||||
|
||||
#### baoyu-electron-extract
|
||||
|
||||
Extract resources and JavaScript from any installed Electron app's `app.asar`. When `.js.map` files embed `sourcesContent`, restores the original source tree (TypeScript/JSX included); otherwise formats the minified JS/CSS with Prettier in place. Always skips `node_modules`. Works on macOS and Windows; pass `--asar <path>` on other platforms.
|
||||
|
||||
```bash
|
||||
# Extract by app name (default output: ~/Downloads/Codex-electron-extract/)
|
||||
/baoyu-electron-extract Codex
|
||||
|
||||
# Extract by absolute path (.app bundle, install dir, or .asar file)
|
||||
/baoyu-electron-extract "/Applications/Visual Studio Code.app"
|
||||
/baoyu-electron-extract --asar /Applications/Codex.app/Contents/Resources/app.asar Codex
|
||||
|
||||
# Custom output directory
|
||||
/baoyu-electron-extract Codex --output ~/work/codex-source
|
||||
|
||||
# Preview discovery without writing anything
|
||||
/baoyu-electron-extract Codex --dry-run
|
||||
|
||||
# Overwrite an existing output directory
|
||||
/baoyu-electron-extract Codex --force
|
||||
```
|
||||
|
||||
**Options**:
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `<app>` | App name or absolute path (required unless `--asar`) | — |
|
||||
| `--output`, `-o` | Output directory | `~/Downloads/<AppName>-electron-extract` |
|
||||
| `--asar` | Override the resolved `.asar` path | auto-discovered |
|
||||
| `--force`, `-f` | Allow writing into a non-empty existing output dir | false |
|
||||
| `--skip-format` | Skip Prettier formatting | false |
|
||||
| `--skip-restore` | Skip source-map restoration | false |
|
||||
| `--no-unpacked` | Don't copy `app.asar.unpacked/` alongside | false |
|
||||
| `--dry-run` | Print resolved paths and exit without writing | false |
|
||||
| `--json` | Emit one JSON-line summary on stdout | false |
|
||||
|
||||
**Output layout**: `extract-report.json` (counts, warnings, paths), `extracted/` (raw asar, formatted in place when no map), `extracted.unpacked/` (native modules if present), and `restored/` (rebuilt source tree from `.js.map` files).
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Some skills require API keys or custom configuration. Environment variables can be set in `.env` files:
|
||||
|
||||
**Load Priority** (higher priority overrides lower):
|
||||
1. CLI environment variables (e.g., `OPENAI_API_KEY=xxx /baoyu-imagine ...`)
|
||||
1. CLI environment variables (e.g., `OPENAI_API_KEY=xxx /baoyu-image-gen ...`)
|
||||
2. `process.env` (system environment)
|
||||
3. `<cwd>/.baoyu-skills/.env` (project-level)
|
||||
4. `~/.baoyu-skills/.env` (user-level)
|
||||
@@ -1074,14 +1213,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
|
||||
@@ -1101,14 +1240,19 @@ DASHSCOPE_API_KEY=sk-xxx
|
||||
DASHSCOPE_IMAGE_MODEL=qwen-image-2.0-pro
|
||||
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
|
||||
# Z.AI
|
||||
ZAI_API_KEY=xxx
|
||||
ZAI_IMAGE_MODEL=glm-image
|
||||
# ZAI_BASE_URL=https://api.z.ai/api/paas/v4
|
||||
|
||||
# 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
|
||||
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
|
||||
REPLICATE_IMAGE_MODEL=google/nano-banana-2
|
||||
# REPLICATE_BASE_URL=https://api.replicate.com
|
||||
|
||||
# Jimeng (即梦)
|
||||
@@ -1201,6 +1345,7 @@ This project was inspired by and builds upon the following open source projects:
|
||||
- [doocs/md](https://github.com/doocs/md) by [@doocs](https://github.com/doocs) — Core implementation logic for Markdown to HTML conversion
|
||||
- [High-density Infographic Prompt](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg) by AJ@WaytoAGI — Inspiration for the infographic skill
|
||||
- [qiaomu-mondo-poster-design](https://github.com/joeseesun/qiaomu-mondo-poster-design) by [@joeseesun](https://github.com/joeseesun)(乔木) — Inspiration for the Mondo style
|
||||
- [architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) by [@Cocoon-AI](https://github.com/Cocoon-AI) — Inspiration for the diagram skill's design system
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+187
-51
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
宝玉分享的 Claude Code 技能集,提升日常工作效率。
|
||||
宝玉分享的 AI Agent 技能集(适用于 Claude Code、Codex 等),提升日常工作效率。
|
||||
|
||||
## 前置要求
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
## 安装
|
||||
|
||||
> **提示**:本仓库已收录 20+ 个 skill,请按需安装你真正会用到的那几个,不要一次性全装 —— 每个加载的 skill 都会在 Agent 每次运行时占用额外上下文。
|
||||
|
||||
### 快速安装(推荐)
|
||||
|
||||
```bash
|
||||
@@ -32,7 +34,7 @@ npx skills add jimliu/baoyu-skills
|
||||
ClawHub 按“单个 skill”安装,不是把整个 marketplace 一次性装进去。发布后,用户可以按需安装:
|
||||
|
||||
```bash
|
||||
clawhub install baoyu-imagine
|
||||
clawhub install baoyu-image-gen
|
||||
clawhub install baoyu-markdown-to-html
|
||||
```
|
||||
|
||||
@@ -40,7 +42,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
### 注册插件市场
|
||||
|
||||
在 Claude Code 中运行:
|
||||
在 Agent 中运行:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add JimLiu/baoyu-skills
|
||||
@@ -64,7 +66,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
**方式三:告诉 Agent**
|
||||
|
||||
直接告诉 Claude Code:
|
||||
直接告诉 Agent:
|
||||
|
||||
> 请帮我安装 github.com/JimLiu/baoyu-skills 中的 Skills
|
||||
|
||||
@@ -80,7 +82,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
更新技能到最新版本:
|
||||
|
||||
1. 在 Claude Code 中运行 `/plugin`
|
||||
1. 在 Agent 中运行 `/plugin`
|
||||
2. 切换到 **Marketplaces** 标签页(使用方向键或 Tab)
|
||||
3. 选择 **baoyu-skills**
|
||||
4. 选择 **Update marketplace**
|
||||
@@ -97,32 +99,32 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
内容生成和发布技能。
|
||||
|
||||
#### baoyu-image-cards
|
||||
#### baoyu-xhs-images
|
||||
|
||||
图片卡片系列生成器。将内容拆解为 1-10 张卡通风格图片卡片,支持 **风格 × 布局** 系统和可选配色覆盖。
|
||||
小红书图片卡片系列生成器。将内容拆解为 1-10 张卡通风格图片卡片,支持 **风格 × 布局** 系统和可选配色覆盖。
|
||||
|
||||
```bash
|
||||
# 自动选择风格和布局
|
||||
/baoyu-image-cards posts/ai-future/article.md
|
||||
/baoyu-xhs-images posts/ai-future/article.md
|
||||
|
||||
# 指定风格
|
||||
/baoyu-image-cards posts/ai-future/article.md --style notion
|
||||
/baoyu-xhs-images posts/ai-future/article.md --style notion
|
||||
|
||||
# 指定布局
|
||||
/baoyu-image-cards posts/ai-future/article.md --layout dense
|
||||
/baoyu-xhs-images posts/ai-future/article.md --layout dense
|
||||
|
||||
# 组合风格和布局
|
||||
/baoyu-image-cards posts/ai-future/article.md --style notion --layout list
|
||||
/baoyu-xhs-images posts/ai-future/article.md --style notion --layout list
|
||||
|
||||
# 覆盖配色
|
||||
/baoyu-image-cards posts/ai-future/article.md --style notion --palette macaron
|
||||
/baoyu-xhs-images posts/ai-future/article.md --style notion --palette macaron
|
||||
|
||||
# 直接输入内容
|
||||
/baoyu-image-cards 今日星座运势
|
||||
/baoyu-xhs-images 今日星座运势
|
||||
|
||||
# 非交互模式(跳过所有确认,适用于定时任务)
|
||||
/baoyu-image-cards posts/ai-future/article.md --yes
|
||||
/baoyu-image-cards posts/ai-future/article.md --yes --preset knowledge-card
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
|
||||
```
|
||||
|
||||
**风格**(视觉美学):`cute`(默认)、`fresh`、`warm`、`bold`、`minimal`、`retro`、`pop`、`notion`、`chalkboard`、`study-notes`、`screen-print`、`sketch-notes`
|
||||
@@ -272,6 +274,43 @@ clawhub install baoyu-markdown-to-html
|
||||
|  |  | |
|
||||
| knolling | lego-brick | |
|
||||
|
||||
#### baoyu-diagram
|
||||
|
||||
从源素材生成可直接发布的 SVG 图表 —— 包括流程图、时序/协议图、架构/结构图、示意图(直觉图解)。分析输入素材,推荐图表类型和拆分策略,一次确认后批量生成。Claude 直接输出符合统一设计规范的真实 SVG 代码,产物是自包含的 `.svg` 文件,内嵌样式并自动支持深色模式。
|
||||
|
||||
```bash
|
||||
# 主题描述 —— 技能分析并提出方案
|
||||
/baoyu-diagram "JWT 认证流程是怎么工作的"
|
||||
/baoyu-diagram "Kubernetes 架构" --type structural
|
||||
/baoyu-diagram "OAuth 2.0 流程" --type sequence
|
||||
|
||||
# 文件路径 —— 技能读取、分析并提出方案
|
||||
/baoyu-diagram path/to/article.md
|
||||
|
||||
# 语言和输出路径
|
||||
/baoyu-diagram "微服务架构" --lang zh
|
||||
/baoyu-diagram "build pipeline" --out docs/build-pipeline.svg
|
||||
```
|
||||
|
||||
**参数**:
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--type <name>` | `flowchart`、`sequence`、`structural`、`illustrative`、`class`、`auto`(默认)。跳过类型推荐直接生成。 |
|
||||
| `--lang <code>` | 输出语言(en、zh、ja 等) |
|
||||
| `--out <path>` | 输出文件路径。生成聚焦于最重要内容的单张图表。 |
|
||||
|
||||
**五种图表类型**:
|
||||
|
||||
| 类型 | 适用场景 | 触发动词 |
|
||||
|------|----------|----------|
|
||||
| `flowchart` | 按顺序走一遍流程 | 流程、步骤、工作流、生命周期、状态机 |
|
||||
| `sequence` | 谁和谁通信、按什么顺序 | 协议、握手、认证流程、OAuth、TCP、请求/响应 |
|
||||
| `structural` | 展示什么包含什么、如何组织 | 架构、组件、拓扑、布局、什么在什么里面 |
|
||||
| `illustrative` | 建立直觉 —— 画出机制本身 | 怎么工作、原理、为什么、直观解释 |
|
||||
| `class` | 类型是什么、它们如何关联 | 类图、UML、继承、接口、数据模型 |
|
||||
|
||||
本技能不调用任何图像生成模型 —— Claude 通过手算坐标直接写 SVG 代码,确保每个图表都遵守设计规范。内嵌的 `<style>` 块包含 `@media (prefers-color-scheme: dark)`,同一个文件在浅色和深色模式下均正确渲染,可嵌入到任意支持 SVG 的宿主环境中。
|
||||
|
||||
#### baoyu-cover-image
|
||||
|
||||
为文章生成封面图,支持五维定制系统:类型 × 配色 × 渲染 × 文字 × 氛围。11 种配色方案与 7 种渲染风格组合,提供 77 种独特效果。
|
||||
@@ -542,7 +581,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
|
||||
@@ -575,8 +614,9 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
| 方式 | 速度 | 要求 |
|
||||
|------|------|------|
|
||||
| API(推荐) | 快 | API 凭证 |
|
||||
| API(推荐) | 快 | API 凭证(本机 IP 在公众号白名单内) |
|
||||
| 浏览器 | 慢 | Chrome,登录会话 |
|
||||
| 远程 API | 快 | API 凭证 + 一台 IP 在公众号白名单内、可 SSH 登录的服务器 |
|
||||
|
||||
**API 配置**(更快的发布方式):
|
||||
|
||||
@@ -594,6 +634,8 @@ WECHAT_APP_SECRET=你的AppSecret
|
||||
|
||||
**浏览器方式**(无需 API 配置):需已安装 Google Chrome,首次运行需扫码登录(登录状态会保存)
|
||||
|
||||
**远程 API 方式**(适用于本机 IP 不在公众号白名单内的情况):通过 SSH SOCKS5 动态端口转发,将对 `api.weixin.qq.com` 的 HTTPS 调用转发到 IP 在白名单内的服务器上。Markdown 渲染、图片处理、草稿组装仍在本地完成;远端不会落任何文件,`AppSecret` 不会离开本地进程。仅支持 SSH 密钥认证,且只接受类型化的 `remote_publish_*` 配置项,不透传任意 ssh 选项。在 EXTEND.md 中配置 `remote_publish_host` 等字段后,发布时加上 `--remote`(或将 `default_publish_method` 设为 `remote-api`)。
|
||||
|
||||
**多账号支持**:通过 `EXTEND.md` 管理多个微信公众号:
|
||||
|
||||
```bash
|
||||
@@ -675,58 +717,67 @@ accounts:
|
||||
|
||||
AI 驱动的生成后端。
|
||||
|
||||
#### baoyu-imagine
|
||||
#### baoyu-image-gen
|
||||
|
||||
基于 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
|
||||
# 基础生成(自动检测服务商)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png
|
||||
|
||||
# 指定宽高比
|
||||
/baoyu-imagine --prompt "风景图" --image landscape.png --ar 16:9
|
||||
/baoyu-image-gen --prompt "风景图" --image landscape.png --ar 16:9
|
||||
|
||||
# 高质量(2k 分辨率)
|
||||
/baoyu-imagine --prompt "横幅图" --image banner.png --quality 2k
|
||||
/baoyu-image-gen --prompt "横幅图" --image banner.png --quality 2k
|
||||
|
||||
# 指定服务商
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider openai
|
||||
/baoyu-image-gen --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-image-gen --prompt "一只猫" --image cat.png --provider azure --model gpt-image-2
|
||||
|
||||
# OpenRouter
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider openrouter
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openrouter
|
||||
|
||||
# OpenRouter + 参考图
|
||||
/baoyu-imagine --prompt "把它变成蓝色" --image out.png --provider openrouter --model google/gemini-3.1-flash-image-preview --ref source.png
|
||||
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --provider openrouter --model google/gemini-3.1-flash-image-preview --ref source.png
|
||||
|
||||
# DashScope(阿里通义万相)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
||||
|
||||
# DashScope 自定义尺寸
|
||||
/baoyu-imagine --prompt "为咖啡品牌设计一张 21:9 横幅海报,包含清晰中文标题" --image banner.png --provider dashscope --model qwen-image-2.0-pro --size 2048x872
|
||||
/baoyu-image-gen --prompt "为咖啡品牌设计一张 21:9 横幅海报,包含清晰中文标题" --image banner.png --provider dashscope --model qwen-image-2.0-pro --size 2048x872
|
||||
|
||||
# Z.AI GLM-Image
|
||||
/baoyu-image-gen --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
|
||||
|
||||
# MiniMax
|
||||
/baoyu-imagine --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
|
||||
/baoyu-image-gen --prompt "A fashion editorial portrait by a bright studio window" --image out.jpg --provider minimax
|
||||
|
||||
# MiniMax + 角色参考图
|
||||
/baoyu-imagine --prompt "A girl stands by the library window, cinematic lighting" --image out.jpg --provider minimax --model image-01 --ref portrait.png --ar 16:9
|
||||
/baoyu-image-gen --prompt "A girl stands by the library window, cinematic lighting" --image out.jpg --provider minimax --model image-01 --ref portrait.png --ar 16:9
|
||||
|
||||
# Replicate
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider replicate
|
||||
# Replicate(默认:google/nano-banana-2)
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider replicate
|
||||
|
||||
# Replicate Seedream 4.5
|
||||
/baoyu-image-gen --prompt "一张影棚人像" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
|
||||
|
||||
# Replicate Wan 2.7 Image Pro
|
||||
/baoyu-image-gen --prompt "一张概念分镜" --image frame.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
|
||||
|
||||
# 即梦(Jimeng)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider jimeng
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider jimeng
|
||||
|
||||
# 豆包(Seedream)
|
||||
/baoyu-imagine --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||
|
||||
# 带参考图(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate、MiniMax 或 Seedream 5.0/4.5/4.0)
|
||||
/baoyu-imagine --prompt "把它变成蓝色" --image out.png --ref source.png
|
||||
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
||||
|
||||
# 批量模式
|
||||
/baoyu-imagine --batchfile batch.json --jobs 4 --json
|
||||
/baoyu-image-gen --batchfile batch.json --jobs 4 --json
|
||||
```
|
||||
|
||||
**选项**:
|
||||
@@ -737,14 +788,15 @@ AI 驱动的生成后端。
|
||||
| `--image` | 输出图片路径(必需) |
|
||||
| `--batchfile` | 多图批量生成的 JSON 文件 |
|
||||
| `--jobs` | 批量模式的并发 worker 数 |
|
||||
| `--provider` | `google`、`openai`、`azure`、`openrouter`、`dashscope`、`minimax`、`jimeng`、`seedream` 或 `replicate` |
|
||||
| `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 ID;MiniMax 使用 `image-01` / `image-01-live` |
|
||||
| `--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` |
|
||||
| `--ref` | 参考图片(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate、MiniMax 或 Seedream 5.0/4.5/4.0) |
|
||||
| `--n` | 单次请求生成图片数量 |
|
||||
| `--imageApiDialect` | OpenAI 兼容网关的图像 API 方言(`openai-native` 或 `ratio-metadata`) |
|
||||
| `--ref` | 参考图片(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate 支持的模型家族、MiniMax 或 Seedream 5.0/4.5/4.0) |
|
||||
| `--n` | 单次请求生成图片数量(`replicate` 当前只支持 `--n 1`) |
|
||||
| `--json` | 输出 JSON 结果 |
|
||||
|
||||
**环境变量**(配置方法见[环境配置](#环境配置)):
|
||||
@@ -756,22 +808,27 @@ AI 驱动的生成后端。
|
||||
| `GOOGLE_API_KEY` | Google API 密钥 | - |
|
||||
| `GEMINI_API_KEY` | `GOOGLE_API_KEY` 的别名 | - |
|
||||
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
|
||||
| `ZAI_API_KEY` | Z.AI API 密钥 | - |
|
||||
| `BIGMODEL_API_KEY` | Z.AI API 密钥向后兼容别名 | - |
|
||||
| `MINIMAX_API_KEY` | MiniMax API 密钥 | - |
|
||||
| `REPLICATE_API_TOKEN` | Replicate API Token | - |
|
||||
| `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` |
|
||||
| `ZAI_IMAGE_MODEL` | Z.AI 模型 | `glm-image` |
|
||||
| `BIGMODEL_IMAGE_MODEL` | Z.AI 模型向后兼容别名 | `glm-image` |
|
||||
| `MINIMAX_IMAGE_MODEL` | MiniMax 模型 | `image-01` |
|
||||
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-pro` |
|
||||
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-2` |
|
||||
| `JIMENG_IMAGE_MODEL` | 即梦模型 | `jimeng_t2i_v40` |
|
||||
| `SEEDREAM_IMAGE_MODEL` | 豆包模型 | `doubao-seedream-5-0-260128` |
|
||||
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
|
||||
| `OPENAI_IMAGE_API_DIALECT` | OpenAI 兼容图像 API 方言(`openai-native` 或 `ratio-metadata`) | `openai-native` |
|
||||
| `OPENAI_IMAGE_USE_CHAT` | OpenAI 改走 `/chat/completions` | `false` |
|
||||
| `AZURE_OPENAI_BASE_URL` | Azure 资源或部署端点 | - |
|
||||
| `AZURE_API_VERSION` | Azure 图像 API 版本 | `2025-04-01-preview` |
|
||||
@@ -780,7 +837,9 @@ AI 驱动的生成后端。
|
||||
| `OPENROUTER_TITLE` | OpenRouter 归因用应用名 | - |
|
||||
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
|
||||
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
|
||||
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimax.io` |
|
||||
| `ZAI_BASE_URL` | 自定义 Z.AI 端点 | `https://api.z.ai/api/paas/v4` |
|
||||
| `BIGMODEL_BASE_URL` | Z.AI 端点向后兼容别名 | - |
|
||||
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimaxi.com` |
|
||||
| `REPLICATE_BASE_URL` | 自定义 Replicate 端点 | - |
|
||||
| `JIMENG_BASE_URL` | 自定义即梦端点 | `https://visual.volcengineapi.com` |
|
||||
| `JIMENG_REGION` | 即梦区域 | `cn-north-1` |
|
||||
@@ -792,16 +851,20 @@ AI 驱动的生成后端。
|
||||
**Provider 说明**:
|
||||
- Azure OpenAI:`--model` 表示 Azure deployment name,不是底层模型家族名。
|
||||
- DashScope:`qwen-image-2.0-pro` 是自定义 `--size`、`21:9` 和中英文排版的推荐默认模型。
|
||||
- Z.AI:`glm-image` 适合海报、图表和中英文排版密集的图片生成,暂不支持参考图。
|
||||
- MiniMax:`image-01` 支持官方文档里的自定义 `width` / `height`;`image-01-live` 更偏低延迟,适合配合 `--ar` 使用。
|
||||
- MiniMax 参考图会走 `subject_reference`,当前能力更偏角色 / 人像一致性。
|
||||
- 即梦不支持参考图。
|
||||
- 豆包参考图能力仅适用于 Seedream 5.0 / 4.5 / 4.0,不适用于 Seedream 3.0。
|
||||
- Replicate 默认模型改为 `google/nano-banana-2`。`baoyu-image-gen` 目前只对 `google/nano-banana*`、`bytedance/seedream-4.5`、`bytedance/seedream-5-lite`、`wan-video/wan-2.7-image` 和 `wan-video/wan-2.7-image-pro` 开启本地能力识别与校验。
|
||||
- Replicate 当前只保存单张输出图,`--n > 1` 会在本地直接报错,避免多图结果被静默丢弃。
|
||||
- Replicate 的参数能力按模型家族区分:nano-banana 走 `--quality` / `--ar`,Seedream 走校验后的 `--size` / `--ar`,Wan 走校验后的 `--size`(`--ar` 会先在本地换算成具体尺寸)。
|
||||
|
||||
**服务商自动选择**:
|
||||
1. 如果指定了 `--provider` → 使用指定的
|
||||
2. 如果传了 `--ref` 且未指定 provider → 依次尝试 Google、OpenAI、Azure、OpenRouter、Replicate、Seedream,最后是 MiniMax
|
||||
3. 如果只有一个 API 密钥 → 使用对应服务商
|
||||
4. 如果多个可用 → 默认使用 Google
|
||||
4. 如果多个可用 → 默认使用 Google,然后依次为 OpenAI、Azure、OpenRouter、DashScope、Z.AI、MiniMax、Replicate、即梦、豆包
|
||||
|
||||
#### baoyu-danger-gemini-web
|
||||
|
||||
@@ -1054,12 +1117,79 @@ 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
|
||||
|
||||
**特性**:
|
||||
- 话题提取,带归属和引言
|
||||
- 发言排行榜和群友画像
|
||||
- 增量模式(从上次摘要断点继续)
|
||||
- 大批量消息自动按天分割
|
||||
- 正常版和毒舌版两种风格
|
||||
- 支持从历史摘要回溯初始化画像
|
||||
|
||||
#### baoyu-electron-extract
|
||||
|
||||
从任意已安装的 Electron 应用的 `app.asar` 中提取资源和 JavaScript。当 `.js.map` 内嵌 `sourcesContent` 时,还原原始源码树(含 TypeScript/JSX);否则用 Prettier 原地美化压缩后的 JS/CSS。始终跳过 `node_modules`。支持 macOS 和 Windows,其他平台请用 `--asar <path>` 指定 asar 文件。
|
||||
|
||||
```bash
|
||||
# 按应用名提取(默认输出:~/Downloads/<AppName>-electron-extract/)
|
||||
/baoyu-electron-extract Codex
|
||||
|
||||
# 按绝对路径提取(.app 包、安装目录或 .asar 文件均可)
|
||||
/baoyu-electron-extract "/Applications/Visual Studio Code.app"
|
||||
/baoyu-electron-extract --asar /Applications/Codex.app/Contents/Resources/app.asar Codex
|
||||
|
||||
# 自定义输出目录
|
||||
/baoyu-electron-extract Codex --output ~/work/codex-source
|
||||
|
||||
# 仅预览发现的路径,不写入任何文件
|
||||
/baoyu-electron-extract Codex --dry-run
|
||||
|
||||
# 覆盖已存在的输出目录
|
||||
/baoyu-electron-extract Codex --force
|
||||
```
|
||||
|
||||
**选项**:
|
||||
| 选项 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `<app>` | 应用名或绝对路径(未给 `--asar` 时必填) | — |
|
||||
| `--output`, `-o` | 输出目录 | `~/Downloads/<AppName>-electron-extract` |
|
||||
| `--asar` | 覆盖解析得到的 `.asar` 路径 | 自动发现 |
|
||||
| `--force`, `-f` | 允许写入非空的已有输出目录 | false |
|
||||
| `--skip-format` | 跳过 Prettier 格式化 | false |
|
||||
| `--skip-restore` | 跳过 source-map 还原 | false |
|
||||
| `--no-unpacked` | 不复制同级的 `app.asar.unpacked/` | false |
|
||||
| `--dry-run` | 打印解析路径后退出,不写文件 | false |
|
||||
| `--json` | 在 stdout 输出一行 JSON 概要 | false |
|
||||
|
||||
**输出结构**:`extract-report.json`(计数、警告、路径),`extracted/`(asar 原始内容,无 map 时原地美化),`extracted.unpacked/`(存在时复制的原生模块),以及 `restored/`(基于 `.js.map` 重建的源码树)。
|
||||
|
||||
## 环境配置
|
||||
|
||||
部分技能需要 API 密钥或自定义配置。环境变量可以在 `.env` 文件中设置:
|
||||
|
||||
**加载优先级**(高优先级覆盖低优先级):
|
||||
1. 命令行环境变量(如 `OPENAI_API_KEY=xxx /baoyu-imagine ...`)
|
||||
1. 命令行环境变量(如 `OPENAI_API_KEY=xxx /baoyu-image-gen ...`)
|
||||
2. `process.env`(系统环境变量)
|
||||
3. `<cwd>/.baoyu-skills/.env`(项目级)
|
||||
4. `~/.baoyu-skills/.env`(用户级)
|
||||
@@ -1074,14 +1204,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
|
||||
@@ -1101,14 +1231,19 @@ DASHSCOPE_API_KEY=sk-xxx
|
||||
DASHSCOPE_IMAGE_MODEL=qwen-image-2.0-pro
|
||||
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
|
||||
# Z.AI
|
||||
ZAI_API_KEY=xxx
|
||||
ZAI_IMAGE_MODEL=glm-image
|
||||
# ZAI_BASE_URL=https://api.z.ai/api/paas/v4
|
||||
|
||||
# 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
|
||||
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
|
||||
REPLICATE_IMAGE_MODEL=google/nano-banana-2
|
||||
# REPLICATE_BASE_URL=https://api.replicate.com
|
||||
|
||||
# 即梦(Jimeng)
|
||||
@@ -1201,6 +1336,7 @@ HTTP_PROXY=http://127.0.0.1:7890 HTTPS_PROXY=http://127.0.0.1:7890 /baoyu-danger
|
||||
- [doocs/md](https://github.com/doocs/md) by [@doocs](https://github.com/doocs) — Markdown 转 HTML 的核心实现逻辑
|
||||
- [高密度信息图 Prompt](https://waytoagi.feishu.cn/wiki/YG0zwalijihRREkgmPzcWRInnUg) by AJ@WaytoAGI — 信息图技能的灵感来源
|
||||
- [qiaomu-mondo-poster-design](https://github.com/joeseesun/qiaomu-mondo-poster-design) by [@joeseesun](https://github.com/joeseesun)(乔木) — Mondo 风格的灵感来源
|
||||
- [architecture-diagram-generator](https://github.com/Cocoon-AI/architecture-diagram-generator) by [@Cocoon-AI](https://github.com/Cocoon-AI) — 图表技能设计体系的灵感来源
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
# `codex-imagegen` Backend
|
||||
|
||||
Generate images via Codex CLI's built-in `image_gen` tool from non-Codex runtimes (e.g., Claude Code). The wrapper spawns `codex exec --json` and lets the user's existing Codex subscription drive image generation — **no `OPENAI_API_KEY` required**.
|
||||
|
||||
This backend implements the `preferred_image_backend: codex-imagegen` config key already referenced in several `SKILL.md` files across this repo.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| **Reliability**: retry + exponential backoff | Default 2 retries |
|
||||
| **Verification**: confirms `image_gen` was actually invoked (not bypassed) | Checks `$CODEX_HOME/generated_images/{thread_id}/` |
|
||||
| **Verification**: PNG magic-byte sanity check | ✓ |
|
||||
| **Idempotency cache**: reuses output for same prompt+aspect+refs | `--cache-dir` |
|
||||
| **Concurrency control**: file lock prevents parallel `codex exec` collisions | Built-in |
|
||||
| **Structured logging**: JSONL log file | `--log-file` |
|
||||
| **Token usage returned** | Embedded in result JSON |
|
||||
| **`--ref` reference images** | Repeatable |
|
||||
| **Unit tests** | 16 tests (parser / cache / validator) |
|
||||
| **Error classification**: retryable vs non-retryable | 9 `error_kind` values |
|
||||
|
||||
## Why this backend
|
||||
|
||||
| Scenario | Conventional backend | This backend |
|
||||
|----------|---------------------|--------------|
|
||||
| You have a Codex subscription | OpenAI Images API costs add up per image | Subscription already covers it — zero marginal API cost |
|
||||
| No `OPENAI_API_KEY` available | `baoyu-image-gen` needs an API key | `codex login` is enough |
|
||||
| Want to use GPT Image 2 | Only via OpenAI API | Codex's `image_gen` *is* GPT Image 2 |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
codex login # signs in with your OpenAI account (subscription)
|
||||
codex --version # confirm >= 0.130
|
||||
```
|
||||
|
||||
`bun` is preferred for running the wrapper. On macOS:
|
||||
|
||||
```bash
|
||||
brew install oven-sh/bun/bun
|
||||
```
|
||||
|
||||
If `bun` is missing, the shell entrypoint falls back to `npx -y bun`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Direct CLI
|
||||
|
||||
```bash
|
||||
# Inline prompt
|
||||
./scripts/codex-imagegen.sh \
|
||||
--image /tmp/cat.png \
|
||||
--prompt "A friendly orange cat, watercolor"
|
||||
|
||||
# Prompt from file
|
||||
./scripts/codex-imagegen.sh \
|
||||
--image cover.png \
|
||||
--prompt-file prompts/01-cover.md \
|
||||
--aspect 16:9
|
||||
|
||||
# Verbose mode for debugging
|
||||
./scripts/codex-imagegen.sh -v --image dog.png --prompt "A corgi" --aspect 1:1
|
||||
```
|
||||
|
||||
On success, stdout emits a single JSON line:
|
||||
|
||||
```json
|
||||
{"status":"ok","path":"/tmp/cat.png","bytes":2567101,"elapsed_seconds":53}
|
||||
```
|
||||
|
||||
On failure, exit code is non-zero and stderr contains the error message.
|
||||
|
||||
### Enabling within image skills
|
||||
|
||||
Image-generating skills (e.g., `baoyu-cover-image`, `baoyu-article-illustrator`) already support a `preferred_image_backend` preference. To route them through this backend, set the following in the corresponding `EXTEND.md`:
|
||||
|
||||
```yaml
|
||||
# ~/.baoyu-skills/baoyu-cover-image/EXTEND.md
|
||||
preferred_image_backend: codex-imagegen
|
||||
```
|
||||
|
||||
When the LLM runs the skill, it reads the preference and — guided by the `### codex-imagegen Backend` section in `CLAUDE.md` — invokes `scripts/codex-imagegen.sh`.
|
||||
|
||||
> **Note**: The integration is mediated by the LLM reading `CLAUDE.md`. It is not a hard binding. If a skill does not route to the backend automatically, mentioning it explicitly in the prompt works.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Flag | Required | Description |
|
||||
|------|----------|-------------|
|
||||
| `--image <path>` | ✓ | Output PNG path (absolute recommended; relative paths are resolved against cwd) |
|
||||
| `--prompt <text>` | one of | Prompt string (mutually exclusive with `--prompt-file`) |
|
||||
| `--prompt-file <path>` | one of | Read prompt from file (mutually exclusive with `--prompt`) |
|
||||
| `--aspect <ratio>` | | Aspect ratio. Default `1:1`. Common: `16:9`, `9:16`, `4:3`, `2.35:1` |
|
||||
| `--ref <file>` | | Reference image path (repeatable) |
|
||||
| `--timeout <ms>` | | `codex exec` timeout in ms. Default `300000` |
|
||||
| `--retries <n>` | | Retry count on retryable errors. Default `2` (total attempts = retries + 1) |
|
||||
| `--retry-delay <ms>` | | Base delay between retries (exponential backoff). Default `1500` |
|
||||
| `--cache-dir <path>` | | Enable idempotency cache (reuses output for same prompt+aspect+refs) |
|
||||
| `--log-file <path>` | | Structured JSONL log path (appended) |
|
||||
| `-v` / `--verbose` | | Mirror log entries to stderr |
|
||||
| `-h` / `--help` | | Show usage |
|
||||
|
||||
## Structured Output
|
||||
|
||||
On success, stdout contains a single JSON line:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"path": "/tmp/owl.png",
|
||||
"bytes": 1693831,
|
||||
"elapsed_seconds": 87,
|
||||
"thread_id": "019e40e8-daef-7c60-943d-5e7bb3f6cb3d",
|
||||
"attempts": 1,
|
||||
"cached": false,
|
||||
"usage": {
|
||||
"input": 110899,
|
||||
"cached_input": 83456,
|
||||
"output": 457,
|
||||
"reasoning": 47
|
||||
},
|
||||
"tool_calls": [
|
||||
{"tool": "shell", "status": "completed"},
|
||||
{"tool": "agent_message", "status": "completed"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Cache hits return with `elapsed_seconds: 0`, `cached: true`, `attempts: 0`.
|
||||
|
||||
On failure, exit code is `1` and the JSON contains `error` and `error_kind`:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"error": "image_gen was not invoked: no PNG in ...",
|
||||
"error_kind": "no_image_gen_tool_use"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Kinds
|
||||
|
||||
| `error_kind` | Retryable | Meaning |
|
||||
|--------------|-----------|---------|
|
||||
| `codex_not_installed` | ✗ | `codex` CLI not found |
|
||||
| `invalid_args` | ✗ | Argument parsing error |
|
||||
| `prompt_file_missing` | ✗ | `--prompt-file` path does not exist |
|
||||
| `spawn_failed` | ✓ | `codex exec` exited non-zero |
|
||||
| `timeout` | ✓ | Exceeded `--timeout` |
|
||||
| `no_image_gen_tool_use` | ✓ | Agent did not invoke `image_gen` (it took another path) |
|
||||
| `output_missing` | ✓ | Output file not created |
|
||||
| `invalid_png` | ✓ | Output is not a valid PNG |
|
||||
| `agent_refused` | ✓ | No `thread_id` in event stream (Codex refused to respond) |
|
||||
| `lock_busy` | ✗ | Concurrency lock acquisition timed out |
|
||||
|
||||
## Measured Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| First-run latency | 50–90 s |
|
||||
| Cache-hit latency | < 0.3 s |
|
||||
| Output dimensions | 1024×1024, 1672×941 (16:9), etc. — chosen by `image_gen` |
|
||||
| Output format | PNG (RGB, 8-bit) |
|
||||
| Token usage per call | ~110k input (~80k cached) + ~500 output |
|
||||
| Quota source | Codex subscription (does not consume OpenAI API quota) |
|
||||
| Default timeout | 300 s (5 min) |
|
||||
|
||||
## Limitations & Risks
|
||||
|
||||
1. **5–10× slower than direct API**. `codex exec` cold-starts the agent, loads the built-in `image_gen` SKILL.md, and runs reasoning before invoking the tool. Cache hits avoid this for repeated prompts.
|
||||
2. **ToS gray area**. Codex's `image_gen` tool is designed for interactive use. Invoking it programmatically via `codex exec` from an external agent is not explicitly addressed by current OpenAI policies. Suggested guardrails:
|
||||
- Personal, low-volume use is reasonable.
|
||||
- Not recommended for production automation or high-volume batch jobs.
|
||||
- Users are responsible for ensuring their usage complies with applicable terms of service.
|
||||
3. **Sandbox permissions**. The wrapper passes `--sandbox danger-full-access` so the spawned agent can move the rendered PNG out of `$CODEX_HOME/generated_images/`. This is necessary because the agent must `cp`/`mv` the file to the user-specified output path.
|
||||
4. **Concurrency = 1**. The file lock serializes concurrent invocations to avoid `codex exec` collisions. Parallel calls queue.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | `error_kind` | Resolution |
|
||||
|---------|--------------|------------|
|
||||
| `command not found: codex` | `codex_not_installed` | `npm install -g @openai/codex` |
|
||||
| `codex exec` fails | `spawn_failed` | Check `codex login` status; inspect `raw_log` path |
|
||||
| Timeout | `timeout` | Pass `--timeout 600000` (10 min) for slow networks |
|
||||
| Agent skipped `image_gen` | `no_image_gen_tool_use` | Auto-retries; consider sharpening the prompt — abstract prompts let the agent wander |
|
||||
| Output missing | `output_missing` | Agent did not `cp` to the target path; check `raw_log` for the actual save location under `generated_images/` |
|
||||
| Lock held | `lock_busy` | Wait for the in-flight request to finish; or `rm ~/.cache/baoyu-codex-imagegen/codex-exec.lock` |
|
||||
| Low image quality | — | Sharpen the prompt, try a different aspect, or supply `--ref` |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
scripts/codex-imagegen.sh # thin bash entrypoint
|
||||
scripts/codex-imagegen/
|
||||
├── main.ts # parseArgs → cache → lock → retry loop → emit JSON
|
||||
├── types.ts # CliOptions, GenerateResult, GenError, ErrorKind
|
||||
├── spawn.ts # spawn codex exec --json --sandbox danger-full-access
|
||||
├── parser.ts # parse JSONL event stream → toolCalls, usage, thread_id
|
||||
├── validator.ts # verify image_gen invocation + PNG magic + file size
|
||||
├── cache.ts # cacheKey(sha256), FileLock, lookup/store
|
||||
├── logger.ts # JsonLogger (verbose stderr + JSONL file)
|
||||
├── parser.test.ts
|
||||
├── cache.test.ts
|
||||
└── validator.test.ts
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
cd scripts/codex-imagegen && bun test
|
||||
```
|
||||
|
||||
## Internal Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CC[Claude Code / any caller]
|
||||
WRAPPER[scripts/codex-imagegen.sh]
|
||||
CODEX["codex exec --json<br/>--sandbox danger-full-access"]
|
||||
AGENT[Codex agent]
|
||||
TOOL[image_gen built-in tool]
|
||||
DEFAULT["$CODEX_HOME/<br/>generated_images/{thread_id}/"]
|
||||
OUT[/specified OUTPUT path/]
|
||||
|
||||
CC -->|exec wrapper| WRAPPER
|
||||
WRAPPER -->|stdin: instruction| CODEX
|
||||
CODEX --> AGENT
|
||||
AGENT -->|tool call| TOOL
|
||||
TOOL -->|writes file| DEFAULT
|
||||
AGENT -->|agent cp/mv| OUT
|
||||
WRAPPER -->|verify + parse| CC
|
||||
|
||||
classDef cc fill:#1e40af,color:#fff,stroke:#93c5fd
|
||||
classDef cdx fill:#7c2d12,color:#fff,stroke:#fdba74
|
||||
class CC,WRAPPER cc
|
||||
class CODEX,AGENT,TOOL cdx
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Bash entrypoint + TypeScript implementation** — the shell wrapper picks the runtime (`bun` preferred, falling back to `npx -y bun`); TypeScript handles the orchestration, parsing, retry, cache, and logging. This mirrors the project's existing `scripts/*.mjs` and `skills/<skill>/scripts/main.ts` pattern.
|
||||
2. **`--sandbox danger-full-access`** — necessary so the spawned agent can `cp`/`mv` the rendered PNG out of `$CODEX_HOME/generated_images/` to the user-specified path. Standard sandboxes block this.
|
||||
3. **Parse the JSONL event stream** — the final `agent_message` and intermediate `command_execution` events let the wrapper verify what actually happened (was `image_gen` called? did `cp` reach the right destination?), which is far more reliable than scraping freeform stdout.
|
||||
4. **Infrastructure, not a skill** — this backend is a CLI utility that skills route to via `preferred_image_backend`. It belongs in `scripts/`, not `skills/`, because it has no `SKILL.md` and is never loaded directly by an agent.
|
||||
5. **File lock instead of internal queue** — keeps the implementation small and works across multiple shell sessions or processes invoking the same wrapper concurrently.
|
||||
|
||||
## Related Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `scripts/codex-imagegen.sh` | CLI entrypoint |
|
||||
| `scripts/codex-imagegen/` | TypeScript implementation |
|
||||
| `docs/codex-imagegen-backend.md` | This document |
|
||||
| `CLAUDE.md` | Tells LLMs how to invoke this backend |
|
||||
| `.github/workflows/codex-imagegen-tests.yml` | CI unit tests |
|
||||
+39
-1
@@ -127,7 +127,7 @@ test -f "$HOME/.baoyu-skills/<skill-name>/EXTEND.md" && echo "user"
|
||||
| Result | Action |
|
||||
|--------|--------|
|
||||
| Found | Read, parse, display summary |
|
||||
| Not found | Ask user with AskUserQuestion |
|
||||
| Not found | Ask user via the runtime's user-input tool (see [user-input-tools.md](user-input-tools.md)) |
|
||||
```
|
||||
|
||||
End of SKILL.md should include:
|
||||
@@ -135,3 +135,41 @@ End of SKILL.md should include:
|
||||
## Extension Support
|
||||
Custom configurations via EXTEND.md. See **Step 1.1** for paths and supported options.
|
||||
```
|
||||
|
||||
## User Input Tools Section (Required)
|
||||
|
||||
Every SKILL.md that prompts the user for choices MUST include exactly one `## User Input Tools` section near the top (right after the intro, before the main workflow). The rule must be **inlined** — do NOT link to `docs/user-input-tools.md` (skills are self-contained; see [CLAUDE.md → Skill Self-Containment](../CLAUDE.md)). The author-side canonical reference lives at [user-input-tools.md](user-input-tools.md); copy its body into each new SKILL.md.
|
||||
|
||||
Standard snippet (copy verbatim):
|
||||
|
||||
```markdown
|
||||
## 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.
|
||||
```
|
||||
|
||||
## Image Generation Tools Section (Required for image-gen skills)
|
||||
|
||||
Every SKILL.md that renders images — whether by calling an image-generation API directly or by delegating to another skill — MUST include exactly one `## Image Generation Tools` section near the top (after `## User Input Tools`, before the main workflow). The rule must be **inlined** — do NOT link to `docs/image-generation-tools.md` (skills are self-contained; see [CLAUDE.md → Skill Self-Containment](../CLAUDE.md)). The author-side canonical reference lives at [image-generation-tools.md](image-generation-tools.md); copy its body into each new SKILL.md.
|
||||
|
||||
Standard snippet (copy verbatim):
|
||||
|
||||
```markdown
|
||||
## Image Generation Tools
|
||||
|
||||
When this skill needs to render an image:
|
||||
|
||||
- **Use whatever image-generation tool or skill is available** in the current runtime — e.g., Codex `imagegen`, Hermes `image_generate`, `baoyu-image-gen`, 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.
|
||||
|
||||
**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-image-gen`) above are examples — substitute the local equivalents under the same rule.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Image Generation Tools
|
||||
|
||||
Skills in this repo are loaded by multiple agent runtimes (Claude Code, Codex, Hermes, other agents, bare CLI). Each runtime exposes a different image-generation capability — some have a runtime-native tool (Codex `imagegen`, Hermes `image_generate`), others rely on an installed skill (`baoyu-image-gen`, or user-defined). This document defines the canonical **backend-selection rule** every skill that renders images follows so skills stay portable.
|
||||
|
||||
## The Rule
|
||||
|
||||
When a skill needs to render an image, resolve the backend in this order:
|
||||
|
||||
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-image-gen`) 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-image-gen`), 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.
|
||||
|
||||
## 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-image-gen`, `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)
|
||||
|
||||
Regardless of which backend is chosen, every skill that renders images MUST 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 allows switching backends without regenerating prompts.
|
||||
|
||||
## How Skills Declare This
|
||||
|
||||
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-image-gen`) 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
|
||||
|
||||
Skills that **are themselves** image-generation backends — currently `baoyu-image-gen`, `baoyu-image-gen` (deprecated), and `baoyu-danger-gemini-web` — do NOT include a `## Image Generation Tools` section. They render directly via their own provider integrations and have no need to "select a backend." The rule applies only to consumer skills that delegate rendering to whatever backend the runtime exposes.
|
||||
+16
-15
@@ -1,33 +1,34 @@
|
||||
# Image Generation Guidelines
|
||||
|
||||
Skills that require image generation MUST delegate to available image generation skills.
|
||||
Skills that require image generation MUST delegate to available image generation tools (runtime-native tools or installed skills).
|
||||
|
||||
**Backend selection convention**: see [image-generation-tools.md](image-generation-tools.md) for the runtime-neutral rule. Short version: use whatever backend is available; if multiple, ask the user once; if none, ask how to proceed. This document covers output conventions (naming, paths) that apply regardless of which backend is selected.
|
||||
|
||||
## Skill Selection
|
||||
|
||||
**Default**: `skills/baoyu-imagine/SKILL.md` (unless user specifies otherwise).
|
||||
|
||||
1. Read skill's SKILL.md for parameters and capabilities
|
||||
2. If user requests different skill, check `skills/` for alternatives
|
||||
3. Only ask user when multiple viable options exist
|
||||
1. Follow the rule in [image-generation-tools.md](image-generation-tools.md): use whatever backend is available; ask only on ambiguity.
|
||||
2. Read the chosen backend's documentation for parameters and capabilities.
|
||||
3. If user requests a specific backend, honor it.
|
||||
|
||||
## Generation Flow Template
|
||||
|
||||
```markdown
|
||||
### Step N: Generate Images
|
||||
|
||||
**Skill Selection**:
|
||||
1. Check available skills (`baoyu-imagine` default, or `baoyu-danger-gemini-web`)
|
||||
2. Read selected skill's SKILL.md for parameters
|
||||
3. If multiple skills available, ask user to choose
|
||||
**Backend Selection**:
|
||||
1. Detect available image-generation tools/skills (runtime-native + installed)
|
||||
2. If one available → use it. If multiple → ask user once. If none → ask how to proceed.
|
||||
3. Read the chosen backend's docs for parameters
|
||||
|
||||
**Generation Flow**:
|
||||
1. Call skill with prompt, output path, and skill-specific parameters
|
||||
2. Generate sequentially by default (batch parallel only when user has multiple prompts)
|
||||
3. Output progress: "Generated X/N"
|
||||
4. On failure, auto-retry once before reporting error
|
||||
1. Write the full prompt to `prompts/NN-{type}-[slug].md` BEFORE invoking the backend
|
||||
2. Call backend with the prompt (or prompt file), output path, and parameters
|
||||
3. Generate sequentially by default (batch parallel only when backend supports it and user has multiple prompts)
|
||||
4. Output progress: "Generated X/N"
|
||||
5. On failure, auto-retry once before reporting error
|
||||
```
|
||||
|
||||
**Batch Parallel** (`baoyu-imagine` only): concurrent workers with per-provider throttling via `batch.max_workers` in EXTEND.md.
|
||||
**Batch Parallel** (`baoyu-image-gen` only): concurrent workers with per-provider throttling via `batch.max_workers` in EXTEND.md.
|
||||
|
||||
## Output Path Convention
|
||||
|
||||
|
||||
+14
-7
@@ -21,21 +21,28 @@ bash scripts/sync-clawhub.sh # sync all skills
|
||||
bash scripts/sync-clawhub.sh <skill> # sync one skill
|
||||
```
|
||||
|
||||
Release hooks are configured via `.releaserc.yml`. This repo does not stage a separate release directory: release prep only syncs `packages/` into each skill's committed `scripts/vendor/`, and publish reads the skill directory directly.
|
||||
Release hooks are configured via `.releaserc.yml`. This repo does not stage a separate release directory: publish reads the skill directory directly and validates that local package references and CLI bin targets are self-contained.
|
||||
|
||||
Every skill release must keep the `version:` in that skill's `SKILL.md` aligned with the version being published. `publish-skill.mjs` and `sync-clawhub.mjs` both reject mismatches so a registry payload cannot ship with stale skill metadata.
|
||||
|
||||
Commits that touch `skills/<name>/**` must use Conventional Commit subjects, for example `fix(baoyu-post-to-wechat): handle WeChat editor focus`. CI runs `npm run verify:skill-release-commits` against the pushed or PR commit range so bare subjects like `Fix WeChat browser article publishing` cannot bypass per-skill release versioning silently.
|
||||
|
||||
## Shared Workspace Packages
|
||||
|
||||
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
|
||||
`packages/` is the source of truth for shared runtime code. Most skills consume shared packages from npm with semver ranges. `baoyu-url-to-markdown` is the exception: it vendors the `baoyu-fetch` runtime into `skills/baoyu-url-to-markdown/scripts/lib/` so the published skill is self-contained and does not depend on the `baoyu-fetch` npm package.
|
||||
|
||||
Current packages:
|
||||
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`)
|
||||
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 5 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`)
|
||||
- `baoyu-md` (shared Markdown rendering and placeholder pipeline), consumed by 3 skills (`baoyu-markdown-to-html`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`)
|
||||
- `baoyu-fetch` (URL-to-Markdown CLI), vendored into 1 skill (`baoyu-url-to-markdown`)
|
||||
|
||||
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
|
||||
**How it works**: npm packages are built from `packages/` and published to the public npm registry. Skills normally depend on those packages with `^<version>` specs. Release prep runs `node scripts/verify-shared-package-deps.mjs` so accidental `file:` dependencies cannot slip back in. For vendored skill runtimes, keep the copied code under the skill directory and run `node scripts/publish-skill.mjs --skill-dir <skill> --version <version> --dry-run` before publishing.
|
||||
|
||||
**Update workflow**:
|
||||
1. Edit package under `packages/`
|
||||
2. Run `node scripts/sync-shared-skill-packages.mjs`
|
||||
3. Commit synced `vendor/`, `package.json`, and `bun.lock` together
|
||||
2. Run the package build, e.g. `bun run --cwd packages/baoyu-md build`
|
||||
3. Publish the changed npm package with `npm publish --access public`
|
||||
4. Update consuming skill `package.json` semver ranges if the package version changed
|
||||
5. Run `node scripts/verify-shared-package-deps.mjs`
|
||||
|
||||
**Git hook**: Run `node scripts/install-git-hooks.mjs` once to enable the `pre-push` hook. It re-syncs and blocks push if vendor copies are stale (`--enforce-clean`).
|
||||
**Git hook**: Run `node scripts/install-git-hooks.mjs` once to enable the `pre-push` hook. It blocks pushes when a skill uses a local `file:` dependency or a vendored workspace package.
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ This repository has many scripts, but they do not share a single runtime or depe
|
||||
- Coverage command: `npm run test:coverage`
|
||||
- CI trigger: GitHub Actions on `push`, `pull_request`, and manual dispatch
|
||||
|
||||
This avoids introducing Jest/Vitest across a repo that already mixes plain Node scripts, Bun-based skill packages, vendored code, and browser automation.
|
||||
This avoids introducing Jest/Vitest across a repo that already mixes plain Node scripts, Bun-based skill packages, npm-published shared packages, and browser automation.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
@@ -18,19 +18,19 @@ This avoids introducing Jest/Vitest across a repo that already mixes plain Node
|
||||
Focus on pure functions under `scripts/lib/` first.
|
||||
|
||||
- `scripts/lib/release-files.mjs`
|
||||
- `scripts/lib/shared-skill-packages.mjs`
|
||||
- `scripts/verify-shared-package-deps.mjs`
|
||||
|
||||
Goals:
|
||||
|
||||
- Validate file filtering and release packaging rules
|
||||
- Catch regressions in package vendoring and dependency rewriting
|
||||
- Catch regressions that reintroduce local `file:` dependencies or vendored workspace packages
|
||||
- Keep tests deterministic and free of network, Bun, or browser requirements
|
||||
|
||||
### Phase 2: Root CLI integration tests
|
||||
|
||||
Add temp-directory integration tests for root CLIs that already support dry-run or local-only flows.
|
||||
|
||||
- `scripts/sync-shared-skill-packages.mjs`
|
||||
- `scripts/verify-shared-package-deps.mjs`
|
||||
- `scripts/publish-skill.mjs --dry-run`
|
||||
- `scripts/sync-clawhub.mjs` argument handling and local skill discovery
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# User Input Tools
|
||||
|
||||
Skills in this repo are loaded by multiple agent runtimes (Claude Code, other agents, bare CLI). Each runtime exposes a different API for asking the user questions. This document defines the canonical **tool-selection rule** every skill follows so skills stay portable.
|
||||
|
||||
## Tool Selection (priority order)
|
||||
|
||||
1. **Prefer built-in user-input tools** if the current agent runtime exposes one — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent.
|
||||
2. **Fallback to plain text**: 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 rule**:
|
||||
- If the tool supports **multiple questions per call** (e.g., `AskUserQuestion`): **Combine all applicable questions into a single call. Do NOT split into separate calls.**
|
||||
- If the tool supports **only one question per call** (e.g., single-prompt `clarify`): ask **one question per call, in priority order**.
|
||||
|
||||
## How Skills Declare This
|
||||
|
||||
Each `SKILL.md` that uses interactive user input includes **exactly one** `## User Input Tools` section (typically near the top, right after the intro) that **inlines** this rule. Do NOT link here from a SKILL.md — skills are self-contained (see [CLAUDE.md → Skill Self-Containment](../CLAUDE.md)). This document is the author-side canonical source; copy its body into each SKILL.md. The rule then governs every user-input interaction in that skill and its `references/` files.
|
||||
|
||||
Specific mentions of a concrete tool (e.g., `AskUserQuestion`) elsewhere in a skill are **concrete examples** — agents in other runtimes apply the rule above and substitute the local equivalent. Tool-specific parameters (e.g., `header:`, `multiSelect:`) are illustrative; runtimes without those knobs can omit them.
|
||||
Generated
+881
-163
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -7,7 +7,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node ./scripts/run-node-tests.mjs",
|
||||
"test:coverage": "node ./scripts/run-node-tests.mjs --experimental-test-coverage"
|
||||
"test:coverage": "node ./scripts/run-node-tests.mjs --experimental-test-coverage",
|
||||
"verify:skill-release-commits": "node ./scripts/verify-skill-release-commits.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
@@ -18,6 +19,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pptxgenjs": "^4.0.1"
|
||||
"pptxgenjs": "^4.0.1",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.13"
|
||||
}
|
||||
}
|
||||
|
||||
+479
@@ -0,0 +1,479 @@
|
||||
var __create = Object.create;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
function __accessProp(key) {
|
||||
return this[key];
|
||||
}
|
||||
var __toESMCache_node;
|
||||
var __toESMCache_esm;
|
||||
var __toESM = (mod, isNodeMode, target) => {
|
||||
var canCache = mod != null && typeof mod === "object";
|
||||
if (canCache) {
|
||||
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
||||
var cached = cache.get(mod);
|
||||
if (cached)
|
||||
return cached;
|
||||
}
|
||||
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
if (canCache)
|
||||
cache.set(mod, to);
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (from) => {
|
||||
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
||||
if (entry)
|
||||
return entry;
|
||||
entry = __defProp({}, "__esModule", { value: true });
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (var key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(entry, key))
|
||||
__defProp(entry, key, {
|
||||
get: __accessProp.bind(from, key),
|
||||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||
});
|
||||
}
|
||||
__moduleCache.set(from, entry);
|
||||
return entry;
|
||||
};
|
||||
var __moduleCache;
|
||||
var __returnValue = (v) => v;
|
||||
function __exportSetter(name, newValue) {
|
||||
this[name] = __returnValue.bind(null, newValue);
|
||||
}
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, {
|
||||
get: all[name],
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
set: __exportSetter.bind(all, name)
|
||||
});
|
||||
};
|
||||
|
||||
// src/index.ts
|
||||
var exports_src = {};
|
||||
__export(exports_src, {
|
||||
waitForChromeDebugPort: () => waitForChromeDebugPort,
|
||||
sleep: () => sleep,
|
||||
resolveSharedChromeProfileDir: () => resolveSharedChromeProfileDir,
|
||||
openPageSession: () => openPageSession,
|
||||
launchChrome: () => launchChrome,
|
||||
killChrome: () => killChrome,
|
||||
gracefulKillChrome: () => gracefulKillChrome,
|
||||
getFreePort: () => getFreePort,
|
||||
getDefaultChromeUserDataDirs: () => getDefaultChromeUserDataDirs,
|
||||
findExistingChromeDebugPort: () => findExistingChromeDebugPort,
|
||||
findChromeExecutable: () => findChromeExecutable,
|
||||
discoverRunningChromeDebugPort: () => discoverRunningChromeDebugPort,
|
||||
CdpConnection: () => CdpConnection
|
||||
});
|
||||
module.exports = __toCommonJS(exports_src);
|
||||
var import_node_child_process = require("node:child_process");
|
||||
var import_node_fs = __toESM(require("node:fs"));
|
||||
var import_node_net = __toESM(require("node:net"));
|
||||
var import_node_os = __toESM(require("node:os"));
|
||||
var import_node_path = __toESM(require("node:path"));
|
||||
var import_node_process = __toESM(require("node:process"));
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
async function getFreePort(fixedEnvName) {
|
||||
const fixed = fixedEnvName ? Number.parseInt(import_node_process.default.env[fixedEnvName] ?? "", 10) : NaN;
|
||||
if (Number.isInteger(fixed) && fixed > 0)
|
||||
return fixed;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = import_node_net.default.createServer();
|
||||
server.unref();
|
||||
server.on("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Unable to allocate a free TCP port.")));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
server.close((err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function findChromeExecutable(options) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = import_node_process.default.env[envName]?.trim();
|
||||
if (override && import_node_fs.default.existsSync(override))
|
||||
return override;
|
||||
}
|
||||
const candidates = import_node_process.default.platform === "darwin" ? options.candidates.darwin ?? options.candidates.default : import_node_process.default.platform === "win32" ? options.candidates.win32 ?? options.candidates.default : options.candidates.default;
|
||||
for (const candidate of candidates) {
|
||||
if (import_node_fs.default.existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function resolveSharedChromeProfileDir(options = {}) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = import_node_process.default.env[envName]?.trim();
|
||||
if (override)
|
||||
return import_node_path.default.resolve(override);
|
||||
}
|
||||
const appDataDirName = options.appDataDirName ?? "baoyu-skills";
|
||||
const profileDirName = options.profileDirName ?? "chrome-profile";
|
||||
if (options.wslWindowsHome) {
|
||||
return import_node_path.default.join(options.wslWindowsHome, ".local", "share", appDataDirName, profileDirName);
|
||||
}
|
||||
const base = import_node_process.default.platform === "darwin" ? import_node_path.default.join(import_node_os.default.homedir(), "Library", "Application Support") : import_node_process.default.platform === "win32" ? import_node_process.default.env.APPDATA ?? import_node_path.default.join(import_node_os.default.homedir(), "AppData", "Roaming") : import_node_process.default.env.XDG_DATA_HOME ?? import_node_path.default.join(import_node_os.default.homedir(), ".local", "share");
|
||||
return import_node_path.default.join(base, appDataDirName, profileDirName);
|
||||
}
|
||||
async function fetchWithTimeout(url, timeoutMs) {
|
||||
if (!timeoutMs || timeoutMs <= 0)
|
||||
return await fetch(url, { redirect: "follow" });
|
||||
const ctl = new AbortController;
|
||||
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { redirect: "follow", signal: ctl.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
async function fetchJson(url, options = {}) {
|
||||
const response = await fetchWithTimeout(url, options.timeoutMs);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
async function isDebugPortReady(port, timeoutMs = 3000) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
return !!version.webSocketDebuggerUrl;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function isPortListening(port, timeoutMs = 3000) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new import_node_net.default.Socket;
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
socket.once("connect", () => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.once("error", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
socket.connect(port, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
function parseDevToolsActivePort(filePath) {
|
||||
try {
|
||||
const content = import_node_fs.default.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split(/\r?\n/);
|
||||
const port = Number.parseInt(lines[0]?.trim() ?? "", 10);
|
||||
const wsPath = lines[1]?.trim();
|
||||
if (port > 0 && wsPath)
|
||||
return { port, wsPath };
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
async function findExistingChromeDebugPort(options) {
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const parsed = parseDevToolsActivePort(import_node_path.default.join(options.profileDir, "DevToolsActivePort"));
|
||||
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs))
|
||||
return parsed.port;
|
||||
if (import_node_process.default.platform === "win32")
|
||||
return null;
|
||||
try {
|
||||
const result = import_node_child_process.spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status !== 0 || !result.stdout)
|
||||
return null;
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes(options.profileDir) && line.includes("--remote-debugging-port="));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs))
|
||||
return port;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function getDefaultChromeUserDataDirs(channels = ["stable"]) {
|
||||
const home = import_node_os.default.homedir();
|
||||
const dirs = [];
|
||||
const channelDirs = {
|
||||
stable: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome", "User Data")
|
||||
},
|
||||
beta: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome-beta"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data")
|
||||
},
|
||||
canary: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome-canary"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data")
|
||||
},
|
||||
dev: {
|
||||
darwin: import_node_path.default.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
linux: import_node_path.default.join(home, ".config", "google-chrome-dev"),
|
||||
win32: import_node_path.default.join(import_node_process.default.env.LOCALAPPDATA ?? import_node_path.default.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data")
|
||||
}
|
||||
};
|
||||
const platform = import_node_process.default.platform === "darwin" ? "darwin" : import_node_process.default.platform === "win32" ? "win32" : "linux";
|
||||
for (const ch of channels) {
|
||||
const entry = channelDirs[ch];
|
||||
if (entry)
|
||||
dirs.push(entry[platform]);
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
async function discoverRunningChromeDebugPort(options = {}) {
|
||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels)).map((dir) => import_node_path.default.resolve(dir));
|
||||
for (const dir of userDataDirs) {
|
||||
const parsed = parseDevToolsActivePort(import_node_path.default.join(dir, "DevToolsActivePort"));
|
||||
if (!parsed)
|
||||
continue;
|
||||
if (await isPortListening(parsed.port, timeoutMs)) {
|
||||
return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` };
|
||||
}
|
||||
}
|
||||
if (import_node_process.default.platform !== "win32") {
|
||||
try {
|
||||
const result = import_node_child_process.spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes("--remote-debugging-port=") && userDataDirs.some((dir) => line.includes(dir)));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return { port, wsUrl: version.webSocketDebuggerUrl };
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function waitForChromeDebugPort(port, timeoutMs, options) {
|
||||
const start = Date.now();
|
||||
let lastError = null;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 5000 });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return version.webSocketDebuggerUrl;
|
||||
lastError = new Error("Missing webSocketDebuggerUrl");
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
if (options?.includeLastError && lastError) {
|
||||
throw new Error(`Chrome debug port not ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
||||
}
|
||||
throw new Error("Chrome debug port not ready");
|
||||
}
|
||||
|
||||
class CdpConnection {
|
||||
ws;
|
||||
nextId = 0;
|
||||
pending = new Map;
|
||||
eventHandlers = new Map;
|
||||
defaultTimeoutMs;
|
||||
constructor(ws, defaultTimeoutMs = 15000) {
|
||||
this.ws = ws;
|
||||
this.defaultTimeoutMs = defaultTimeoutMs;
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.method) {
|
||||
const handlers = this.eventHandlers.get(msg.method);
|
||||
if (handlers) {
|
||||
handlers.forEach((handler) => handler(msg.params));
|
||||
}
|
||||
}
|
||||
if (msg.id) {
|
||||
const pending = this.pending.get(msg.id);
|
||||
if (pending) {
|
||||
this.pending.delete(msg.id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
if (msg.error?.message)
|
||||
pending.reject(new Error(msg.error.message));
|
||||
else
|
||||
pending.resolve(msg.result);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
this.ws.addEventListener("close", () => {
|
||||
for (const [id, pending] of this.pending.entries()) {
|
||||
this.pending.delete(id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error("CDP connection closed."));
|
||||
}
|
||||
});
|
||||
}
|
||||
static async connect(url, timeoutMs, options) {
|
||||
const ws = new WebSocket(url);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("CDP connection timeout.")), timeoutMs);
|
||||
ws.addEventListener("open", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
ws.addEventListener("error", () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error("CDP connection failed."));
|
||||
});
|
||||
});
|
||||
return new CdpConnection(ws, options?.defaultTimeoutMs ?? 15000);
|
||||
}
|
||||
on(method, handler) {
|
||||
if (!this.eventHandlers.has(method)) {
|
||||
this.eventHandlers.set(method, new Set);
|
||||
}
|
||||
this.eventHandlers.get(method)?.add(handler);
|
||||
}
|
||||
off(method, handler) {
|
||||
this.eventHandlers.get(method)?.delete(handler);
|
||||
}
|
||||
async send(method, params, options) {
|
||||
const id = ++this.nextId;
|
||||
const message = { id, method };
|
||||
if (params)
|
||||
message.params = params;
|
||||
if (options?.sessionId)
|
||||
message.sessionId = options.sessionId;
|
||||
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timer = timeoutMs > 0 ? setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP timeout: ${method}`));
|
||||
}, timeoutMs) : null;
|
||||
this.pending.set(id, { resolve, reject, timer });
|
||||
this.ws.send(JSON.stringify(message));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
close() {
|
||||
try {
|
||||
this.ws.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
async function launchChrome(options) {
|
||||
await import_node_fs.default.promises.mkdir(options.profileDir, { recursive: true });
|
||||
const args = [
|
||||
`--remote-debugging-port=${options.port}`,
|
||||
`--user-data-dir=${options.profileDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
...options.extraArgs ?? []
|
||||
];
|
||||
if (options.headless)
|
||||
args.push("--headless=new");
|
||||
if (options.url)
|
||||
args.push(options.url);
|
||||
return import_node_child_process.spawn(options.chromePath, args, { stdio: "ignore" });
|
||||
}
|
||||
function killChrome(chrome) {
|
||||
try {
|
||||
chrome.kill("SIGTERM");
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
if (chrome.exitCode === null && chrome.signalCode === null) {
|
||||
try {
|
||||
chrome.kill("SIGKILL");
|
||||
} catch {}
|
||||
}
|
||||
}, 2000).unref?.();
|
||||
}
|
||||
async function gracefulKillChrome(chrome, port, timeoutMs = 6000) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
const exitPromise = new Promise((resolve) => {
|
||||
chrome.once("exit", () => resolve());
|
||||
});
|
||||
killChrome(chrome);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
if (port !== undefined && !await isPortListening(port, 250))
|
||||
return;
|
||||
const exited = await Promise.race([
|
||||
exitPromise.then(() => true),
|
||||
sleep(100).then(() => false)
|
||||
]);
|
||||
if (exited)
|
||||
return;
|
||||
}
|
||||
await Promise.race([
|
||||
exitPromise,
|
||||
sleep(250)
|
||||
]);
|
||||
}
|
||||
async function openPageSession(options) {
|
||||
let targetId;
|
||||
let createdTarget = false;
|
||||
if (options.reusing) {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
} else {
|
||||
const targets = await options.cdp.send("Target.getTargets");
|
||||
const existing = targets.targetInfos.find(options.matchTarget);
|
||||
if (existing) {
|
||||
targetId = existing.targetId;
|
||||
} else {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
}
|
||||
}
|
||||
const { sessionId } = await options.cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
||||
if (options.activateTarget ?? true) {
|
||||
await options.cdp.send("Target.activateTarget", { targetId });
|
||||
}
|
||||
if (options.enablePage)
|
||||
await options.cdp.send("Page.enable", {}, { sessionId });
|
||||
if (options.enableRuntime)
|
||||
await options.cdp.send("Runtime.enable", {}, { sessionId });
|
||||
if (options.enableDom)
|
||||
await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||
if (options.enableNetwork)
|
||||
await options.cdp.send("Network.enable", {}, { sessionId });
|
||||
return { sessionId, targetId, createdTarget };
|
||||
}
|
||||
+415
@@ -0,0 +1,415 @@
|
||||
// src/index.ts
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
async function getFreePort(fixedEnvName) {
|
||||
const fixed = fixedEnvName ? Number.parseInt(process.env[fixedEnvName] ?? "", 10) : NaN;
|
||||
if (Number.isInteger(fixed) && fixed > 0)
|
||||
return fixed;
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.unref();
|
||||
server.on("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Unable to allocate a free TCP port.")));
|
||||
return;
|
||||
}
|
||||
const port = address.port;
|
||||
server.close((err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
function findChromeExecutable(options) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = process.env[envName]?.trim();
|
||||
if (override && fs.existsSync(override))
|
||||
return override;
|
||||
}
|
||||
const candidates = process.platform === "darwin" ? options.candidates.darwin ?? options.candidates.default : process.platform === "win32" ? options.candidates.win32 ?? options.candidates.default : options.candidates.default;
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function resolveSharedChromeProfileDir(options = {}) {
|
||||
for (const envName of options.envNames ?? []) {
|
||||
const override = process.env[envName]?.trim();
|
||||
if (override)
|
||||
return path.resolve(override);
|
||||
}
|
||||
const appDataDirName = options.appDataDirName ?? "baoyu-skills";
|
||||
const profileDirName = options.profileDirName ?? "chrome-profile";
|
||||
if (options.wslWindowsHome) {
|
||||
return path.join(options.wslWindowsHome, ".local", "share", appDataDirName, profileDirName);
|
||||
}
|
||||
const base = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support") : process.platform === "win32" ? process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming") : process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
|
||||
return path.join(base, appDataDirName, profileDirName);
|
||||
}
|
||||
async function fetchWithTimeout(url, timeoutMs) {
|
||||
if (!timeoutMs || timeoutMs <= 0)
|
||||
return await fetch(url, { redirect: "follow" });
|
||||
const ctl = new AbortController;
|
||||
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { redirect: "follow", signal: ctl.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
async function fetchJson(url, options = {}) {
|
||||
const response = await fetchWithTimeout(url, options.timeoutMs);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
async function isDebugPortReady(port, timeoutMs = 3000) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
return !!version.webSocketDebuggerUrl;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function isPortListening(port, timeoutMs = 3000) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket;
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
socket.once("connect", () => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
socket.once("error", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
socket.connect(port, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
function parseDevToolsActivePort(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split(/\r?\n/);
|
||||
const port = Number.parseInt(lines[0]?.trim() ?? "", 10);
|
||||
const wsPath = lines[1]?.trim();
|
||||
if (port > 0 && wsPath)
|
||||
return { port, wsPath };
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
async function findExistingChromeDebugPort(options) {
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const parsed = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));
|
||||
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs))
|
||||
return parsed.port;
|
||||
if (process.platform === "win32")
|
||||
return null;
|
||||
try {
|
||||
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status !== 0 || !result.stdout)
|
||||
return null;
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes(options.profileDir) && line.includes("--remote-debugging-port="));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs))
|
||||
return port;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function getDefaultChromeUserDataDirs(channels = ["stable"]) {
|
||||
const home = os.homedir();
|
||||
const dirs = [];
|
||||
const channelDirs = {
|
||||
stable: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome"),
|
||||
linux: path.join(home, ".config", "google-chrome"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data")
|
||||
},
|
||||
beta: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
linux: path.join(home, ".config", "google-chrome-beta"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data")
|
||||
},
|
||||
canary: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
linux: path.join(home, ".config", "google-chrome-canary"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data")
|
||||
},
|
||||
dev: {
|
||||
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
linux: path.join(home, ".config", "google-chrome-dev"),
|
||||
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data")
|
||||
}
|
||||
};
|
||||
const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "win32" : "linux";
|
||||
for (const ch of channels) {
|
||||
const entry = channelDirs[ch];
|
||||
if (entry)
|
||||
dirs.push(entry[platform]);
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
async function discoverRunningChromeDebugPort(options = {}) {
|
||||
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
|
||||
const timeoutMs = options.timeoutMs ?? 3000;
|
||||
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels)).map((dir) => path.resolve(dir));
|
||||
for (const dir of userDataDirs) {
|
||||
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
|
||||
if (!parsed)
|
||||
continue;
|
||||
if (await isPortListening(parsed.port, timeoutMs)) {
|
||||
return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` };
|
||||
}
|
||||
}
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5000 });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const lines = result.stdout.split(`
|
||||
`).filter((line) => line.includes("--remote-debugging-port=") && userDataDirs.some((dir) => line.includes(dir)));
|
||||
for (const line of lines) {
|
||||
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
||||
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
|
||||
if (port > 0 && await isDebugPortReady(port, timeoutMs)) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return { port, wsUrl: version.webSocketDebuggerUrl };
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function waitForChromeDebugPort(port, timeoutMs, options) {
|
||||
const start = Date.now();
|
||||
let lastError = null;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const version = await fetchJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 5000 });
|
||||
if (version.webSocketDebuggerUrl)
|
||||
return version.webSocketDebuggerUrl;
|
||||
lastError = new Error("Missing webSocketDebuggerUrl");
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
if (options?.includeLastError && lastError) {
|
||||
throw new Error(`Chrome debug port not ready: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
||||
}
|
||||
throw new Error("Chrome debug port not ready");
|
||||
}
|
||||
|
||||
class CdpConnection {
|
||||
ws;
|
||||
nextId = 0;
|
||||
pending = new Map;
|
||||
eventHandlers = new Map;
|
||||
defaultTimeoutMs;
|
||||
constructor(ws, defaultTimeoutMs = 15000) {
|
||||
this.ws = ws;
|
||||
this.defaultTimeoutMs = defaultTimeoutMs;
|
||||
this.ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.method) {
|
||||
const handlers = this.eventHandlers.get(msg.method);
|
||||
if (handlers) {
|
||||
handlers.forEach((handler) => handler(msg.params));
|
||||
}
|
||||
}
|
||||
if (msg.id) {
|
||||
const pending = this.pending.get(msg.id);
|
||||
if (pending) {
|
||||
this.pending.delete(msg.id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
if (msg.error?.message)
|
||||
pending.reject(new Error(msg.error.message));
|
||||
else
|
||||
pending.resolve(msg.result);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
this.ws.addEventListener("close", () => {
|
||||
for (const [id, pending] of this.pending.entries()) {
|
||||
this.pending.delete(id);
|
||||
if (pending.timer)
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error("CDP connection closed."));
|
||||
}
|
||||
});
|
||||
}
|
||||
static async connect(url, timeoutMs, options) {
|
||||
const ws = new WebSocket(url);
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("CDP connection timeout.")), timeoutMs);
|
||||
ws.addEventListener("open", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
ws.addEventListener("error", () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error("CDP connection failed."));
|
||||
});
|
||||
});
|
||||
return new CdpConnection(ws, options?.defaultTimeoutMs ?? 15000);
|
||||
}
|
||||
on(method, handler) {
|
||||
if (!this.eventHandlers.has(method)) {
|
||||
this.eventHandlers.set(method, new Set);
|
||||
}
|
||||
this.eventHandlers.get(method)?.add(handler);
|
||||
}
|
||||
off(method, handler) {
|
||||
this.eventHandlers.get(method)?.delete(handler);
|
||||
}
|
||||
async send(method, params, options) {
|
||||
const id = ++this.nextId;
|
||||
const message = { id, method };
|
||||
if (params)
|
||||
message.params = params;
|
||||
if (options?.sessionId)
|
||||
message.sessionId = options.sessionId;
|
||||
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timer = timeoutMs > 0 ? setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP timeout: ${method}`));
|
||||
}, timeoutMs) : null;
|
||||
this.pending.set(id, { resolve, reject, timer });
|
||||
this.ws.send(JSON.stringify(message));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
close() {
|
||||
try {
|
||||
this.ws.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
async function launchChrome(options) {
|
||||
await fs.promises.mkdir(options.profileDir, { recursive: true });
|
||||
const args = [
|
||||
`--remote-debugging-port=${options.port}`,
|
||||
`--user-data-dir=${options.profileDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
...options.extraArgs ?? []
|
||||
];
|
||||
if (options.headless)
|
||||
args.push("--headless=new");
|
||||
if (options.url)
|
||||
args.push(options.url);
|
||||
return spawn(options.chromePath, args, { stdio: "ignore" });
|
||||
}
|
||||
function killChrome(chrome) {
|
||||
try {
|
||||
chrome.kill("SIGTERM");
|
||||
} catch {}
|
||||
setTimeout(() => {
|
||||
if (chrome.exitCode === null && chrome.signalCode === null) {
|
||||
try {
|
||||
chrome.kill("SIGKILL");
|
||||
} catch {}
|
||||
}
|
||||
}, 2000).unref?.();
|
||||
}
|
||||
async function gracefulKillChrome(chrome, port, timeoutMs = 6000) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
const exitPromise = new Promise((resolve) => {
|
||||
chrome.once("exit", () => resolve());
|
||||
});
|
||||
killChrome(chrome);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (chrome.exitCode !== null || chrome.signalCode !== null)
|
||||
return;
|
||||
if (port !== undefined && !await isPortListening(port, 250))
|
||||
return;
|
||||
const exited = await Promise.race([
|
||||
exitPromise.then(() => true),
|
||||
sleep(100).then(() => false)
|
||||
]);
|
||||
if (exited)
|
||||
return;
|
||||
}
|
||||
await Promise.race([
|
||||
exitPromise,
|
||||
sleep(250)
|
||||
]);
|
||||
}
|
||||
async function openPageSession(options) {
|
||||
let targetId;
|
||||
let createdTarget = false;
|
||||
if (options.reusing) {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
} else {
|
||||
const targets = await options.cdp.send("Target.getTargets");
|
||||
const existing = targets.targetInfos.find(options.matchTarget);
|
||||
if (existing) {
|
||||
targetId = existing.targetId;
|
||||
} else {
|
||||
const created = await options.cdp.send("Target.createTarget", { url: options.url });
|
||||
targetId = created.targetId;
|
||||
createdTarget = true;
|
||||
}
|
||||
}
|
||||
const { sessionId } = await options.cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
||||
if (options.activateTarget ?? true) {
|
||||
await options.cdp.send("Target.activateTarget", { targetId });
|
||||
}
|
||||
if (options.enablePage)
|
||||
await options.cdp.send("Page.enable", {}, { sessionId });
|
||||
if (options.enableRuntime)
|
||||
await options.cdp.send("Runtime.enable", {}, { sessionId });
|
||||
if (options.enableDom)
|
||||
await options.cdp.send("DOM.enable", {}, { sessionId });
|
||||
if (options.enableNetwork)
|
||||
await options.cdp.send("Network.enable", {}, { sessionId });
|
||||
return { sessionId, targetId, createdTarget };
|
||||
}
|
||||
export {
|
||||
waitForChromeDebugPort,
|
||||
sleep,
|
||||
resolveSharedChromeProfileDir,
|
||||
openPageSession,
|
||||
launchChrome,
|
||||
killChrome,
|
||||
gracefulKillChrome,
|
||||
getFreePort,
|
||||
getDefaultChromeUserDataDirs,
|
||||
findExistingChromeDebugPort,
|
||||
findChromeExecutable,
|
||||
discoverRunningChromeDebugPort,
|
||||
CdpConnection
|
||||
};
|
||||
@@ -1,12 +1,42 @@
|
||||
{
|
||||
"name": "baoyu-chrome-cdp",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"src"
|
||||
"dist",
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.test.ts"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./src/*": "./src/*"
|
||||
},
|
||||
"description": "Shared Chrome DevTools Protocol utilities for baoyu skills.",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/JimLiu/baoyu-skills.git",
|
||||
"directory": "packages/baoyu-chrome-cdp"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/JimLiu/baoyu-skills/issues"
|
||||
},
|
||||
"homepage": "https://github.com/JimLiu/baoyu-skills/tree/main/packages/baoyu-chrome-cdp#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../scripts/build-shared-package.mjs",
|
||||
"prepack": "bun run build"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,21 @@ English | [简体中文](./CHANGELOG.zh-CN.md)
|
||||
The format is inspired by Keep a Changelog, and the project follows Semantic
|
||||
Versioning.
|
||||
|
||||
## [0.1.2] - 2026-04-21
|
||||
|
||||
#### Changed
|
||||
|
||||
- Upgraded Defuddle to 0.17.0 and jsdom to 29.0.2 for generic extraction.
|
||||
- Added an `@xmldom/xmldom` override to keep the optional Defuddle MathML
|
||||
dependency chain on a non-vulnerable release.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Fixed X/Twitter video extraction for single posts and X Articles by selecting
|
||||
the highest-bitrate MP4 variant instead of the preview image URL.
|
||||
- Fixed X Article media rendering so video entities are emitted as
|
||||
`[video](...)` links instead of image embeds.
|
||||
|
||||
## [0.1.1] - 2026-03-27
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
|
||||
格式参考 Keep a Changelog,版本号遵循 Semantic Versioning。
|
||||
|
||||
## [0.1.2] - 2026-04-21
|
||||
|
||||
### 变更
|
||||
|
||||
- 将 Defuddle 升级到 0.17.0、jsdom 升级到 29.0.2,用于通用页面提取。
|
||||
- 新增 `@xmldom/xmldom` override,使 Defuddle 的可选 MathML 依赖链保持在无漏洞版本。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 X/Twitter 单条内容和 X Article 的视频提取逻辑,改为选择最高码率 MP4 变体,而不是预览图 URL。
|
||||
- 修复 X Article 媒体渲染,视频实体现在输出为 `[video](...)` 链接,而不是图片嵌入。
|
||||
|
||||
## [0.1.1] - 2026-03-27
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "baoyu-fetch",
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"chrome-launcher": "^1.2.1",
|
||||
"defuddle": "^0.14.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"defuddle": "^0.17.0",
|
||||
"jsdom": "^29.0.2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
@@ -24,11 +25,22 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.13",
|
||||
},
|
||||
"packages": {
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
|
||||
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
|
||||
|
||||
"@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
|
||||
|
||||
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
|
||||
|
||||
"@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.0", "", { "dependencies": { "@changesets/config": "^3.1.3", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ=="],
|
||||
|
||||
"@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="],
|
||||
@@ -63,15 +75,19 @@
|
||||
|
||||
"@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="],
|
||||
|
||||
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
|
||||
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
|
||||
|
||||
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
|
||||
"@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="],
|
||||
|
||||
"@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
|
||||
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="],
|
||||
|
||||
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
|
||||
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="],
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
|
||||
|
||||
"@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
|
||||
|
||||
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="],
|
||||
|
||||
@@ -107,9 +123,7 @@
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
@@ -123,6 +137,8 @@
|
||||
|
||||
"better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
@@ -143,13 +159,13 @@
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
|
||||
|
||||
"cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
|
||||
|
||||
"data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
|
||||
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
@@ -157,7 +173,7 @@
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"defuddle": ["defuddle@0.14.0", "", { "dependencies": { "commander": "^12.1.0" }, "optionalDependencies": { "linkedom": "^0.18.12", "mathml-to-latex": "^1.5.0", "temml": "^0.13.1", "turndown": "^7.2.0" }, "bin": { "defuddle": "dist/cli.js" } }, "sha512-btavZGd1WgiVqrVM62WGRXMUi/aU7ckTZiq0xXWLZMHvzIqNZjwIFQEDRx8MarD7fIgsB90NXZ9xHJkKtapt2Q=="],
|
||||
"defuddle": ["defuddle@0.17.0", "", { "dependencies": { "commander": "^12.1.0" }, "optionalDependencies": { "linkedom": "^0.18.12", "mathml-to-latex": "^1.5.0", "temml": "^0.13.1", "turndown": "^7.2.0" }, "bin": { "defuddle": "dist/cli.js" } }, "sha512-rjRb9kxgrJLFhFJjXTSAitbmmSyvjEqGCi5JiyM6ImOf4leC/O+f7TPVPAymk/gnENFTrn9j7T/LTfW9IVwiLw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
@@ -203,16 +219,12 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
|
||||
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
@@ -241,7 +253,7 @@
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="],
|
||||
"jsdom": ["jsdom@29.0.2", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w=="],
|
||||
|
||||
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
|
||||
@@ -255,7 +267,7 @@
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
"lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
@@ -285,6 +297,8 @@
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
@@ -351,8 +365,6 @@
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
|
||||
|
||||
"outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="],
|
||||
|
||||
"p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="],
|
||||
@@ -397,12 +409,12 @@
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
@@ -419,6 +431,8 @@
|
||||
|
||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
@@ -433,15 +447,15 @@
|
||||
|
||||
"term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="],
|
||||
|
||||
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
|
||||
"tldts": ["tldts@7.0.28", "", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="],
|
||||
|
||||
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
|
||||
"tldts-core": ["tldts-core@7.0.28", "", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
|
||||
"tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
|
||||
|
||||
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
|
||||
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
@@ -453,6 +467,8 @@
|
||||
|
||||
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
|
||||
|
||||
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
@@ -473,13 +489,11 @@
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
|
||||
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
|
||||
|
||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||
"whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -503,11 +517,13 @@
|
||||
|
||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"jsdom/parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
"jsdom/parse5/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||
|
||||
"read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "baoyu-fetch",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "Read URLs into high-quality Markdown or JSON with Chrome CDP and site adapters.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"baoyu-fetch": "./src/cli.ts"
|
||||
"baoyu-fetch": "dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.zh-CN.md",
|
||||
"src/adapters",
|
||||
"src/browser",
|
||||
@@ -36,7 +37,8 @@
|
||||
"dev": "bun run ./src/cli.ts",
|
||||
"release": "changeset publish",
|
||||
"test": "bun test",
|
||||
"version-packages": "changeset version"
|
||||
"version-packages": "changeset version",
|
||||
"prepack": "bun run build"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
@@ -44,8 +46,8 @@
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"chrome-launcher": "^1.2.1",
|
||||
"defuddle": "^0.14.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"defuddle": "^0.17.0",
|
||||
"jsdom": "^29.0.2",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
@@ -59,5 +61,8 @@
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/ws": "^8.18.1",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,4 +339,105 @@ describe("x article extraction", () => {
|
||||
expect(content.markdown).toContain("https://example.com/report");
|
||||
expect(content.markdown).not.toContain("https://t.co/example");
|
||||
});
|
||||
|
||||
test("renders article video media as the highest bitrate mp4 link", () => {
|
||||
const payload = {
|
||||
data: {
|
||||
tweetResult: {
|
||||
result: {
|
||||
rest_id: "2046628728210350366",
|
||||
legacy: {
|
||||
full_text: "Fallback text",
|
||||
favorite_count: 12,
|
||||
retweet_count: 3,
|
||||
reply_count: 1,
|
||||
created_at: "Tue Apr 21 16:34:47 +0000 2026",
|
||||
},
|
||||
core: {
|
||||
user_results: {
|
||||
result: {
|
||||
legacy: {
|
||||
name: "Google AI Studio",
|
||||
screen_name: "GoogleAIStudio",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
article: {
|
||||
article_results: {
|
||||
result: {
|
||||
title: "Article with video",
|
||||
media_entities: [
|
||||
{
|
||||
media_id: "2046627051822530560",
|
||||
media_info: {
|
||||
__typename: "ApiVideo",
|
||||
variants: [
|
||||
{
|
||||
bit_rate: 2176000,
|
||||
content_type: "video/mp4",
|
||||
url: "https://video.twimg.com/amplify_video/2046627051822530560/vid/avc1/1280x720/medium.mp4",
|
||||
},
|
||||
{
|
||||
content_type: "application/x-mpegURL",
|
||||
url: "https://video.twimg.com/amplify_video/2046627051822530560/pl/playlist.m3u8",
|
||||
},
|
||||
{
|
||||
bit_rate: 10368000,
|
||||
content_type: "video/mp4",
|
||||
url: "https://video.twimg.com/amplify_video/2046627051822530560/vid/avc1/1920x1080/high.mp4",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
content_state: {
|
||||
blocks: [
|
||||
{
|
||||
type: "atomic",
|
||||
text: " ",
|
||||
data: {},
|
||||
entityRanges: [{ key: 0, length: 1, offset: 0 }],
|
||||
inlineStyleRanges: [],
|
||||
},
|
||||
],
|
||||
entityMap: [
|
||||
{
|
||||
key: "0",
|
||||
value: {
|
||||
type: "MEDIA",
|
||||
mutability: "Immutable",
|
||||
data: {
|
||||
mediaItems: [{ mediaId: "2046627051822530560" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const document = extractArticleDocumentFromPayload(
|
||||
payload,
|
||||
"2046628728210350366",
|
||||
"https://x.com/GoogleAIStudio/status/2046628728210350366",
|
||||
);
|
||||
|
||||
expect(document).not.toBeNull();
|
||||
|
||||
const content = document?.content[0];
|
||||
expect(content?.type).toBe("markdown");
|
||||
if (!content || content.type !== "markdown") {
|
||||
throw new Error("Expected markdown content");
|
||||
}
|
||||
|
||||
expect(content.markdown).toBe(
|
||||
"[video](https://video.twimg.com/amplify_video/2046627051822530560/vid/avc1/1920x1080/high.mp4)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,4 +184,77 @@ describe("x single tweet extraction", () => {
|
||||
"Quoted Author (@quoted_author)\n\nQuoted tweet text\n\nphoto: https://pbs.twimg.com/media/quoted?format=jpg&name=4096x4096",
|
||||
});
|
||||
});
|
||||
|
||||
test("uses the highest bitrate mp4 variant for tweet video media", () => {
|
||||
const payload = {
|
||||
data: {
|
||||
tweetResult: {
|
||||
result: {
|
||||
rest_id: "2046628728210350366",
|
||||
legacy: {
|
||||
full_text: "Video post https://t.co/video",
|
||||
favorite_count: 12,
|
||||
retweet_count: 3,
|
||||
reply_count: 1,
|
||||
created_at: "Tue Apr 21 16:34:47 +0000 2026",
|
||||
extended_entities: {
|
||||
media: [
|
||||
{
|
||||
type: "video",
|
||||
media_url_https: "https://pbs.twimg.com/amplify_video_thumb/2046627051822530560/img/poster.jpg",
|
||||
url: "https://t.co/video",
|
||||
video_info: {
|
||||
variants: [
|
||||
{
|
||||
content_type: "application/x-mpegURL",
|
||||
url: "https://video.twimg.com/amplify_video/2046627051822530560/pl/playlist.m3u8",
|
||||
},
|
||||
{
|
||||
bitrate: 256000,
|
||||
content_type: "video/mp4",
|
||||
url: "https://video.twimg.com/amplify_video/2046627051822530560/vid/avc1/480x270/low.mp4",
|
||||
},
|
||||
{
|
||||
bitrate: 10368000,
|
||||
content_type: "video/mp4",
|
||||
url: "https://video.twimg.com/amplify_video/2046627051822530560/vid/avc1/1920x1080/high.mp4",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
core: {
|
||||
user_results: {
|
||||
result: {
|
||||
legacy: {
|
||||
name: "Google AI Studio",
|
||||
screen_name: "GoogleAIStudio",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const document = extractSingleTweetDocumentFromPayload(
|
||||
payload,
|
||||
"2046628728210350366",
|
||||
"https://x.com/GoogleAIStudio/status/2046628728210350366",
|
||||
);
|
||||
|
||||
expect(document).not.toBeNull();
|
||||
|
||||
const listBlock = document?.content.find((block) => block.type === "list");
|
||||
expect(listBlock).toEqual({
|
||||
type: "list",
|
||||
ordered: false,
|
||||
items: [
|
||||
"video: https://video.twimg.com/amplify_video/2046627051822530560/vid/avc1/1920x1080/high.mp4",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,18 +9,43 @@ import {
|
||||
getUser,
|
||||
isRecord,
|
||||
normalizeTitle,
|
||||
resolveBestXVideoVariantUrl,
|
||||
toHighResXImageUrl,
|
||||
toXTweet,
|
||||
} from "./shared";
|
||||
import type { JsonObject } from "./types";
|
||||
|
||||
function resolveArticleMediaUrl(mediaInfo: JsonObject): string {
|
||||
interface ArticleMedia {
|
||||
kind: "image" | "video";
|
||||
url: string;
|
||||
}
|
||||
|
||||
function resolveArticleMedia(mediaInfo: JsonObject): ArticleMedia | null {
|
||||
const videoUrl = resolveBestXVideoVariantUrl(mediaInfo);
|
||||
if (videoUrl) {
|
||||
return {
|
||||
kind: "video",
|
||||
url: videoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const rawUrl =
|
||||
(typeof mediaInfo.original_img_url === "string" && mediaInfo.original_img_url) ||
|
||||
(typeof mediaInfo.url === "string" && mediaInfo.url) ||
|
||||
"";
|
||||
|
||||
return rawUrl ? toHighResXImageUrl(rawUrl) : "";
|
||||
if (!rawUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "image",
|
||||
url: toHighResXImageUrl(rawUrl),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveArticleMediaUrl(mediaInfo: JsonObject): string {
|
||||
return resolveArticleMedia(mediaInfo)?.url ?? "";
|
||||
}
|
||||
|
||||
function normalizeEntityMap(entityMap: unknown): Map<string, JsonObject> {
|
||||
@@ -139,8 +164,8 @@ function getTweetId(entityMap: Map<string, JsonObject>, entityKey: unknown): str
|
||||
return data.tweetId;
|
||||
}
|
||||
|
||||
function buildMediaUrlMap(articleResult: JsonObject): Map<string, string> {
|
||||
const mediaMap = new Map<string, string>();
|
||||
function buildMediaMap(articleResult: JsonObject): Map<string, ArticleMedia> {
|
||||
const mediaMap = new Map<string, ArticleMedia>();
|
||||
const mediaEntities = Array.isArray(articleResult.media_entities) ? articleResult.media_entities : [];
|
||||
|
||||
for (const entity of mediaEntities) {
|
||||
@@ -148,25 +173,28 @@ function buildMediaUrlMap(articleResult: JsonObject): Map<string, string> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mediaInfo = entity.media_info;
|
||||
const url = resolveArticleMediaUrl(mediaInfo);
|
||||
if (url) {
|
||||
mediaMap.set(entity.media_id, url);
|
||||
const media = resolveArticleMedia(entity.media_info);
|
||||
if (media) {
|
||||
mediaMap.set(entity.media_id, media);
|
||||
}
|
||||
}
|
||||
|
||||
const coverMedia = isRecord(articleResult.cover_media) ? articleResult.cover_media : null;
|
||||
if (coverMedia && typeof coverMedia.media_id === "string" && isRecord(coverMedia.media_info)) {
|
||||
const url = resolveArticleMediaUrl(coverMedia.media_info);
|
||||
if (url) {
|
||||
mediaMap.set(coverMedia.media_id, url);
|
||||
const media = resolveArticleMedia(coverMedia.media_info);
|
||||
if (media) {
|
||||
mediaMap.set(coverMedia.media_id, media);
|
||||
}
|
||||
}
|
||||
|
||||
return mediaMap;
|
||||
}
|
||||
|
||||
function getMediaMarkdown(entityMap: Map<string, JsonObject>, entityKey: unknown, mediaMap: Map<string, string>): string[] {
|
||||
function getMediaMarkdown(
|
||||
entityMap: Map<string, JsonObject>,
|
||||
entityKey: unknown,
|
||||
mediaMap: Map<string, ArticleMedia>,
|
||||
): string[] {
|
||||
const key =
|
||||
typeof entityKey === "string" || typeof entityKey === "number"
|
||||
? String(entityKey)
|
||||
@@ -182,19 +210,19 @@ function getMediaMarkdown(entityMap: Map<string, JsonObject>, entityKey: unknown
|
||||
|
||||
const data = isRecord(entity.data) ? entity.data : {};
|
||||
const mediaItems = Array.isArray(data.mediaItems) ? data.mediaItems : [];
|
||||
const urls: string[] = [];
|
||||
const media: ArticleMedia[] = [];
|
||||
|
||||
for (const item of mediaItems) {
|
||||
if (!isRecord(item) || typeof item.mediaId !== "string") {
|
||||
continue;
|
||||
}
|
||||
const url = mediaMap.get(item.mediaId);
|
||||
if (url && !urls.includes(url)) {
|
||||
urls.push(url);
|
||||
const mediaItem = mediaMap.get(item.mediaId);
|
||||
if (mediaItem && !media.some((value) => value.url === mediaItem.url)) {
|
||||
media.push(mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
return urls.map((url) => ``);
|
||||
return media.map((item) => item.kind === "image" ? `` : `[video](${item.url})`);
|
||||
}
|
||||
|
||||
function resolveTweetMarkdown(payloads: unknown[], tweetId: string, pageUrl: string): string | null {
|
||||
@@ -250,7 +278,7 @@ function replaceLinkEntities(text: string, block: JsonObject, entityMap: Map<str
|
||||
function renderAtomicBlock(
|
||||
block: JsonObject,
|
||||
entityMap: Map<string, JsonObject>,
|
||||
mediaMap: Map<string, string>,
|
||||
mediaMap: Map<string, ArticleMedia>,
|
||||
payloads: unknown[],
|
||||
pageUrl: string,
|
||||
): string | null {
|
||||
@@ -293,7 +321,7 @@ function renderAtomicBlock(
|
||||
function renderArticleBlocks(
|
||||
blocks: unknown[],
|
||||
entityMap: Map<string, JsonObject>,
|
||||
mediaMap: Map<string, string>,
|
||||
mediaMap: Map<string, ArticleMedia>,
|
||||
payloads: unknown[],
|
||||
pageUrl: string,
|
||||
): string {
|
||||
@@ -396,7 +424,7 @@ export function extractArticleDocumentFromPayload(
|
||||
const contentState = isRecord(articleResult.content_state) ? articleResult.content_state : {};
|
||||
const blocks = Array.isArray(contentState.blocks) ? contentState.blocks : [];
|
||||
const entityMap = normalizeEntityMap(contentState.entityMap);
|
||||
const mediaMap = buildMediaUrlMap(articleResult);
|
||||
const mediaMap = buildMediaMap(articleResult);
|
||||
const richMarkdown = renderArticleBlocks(blocks, entityMap, mediaMap, payloads, pageUrl);
|
||||
const plainText = typeof articleResult.plain_text === "string" ? articleResult.plain_text.trim() : "";
|
||||
const markdown = richMarkdown || plainText || getTweetText(tweet);
|
||||
|
||||
@@ -251,6 +251,39 @@ export function toHighResXImageUrl(rawUrl: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getVideoVariantBitrate(variant: JsonObject): number {
|
||||
const value = variant.bitrate ?? variant.bit_rate;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function getVideoVariantContentType(variant: JsonObject): string {
|
||||
const value = variant.content_type ?? variant.contentType;
|
||||
return typeof value === "string" ? value.toLowerCase() : "";
|
||||
}
|
||||
|
||||
export function resolveBestXVideoVariantUrl(mediaInfo: unknown): string | undefined {
|
||||
if (!isRecord(mediaInfo)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const variantsSource =
|
||||
Array.isArray(mediaInfo.variants)
|
||||
? mediaInfo.variants
|
||||
: isRecord(mediaInfo.video_info) && Array.isArray(mediaInfo.video_info.variants)
|
||||
? mediaInfo.video_info.variants
|
||||
: [];
|
||||
|
||||
const variants = variantsSource
|
||||
.filter(
|
||||
(variant): variant is JsonObject =>
|
||||
isRecord(variant) && typeof variant.url === "string" && variant.url.length > 0,
|
||||
)
|
||||
.filter((variant) => getVideoVariantContentType(variant) === "video/mp4")
|
||||
.sort((left, right) => getVideoVariantBitrate(right) - getVideoVariantBitrate(left));
|
||||
|
||||
return typeof variants[0]?.url === "string" ? variants[0].url : undefined;
|
||||
}
|
||||
|
||||
export function getTweetMedia(tweet: JsonObject): XMedia[] {
|
||||
const legacy = getLegacy(tweet);
|
||||
const extendedEntities = isRecord(legacy.extended_entities) ? legacy.extended_entities : emptyObject();
|
||||
@@ -268,10 +301,14 @@ export function getTweetMedia(tweet: JsonObject): XMedia[] {
|
||||
alt: typeof value.ext_alt_text === "string" ? value.ext_alt_text : undefined,
|
||||
};
|
||||
}
|
||||
if ((value.type === "video" || value.type === "animated_gif") && typeof value.media_url_https === "string") {
|
||||
if (value.type === "video" || value.type === "animated_gif") {
|
||||
const videoUrl = resolveBestXVideoVariantUrl(value);
|
||||
if (!videoUrl) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: value.type,
|
||||
url: value.media_url_https,
|
||||
url: videoUrl,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user