mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-13 06:19:46 +08:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e63e21786d | |||
| b3a3b632c8 | |||
| 3aafe60463 | |||
| 246e90bf28 | |||
| 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 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.107.0"
|
||||
"version": "2.0.1"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -22,8 +22,9 @@
|
||||
"./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",
|
||||
@@ -32,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
|
||||
@@ -169,3 +173,6 @@ diagram/
|
||||
.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}"
|
||||
|
||||
+219
-1
@@ -2,6 +2,224 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 2.0.1 - 2026-05-24
|
||||
|
||||
### Fixes
|
||||
- `baoyu-post-to-wechat`: convert `newspic` (image-text) `content` from HTML to plain text before sending to the WeChat draft API. The previous behavior passed raw HTML, triggering error 45166 (invalid content) when multiple images pushed the body over WeChat's length limit. The new `htmlToPlainText` helper turns `<br>` and block-closing tags into line breaks, strips remaining tags, decodes named/numeric (decimal and hex) entities, and collapses whitespace (by @Go1dFinger, [#164](https://github.com/JimLiu/baoyu-skills/pull/164), closes [#163](https://github.com/JimLiu/baoyu-skills/issues/163))
|
||||
|
||||
### Credits
|
||||
- `baoyu-post-to-wechat` newspic plain-text fix contributed by @Go1dFinger ([#164](https://github.com/JimLiu/baoyu-skills/pull/164))
|
||||
|
||||
## 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
|
||||
@@ -630,7 +848,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
|
||||
|
||||
|
||||
+221
-3
@@ -2,6 +2,224 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 2.0.1 - 2026-05-24
|
||||
|
||||
### 修复
|
||||
- `baoyu-post-to-wechat`:发布 `newspic`(图文)时,将 `content` 字段从 HTML 转换为纯文本后再提交微信草稿接口。此前直接传 HTML,多图场景下内容长度超出限制会触发错误 45166(invalid content)。新增 `htmlToPlainText` 工具:把 `<br>` 和块级闭合标签转成换行、剥离其余标签、解码命名实体与十进制/十六进制数字实体、合并多余空白 (by @Go1dFinger, [#164](https://github.com/JimLiu/baoyu-skills/pull/164), 关联 [#163](https://github.com/JimLiu/baoyu-skills/issues/163))
|
||||
|
||||
### 致谢
|
||||
- `baoyu-post-to-wechat` 的 newspic 纯文本修复由 @Go1dFinger 贡献([#164](https://github.com/JimLiu/baoyu-skills/pull/164))
|
||||
|
||||
## 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
|
||||
|
||||
### 新功能
|
||||
@@ -586,7 +804,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`,提升上传可靠性
|
||||
|
||||
### 修复
|
||||
@@ -1049,7 +1267,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`(氛围强度级别)。
|
||||
@@ -1069,7 +1287,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.107.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**
|
||||
@@ -579,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
|
||||
@@ -612,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):
|
||||
|
||||
@@ -631,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
|
||||
@@ -712,67 +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-imagine --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
|
||||
/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 (default: google/nano-banana-2)
|
||||
/baoyu-imagine --prompt "A cat" --image cat.png --provider replicate
|
||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider replicate
|
||||
|
||||
# Replicate Seedream 4.5
|
||||
/baoyu-imagine --prompt "A studio portrait" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
|
||||
/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-imagine --prompt "A concept frame" --image frame.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
|
||||
/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**:
|
||||
@@ -786,7 +800,7 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `--provider` | `google`, `openai`, `azure`, `openrouter`, `dashscope`, `zai`, `minimax`, `jimeng`, `seedream`, or `replicate` |
|
||||
| `--model`, `-m` | Model ID or deployment name. Azure uses deployment name; OpenRouter uses full model IDs; Z.AI uses `glm-image`; MiniMax uses `image-01` / `image-01-live` |
|
||||
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||
| `--size` | Size (e.g., `1024x1024`) |
|
||||
| `--size` | Size (e.g., `1024x1024`; `gpt-image-2` accepts valid custom sizes up to 3840px max edge) |
|
||||
| `--quality` | `normal` or `2k` (default: `2k`) |
|
||||
| `--imageSize` | `1K`, `2K`, or `4K` for Google/OpenRouter |
|
||||
| `--imageApiDialect` | `openai-native` or `ratio-metadata` for OpenAI-compatible gateways |
|
||||
@@ -810,9 +824,9 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `JIMENG_ACCESS_KEY_ID` | Jimeng Volcengine access key | - |
|
||||
| `JIMENG_SECRET_ACCESS_KEY` | Jimeng Volcengine secret key | - |
|
||||
| `ARK_API_KEY` | Seedream Volcengine ARK API key | - |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-2` |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Azure default deployment name | - |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | Backward-compatible Azure deployment/model alias | `gpt-image-1.5` |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | Backward-compatible Azure deployment/model alias | `gpt-image-2` |
|
||||
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model | `google/gemini-3.1-flash-image-preview` |
|
||||
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
|
||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `qwen-image-2.0-pro` |
|
||||
@@ -834,7 +848,7 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
|
||||
| `ZAI_BASE_URL` | Custom Z.AI endpoint | `https://api.z.ai/api/paas/v4` |
|
||||
| `BIGMODEL_BASE_URL` | Backward-compatible alias for Z.AI endpoint | - |
|
||||
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint | `https://api.minimax.io` |
|
||||
| `MINIMAX_BASE_URL` | Custom MiniMax endpoint | `https://api.minimaxi.com` |
|
||||
| `REPLICATE_BASE_URL` | Custom Replicate endpoint | - |
|
||||
| `JIMENG_BASE_URL` | Custom Jimeng endpoint | `https://visual.volcengineapi.com` |
|
||||
| `JIMENG_REGION` | Jimeng region | `cn-north-1` |
|
||||
@@ -851,7 +865,7 @@ AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, Da
|
||||
- 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-imagine` 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 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).
|
||||
|
||||
@@ -1112,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)
|
||||
@@ -1132,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
|
||||
@@ -1167,7 +1248,7 @@ ZAI_IMAGE_MODEL=glm-image
|
||||
# MiniMax
|
||||
MINIMAX_API_KEY=xxx
|
||||
MINIMAX_IMAGE_MODEL=image-01
|
||||
# MINIMAX_BASE_URL=https://api.minimax.io
|
||||
# MINIMAX_BASE_URL=https://api.minimaxi.com
|
||||
|
||||
# Replicate
|
||||
REPLICATE_API_TOKEN=r8_xxx
|
||||
|
||||
+109
-37
@@ -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**
|
||||
@@ -579,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
|
||||
@@ -612,8 +614,9 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
| 方式 | 速度 | 要求 |
|
||||
|------|------|------|
|
||||
| API(推荐) | 快 | API 凭证 |
|
||||
| API(推荐) | 快 | API 凭证(本机 IP 在公众号白名单内) |
|
||||
| 浏览器 | 慢 | Chrome,登录会话 |
|
||||
| 远程 API | 快 | API 凭证 + 一台 IP 在公众号白名单内、可 SSH 登录的服务器 |
|
||||
|
||||
**API 配置**(更快的发布方式):
|
||||
|
||||
@@ -631,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
|
||||
@@ -712,67 +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-imagine --prompt "一张带清晰中文标题的科技海报" --image out.png --provider zai
|
||||
/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(默认:google/nano-banana-2)
|
||||
/baoyu-imagine --prompt "一只猫" --image cat.png --provider replicate
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider replicate
|
||||
|
||||
# Replicate Seedream 4.5
|
||||
/baoyu-imagine --prompt "一张影棚人像" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
|
||||
/baoyu-image-gen --prompt "一张影棚人像" --image portrait.png --provider replicate --model bytedance/seedream-4.5 --ar 3:2
|
||||
|
||||
# Replicate Wan 2.7 Image Pro
|
||||
/baoyu-imagine --prompt "一张概念分镜" --image frame.png --provider replicate --model wan-video/wan-2.7-image-pro --size 2048x1152
|
||||
/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
|
||||
```
|
||||
|
||||
**选项**:
|
||||
@@ -786,7 +791,7 @@ AI 驱动的生成后端。
|
||||
| `--provider` | `google`、`openai`、`azure`、`openrouter`、`dashscope`、`zai`、`minimax`、`jimeng`、`seedream` 或 `replicate` |
|
||||
| `--model`, `-m` | 模型 ID 或部署名。Azure 使用部署名;OpenRouter 使用完整模型 ID;Z.AI 使用 `glm-image`;MiniMax 使用 `image-01` / `image-01-live` |
|
||||
| `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) |
|
||||
| `--size` | 尺寸(如 `1024x1024`) |
|
||||
| `--size` | 尺寸(如 `1024x1024`;`gpt-image-2` 支持最长边不超过 3840px 的有效自定义尺寸) |
|
||||
| `--quality` | `normal` 或 `2k`(默认:`2k`) |
|
||||
| `--imageSize` | Google/OpenRouter 使用的 `1K`、`2K`、`4K` |
|
||||
| `--imageApiDialect` | OpenAI 兼容网关的图像 API 方言(`openai-native` 或 `ratio-metadata`) |
|
||||
@@ -810,9 +815,9 @@ AI 驱动的生成后端。
|
||||
| `JIMENG_ACCESS_KEY_ID` | 即梦火山引擎 Access Key | - |
|
||||
| `JIMENG_SECRET_ACCESS_KEY` | 即梦火山引擎 Secret Key | - |
|
||||
| `ARK_API_KEY` | 豆包火山引擎 ARK API 密钥 | - |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-2` |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Azure 默认部署名 | - |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | 兼容旧配置的 Azure 部署/模型别名 | `gpt-image-1.5` |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | 兼容旧配置的 Azure 部署/模型别名 | `gpt-image-2` |
|
||||
| `OPENROUTER_IMAGE_MODEL` | OpenRouter 模型 | `google/gemini-3.1-flash-image-preview` |
|
||||
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
|
||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `qwen-image-2.0-pro` |
|
||||
@@ -834,7 +839,7 @@ AI 驱动的生成后端。
|
||||
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
|
||||
| `ZAI_BASE_URL` | 自定义 Z.AI 端点 | `https://api.z.ai/api/paas/v4` |
|
||||
| `BIGMODEL_BASE_URL` | Z.AI 端点向后兼容别名 | - |
|
||||
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimax.io` |
|
||||
| `MINIMAX_BASE_URL` | 自定义 MiniMax 端点 | `https://api.minimaxi.com` |
|
||||
| `REPLICATE_BASE_URL` | 自定义 Replicate 端点 | - |
|
||||
| `JIMENG_BASE_URL` | 自定义即梦端点 | `https://visual.volcengineapi.com` |
|
||||
| `JIMENG_REGION` | 即梦区域 | `cn-north-1` |
|
||||
@@ -851,7 +856,7 @@ AI 驱动的生成后端。
|
||||
- MiniMax 参考图会走 `subject_reference`,当前能力更偏角色 / 人像一致性。
|
||||
- 即梦不支持参考图。
|
||||
- 豆包参考图能力仅适用于 Seedream 5.0 / 4.5 / 4.0,不适用于 Seedream 3.0。
|
||||
- Replicate 默认模型改为 `google/nano-banana-2`。`baoyu-imagine` 目前只对 `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 默认模型改为 `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` 会先在本地换算成具体尺寸)。
|
||||
|
||||
@@ -1112,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`(用户级)
|
||||
@@ -1132,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
|
||||
@@ -1167,7 +1239,7 @@ ZAI_IMAGE_MODEL=glm-image
|
||||
# MiniMax
|
||||
MINIMAX_API_KEY=xxx
|
||||
MINIMAX_IMAGE_MODEL=image-01
|
||||
# MINIMAX_BASE_URL=https://api.minimax.io
|
||||
# MINIMAX_BASE_URL=https://api.minimaxi.com
|
||||
|
||||
# Replicate
|
||||
REPLICATE_API_TOKEN=r8_xxx
|
||||
|
||||
@@ -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
+5
-1
@@ -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",
|
||||
@@ -20,5 +21,8 @@
|
||||
"pdf-lib": "^1.17.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