Compare commits

...

23 Commits

Author SHA1 Message Date
Jim Liu 宝玉 7a193481d4 chore: release v1.4.0 2026-01-18 23:46:07 -06:00
Jim Liu 宝玉 aea27950ad chore: release v1.3.0 2026-01-18 22:16:24 -06:00
Jim Liu 宝玉 76ddcf134e chore: release v1.2.0 2026-01-18 20:42:40 -06:00
Jim Liu 宝玉 5a0feba96e docs: update installation instructions and add available plugins section 2026-01-18 20:28:36 -06:00
Jim Liu 宝玉 97bc68efd8 chore: release v1.1.0 2026-01-18 20:11:49 -06:00
Jim Liu 宝玉 464edf0656 chore: release v1.0.1 2026-01-18 19:15:34 -06:00
Jim Liu 宝玉 43c2589a38 Refactor code structure for improved readability and maintainability 2026-01-18 18:18:54 -06:00
Jim Liu 宝玉 4c82884722 chore: release v1.0.0
- Add baoyu-danger-x-to-markdown skill to marketplace
- Update README files with new skill documentation
- Bump version to 1.0.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 18:00:58 -06:00
Jim Liu 宝玉 56d0485412 chore: rename baoyu-gemini-web to baoyu-danger-gemini-web 2026-01-18 17:58:12 -06:00
Jim Liu 宝玉 4998eaf8c2 feat: add baoyu-danger-x-to-markdown skill
Ignore generated x-to-markdown output directory.
2026-01-18 17:42:25 -06:00
Jim Liu 宝玉 5993b6969d chore: release v0.11.0 2026-01-18 09:53:48 -06:00
Jim Liu 宝玉 dc0201b63f chore: release v0.10.0 2026-01-18 00:01:06 -06:00
Jim Liu 宝玉 3811512750 docs: add Customization section to README files 2026-01-17 22:12:01 -06:00
Jim Liu 宝玉 9a9f6a42cd chore: release v0.9.0 2026-01-17 22:02:40 -06:00
Jim Liu 宝玉 3b13b25f1a chore: release v0.8.2 2026-01-17 19:00:27 -06:00
Jim Liu 宝玉 688d1760ed chore: release v0.8.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 16:44:50 -06:00
Jim Liu 宝玉 a701be873b update Image Generation Guidelines 2026-01-17 16:24:23 -06:00
Jim Liu 宝玉 1df5e4974d feat: add analysis framework for Xiaohongshu content 2026-01-17 15:52:00 -06:00
Jim Liu 宝玉 2677e730b9 Update release-skills to update readme 2026-01-17 15:11:50 -06:00
Jim Liu 宝玉 080f2eff48 chore: release v0.7.0
- baoyu-comic: adds --aspect (3:4, 4:3, 16:9) and --lang options; multi-variant storyboard workflow
- baoyu-comic/baoyu-slide-deck: adds analysis-framework and template references
- Multiple skills: restructured SKILL.md, moved details to references/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 15:03:43 -06:00
Jim Liu 宝玉 bb4f0dc52c feat: add PDF export for slide-deck and comic skills 2026-01-17 12:08:08 -06:00
Jim Liu 宝玉 c731faea8f feat: update slide deck 2026-01-17 04:28:16 -06:00
Jim Liu 宝玉 fa155da15d fix: update version to 0.5.3 and enhance placeholder selection logic in publishArticle function 2026-01-17 01:29:10 -06:00
170 changed files with 11817 additions and 3178 deletions
+21 -3
View File
@@ -6,16 +6,15 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "0.5.2"
"version": "1.4.0"
},
"plugins": [
{
"name": "content-skills",
"description": "Skills shared by Baoyu for improving daily work efficiency",
"description": "Content generation and publishing skills",
"source": "./",
"strict": false,
"skills": [
"./skills/baoyu-gemini-web",
"./skills/baoyu-xhs-images",
"./skills/baoyu-post-to-x",
"./skills/baoyu-post-to-wechat",
@@ -24,6 +23,25 @@
"./skills/baoyu-slide-deck",
"./skills/baoyu-comic"
]
},
{
"name": "ai-generation-skills",
"description": "AI-powered generation backends",
"source": "./",
"strict": false,
"skills": [
"./skills/baoyu-danger-gemini-web"
]
},
{
"name": "utility-skills",
"description": "Utility tools for content processing",
"source": "./",
"strict": false,
"skills": [
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-compress-image"
]
}
]
}
+203
View File
@@ -0,0 +1,203 @@
---
name: release-skills
description: Release workflow for baoyu-skills plugin. This skill should be used when the user wants to create a new release version. It analyzes changes since the last version tag, updates changelogs (EN/CN), bumps the version in marketplace.json, commits changes, and creates a version tag. Supports dry-run mode and breaking change detection.
---
# Release Skills
Automate the release process for baoyu-skills plugin: analyze changes, update changelogs, bump version, commit, and tag.
## When to Use
Trigger this skill when user requests:
- "release", "发布", "create release", "new version"
- "bump version", "update version"
- "prepare release"
## Workflow
### Step 1: Analyze Changes Since Last Tag
```bash
# Get the latest version tag
LAST_TAG=$(git tag --sort=-v:refname | head -1)
# Show changes since last tag
git log ${LAST_TAG}..HEAD --oneline
git diff ${LAST_TAG}..HEAD --stat
```
Categorize changes by type based on commit messages and file changes:
| Type | Prefix | Description |
|------|--------|-------------|
| feat | `feat:` | New features, new skills |
| fix | `fix:` | Bug fixes |
| docs | `docs:` | Documentation only |
| refactor | `refactor:` | Code refactoring |
| style | `style:` | Formatting, styling |
| chore | `chore:` | Build, tooling, maintenance |
**Breaking Change Detection**: If changes include:
- Removed skills or scripts
- Changed API/interfaces
- Renamed public functions/options
Warn user: "Breaking changes detected. Consider major version bump (--major flag)."
### Step 2: Determine Version Bump
Current version location: `.claude-plugin/marketplace.json``metadata.version`
Version rules:
- **Patch** (0.6.1 → 0.6.2): Bug fixes, docs updates, minor improvements
- **Minor** (0.6.x → 0.7.0): New features, new skills, significant enhancements
- **Major** (0.x → 1.0): Breaking changes, only when user explicitly requests with `--major`
Default behavior:
- If changes include `feat:` or new skills → Minor bump
- Otherwise → Patch bump
### Step 3: Check and Update README
Before updating changelogs, check if README files need updates based on changes:
**When to update README**:
- New skills added → Add to skill list
- Skills removed → Remove from skill list
- Skill renamed → Update references
- New features affecting usage → Update usage section
- Breaking changes → Update migration notes
**Files to sync**:
- `README.md` (English)
- `README.zh.md` (Chinese)
If changes include new skills or significant feature changes, update both README files to reflect the new capabilities. Keep both files in sync with the same structure and information.
### Step 4: Update Changelogs
Files to update:
- `CHANGELOG.md` (English)
- `CHANGELOG.zh.md` (Chinese)
Format (insert after header, before previous version):
```markdown
## {NEW_VERSION} - {YYYY-MM-DD}
### Features
- `skill-name`: description of new feature
### Fixes
- `skill-name`: description of fix
### Documentation
- description of docs changes
### Other
- description of other changes
```
Only include sections that have changes. Omit empty sections.
For Chinese changelog, translate the content maintaining the same structure.
### Step 5: Update marketplace.json
Update `.claude-plugin/marketplace.json`:
```json
{
"metadata": {
"version": "{NEW_VERSION}"
}
}
```
### Step 6: Commit Changes
```bash
git add README.md README.zh.md CHANGELOG.md CHANGELOG.zh.md .claude-plugin/marketplace.json
git commit -m "chore: release v{NEW_VERSION}"
```
**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.
### Step 7: Create Version Tag
```bash
git tag v{NEW_VERSION}
```
**Important**: Do NOT push to remote. User will push manually when ready.
## Options
| Flag | Description |
|------|-------------|
| `--dry-run` | Preview changes without executing. Show what would be updated. |
| `--major` | Force major version bump (0.x → 1.0 or 1.x → 2.0) |
| `--minor` | Force minor version bump |
| `--patch` | Force patch version bump |
| `--pre <tag>` | (Reserved) Create pre-release version, e.g., `--pre beta``0.7.0-beta.1` |
## Dry-Run Mode
When `--dry-run` is specified:
1. Show all changes since last tag
2. Show proposed version bump (current → new)
3. Show draft changelog entries (EN and CN)
4. Show files that would be modified
5. Do NOT make any actual changes
Output format:
```
=== DRY RUN MODE ===
Last tag: v0.6.1
Proposed version: v0.7.0
Changes detected:
- feat: new skill baoyu-foo added
- fix: baoyu-bar timeout issue
- docs: updated README
Changelog preview (EN):
## 0.7.0 - 2026-01-17
### Features
- `baoyu-foo`: new skill for ...
### Fixes
- `baoyu-bar`: fixed timeout issue
README updates needed: Yes/No
(If yes, show proposed changes)
Files to modify:
- README.md (if updates needed)
- README.zh.md (if updates needed)
- CHANGELOG.md
- CHANGELOG.zh.md
- .claude-plugin/marketplace.json
No changes made. Run without --dry-run to execute.
```
## Example Usage
```
/release-skills # Auto-detect version bump
/release-skills --dry-run # Preview only
/release-skills --minor # Force minor bump
/release-skills --major # Force major bump (with confirmation)
```
## Post-Release Reminder
After successful release, remind user:
```
Release v{NEW_VERSION} created locally.
To publish:
git push origin main
git push origin v{NEW_VERSION}
```
+4
View File
@@ -140,3 +140,7 @@ vite.config.ts.timestamp-*
tests-data/
.DS_Store
# Skill extensions (user customization)
.baoyu-skills/
x-to-markdown/
+197
View File
@@ -0,0 +1,197 @@
# Changelog
English | [中文](./CHANGELOG.zh.md)
## 1.4.0 - 2026-01-18
### Features
- `baoyu-cover-image`: expands style library from 8 to 19 styles with 12 new additions—`blueprint`, `bold-editorial`, `chalkboard`, `dark-atmospheric`, `editorial-infographic`, `fantasy-animation`, `intuition-machine`, `notion`, `pixel-art`, `sketch-notes`, `vector-illustration`, `vintage`, `watercolor`.
- `baoyu-slide-deck`: adds `chalkboard` style—black chalkboard background with colorful chalk drawings for education and tutorials.
### Breaking Changes
- `baoyu-cover-image`: removes `tech` style (use `blueprint` or `editorial-infographic` for technical content).
### Documentation
- `README.md`, `README.zh.md`: updates style preview screenshots for cover-image and slide-deck.
## 1.3.0 - 2026-01-18
### Features
- `baoyu-comic`: adds `wuxia` style—Hong Kong martial arts comic style with ink brush strokes, dynamic combat poses, and qi energy effects. Best for wuxia/xianxia and Chinese historical fiction.
- `baoyu-comic`: adds style and layout preview screenshots for all 8 styles and 6 layouts in README.
### Refactor
- `baoyu-comic`: removes `tech` style (replaced by `ohmsha` for technical content).
## 1.2.0 - 2026-01-18
### Features
- Session-independent output directories: each generation session creates a new directory (`<skill-suffix>/<topic-slug>/`), even for the same source file. Conflicts resolved by appending timestamp.
- Multi-source file support: source files now saved as `source-{slug}.{ext}`, supporting multiple inputs (text, images, files from conversation).
### Documentation
- `CLAUDE.md`: updates Output Path Convention with new session-independent directory structure and multi-source file naming.
- Multiple skills: updates file management sections to reflect new directory and source file conventions.
- `baoyu-slide-deck`, `baoyu-article-illustrator`, `baoyu-cover-image`, `baoyu-xhs-images`, `baoyu-comic`
## 1.1.0 - 2026-01-18
### Features
- `baoyu-compress-image`: new utility skill for cross-platform image compression. Converts to WebP by default with PNG-to-PNG support. Uses system tools (sips, cwebp, ImageMagick) with Sharp fallback.
### Refactor
- Marketplace structure: reorganizes plugins into three categories—`content-skills`, `ai-generation-skills`, and `utility-skills`—for better organization.
### Documentation
- `CLAUDE.md`, `README.md`, `README.zh.md`: updates skill architecture documentation to reflect the new three-category structure.
## 1.0.1 - 2026-01-18
### Refactor
- Code structure improvements for better readability and maintainability.
- `baoyu-slide-deck`: unified style reference file formats.
### Other
- Screenshots: converted from PNG to WebP format for smaller file sizes; added screenshots for new styles.
## 1.0.0 - 2026-01-18
### Features
- `baoyu-danger-x-to-markdown`: new skill to convert X/Twitter posts and threads to Markdown format.
### Breaking Changes
- `baoyu-gemini-web` renamed to `baoyu-danger-gemini-web` to indicate potential risks of using reverse-engineered APIs.
## 0.11.0 - 2026-01-18
### Features
- `baoyu-danger-gemini-web`: adds disclaimer consent check flow—requires user acceptance before first use, with persistent consent storage per platform.
## 0.10.0 - 2026-01-18
### Features
- `baoyu-slide-deck`: expands style library from 10 to 15 styles with 8 new additions—`dark-atmospheric`, `editorial-infographic`, `fantasy-animation`, `intuition-machine`, `pixel-art`, `scientific`, `vintage`, `watercolor`.
### Breaking Changes
- `baoyu-slide-deck`: removes 3 styles (`playful`, `storytelling`, `warm`); changes default style from `notion` to `blueprint`.
## 0.9.0 - 2026-01-17
### Features
- Extension support: all skills now support customization via `EXTEND.md` files. Check `.baoyu-skills/<skill-name>/EXTEND.md` (project) or `~/.baoyu-skills/<skill-name>/EXTEND.md` (user) for custom styles and configurations.
### Other
- `.gitignore`: adds `.baoyu-skills/` directory for user extension files.
## 0.8.2 - 2026-01-17
### Refactor
- `baoyu-danger-gemini-web`: reorganizes script architecture—moves modular files into `gemini-webapi/` subdirectory and updates SKILL.md with `${SKILL_DIR}` path references.
## 0.8.1 - 2026-01-17
### Refactor
- `baoyu-danger-gemini-web`: refactors script architecture—consolidates 10 separate files into a structured `gemini-webapi/` module (TypeScript port of gemini_webapi Python library).
## 0.8.0 - 2026-01-17
### Features
- `baoyu-xhs-images`: adds content analysis framework (`analysis-framework.md`, `outline-template.md`) for structured content breakdown and outline generation.
### Documentation
- `CLAUDE.md`: adds Output Path Convention (directory structure, backup rules) and Image Naming Convention (format, slug rules) to standardize image generation outputs.
- Multiple skills: updates file management conventions to use unified directory structure (`[source-name-no-ext]/<skill-suffix>/`).
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-slide-deck`, `baoyu-xhs-images`
## 0.7.0 - 2026-01-17
### Features
- `baoyu-comic`: adds `--aspect` (3:4, 4:3, 16:9) and `--lang` options; introduces multi-variant storyboard workflow (chronological, thematic, character-centric) with user selection.
### Enhancements
- `baoyu-comic`: adds `analysis-framework.md` and `storyboard-template.md` for structured content analysis and variant generation.
- `baoyu-slide-deck`: adds `analysis-framework.md`, `content-rules.md`, `modification-guide.md`, and `outline-template.md` references for improved outline quality.
- `baoyu-article-illustrator`, `baoyu-cover-image`, `baoyu-xhs-images`: enhanced SKILL.md documentation with clearer workflows.
### Documentation
- Multiple skills: restructured SKILL.md files—moved detailed content to `references/` directory for maintainability.
- `baoyu-slide-deck`: simplified SKILL.md, consolidated style descriptions.
## 0.6.1 - 2026-01-17
- `baoyu-slide-deck`: adds `scripts/merge-to-pdf.ts` to export generated slides into a single PDF; docs updated with pptx/pdf outputs.
- `baoyu-comic`: adds `scripts/merge-to-pdf.ts` to merge cover/pages into a PDF; docs clarify character reference handling (image vs text).
- Docs conventions: adds a “Script Directory” template to `CLAUDE.md`; aligns `baoyu-danger-gemini-web` / `baoyu-slide-deck` / `baoyu-comic` docs to use `${SKILL_DIR}` in commands so agents can run scripts from any install location.
## 0.6.0 - 2026-01-17
- `baoyu-slide-deck`: adds `scripts/merge-to-pptx.ts` to merge slide images into a PPTX and attach `prompts/` content as speaker notes.
- `baoyu-slide-deck`: reshapes/expands the style library (adds `blueprint` / `bold-editorial` / `sketch-notes` / `vector-illustration`, and adjusts/replaces some older styles).
- `baoyu-comic`: adds a `realistic` style reference.
- Docs: refreshes `README.md` / `README.zh.md`.
## 0.5.3 - 2026-01-17
- `baoyu-post-to-x` (X Articles): makes image placeholder replacement more reliable (selection retry + verification; deletes via Backspace and verifies deletion before pasting), reducing mis-insertions/failures.
## 0.5.2 - 2026-01-16
- `baoyu-danger-gemini-web`: adds `--sessionId` (local persisted sessions, plus `--list-sessions`) for multi-turn conversations and consistent multi-image generation.
- `baoyu-danger-gemini-web`: adds `--reference/--ref` for reference images (vision input), plus stronger timeout handling and cookie refresh recovery.
- Docs: `baoyu-xhs-images` / `baoyu-slide-deck` / `baoyu-comic` document session usage (reuse one `sessionId` per set) to improve visual consistency.
## 0.5.1 - 2026-01-16
- `baoyu-comic`: adds creation templates/references (character template, Ohmsha guide, outline template) to speed up “characters → storyboard → generation”.
## 0.5.0 - 2026-01-16
- Adds `baoyu-comic`: a knowledge-comic generator with `style × layout` and a full set of style/layout references for more stable output.
- `baoyu-xhs-images`: moves style/layout details into `references/styles/*` and `references/layouts/*`, and migrates the base prompt into `references/base-prompt.md` for easier maintenance/reuse.
- `baoyu-slide-deck` / `baoyu-cover-image`: similarly split base prompt and style references into `references/`, reducing SKILL.md complexity and making style expansion easier.
- Docs: updates `README.md` / `README.zh.md` skill list and examples.
## 0.4.2 - 2026-01-15
- `baoyu-danger-gemini-web`: updates description to clarify it as the image-generation backend for other skills (e.g. `cover-image`, `xhs-images`, `article-illustrator`).
## 0.4.1 - 2026-01-15
- `baoyu-post-to-x` / `baoyu-post-to-wechat`: adds `scripts/paste-from-clipboard.ts` to send a “real paste” keystroke (Cmd/Ctrl+V), avoiding sites ignoring CDP synthetic events.
- `baoyu-post-to-x`: adds docs for X Articles/regular posts, and switches image upload to prefer real paste (with a CDP fallback).
- `baoyu-post-to-wechat`: docs add script-location guidance and `${SKILL_DIR}` path usage for reliable agent execution.
- Docs: adds `screenshots/update-plugins.png` for the marketplace update flow.
## 0.4.0 - 2026-01-15
- Adds `baoyu-` prefix to skill directories and updates marketplace paths/docs accordingly to reduce naming collisions.
## 0.3.1 - 2026-01-15
- `xhs-images`: upgrades docs to a Style × Layout system (adds `--layout`, auto layout selection, and a `notion` style), with more complete usage examples.
- `article-illustrator` / `cover-image`: docs no longer hard-code `gemini-web`; instead they instruct the agent to pick an available image-generation skill.
- `slide-deck`: docs add the `notion` style and update auto-style mapping.
- Tooling/docs: adds `.DS_Store` to `.gitignore`; refreshes `README.md` / `README.zh.md`.
## 0.3.0 - 2026-01-14
- Adds `post-to-wechat`: Chrome CDP automation for WeChat Official Account posting (image-text + full article), including Markdown → WeChat HTML conversion and multiple themes.
- Adds `CLAUDE.md`: repository structure, running conventions, and “add new skill” guidelines.
- Docs: updates `README.md` / `README.zh.md` install/update/usage instructions.
## 0.2.0 - 2026-01-13
- Adds new skills: `post-to-x` (real Chrome/CDP automation for posts and X Articles), `article-illustrator`, `cover-image`, and `slide-deck`.
- `xhs-images`: adds multi-style support (`--style`) with auto style selection and updates the base prompt (e.g. language follows input, hand-drawn infographic constraints).
- Docs: adds `README.zh.md` and improves `README.md` and `.gitignore`.
## 0.1.1 - 2026-01-13
- Marketplace refactor: introduces `metadata` (including `version`), renames the plugin entry to `content-skills` and explicitly lists installable skills; removes legacy `.claude-plugin/plugin.json`.
- Adds `xhs-images`: Xiaohongshu infographic series generator (outline + per-image prompts).
- `gemini-web`: adds `--promptfiles` to build prompts from multiple files (system/content separation).
- Docs: adds `README.md`.
## 0.1.0 - 2026-01-13
- Initial release: `.claude-plugin/marketplace.json` plus `gemini-web` (text/image generation, browser login + cookie cache).
+196
View File
@@ -0,0 +1,196 @@
# Changelog
[English](./CHANGELOG.md) | 中文
## 1.4.0 - 2026-01-18
### 新功能
- `baoyu-cover-image`:风格库从 8 个扩展至 19 个,新增 12 种风格——`blueprint`(蓝图)、`bold-editorial`(大胆编辑)、`chalkboard`(黑板)、`dark-atmospheric`(暗黑氛围)、`editorial-infographic`(杂志信息图)、`fantasy-animation`(奇幻动画)、`intuition-machine`(技术简报)、`notion`Notion 风格)、`pixel-art`(像素艺术)、`sketch-notes`(手绘笔记)、`vector-illustration`(矢量插画)、`vintage`(复古文献)、`watercolor`(水彩)。
- `baoyu-slide-deck`:新增 `chalkboard`(黑板)风格——黑色黑板背景配彩色粉笔绘画,适合教育和教程内容。
### 破坏性变更
- `baoyu-cover-image`:移除 `tech` 风格(技术内容改用 `blueprint``editorial-infographic` 风格)。
### 文档
- `README.md``README.zh.md`:更新 cover-image 和 slide-deck 风格预览截图。
## 1.3.0 - 2026-01-18
### 新功能
- `baoyu-comic`:新增 `wuxia` 武侠风格——港漫武侠风格,水墨笔触、动态打斗、气功特效。适用于武侠、仙侠、中国历史小说。
- `baoyu-comic`:README 新增风格和布局预览截图(8 种风格 + 6 种布局)。
### 重构
- `baoyu-comic`:移除 `tech` 风格(技术内容改用 `ohmsha` 风格)。
## 1.2.0 - 2026-01-18
### 新功能
- Session 独立输出目录:每次生成创建独立目录(`<skill-suffix>/<topic-slug>/`),即使是同一源文件也会新建目录。目录冲突时追加时间戳。
- 多源文件支持:源文件现以 `source-{slug}.{ext}` 命名,支持多个输入(文本、图片、会话中的文件)。
### 文档
- `CLAUDE.md`:更新 Output Path Convention,采用新的 session 独立目录结构和多源文件命名规范。
- 多个技能:更新文件管理部分,反映新的目录和源文件规范。
- `baoyu-slide-deck``baoyu-article-illustrator``baoyu-cover-image``baoyu-xhs-images``baoyu-comic`
## 1.1.0 - 2026-01-18
### 新功能
- `baoyu-compress-image`:新增跨平台图片压缩技能。默认转换为 WebP 格式,支持 PNG 转 PNG。自动选择系统工具(sips、cwebp、ImageMagick),Sharp 作为兜底方案。
### 重构
- Marketplace 结构重组:将插件分为三大类——`content-skills`(内容技能)、`ai-generation-skills`AI 生成技能)和 `utility-skills`(工具技能),便于管理和发现。
### 文档
- `CLAUDE.md``README.md``README.zh.md`:更新技能架构文档,反映新的三类分组结构。
## 1.0.1 - 2026-01-18
### 重构
- 代码结构优化,提升可读性和可维护性。
- `baoyu-slide-deck`:统一风格参考文件格式。
### 其他
- 截图:从 PNG 转换为 WebP 格式,减小文件体积;新增新风格的截图。
## 1.0.0 - 2026-01-18
### 新功能
- `baoyu-danger-x-to-markdown`:新增技能,将 X/Twitter 帖子和线程转换为 Markdown 格式。
### 破坏性变更
- `baoyu-gemini-web` 重命名为 `baoyu-danger-gemini-web`,以提示使用逆向工程 API 的潜在风险。
## 0.11.0 - 2026-01-18
### 新功能
- `baoyu-danger-gemini-web`:新增 Disclaimer 同意检查流程——首次使用前需用户确认接受,同意状态按平台持久化存储。
## 0.10.0 - 2026-01-18
### 新功能
- `baoyu-slide-deck`:风格库从 10 个扩展至 15 个,新增 8 种风格——`dark-atmospheric`(暗黑氛围)、`editorial-infographic`(杂志信息图)、`fantasy-animation`(奇幻动画)、`intuition-machine`(技术简报)、`pixel-art`(像素艺术)、`scientific`(科学图解)、`vintage`(复古文献)、`watercolor`(水彩手绘)。
### 破坏性变更
- `baoyu-slide-deck`:移除 3 种风格(`playful``storytelling``warm`);默认风格从 `notion` 改为 `blueprint`
## 0.9.0 - 2026-01-17
### 新功能
- 扩展支持:所有技能现支持通过 `EXTEND.md` 文件自定义。检查 `.baoyu-skills/<skill-name>/EXTEND.md`(项目级)或 `~/.baoyu-skills/<skill-name>/EXTEND.md`(用户级)配置自定义样式与设置。
### 其他
- `.gitignore`:添加 `.baoyu-skills/` 目录忽略,存放用户扩展文件。
## 0.8.2 - 2026-01-17
### 重构
- `baoyu-danger-gemini-web`:重组脚本架构——将模块文件移至 `gemini-webapi/` 子目录,并更新 SKILL.md 使用 `${SKILL_DIR}` 路径引用。
## 0.8.1 - 2026-01-17
### 重构
- `baoyu-danger-gemini-web`:重构脚本架构——将 10 个分散的脚本文件整合为结构化的 `gemini-webapi/` 模块(gemini_webapi Python 库的 TypeScript 移植版)。
## 0.8.0 - 2026-01-17
### 新功能
- `baoyu-xhs-images`:新增内容分析框架(`analysis-framework.md``outline-template.md`),提供结构化内容拆解与大纲生成方案。
### 文档
- `CLAUDE.md`:新增 Output Path Convention(目录结构、备份规则)和 Image Naming Convention(文件命名格式、slug 规则),统一图片生成输出规范。
- 多个技能:更新文件管理规范,采用统一目录结构(`[source-name-no-ext]/<skill-suffix>/`)。
- `baoyu-article-illustrator``baoyu-comic``baoyu-cover-image``baoyu-slide-deck``baoyu-xhs-images`
## 0.7.0 - 2026-01-17
### 新功能
- `baoyu-comic`:新增 `--aspect`3:4、4:3、16:9)和 `--lang` 选项;引入多变体分镜工作流(时间线、主题、人物视角),支持用户选择最佳方案。
### 增强
- `baoyu-comic`:新增 `analysis-framework.md``storyboard-template.md`,提供结构化内容分析与变体生成框架。
- `baoyu-slide-deck`:新增 `analysis-framework.md``content-rules.md``modification-guide.md``outline-template.md` 参考文档,提升大纲质量。
- `baoyu-article-illustrator``baoyu-cover-image``baoyu-xhs-images`:SKILL.md 文档增强,工作流程更清晰。
### 文档
- 多个技能:重构 SKILL.md 结构,将详细内容移至 `references/` 目录,便于维护。
- `baoyu-slide-deck`:精简 SKILL.md,整合风格描述。
## 0.6.1 - 2026-01-17
- `baoyu-slide-deck`:新增 `scripts/merge-to-pdf.ts`,可将生成的 slide 图片一键合并为 PDF;文档补充导出步骤与产物命名(pptx/pdf)。
- `baoyu-comic`:新增 `scripts/merge-to-pdf.ts`,将封面/分页图片合并为 PDF;补充角色参考(图片/文本)处理说明。
- 文档规范:在 `CLAUDE.md` 中补充“Script Directory”模板;`baoyu-danger-gemini-web` / `baoyu-slide-deck` / `baoyu-comic` 文档统一用 `${SKILL_DIR}` 引用脚本路径,方便 agent 在任意安装目录运行。
## 0.6.0 - 2026-01-17
- `baoyu-slide-deck`:新增 `scripts/merge-to-pptx.ts`,将生成的 slide 图片合并为 PPTX,并可把 `prompts/` 写入 speaker notes。
- `baoyu-slide-deck`:风格库重组与扩充(新增 `blueprint` / `bold-editorial` / `sketch-notes` / `vector-illustration`,并调整/替换部分旧风格定义)。
- `baoyu-comic`:新增 `realistic` 风格参考文件。
- 文档:README / README.zh 同步更新技能说明与用法示例。
## 0.5.3 - 2026-01-17
- `baoyu-post-to-x`(X Articles):插图占位符替换更稳定——选中占位符增加重试与校验,改用 Backspace 删除并确认删除后再粘贴图片,降低插图错位/替换失败概率。
## 0.5.2 - 2026-01-16
- `baoyu-danger-gemini-web`:新增 `--sessionId`(本地持久化会话,支持 `--list-sessions`),用于多轮对话/多图生成保持上下文一致。
- `baoyu-danger-gemini-web`:新增 `--reference/--ref` 传入参考图片(vision 输入),并增强超时与 cookie 失效自动恢复逻辑。
- `baoyu-xhs-images` / `baoyu-slide-deck` / `baoyu-comic`:文档补充 session 约定(整套图使用同一 `sessionId`,增强风格一致性)。
## 0.5.1 - 2026-01-16
- `baoyu-comic`:补齐创作模板与参考(角色模板、Ohmsha 教学漫画指南、大纲模板),更适合从“设定 → 分镜 → 生成”快速落地。
## 0.5.0 - 2026-01-16
- 新增 `baoyu-comic`:知识漫画生成器,支持 `style × layout` 组合,并提供风格/布局参考文件用于稳定出图。
- `baoyu-xhs-images`:将 Style/Layout 的细节从 SKILL.md 拆分到 `references/styles/*``references/layouts/*`,并将基础提示词迁移到 `references/base-prompt.md`,便于维护和复用。
- `baoyu-slide-deck` / `baoyu-cover-image`:同样将基础提示词与风格拆分到 `references/`,降低 SKILL.md 复杂度,便于扩展更多风格。
- 文档:README / README.zh 更新技能清单与用法示例。
## 0.4.2 - 2026-01-15
- `baoyu-danger-gemini-web`:描述信息更新,明确其作为 `cover-image` / `xhs-images` / `article-illustrator` 等技能的图片生成后端。
## 0.4.1 - 2026-01-15
- `baoyu-post-to-x` / `baoyu-post-to-wechat`:新增 `scripts/paste-from-clipboard.ts`,通过系统级 Cmd/Ctrl+V 发送“真实粘贴”按键,规避 CDP 合成事件在站点侧被忽略的问题。
- `baoyu-post-to-x`:补充 X Articles/普通推文的操作文档(`references/articles.md``references/regular-posts.md`),并将发图流程改为优先使用“真实粘贴”(保留 CDP 兜底)。
- `baoyu-post-to-wechat`:文档补充脚本目录说明与 `${SKILL_DIR}` 路径写法,便于 agent 可靠定位脚本。
- 文档:新增插件更新流程截图 `screenshots/update-plugins.png`
## 0.4.0 - 2026-01-15
- 技能命名统一加 `baoyu-` 前缀:目录结构、marketplace 清单与文档示例命令同步更新,减少与其它插件技能的命名冲突。
## 0.3.1 - 2026-01-15
- `xhs-images`:升级为 Style × Layout 二维系统(新增 `--layout`、自动布局选择与 Notion 风格),文档示例更完整。
- `article-illustrator` / `slide-deck` / `cover-image`:文档改为“选择可用的图片生成技能”而非强绑定 `gemini-web`,并补充 Notion 风格相关说明。
- 工程化:`.gitignore` 增加 `.DS_Store` 忽略;README / README.zh 同步调整。
## 0.3.0 - 2026-01-14
- 新增 `post-to-wechat`:基于 Chrome CDP 自动化发布公众号图文/文章,包含 Markdown → 微信 HTML 转换与多主题样式支持。
- 新增 `CLAUDE.md`:补充仓库结构、运行方式与添加新技能的约定,方便协作与二次开发。
- 文档:README / README.zh 更新安装、更新与使用说明。
## 0.2.0 - 2026-01-13
- 新增技能:`post-to-x`(真实 Chrome/CDP 自动化发布推文与 X Articles)、`article-illustrator`(文章智能插图规划)、`cover-image`(文章封面图生成)、`slide-deck`(幻灯片大纲与图片生成)。
- `xhs-images`:新增 `--style` 多风格与自动风格选择,并更新基础提示词(例如语言随内容、强调手绘信息图等)。
- 文档:新增 `README.zh.md`,并完善 README 与 `.gitignore`
## 0.1.1 - 2026-01-13
- marketplace 结构重构:引入 `metadata`(含 `version`),插件名调整为 `content-skills` 并显式列出可安装 skills;移除旧 `.claude-plugin/plugin.json`
- 新增 `xhs-images`:小红书信息图系列生成技能(拆解内容、生成 outline 与提示词)。
- `gemini-web`:新增 `--promptfiles`,支持从多个文件拼接 prompt(便于 system/content 分离)。
- 文档:新增 `README.md`
## 0.1.0 - 2026-01-13
- 初始发布:提供 `.claude-plugin/marketplace.json``gemini-web`(文本/图片生成、cookie 登录与缓存流程)。
+183 -13
View File
@@ -8,17 +8,34 @@ Claude Code marketplace plugin providing AI-powered content generation skills. S
## Architecture
Skills are organized into three plugin categories in `marketplace.json`:
```
skills/
├── baoyu-gemini-web/ # Core: Gemini API wrapper (text + image gen)
├── baoyu-xhs-images/ # Xiaohongshu infographic series (1-10 images)
├── baoyu-cover-image/ # Article cover images (2.35:1 aspect)
├── baoyu-slide-deck/ # Presentation slides with outlines
├── baoyu-article-illustrator/ # Smart illustration placement
├── baoyu-post-to-x/ # X/Twitter posting automation
── baoyu-post-to-wechat/ # WeChat Official Account posting
├── [content-skills] # Content generation and publishing
├── baoyu-xhs-images/ # Xiaohongshu infographic series (1-10 images)
├── baoyu-cover-image/ # Article cover images (2.35:1 aspect)
├── baoyu-slide-deck/ # Presentation slides with outlines
├── baoyu-article-illustrator/ # Smart illustration placement
├── baoyu-comic/ # Knowledge comics (Logicomix/Ohmsha style)
│ ├── baoyu-post-to-x/ # X/Twitter posting automation
│ └── baoyu-post-to-wechat/ # WeChat Official Account posting
├── [ai-generation-skills] # AI-powered generation backends
│ └── baoyu-danger-gemini-web/ # Gemini API wrapper (text + image gen)
└── [utility-skills] # Utility tools for content processing
├── baoyu-danger-x-to-markdown/ # X/Twitter content to markdown
└── baoyu-compress-image/ # Image compression
```
**Plugin Categories**:
| Category | Description |
|----------|-------------|
| `content-skills` | Skills that generate or publish content (images, slides, comics, posts) |
| `ai-generation-skills` | Backend skills providing AI generation capabilities |
| `utility-skills` | Helper tools for content processing (conversion, compression) |
Each skill contains:
- `SKILL.md` - YAML front matter (name, description) + documentation
- `scripts/` - TypeScript implementations
@@ -35,24 +52,24 @@ npx -y bun skills/<skill>/scripts/main.ts [options]
Examples:
```bash
# Text generation
npx -y bun skills/baoyu-gemini-web/scripts/main.ts "Hello"
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "Hello"
# Image generation
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --prompt "A cat" --image cat.png
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "A cat" --image cat.png
# From prompt files
npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
```
## Key Dependencies
- **Bun**: TypeScript runtime (via `npx -y bun`)
- **Chrome**: Required for `baoyu-gemini-web` auth and `baoyu-post-to-x` automation
- **Chrome**: Required for `baoyu-danger-gemini-web` auth and `baoyu-post-to-x` automation
- **No npm packages**: Self-contained TypeScript, no external dependencies
## Authentication
`baoyu-gemini-web` uses browser cookies for Google auth:
`baoyu-danger-gemini-web` uses browser cookies for Google auth:
- First run opens Chrome for login
- Cookies cached in data directory
- Force refresh: `--login` flag
@@ -70,7 +87,50 @@ npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md conte
- SKILL.md `name` field: `baoyu-<name>`
2. Add TypeScript in `skills/baoyu-<name>/scripts/`
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
4. Register in `marketplace.json` plugins[0].skills array as `./skills/baoyu-<name>`
4. **Choose the appropriate category** and register in `marketplace.json`:
- `content-skills`: For content generation/publishing (images, slides, posts)
- `ai-generation-skills`: For AI backend capabilities
- `utility-skills`: For helper tools (conversion, compression)
- If none fit, create a new category with descriptive name
5. **Add Script Directory section** to SKILL.md (see template below)
### Choosing a Category
| If your skill... | Use category |
|------------------|--------------|
| Generates visual content (images, slides, comics) | `content-skills` |
| Publishes to platforms (X, WeChat, etc.) | `content-skills` |
| Provides AI generation backend | `ai-generation-skills` |
| Converts or processes content | `utility-skills` |
| Compresses or optimizes files | `utility-skills` |
**Creating a new category**: If the skill doesn't fit existing categories, add a new plugin object to `marketplace.json` with:
- `name`: Descriptive kebab-case name (e.g., `analytics-skills`)
- `description`: Brief description of the category
- `skills`: Array with the skill path
### Script Directory Template
Every SKILL.md with scripts MUST include this section after Usage:
```markdown
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | Main entry point |
| `scripts/other.ts` | Other functionality |
```
When referencing scripts in workflow sections, use `${SKILL_DIR}/scripts/<name>.ts` so agents can resolve the correct path.
## Code Style
@@ -78,3 +138,113 @@ npx -y bun skills/baoyu-gemini-web/scripts/main.ts --promptfiles system.md conte
- Async/await patterns
- Short variable names
- Type-safe interfaces
## Image Generation Guidelines
Skills that require image generation MUST delegate to available image generation skills rather than implementing their own.
### Image Generation Skill Selection
1. Check available image generation skills in `skills/` directory
2. Read each skill's SKILL.md to understand parameters and capabilities
3. If multiple image generation skills available, ask user to choose preferred skill
### Generation Flow Template
Use this template when implementing image generation in skills:
```markdown
### Step N: Generate Images
**Skill Selection**:
1. Check available image generation skills (e.g., `baoyu-danger-gemini-web`)
2. Read selected skill's SKILL.md for parameter reference
3. If multiple skills available, ask user to choose
**Generation Flow**:
1. Call selected image generation skill with:
- Prompt file path (or inline prompt)
- Output image path
- Any skill-specific parameters (refer to skill's SKILL.md)
2. Generate images sequentially (one at a time)
3. After each image, output progress: "Generated X/N"
4. On failure, auto-retry once before reporting error
```
### Output Path Convention
Each session creates an independent directory. Even the same source file generates a new directory per session.
**Output Directory**:
```
<skill-suffix>/<topic-slug>/
```
- `<skill-suffix>`: Skill name suffix (e.g., `xhs-images`, `cover-image`, `slide-deck`, `comic`)
- `<topic-slug>`: Generated from content topic (2-4 words, kebab-case)
- Example: `xhs-images/ai-future-trends/`
**Slug Generation**:
1. Extract main topic from content (2-4 words, kebab-case)
2. Example: "Introduction to Machine Learning" → `intro-machine-learning`
**Conflict Resolution**:
If `<skill-suffix>/<topic-slug>/` already exists:
- Append timestamp: `<topic-slug>-YYYYMMDD-HHMMSS`
- Example: `ai-future` exists → `ai-future-20260118-143052`
**Source Files**:
- Copy all sources to `<skill-suffix>/<topic-slug>/` with naming: `source-{slug}.{ext}`
- Multiple sources supported: text, images, files from conversation
- Examples:
- `source-article.md` (main text content)
- `source-reference.png` (image from conversation)
- `source-data.csv` (additional file)
- Original source files remain unchanged
### Image Naming Convention
Image filenames MUST include meaningful slugs for readability:
**Format**: `NN-{type}-[slug].png`
- `NN`: Two-digit sequence number (01, 02, ...)
- `{type}`: Image type (cover, content, page, slide, illustration, etc.)
- `[slug]`: Descriptive kebab-case slug derived from content
**Examples**:
```
01-cover-ai-future.png
02-content-key-benefits.png
03-page-enigma-machine.png
04-slide-architecture-overview.png
```
**Slug Rules**:
- Derived from image purpose or content (kebab-case)
- Must be unique within the output directory
- 2-5 words, concise but descriptive
- When content changes significantly, update slug accordingly
### Best Practices
- Always read the image generation skill's SKILL.md before calling
- Pass parameters exactly as documented in the skill
- Handle failures gracefully with retry logic
- Provide clear progress feedback to user
## Extension Support
Every SKILL.md MUST include an Extension Support section at the end:
```markdown
## Extension Support
Custom styles and configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/<skill-name>/EXTEND.md` (project)
2. `~/.baoyu-skills/<skill-name>/EXTEND.md` (user)
If found, load before Step 1. Extension content overrides defaults.
```
Replace `<skill-name>` with the actual skill name (e.g., `baoyu-cover-image`).
+244 -34
View File
@@ -31,13 +31,16 @@ Run the following command in Claude Code:
1. Select **Browse and install plugins**
2. Select **baoyu-skills**
3. Select **content-skills**
3. Select the plugin(s) you want to install
4. Select **Install now**
**Option 2: Direct Install**
```bash
# Install specific plugin
/plugin install content-skills@baoyu-skills
/plugin install ai-generation-skills@baoyu-skills
/plugin install utility-skills@baoyu-skills
```
**Option 3: Ask the Agent**
@@ -46,6 +49,14 @@ Simply tell Claude Code:
> Please install Skills from github.com/JimLiu/baoyu-skills
### Available Plugins
| Plugin | Description | Skills |
|--------|-------------|--------|
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat) |
| **ai-generation-skills** | AI-powered generation backends | [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | Utility tools for content processing | [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image) |
## Update Skills
To update skills to the latest version:
@@ -61,25 +72,13 @@ You can also **Enable auto-update** to get the latest versions automatically.
## Available Skills
### baoyu-gemini-web
Skills are organized into three categories:
Interacts with Gemini Web to generate text and images.
### Content Skills
**Text Generation:**
Content generation and publishing skills.
```bash
/baoyu-gemini-web "Hello, Gemini"
/baoyu-gemini-web --prompt "Explain quantum computing"
```
**Image Generation:**
```bash
/baoyu-gemini-web --prompt "A cute cat" --image cat.png
/baoyu-gemini-web --promptfiles system.md content.md --image out.png
```
### baoyu-xhs-images
#### baoyu-xhs-images
Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-10 cartoon-style infographics with **Style × Layout** two-dimensional system.
@@ -112,7 +111,7 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
| `comparison` | 2 sides | Before/after, pros/cons |
| `flow` | 3-6 steps | Processes, timelines |
### baoyu-cover-image
#### baoyu-cover-image
Generate hand-drawn style cover images for articles with multiple style options.
@@ -128,9 +127,28 @@ Generate hand-drawn style cover images for articles with multiple style options.
/baoyu-cover-image path/to/article.md --no-title
```
Available styles: `elegant` (default), `tech`, `warm`, `bold`, `minimal`, `playful`, `nature`, `retro`
Available styles: `elegant` (default), `blueprint`, `bold`, `bold-editorial`, `chalkboard`, `dark-atmospheric`, `editorial-infographic`, `fantasy-animation`, `intuition-machine`, `minimal`, `nature`, `notion`, `pixel-art`, `playful`, `retro`, `sketch-notes`, `vector-illustration`, `vintage`, `warm`, `watercolor`
### baoyu-slide-deck
**Style Previews**:
| | | |
|:---:|:---:|:---:|
| ![elegant](./screenshots/cover-image-styles/elegant.webp) | ![blueprint](./screenshots/cover-image-styles/blueprint.webp) | ![bold](./screenshots/cover-image-styles/bold.webp) |
| elegant | blueprint | bold |
| ![bold-editorial](./screenshots/cover-image-styles/bold-editorial.webp) | ![chalkboard](./screenshots/cover-image-styles/chalkboard.webp) | ![dark-atmospheric](./screenshots/cover-image-styles/dark-atmospheric.webp) |
| bold-editorial | chalkboard | dark-atmospheric |
| ![editorial-infographic](./screenshots/cover-image-styles/editorial-infographic.webp) | ![fantasy-animation](./screenshots/cover-image-styles/fantasy-animation.webp) | ![intuition-machine](./screenshots/cover-image-styles/intuition-machine.webp) |
| editorial-infographic | fantasy-animation | intuition-machine |
| ![minimal](./screenshots/cover-image-styles/minimal.webp) | ![nature](./screenshots/cover-image-styles/nature.webp) | ![notion](./screenshots/cover-image-styles/notion.webp) |
| minimal | nature | notion |
| ![pixel-art](./screenshots/cover-image-styles/pixel-art.webp) | ![playful](./screenshots/cover-image-styles/playful.webp) | ![retro](./screenshots/cover-image-styles/retro.webp) |
| pixel-art | playful | retro |
| ![sketch-notes](./screenshots/cover-image-styles/sketch-notes.webp) | ![vector-illustration](./screenshots/cover-image-styles/vector-illustration.webp) | ![vintage](./screenshots/cover-image-styles/vintage.webp) |
| sketch-notes | vector-illustration | vintage |
| ![warm](./screenshots/cover-image-styles/warm.webp) | ![watercolor](./screenshots/cover-image-styles/watercolor.webp) | |
| warm | watercolor | |
#### baoyu-slide-deck
Generate professional slide deck images from content. Creates comprehensive outlines with style instructions, then generates individual slide images.
@@ -149,9 +167,46 @@ Generate professional slide deck images from content. Creates comprehensive outl
/baoyu-slide-deck path/to/article.md --lang zh
```
Available styles: `editorial` (default), `corporate`, `technical`, `playful`, `minimal`, `storytelling`, `warm`, `retro-flat`, `notion`
**Styles** (visual aesthetics):
### baoyu-comic
| Style | Description | Best For |
|-------|-------------|----------|
| `blueprint` (default) | Technical schematics, grid texture, engineering precision | Architecture, system design |
| `notion` | SaaS dashboard aesthetic, card-based layouts, clean data focus | Product demos, SaaS, B2B |
| `bold-editorial` | High-impact magazine style, bold typography, dark backgrounds | Product launches, keynotes |
| `corporate` | Navy/gold palette, structured layouts, professional icons | Investor decks, proposals |
| `dark-atmospheric` | Cinematic dark mode, glowing accents, atmospheric depth | Entertainment, gaming, creative |
| `editorial-infographic` | Magazine-style explainers, flat illustrations | Tech explainers, research |
| `fantasy-animation` | Whimsical Ghibli/Disney style, hand-drawn animation | Educational, storytelling |
| `intuition-machine` | Technical briefing, bilingual labels, aged paper texture | Technical docs, bilingual |
| `minimal` | Ultra-clean, maximum whitespace, single accent color | Executive briefings, premium |
| `pixel-art` | Retro 8-bit aesthetic, chunky pixels, nostalgic gaming | Gaming, developer talks |
| `scientific` | Academic diagrams, biological pathways, precise labeling | Biology, chemistry, medical |
| `sketch-notes` | Hand-drawn feel, soft brush strokes, warm background | Educational, tutorials |
| `vector-illustration` | Flat vector, black outlines, retro soft colors | Creative proposals, explainers |
| `vintage` | Aged-paper aesthetic, historical document styling | Historical, heritage, biography |
| `watercolor` | Soft hand-painted textures, natural warmth | Lifestyle, wellness, travel |
**Style Previews**:
| | | |
|:---:|:---:|:---:|
| ![blueprint](./screenshots/slide-deck-styles/blueprint.webp) | ![chalkboard](./screenshots/slide-deck-styles/chalkboard.webp) | ![bold-editorial](./screenshots/slide-deck-styles/bold-editorial.webp) |
| blueprint | chalkboard | bold-editorial |
| ![corporate](./screenshots/slide-deck-styles/corporate.webp) | ![dark-atmospheric](./screenshots/slide-deck-styles/dark-atmospheric.webp) | ![editorial-infographic](./screenshots/slide-deck-styles/editorial-infographic.webp) |
| corporate | dark-atmospheric | editorial-infographic |
| ![fantasy-animation](./screenshots/slide-deck-styles/fantasy-animation.webp) | ![intuition-machine](./screenshots/slide-deck-styles/intuition-machine.webp) | ![minimal](./screenshots/slide-deck-styles/minimal.webp) |
| fantasy-animation | intuition-machine | minimal |
| ![notion](./screenshots/slide-deck-styles/notion.webp) | ![pixel-art](./screenshots/slide-deck-styles/pixel-art.webp) | ![scientific](./screenshots/slide-deck-styles/scientific.webp) |
| notion | pixel-art | scientific |
| ![sketch-notes](./screenshots/slide-deck-styles/sketch-notes.webp) | ![vector-illustration](./screenshots/slide-deck-styles/vector-illustration.webp) | ![vintage](./screenshots/slide-deck-styles/vintage.webp) |
| sketch-notes | vector-illustration | vintage |
| ![watercolor](./screenshots/slide-deck-styles/watercolor.webp) | | |
| watercolor | | |
After generation, slides are automatically merged into a `.pptx` file for easy sharing.
#### baoyu-comic
Knowledge comic creator supporting multiple styles (Logicomix/Ligne Claire, Ohmsha manga guide). Creates original educational comics with detailed panel layouts and sequential image generation.
@@ -163,24 +218,51 @@ Knowledge comic creator supporting multiple styles (Logicomix/Ligne Claire, Ohms
/baoyu-comic posts/turing-story/source.md --style dramatic
/baoyu-comic posts/turing-story/source.md --style ohmsha
# Specify layout
# Custom style (natural language)
/baoyu-comic posts/turing-story/source.md --style "watercolor with soft edges"
# Specify layout and aspect ratio
/baoyu-comic posts/turing-story/source.md --layout cinematic
/baoyu-comic posts/turing-story/source.md --layout webtoon
/baoyu-comic posts/turing-story/source.md --aspect 16:9
# Specify language
/baoyu-comic posts/turing-story/source.md --lang zh
# Direct content input
/baoyu-comic "The story of Alan Turing and the birth of computer science"
```
**Options**:
| Option | Values |
|--------|--------|
| `--style` | `classic` (default), `dramatic`, `warm`, `sepia`, `vibrant`, `ohmsha`, `realistic`, `wuxia`, or custom description |
| `--layout` | `standard` (default), `cinematic`, `dense`, `splash`, `mixed`, `webtoon` |
| `--aspect` | `3:4` (default, portrait), `4:3` (landscape), `16:9` (widescreen) |
| `--lang` | `auto` (default), `zh`, `en`, `ja`, etc. |
**Styles** (visual aesthetics):
| Style | Description | Best for |
| Style | Description | Best For |
|-------|-------------|----------|
| `classic` | Traditional Ligne Claire, balanced | Biographies, balanced narratives |
| `dramatic` | High contrast, intense | Breakthroughs, conflicts |
| `warm` | Nostalgic, personal | Personal stories, mentorship |
| `tech` | Modern, digital | Computing history, AI stories |
| `sepia` | Historical, archival | Pre-1950s, classical science |
| `vibrant` | Energetic, engaging | Science basics, young audience |
| `ohmsha` | Manga guide style, visual metaphors | Tutorials, complex concepts (ML, physics) |
| `classic` (default) | Traditional Ligne Claire with clean uniform outlines, flat colors, detailed backgrounds | Biographies, balanced narratives, educational content |
| `dramatic` | High contrast with heavy shadows, intense expressions, angular compositions | Pivotal discoveries, conflicts, climactic scenes |
| `warm` | Soft edges, golden tones, cozy interiors with nostalgic feel | Personal stories, childhood scenes, mentorship |
| `sepia` | Vintage illustration style with aged paper effect, period-accurate details | Pre-1950s stories, classical science, historical figures |
| `vibrant` | Energetic lines with weight variation, bright colors, dynamic poses | Science explanations, "aha" moments, young audience |
| `ohmsha` | Manga guide style with visual metaphors, gadgets, student/mentor dynamic | Technical tutorials, complex concepts (ML, physics) |
| `realistic` | Full-color realistic manga with digital painting, smooth gradients, accurate proportions | Wine, food, business, lifestyle, professional topics |
| `wuxia` | Hong Kong martial arts style with ink brush strokes, dynamic combat, qi effects | Martial arts, wuxia/xianxia, Chinese historical fiction |
**Style Previews**:
| | | |
|:---:|:---:|:---:|
| ![classic](./screenshots/comic-styles/classic.webp) | ![dramatic](./screenshots/comic-styles/dramatic.webp) | ![warm](./screenshots/comic-styles/warm.webp) |
| classic | dramatic | warm |
| ![sepia](./screenshots/comic-styles/sepia.webp) | ![vibrant](./screenshots/comic-styles/vibrant.webp) | ![ohmsha](./screenshots/comic-styles/ohmsha.webp) |
| sepia | vibrant | ohmsha |
| ![realistic](./screenshots/comic-styles/realistic.webp) | ![wuxia](./screenshots/comic-styles/wuxia.webp) | |
| realistic | wuxia | |
**Layouts** (panel arrangement):
| Layout | Panels/Page | Best for |
@@ -192,7 +274,39 @@ Knowledge comic creator supporting multiple styles (Logicomix/Ligne Claire, Ohms
| `mixed` | 3-7 varies | Complex narratives, emotional arcs |
| `webtoon` | 3-5 vertical | Ohmsha tutorials, mobile reading |
### baoyu-post-to-wechat
**Layout Previews**:
| | | |
|:---:|:---:|:---:|
| ![standard](./screenshots/comic-layouts/standard.webp) | ![cinematic](./screenshots/comic-layouts/cinematic.webp) | ![dense](./screenshots/comic-layouts/dense.webp) |
| standard | cinematic | dense |
| ![splash](./screenshots/comic-layouts/splash.webp) | ![mixed](./screenshots/comic-layouts/mixed.webp) | ![webtoon](./screenshots/comic-layouts/webtoon.webp) |
| splash | mixed | webtoon |
#### baoyu-article-illustrator
Smart article illustration skill. Analyzes article content and generates illustrations at positions requiring visual aids.
```bash
/baoyu-article-illustrator path/to/article.md
```
#### baoyu-post-to-x
Post content and articles to X (Twitter). Supports regular posts with images and X Articles (long-form Markdown). Uses real Chrome with CDP to bypass anti-automation.
```bash
# Post with text
/baoyu-post-to-x "Hello from Claude Code!"
# Post with images
/baoyu-post-to-x "Check this out" --image photo.png
# Post X Article
/baoyu-post-to-x --article path/to/article.md
```
#### baoyu-post-to-wechat
Post content to WeChat Official Account (微信公众号). Two modes available:
@@ -214,9 +328,94 @@ Post content to WeChat Official Account (微信公众号). Two modes available:
Prerequisites: Google Chrome installed. First run requires QR code login (session preserved).
### AI Generation Skills
AI-powered generation backends.
#### baoyu-danger-gemini-web
Interacts with Gemini Web to generate text and images.
**Text Generation:**
```bash
/baoyu-danger-gemini-web "Hello, Gemini"
/baoyu-danger-gemini-web --prompt "Explain quantum computing"
```
**Image Generation:**
```bash
/baoyu-danger-gemini-web --prompt "A cute cat" --image cat.png
/baoyu-danger-gemini-web --promptfiles system.md content.md --image out.png
```
### Utility Skills
Utility tools for content processing.
#### baoyu-danger-x-to-markdown
Converts X (Twitter) content to markdown format. Supports tweet threads and X Articles.
```bash
# Convert tweet to markdown
/baoyu-danger-x-to-markdown https://x.com/username/status/123456
# Save to specific file
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 -o output.md
# JSON output
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 --json
```
**Supported URLs:**
- `https://x.com/<user>/status/<id>`
- `https://twitter.com/<user>/status/<id>`
- `https://x.com/i/article/<id>`
**Authentication:** Uses environment variables (`X_AUTH_TOKEN`, `X_CT0`) or Chrome login for cookie-based auth.
#### baoyu-compress-image
Compress images to reduce file size while maintaining quality.
```bash
/baoyu-compress-image path/to/image.png
/baoyu-compress-image path/to/images/ --quality 80
```
## Customization
All skills support customization via `EXTEND.md` files. Create an extension file to override default styles, add custom configurations, or define your own presets.
**Extension paths** (checked in priority order):
1. `.baoyu-skills/<skill-name>/EXTEND.md` - Project-level (for team/project-specific settings)
2. `~/.baoyu-skills/<skill-name>/EXTEND.md` - User-level (for personal preferences)
**Example**: To customize `baoyu-cover-image` with your brand colors:
```bash
mkdir -p .baoyu-skills/baoyu-cover-image
```
Then create `.baoyu-skills/baoyu-cover-image/EXTEND.md`:
```markdown
## Custom Styles
### brand
- Primary color: #1a73e8
- Secondary color: #34a853
- Font style: Modern sans-serif
- Always include company logo watermark
```
The extension content will be loaded before skill execution and override defaults.
## Disclaimer
### baoyu-gemini-web
### baoyu-danger-gemini-web
This skill uses the Gemini Web API (reverse-engineered).
@@ -226,6 +425,17 @@ This skill uses the Gemini Web API (reverse-engineered).
- Cookies are cached for subsequent runs
- No guarantees on API stability or availability
### baoyu-danger-x-to-markdown
This skill uses a reverse-engineered X (Twitter) API.
**Warning:** This is NOT an official API. Use at your own risk.
- May break without notice if X changes their API
- Account restrictions possible if API usage detected
- First use requires consent acknowledgment
- Authentication via environment variables or Chrome login
## License
MIT
+243 -33
View File
@@ -31,13 +31,16 @@ npx add-skill jimliu/baoyu-skills
1. 选择 **Browse and install plugins**
2. 选择 **baoyu-skills**
3. 选择 **content-skills**
3. 选择要安装的插件
4. 选择 **Install now**
**方式二:直接安装**
```bash
# 安装指定插件
/plugin install content-skills@baoyu-skills
/plugin install ai-generation-skills@baoyu-skills
/plugin install utility-skills@baoyu-skills
```
**方式三:告诉 Agent**
@@ -46,6 +49,14 @@ npx add-skill jimliu/baoyu-skills
> 请帮我安装 github.com/JimLiu/baoyu-skills 中的 Skills
### 可用插件
| 插件 | 说明 | 包含技能 |
|------|------|----------|
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat) |
| **ai-generation-skills** | AI 生成后端 | [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | 内容处理工具 | [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image) |
## 更新技能
更新技能到最新版本:
@@ -61,25 +72,13 @@ npx add-skill jimliu/baoyu-skills
## 可用技能
### baoyu-gemini-web
技能分为三大类:
与 Gemini Web 交互,生成文本和图片。
### 内容技能 (Content Skills)
**文本生成:**
内容生成和发布技能。
```bash
/baoyu-gemini-web "你好,Gemini"
/baoyu-gemini-web --prompt "解释量子计算"
```
**图片生成:**
```bash
/baoyu-gemini-web --prompt "一只可爱的猫" --image cat.png
/baoyu-gemini-web --promptfiles system.md content.md --image out.png
```
### baoyu-xhs-images
#### baoyu-xhs-images
小红书信息图系列生成器。将内容拆解为 1-10 张卡通风格信息图,支持 **风格 × 布局** 二维系统。
@@ -112,7 +111,7 @@ npx add-skill jimliu/baoyu-skills
| `comparison` | 双栏 | 对比、优劣 |
| `flow` | 3-6 步 | 流程、时间线 |
### baoyu-cover-image
#### baoyu-cover-image
为文章生成手绘风格封面图,支持多种风格选项。
@@ -128,9 +127,28 @@ npx add-skill jimliu/baoyu-skills
/baoyu-cover-image path/to/article.md --no-title
```
可用风格:`elegant`(默认)、`tech``warm``bold``minimal``playful``nature``retro`
可用风格:`elegant`(默认)、`blueprint``bold``bold-editorial``chalkboard``dark-atmospheric``editorial-infographic``fantasy-animation``intuition-machine``minimal``nature``notion``pixel-art``playful``retro``sketch-notes``vector-illustration``vintage``warm``watercolor`
### baoyu-slide-deck
**风格预览**
| | | |
|:---:|:---:|:---:|
| ![elegant](./screenshots/cover-image-styles/elegant.webp) | ![blueprint](./screenshots/cover-image-styles/blueprint.webp) | ![bold](./screenshots/cover-image-styles/bold.webp) |
| elegant | blueprint | bold |
| ![bold-editorial](./screenshots/cover-image-styles/bold-editorial.webp) | ![chalkboard](./screenshots/cover-image-styles/chalkboard.webp) | ![dark-atmospheric](./screenshots/cover-image-styles/dark-atmospheric.webp) |
| bold-editorial | chalkboard | dark-atmospheric |
| ![editorial-infographic](./screenshots/cover-image-styles/editorial-infographic.webp) | ![fantasy-animation](./screenshots/cover-image-styles/fantasy-animation.webp) | ![intuition-machine](./screenshots/cover-image-styles/intuition-machine.webp) |
| editorial-infographic | fantasy-animation | intuition-machine |
| ![minimal](./screenshots/cover-image-styles/minimal.webp) | ![nature](./screenshots/cover-image-styles/nature.webp) | ![notion](./screenshots/cover-image-styles/notion.webp) |
| minimal | nature | notion |
| ![pixel-art](./screenshots/cover-image-styles/pixel-art.webp) | ![playful](./screenshots/cover-image-styles/playful.webp) | ![retro](./screenshots/cover-image-styles/retro.webp) |
| pixel-art | playful | retro |
| ![sketch-notes](./screenshots/cover-image-styles/sketch-notes.webp) | ![vector-illustration](./screenshots/cover-image-styles/vector-illustration.webp) | ![vintage](./screenshots/cover-image-styles/vintage.webp) |
| sketch-notes | vector-illustration | vintage |
| ![warm](./screenshots/cover-image-styles/warm.webp) | ![watercolor](./screenshots/cover-image-styles/watercolor.webp) | |
| warm | watercolor | |
#### baoyu-slide-deck
从内容生成专业的幻灯片图片。先创建包含样式说明的完整大纲,然后逐页生成幻灯片图片。
@@ -149,9 +167,46 @@ npx add-skill jimliu/baoyu-skills
/baoyu-slide-deck path/to/article.md --lang zh
```
可用风格:`editorial`(默认)、`corporate``technical``playful``minimal``storytelling``warm``retro-flat``notion`
**风格**(视觉美学):
### baoyu-comic
| 风格 | 描述 | 适用场景 |
|------|------|----------|
| `blueprint`(默认) | 技术蓝图风格,网格纹理,工程精度 | 架构设计、系统设计 |
| `notion` | SaaS 仪表盘美学,卡片式布局,数据清晰 | 产品演示、SaaS、B2B |
| `bold-editorial` | 杂志社论风格,粗体排版,深色背景 | 产品发布、主题演讲 |
| `corporate` | 海军蓝/金色配色,结构化布局,专业图标 | 投资者演示、客户提案 |
| `dark-atmospheric` | 电影级暗色调,发光效果,氛围感 | 娱乐、游戏、创意 |
| `editorial-infographic` | 杂志风格信息图,扁平插画 | 科技解说、研究报告 |
| `fantasy-animation` | 吉卜力/迪士尼风格,手绘动画 | 教育、故事讲述 |
| `intuition-machine` | 技术简报,双语标签,做旧纸张纹理 | 技术文档、双语内容 |
| `minimal` | 极简风格,大量留白,单一强调色 | 高管简报、高端品牌 |
| `pixel-art` | 复古 8-bit 像素风,怀旧游戏感 | 游戏、开发者分享 |
| `scientific` | 学术图表,生物通路,精确标注 | 生物、化学、医学 |
| `sketch-notes` | 手绘风格,柔和笔触,暖白色背景 | 教育、教程、知识分享 |
| `vector-illustration` | 扁平矢量风格,黑色轮廓线,复古柔和配色 | 创意提案、说明性内容 |
| `vintage` | 做旧纸张美学,历史文档风格 | 历史、传记、人文 |
| `watercolor` | 柔和手绘水彩纹理,自然温暖 | 生活方式、健康、旅行 |
**风格预览**
| | | |
|:---:|:---:|:---:|
| ![blueprint](./screenshots/slide-deck-styles/blueprint.webp) | ![chalkboard](./screenshots/slide-deck-styles/chalkboard.webp) | ![bold-editorial](./screenshots/slide-deck-styles/bold-editorial.webp) |
| blueprint | chalkboard | bold-editorial |
| ![corporate](./screenshots/slide-deck-styles/corporate.webp) | ![dark-atmospheric](./screenshots/slide-deck-styles/dark-atmospheric.webp) | ![editorial-infographic](./screenshots/slide-deck-styles/editorial-infographic.webp) |
| corporate | dark-atmospheric | editorial-infographic |
| ![fantasy-animation](./screenshots/slide-deck-styles/fantasy-animation.webp) | ![intuition-machine](./screenshots/slide-deck-styles/intuition-machine.webp) | ![minimal](./screenshots/slide-deck-styles/minimal.webp) |
| fantasy-animation | intuition-machine | minimal |
| ![notion](./screenshots/slide-deck-styles/notion.webp) | ![pixel-art](./screenshots/slide-deck-styles/pixel-art.webp) | ![scientific](./screenshots/slide-deck-styles/scientific.webp) |
| notion | pixel-art | scientific |
| ![sketch-notes](./screenshots/slide-deck-styles/sketch-notes.webp) | ![vector-illustration](./screenshots/slide-deck-styles/vector-illustration.webp) | ![vintage](./screenshots/slide-deck-styles/vintage.webp) |
| sketch-notes | vector-illustration | vintage |
| ![watercolor](./screenshots/slide-deck-styles/watercolor.webp) | | |
| watercolor | | |
生成完成后,所有幻灯片会自动合并为 `.pptx` 文件,方便分享。
#### baoyu-comic
知识漫画创作器,支持多种风格(Logicomix/清线风格、欧姆社漫画教程风格)。创作带有详细分镜布局的原创教育漫画,逐页生成图片。
@@ -163,24 +218,51 @@ npx add-skill jimliu/baoyu-skills
/baoyu-comic posts/turing-story/source.md --style dramatic
/baoyu-comic posts/turing-story/source.md --style ohmsha
# 指定布局
# 自定义风格(自然语言描述)
/baoyu-comic posts/turing-story/source.md --style "水彩风格,边缘柔和"
# 指定布局和比例
/baoyu-comic posts/turing-story/source.md --layout cinematic
/baoyu-comic posts/turing-story/source.md --layout webtoon
/baoyu-comic posts/turing-story/source.md --aspect 16:9
# 指定语言
/baoyu-comic posts/turing-story/source.md --lang zh
# 直接输入内容
/baoyu-comic "图灵的故事与计算机科学的诞生"
```
**选项**
| 选项 | 取值 |
|------|------|
| `--style` | `classic`(默认)、`dramatic``warm``sepia``vibrant``ohmsha``realistic``wuxia`,或自然语言描述 |
| `--layout` | `standard`(默认)、`cinematic``dense``splash``mixed``webtoon` |
| `--aspect` | `3:4`(默认,竖版)、`4:3`(横版)、`16:9`(宽屏) |
| `--lang` | `auto`(默认)、`zh``en``ja` 等 |
**风格**(视觉美学):
| 风格 | 描述 | 适用场景 |
|------|------|----------|
| `classic` | 传统清线风格,平衡 | 传记、平衡叙事 |
| `dramatic` | 高对比度,紧张感 | 突破时刻、冲突 |
| `warm` | 怀旧、个人化 | 个人故事、师生情 |
| `tech` | 现代、数字化 | 计算机史、AI 故事 |
| `sepia` | 历史、档案感 | 1950 年前、古典科学 |
| `vibrant` | 活力、吸引人 | 科普、青少年读者 |
| `ohmsha` | 欧姆社漫画风格,视觉比喻 | 教程、复杂概念(机器学习、物理) |
| `classic`(默认) | 传统清线风格,统一线条、平涂色彩、精细背景 | 传记、平衡叙事、教育内容 |
| `dramatic` | 高对比度,重阴影、紧张表情、棱角分明的构图 | 重大发现、冲突、高潮场景 |
| `warm` | 柔和边缘、金色调、温馨室内、怀旧感 | 个人故事、童年场景、师生情 |
| `sepia` | 复古插画风格、做旧纸张效果、时代准确细节 | 1950 年前故事、古典科学、历史人物 |
| `vibrant` | 富有活力的线条、明亮色彩、动感姿态 | 科学解说、"顿悟"时刻、青少年读者 |
| `ohmsha` | 欧姆社漫画风格,视觉比喻、道具、学生/导师互动 | 技术教程、复杂概念(机器学习、物理) |
| `realistic` | 全彩写实日漫风格,数字绘画、平滑渐变、准确人体比例 | 红酒、美食、商业、生活方式、专业话题 |
| `wuxia` | 港漫武侠风格,水墨笔触、动态打斗、气功特效 | 武侠、仙侠、中国历史小说 |
**风格预览**
| | | |
|:---:|:---:|:---:|
| ![classic](./screenshots/comic-styles/classic.webp) | ![dramatic](./screenshots/comic-styles/dramatic.webp) | ![warm](./screenshots/comic-styles/warm.webp) |
| classic | dramatic | warm |
| ![sepia](./screenshots/comic-styles/sepia.webp) | ![vibrant](./screenshots/comic-styles/vibrant.webp) | ![ohmsha](./screenshots/comic-styles/ohmsha.webp) |
| sepia | vibrant | ohmsha |
| ![realistic](./screenshots/comic-styles/realistic.webp) | ![wuxia](./screenshots/comic-styles/wuxia.webp) | |
| realistic | wuxia | |
**布局**(分镜排列):
| 布局 | 每页分镜数 | 适用场景 |
@@ -192,7 +274,39 @@ npx add-skill jimliu/baoyu-skills
| `mixed` | 3-7 不等 | 复杂叙事、情感弧线 |
| `webtoon` | 3-5 竖向 | 欧姆社教程、手机阅读 |
### baoyu-post-to-wechat
**布局预览**
| | | |
|:---:|:---:|:---:|
| ![standard](./screenshots/comic-layouts/standard.webp) | ![cinematic](./screenshots/comic-layouts/cinematic.webp) | ![dense](./screenshots/comic-layouts/dense.webp) |
| standard | cinematic | dense |
| ![splash](./screenshots/comic-layouts/splash.webp) | ![mixed](./screenshots/comic-layouts/mixed.webp) | ![webtoon](./screenshots/comic-layouts/webtoon.webp) |
| splash | mixed | webtoon |
#### baoyu-article-illustrator
智能文章插图技能。分析文章内容,在需要视觉辅助的位置生成插图。
```bash
/baoyu-article-illustrator path/to/article.md
```
#### baoyu-post-to-x
发布内容和文章到 X (Twitter)。支持带图片的普通帖子和 X 文章(长篇 Markdown)。使用真实 Chrome + CDP 绕过反自动化检测。
```bash
# 发布文字
/baoyu-post-to-x "Hello from Claude Code!"
# 发布带图片
/baoyu-post-to-x "看看这个" --image photo.png
# 发布 X 文章
/baoyu-post-to-x --article path/to/article.md
```
#### baoyu-post-to-wechat
发布内容到微信公众号,支持两种模式:
@@ -214,9 +328,94 @@ npx add-skill jimliu/baoyu-skills
前置要求:已安装 Google Chrome,首次运行需扫码登录(登录状态会保存)
### AI 生成技能 (AI Generation Skills)
AI 驱动的生成后端。
#### baoyu-danger-gemini-web
与 Gemini Web 交互,生成文本和图片。
**文本生成:**
```bash
/baoyu-danger-gemini-web "你好,Gemini"
/baoyu-danger-gemini-web --prompt "解释量子计算"
```
**图片生成:**
```bash
/baoyu-danger-gemini-web --prompt "一只可爱的猫" --image cat.png
/baoyu-danger-gemini-web --promptfiles system.md content.md --image out.png
```
### 工具技能 (Utility Skills)
内容处理工具。
#### baoyu-danger-x-to-markdown
将 X (Twitter) 内容转换为 markdown 格式。支持推文串和 X 文章。
```bash
# 将推文转换为 markdown
/baoyu-danger-x-to-markdown https://x.com/username/status/123456
# 保存到指定文件
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 -o output.md
# JSON 输出
/baoyu-danger-x-to-markdown https://x.com/username/status/123456 --json
```
**支持的 URL**
- `https://x.com/<user>/status/<id>`
- `https://twitter.com/<user>/status/<id>`
- `https://x.com/i/article/<id>`
**身份验证:** 使用环境变量(`X_AUTH_TOKEN``X_CT0`)或 Chrome 登录进行 cookie 认证。
#### baoyu-compress-image
压缩图片以减小文件大小,同时保持质量。
```bash
/baoyu-compress-image path/to/image.png
/baoyu-compress-image path/to/images/ --quality 80
```
## 自定义扩展
所有技能支持通过 `EXTEND.md` 文件自定义。创建扩展文件可覆盖默认样式、添加自定义配置或定义个人预设。
**扩展路径**(按优先级检查):
1. `.baoyu-skills/<skill-name>/EXTEND.md` - 项目级(团队/项目特定设置)
2. `~/.baoyu-skills/<skill-name>/EXTEND.md` - 用户级(个人偏好设置)
**示例**:为 `baoyu-cover-image` 自定义品牌配色:
```bash
mkdir -p .baoyu-skills/baoyu-cover-image
```
然后创建 `.baoyu-skills/baoyu-cover-image/EXTEND.md`
```markdown
## 自定义风格
### brand
- 主色:#1a73e8
- 辅色:#34a853
- 字体风格:现代无衬线
- 始终包含公司 logo 水印
```
扩展内容会在技能执行前加载,并覆盖默认设置。
## 免责声明
### baoyu-gemini-web
### baoyu-danger-gemini-web
此技能使用 Gemini Web API(逆向工程)。
@@ -226,6 +425,17 @@ npx add-skill jimliu/baoyu-skills
- Cookies 会被缓存供后续使用
- 不保证 API 的稳定性或可用性
### baoyu-danger-x-to-markdown
此技能使用逆向工程的 X (Twitter) API。
**警告:** 这不是官方 API。使用风险自负。
- 如果 X 更改其 API,可能会无预警失效
- 如检测到 API 使用,账号可能受限
- 首次使用需确认免责声明
- 通过环境变量或 Chrome 登录进行身份验证
## 许可证
MIT
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+151 -19
View File
@@ -102,22 +102,43 @@ When no `--style` is specified, analyze content to select the best style:
## File Management
Save illustrations to `imgs/` subdirectory in the same folder as the article:
### Output Directory
Each session creates an independent directory named by content slug:
```
path/to/
├── article.md
── imgs/
├── outline.md
├── prompts/
│ ├── illustration-concept-a.md
│ ├── illustration-concept-b.md
│ └── ...
├── illustration-concept-a.png
├── illustration-concept-b.png
└── ...
illustrations/{topic-slug}/
├── source-{slug}.{ext} # Source files (text, images, etc.)
── outline.md
├── outline-{style}.md # Style variant outlines
├── prompts/
│ ├── illustration-concept-a.md
│ ├── illustration-concept-b.md
│ └── ...
├── illustration-concept-a.png
├── illustration-concept-b.png
└── ...
```
**Slug Generation**:
1. Extract main topic from content (2-4 words, kebab-case)
2. Example: "The Future of AI" → `future-of-ai`
### Conflict Resolution
If `illustrations/{topic-slug}/` already exists:
- Append timestamp: `{topic-slug}-YYYYMMDD-HHMMSS`
- Example: `ai-future` exists → `ai-future-20260118-143052`
### Source Files
Copy all sources with naming `source-{slug}.{ext}`:
- `source-article.md` (main text content)
- `source-photo.jpg` (image from conversation)
- `source-reference.pdf` (additional file)
Multiple sources supported: text, images, files from conversation.
## Workflow
### Step 1: Analyze Content & Select Style
@@ -125,7 +146,11 @@ path/to/
1. Read article content
2. If `--style` specified, use that style
3. Otherwise, scan for style signals and auto-select
4. Extract key information:
4. **Language detection**:
- Detect **source language** from article content
- Detect **user language** from conversation context
- Note if source_language ≠ user_language (will ask in Step 4)
5. Extract key information:
- Main topic and themes
- Core messages per section
- Abstract concepts needing visualization
@@ -173,10 +198,55 @@ path/to/
...
```
### Step 4: Create Prompt Files
### Step 4: Review & Confirm
**Purpose**: Let user confirm all options in a single step before image generation.
**IMPORTANT**: Present ALL options in a single confirmation step using AskUserQuestion. Do NOT interrupt workflow with multiple separate confirmations.
1. **Generate 3 style variants**:
- Analyze content to select 3 most suitable styles
- Generate complete illustration plan for each style variant
- Save as `outline-{style}.md` (e.g., `outline-notion.md`, `outline-tech.md`, `outline-warm.md`)
2. **Determine which questions to ask**:
| Question | When to Ask |
|----------|-------------|
| Style variant | Always (required) |
| Language | Only if `source_language ≠ user_language` |
3. **Present options** (use AskUserQuestion with all applicable questions):
**Question 1 (Style)** - always:
- Style A (recommended): [style name] - [brief description]
- Style B: [style name] - [brief description]
- Style C: [style name] - [brief description]
- Custom: Provide custom style reference
**Question 2 (Language)** - only if source ≠ user language:
- [Source language] (matches article language)
- [User language] (your preference)
**Language handling**:
- If source language = user language: Just inform user (e.g., "Prompts will be in Chinese")
- If different: Ask which language to use for prompts
4. **Apply selection**:
- Copy selected `outline-{style}.md` to `outline.md`
- If custom style provided, generate new plan with that style
- If different language selected, regenerate outline in that language
- User may edit `outline.md` directly for fine-tuning
- If modified, reload plan before proceeding
5. **Proceed only after explicit user confirmation**
### Step 5: Create Prompt Files
Save prompts to `prompts/` directory with style-specific details.
**All prompts are written in the user's confirmed language preference.**
**Prompt Format**:
```markdown
@@ -199,7 +269,7 @@ Text content (if any):
Style notes: [specific style characteristics]
```
### Step 5: Generate Images
### Step 6: Generate Images
**Image Generation Skill Selection**:
1. Check available image generation skills
@@ -212,12 +282,12 @@ Style notes: [specific style characteristics]
4. On failure, auto-retry once
5. If retry fails, log reason, continue to next
### Step 6: Update Article
### Step 7: Update Article
Insert generated images at corresponding positions:
```markdown
![illustration description](imgs/illustration-[slug].png)
![illustration description]([article-name]/illustrations/illustration-[slug].png)
```
**Insertion Rules**:
@@ -225,7 +295,7 @@ Insert generated images at corresponding positions:
- Leave one blank line before and after image
- Alt text uses concise description in article's language
### Step 7: Output Summary
### Step 8: Output Summary
```
Article Illustration Complete!
@@ -244,6 +314,57 @@ Failed:
- illustration-zzz.png: [failure reason]
```
## Illustration Modification
Support for modifying individual illustrations after initial generation.
### Edit Single Illustration
Regenerate a specific illustration with modified prompt:
1. Identify illustration to edit (e.g., `illustration-concept-overview.png`)
2. Update prompt in `prompts/illustration-concept-overview.md` if needed
3. If content changes significantly, update slug in filename
4. Regenerate image
5. Update article if image reference changed
### Add New Illustration
Add a new illustration to the article:
1. Identify insertion position in article
2. Create new prompt with appropriate slug (e.g., `illustration-new-concept.md`)
3. Generate new illustration image
4. Update `outline.md` with new illustration entry
5. Insert image reference in article at the specified position
### Delete Illustration
Remove an illustration from the article:
1. Identify illustration to delete (e.g., `illustration-concept-overview.png`)
2. Remove image file and prompt file
3. Remove image reference from article
4. Update `outline.md` to remove illustration entry
### File Naming Convention
Files use meaningful slugs for better readability:
```
illustration-[slug].png
illustration-[slug].md (in prompts/)
```
Examples:
- `illustration-concept-overview.png`
- `illustration-workflow-diagram.png`
- `illustration-key-benefits.png`
**Slug rules**:
- Derived from illustration purpose/content (kebab-case)
- Must be unique within the article
- When content changes significantly, update slug accordingly
## Style Reference Details
### elegant
@@ -325,4 +446,15 @@ Typography: Clean hand-drawn lettering, simple sans-serif labels
- Maintain selected style consistency across all illustrations in one article
- Image generation typically takes 10-30 seconds per image
- Sensitive figures should use cartoon alternatives
- Prompt and illustration text language should match article language
- Prompts written in user's confirmed language preference
- Illustration text (labels, captions) should match article language
## Extension Support
Custom styles and configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-article-illustrator/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-article-illustrator/EXTEND.md` (user)
If found, load before Step 1. Extension content overrides defaults.
+326 -61
View File
@@ -11,7 +11,6 @@ Create original knowledge comics with multiple visual styles.
```bash
/baoyu-comic posts/turing-story/source.md
/baoyu-comic posts/turing-story/source.md --style dramatic --layout cinematic
/baoyu-comic # then paste content
```
@@ -19,116 +18,371 @@ Create original knowledge comics with multiple visual styles.
| Option | Values |
|--------|--------|
| `--style` | classic (default), dramatic, warm, tech, sepia, vibrant, ohmsha |
| `--style` | classic (default), dramatic, warm, sepia, vibrant, ohmsha, realistic, wuxia, or custom description |
| `--layout` | standard (default), cinematic, dense, splash, mixed, webtoon |
| `--aspect` | 3:4 (default, portrait), 4:3 (landscape), 16:9 (widescreen) |
| `--lang` | auto (default), zh, en, ja, etc. |
Style × Layout can be freely combined.
Style × Layout × Aspect can be freely combined. Custom styles can be described in natural language.
**Aspect ratio is consistent across all pages in a comic.**
## Auto Selection
| Content Signals | Style | Layout |
|-----------------|-------|--------|
| Tutorial, how-to, beginner | ohmsha | webtoon |
| Computing, AI, programming | tech | dense |
| Computing, AI, programming | ohmsha | dense |
| Pre-1950, classical, ancient | sepia | cinematic |
| Personal story, mentor | warm | standard |
| Conflict, breakthrough | dramatic | splash |
| Wine, food, business, lifestyle, professional | realistic | cinematic |
| Martial arts, wuxia, xianxia, Chinese historical | wuxia | splash |
| Biography, balanced | classic | mixed |
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/merge-to-pdf.ts` | Merge comic pages into PDF |
## File Structure
Each session creates an independent directory named by content slug:
```
[target]/
├── outline.md
├── characters/
│ ├── characters.md # Character definitions
│ └── characters.png # Character reference sheet
comic/{topic-slug}/
├── source-{slug}.{ext} # Source files (text, images, etc.)
├── analysis.md # Deep analysis results (YAML+MD)
├── storyboard-chronological.md # Variant A (preserved)
├── storyboard-thematic.md # Variant B (preserved)
├── storyboard-character.md # Variant C (preserved)
├── characters-chronological/ # Variant A chars (preserved)
│ ├── characters.md
│ └── characters.png
├── characters-thematic/ # Variant B chars (preserved)
│ ├── characters.md
│ └── characters.png
├── characters-character/ # Variant C chars (preserved)
│ ├── characters.md
│ └── characters.png
├── storyboard.md # Final selected
├── characters/ # Final selected
│ ├── characters.md
│ └── characters.png
├── prompts/
│ ├── 00-cover.md
│ └── XX-page.md
├── 00-cover.png
── XX-page.png
│ ├── 00-cover-[slug].md
│ └── NN-page-[slug].md
├── 00-cover-[slug].png
── NN-page-[slug].png
└── {topic-slug}.pdf
```
**Target directory**:
- With source path: `[source-dir]/comic/`
- Without source: `comic-outputs/YYYY-MM-DD/[topic-slug]/`
**Slug Generation**:
1. Extract main topic from content (2-4 words, kebab-case)
2. Example: "Alan Turing Biography" → `alan-turing-bio`
**Conflict Resolution**:
If `comic/{topic-slug}/` already exists:
- Append timestamp: `{topic-slug}-YYYYMMDD-HHMMSS`
- Example: `turing-story` exists → `turing-story-20260118-143052`
**Source Files**:
Copy all sources with naming `source-{slug}.{ext}`:
- `source-biography.md`, `source-portrait.jpg`, `source-timeline.png`, etc.
- Multiple sources supported: text, images, files from conversation
## Workflow
### Step 1: Analyze Content
### Step 1: Analyze Content → `analysis.md`
1. Read source content
2. Select style (from `--style` or auto-detect)
3. Select layout (from `--layout` or auto-detect per page)
4. Determine page count:
Read source content, save it if needed, and perform deep analysis.
**Actions**:
1. **Save source content** (if not already a file):
- If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory
2. Read source content
3. **Deep analysis** following `references/analysis-framework.md`:
- Target audience identification
- Value proposition for readers
- Core themes and narrative potential
- Key figures and their story arcs
4. Detect source language
5. Determine recommended page count:
- Short story: 5-8 pages
- Medium complexity: 9-15 pages
- Full biography: 16-25 pages
6. Analyze content signals for style/layout recommendations
7. **Save to `analysis.md`**
### Step 2: Define Characters
**analysis.md Format**:
**Purpose**: Establish visual consistency across all pages.
```yaml
---
title: "Alan Turing: Father of Computing"
topic: Biography
time_span: 1912-1954
source_language: en
user_language: zh
aspect_ratio: "3:4"
recommended_page_count: 12
---
1. Extract all characters from content (protagonist, supporting, antagonist, narrator)
2. Create `characters/characters.md` with visual specs for each character
3. Generate `characters/characters.png` (character reference sheet)
## Target Audience
**Reference**: `references/character-template.md` for detailed format and examples.
- **Primary**: Tech enthusiasts curious about computing history
- **Secondary**: Students learning about scientific breakthroughs
- **Tertiary**: General readers interested in biographical stories
### Step 3: Generate Outline
## Value Proposition
Create `outline.md` with:
- Metadata (title, style, layout, page count, character reference path)
- Cover design
- Each page: layout, panel breakdown, visual prompts
What readers will gain:
1. Understanding of how modern computing was born
2. Emotional connection to a brilliant but tragic figure
3. Appreciation for the human cost of innovation
**Reference**: `references/outline-template.md` for detailed format.
## Core Themes
| Theme | Narrative Potential | Visual Opportunity |
|-------|--------------------|--------------------|
| Genius vs. Society | High conflict, dramatic arcs | Contrast scenes |
| Code-breaking | Mystery, tension | Technical diagrams as art |
| Personal tragedy | Emotional depth | Intimate, somber panels |
## Key Figures & Story Arcs
### Alan Turing (Protagonist)
- **Arc**: Misunderstood genius → War hero → Tragic end
- **Visual identity**: Disheveled academic, intense eyes
- **Key moments**: Enigma breakthrough, arrest, final days
### Christopher Morcom (Catalyst)
- **Role**: Early friend whose death shaped Turing
- **Visual identity**: Youthful, bright
- **Key moments**: School friendship, sudden death
## Content Signals
- "biography" → classic + mixed
- "computing history" → ohmsha + dense
- "personal tragedy" → dramatic + splash
## Recommended Approaches
1. **Chronological** - follow life timeline (recommended for biography)
2. **Thematic** - organize by contributions (good for educational focus)
3. **Character-focused** - relationships drive narrative (good for emotional impact)
```
### Step 2: Generate 3 Storyboard Variants
Create three distinct variants, each combining a narrative approach with a recommended style.
| Variant | Narrative Approach | Recommended Style | Layout |
|---------|-------------------|-------------------|--------|
| A | Chronological | sepia | cinematic |
| B | Thematic | ohmsha | dense |
| C | Character-focused | warm | standard |
**For each variant**:
1. **Generate storyboard** (`storyboard-{approach}.md`):
- YAML front matter with narrative_approach, recommended_style, recommended_layout, aspect_ratio
- Cover design
- Each page: layout, panel breakdown, visual prompts
- **Written in user's preferred language**
- Reference: `references/storyboard-template.md`
2. **Generate matching characters** (`characters-{approach}/`):
- `characters.md` - visual specs matching the recommended style (in user's preferred language)
- `characters.png` - character reference sheet
- Reference: `references/character-template.md`
**All variants are preserved after selection for reference.**
### Step 3: User Confirms All Options
**IMPORTANT**: Present ALL options in a single confirmation step using AskUserQuestion. Do NOT interrupt workflow with multiple separate confirmations.
**Determine which questions to ask**:
| Question | When to Ask |
|----------|-------------|
| Storyboard variant | Always (required) |
| Visual style | Always (required) |
| Language | Only if `source_language ≠ user_language` |
| Aspect ratio | Only if user might prefer non-default (e.g., landscape content) |
**Language handling**:
- If source language = user language: Just inform user (e.g., "Comic will be in Chinese")
- If different: Ask which language to use
**All storyboards and prompts are generated in the user's selected/preferred language.**
**Aspect ratio handling**:
- Default: 3:4 (portrait) - standard comic format
- Offer 4:3 (landscape) if content suits it (e.g., panoramic scenes, technical diagrams)
- Offer 16:9 (widescreen) for cinematic content
**AskUserQuestion format** (example with all questions):
```
Question 1 (Storyboard): Which storyboard variant?
- A: Chronological + sepia (Recommended)
- B: Thematic + ohmsha
- C: Character-focused + warm
- Custom
Question 2 (Style): Which visual style?
- sepia (Recommended from variant)
- classic / dramatic / warm / sepia / vibrant / ohmsha / realistic / wuxia
- Custom description
Question 3 (Language) - only if mismatch:
- Chinese (source material language)
- English (your preference)
Question 4 (Aspect) - only if relevant:
- 3:4 Portrait (Recommended)
- 4:3 Landscape
- 16:9 Widescreen
```
**After confirmation**:
1. Copy selected storyboard → `storyboard.md`
2. Copy selected characters → `characters/`
3. Update YAML front matter with confirmed style, language, aspect_ratio
4. If style differs from variant's recommended: regenerate `characters/characters.png`
5. User may edit files directly for fine-tuning
### Step 4: Generate Images
For each page (cover + pages):
1. Save prompt to `prompts/XX-page.md`
2. Call image generation skill with:
- Base prompt: `references/base-prompt.md`
- Character reference (text or image, depending on skill capability)
- Page prompt
- Output path
**Image Generation Skill Selection**:
- Check available image generation skills in the environment
- Adapt parameters based on skill capabilities:
- If supports `--promptfiles`: pass prompt files
- If supports reference image: pass `characters/characters.png`
- If text-only: concatenate prompts into single text
- If multiple skills available, ask user preference
**Session Management**:
If the image generation skill supports `--sessionId`:
1. Generate a unique session ID at the start (e.g., `comic-{topic-slug}-{timestamp}`)
2. Use the same session ID for character sheet and all pages
3. This ensures visual consistency (character appearance, style) across all generated images
With confirmed storyboard + style + aspect ratio:
**For each page (cover + pages)**:
1. Save prompt to `prompts/NN-{cover|page}-[slug].md` (in user's preferred language)
2. Generate image using confirmed style and aspect ratio
3. Report progress after each generation
### Step 5: Completion Report
**Image Generation Skill Selection**:
- Check available image generation skills
- If multiple skills available, ask user preference
**Character Reference Handling**:
- If skill supports reference image: pass `characters/characters.png`
- If skill does NOT support reference image: include `characters/characters.md` content in prompt
**Session Management**:
If image generation skill supports `--sessionId`:
1. Generate unique session ID: `comic-{topic-slug}-{timestamp}`
2. Use same session ID for all pages
3. Ensures visual consistency across generated images
### Step 5: Merge to PDF
After all images generated:
```bash
npx -y bun ${SKILL_DIR}/scripts/merge-to-pdf.ts <comic-dir>
```
Creates `{topic-slug}.pdf` with all pages as full-page images.
### Step 6: Completion Report
```
Comic Complete!
Title: [title] | Style: [style] | Pages: [count]
Title: [title] | Style: [style] | Pages: [count] | Aspect: [ratio] | Language: [lang]
Location: [path]
✓ analysis.md
✓ characters.png
✓ 00-cover.png ... XX-page.png
✓ 00-cover-[slug].png ... NN-page-[slug].png
✓ {topic-slug}.pdf
```
## Page Modification
Support for modifying individual pages after initial generation.
### Edit Single Page
Regenerate a specific page with modified prompt:
1. Identify page to edit (e.g., `03-page-enigma-machine.png`)
2. Update prompt in `prompts/03-page-enigma-machine.md` if needed
3. If content changes significantly, update slug in filename
4. Regenerate image using same session ID and aspect ratio
5. Regenerate PDF
### Add New Page
Insert a new page at specified position:
1. Specify insertion position (e.g., after page 3)
2. Create new prompt with appropriate slug (e.g., `04-page-bletchley-park.md`)
3. Generate new page image (same aspect ratio)
4. **Renumber files**: All subsequent pages increment NN by 1
- `04-page-tragedy.png``05-page-tragedy.png`
- Slugs remain unchanged
5. Update `storyboard.md` with new page entry
6. Regenerate PDF
### Delete Page
Remove a page and renumber:
1. Identify page to delete (e.g., `03-page-enigma-machine.png`)
2. Remove image file and prompt file
3. **Renumber files**: All subsequent pages decrement NN by 1
- `04-page-tragedy.png``03-page-tragedy.png`
- Slugs remain unchanged
4. Update `storyboard.md` to remove page entry
5. Regenerate PDF
### File Naming Convention
Files use meaningful slugs for better readability:
```
NN-cover-[slug].png / NN-page-[slug].png
NN-cover-[slug].md / NN-page-[slug].md (in prompts/)
```
Examples:
- `00-cover-turing-story.png`
- `01-page-early-life.png`
- `02-page-cambridge-years.png`
- `03-page-enigma-machine.png`
**Slug rules**:
- Derived from page title/content (kebab-case)
- Must be unique within the comic
- When page content changes significantly, update slug accordingly
**Renumbering**:
- After add/delete, update NN prefix for affected pages
- Slug remains unchanged unless content changes
- Maintain sequential numbering with no gaps
## Style-Specific Guidelines
### Ohmsha Style (`--style ohmsha`)
Additional requirements for educational manga:
- Default characters: Student (大雄), Mentor (哆啦A梦), Antagonist (胖虎)
- Custom: `--characters "Student:小明,Mentor:教授"`
- **Default: Use Doraemon characters directly** - No need to create new characters
- 大雄 (Nobita): Student role, curious learner
- 哆啦A梦 (Doraemon): Mentor role, explains concepts with gadgets
- 胖虎 (Gian): Antagonist/challenge role, represents obstacles or misconceptions
- 静香 (Shizuka): Supporting role, asks clarifying questions
- Custom characters only if explicitly requested: `--characters "Student:小明,Mentor:教授"`
- Must use visual metaphors (gadgets, action scenes) - NO talking heads
- Page titles: narrative style, not "Page X: Topic"
@@ -137,8 +391,19 @@ Additional requirements for educational manga:
## References
Detailed templates and guidelines in `references/` directory:
- `analysis-framework.md` - Deep content analysis for comic adaptation
- `character-template.md` - Character definition format and examples
- `outline-template.md` - Outline structure and panel breakdown
- `storyboard-template.md` - Storyboard structure and panel breakdown
- `ohmsha-guide.md` - Ohmsha manga style specifics
- `styles/` - Detailed style definitions
- `layouts/` - Detailed layout definitions
## Extension Support
Custom styles and configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-comic/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-comic/EXTEND.md` (user)
If found, load before Step 1. Extension content overrides defaults.
@@ -0,0 +1,152 @@
# Comic Content Analysis Framework
Deep analysis framework for transforming source content into effective visual storytelling.
## Purpose
Before creating a comic, thoroughly analyze the source material to:
- Identify the target audience and their needs
- Determine what value the comic will deliver
- Extract narrative potential for visual storytelling
- Plan character arcs and key moments
## Analysis Dimensions
### 1. Core Content (Understanding "What")
**Central Message**
- What is the single most important idea readers should take away?
- Can you express it in one sentence?
**Key Concepts**
- What are the essential concepts readers must understand?
- How should these concepts be visualized?
- Which concepts need simplified explanations?
**Content Structure**
- How is the source material organized?
- What is the natural narrative arc?
- Where are the climax and turning points?
**Evidence & Examples**
- What concrete examples, data, or stories support the main ideas?
- Which examples translate well to visual panels?
- What can be shown rather than told?
### 2. Context & Background (Understanding "Why")
**Source Origin**
- Who created this content? What is their perspective?
- What was the original purpose?
- Is there bias to be aware of?
**Historical/Cultural Context**
- When and where does the story take place?
- What background knowledge do readers need?
- What period-specific visual elements are required?
**Underlying Assumptions**
- What does the source assume readers already know?
- What implicit beliefs or values are present?
- Should the comic challenge or reinforce these?
### 3. Audience Analysis
**Primary Audience**
- Who will read this comic?
- What is their existing knowledge level?
- What are their interests and motivations?
**Secondary Audiences**
- Who else might benefit from this comic?
- How might their needs differ?
**Reader Questions**
- What questions will readers have?
- What misconceptions might they bring?
- What "aha moments" can we create?
### 4. Value Proposition
**Knowledge Value**
- What will readers learn?
- What new perspectives will they gain?
- How will this change their understanding?
**Emotional Value**
- What emotions should readers feel?
- What connections will they make with characters?
- What will make this memorable?
**Practical Value**
- Can readers apply what they learn?
- What actions might this inspire?
- What conversations might it spark?
### 5. Narrative Potential
**Story Arc Candidates**
- What natural narratives exist in the content?
- Where is the conflict or tension?
- What transformations occur?
**Character Potential**
- Who are the key figures?
- What are their motivations and obstacles?
- How do they change throughout?
**Visual Opportunities**
- What scenes have strong visual potential?
- Where can abstract concepts become concrete images?
- What metaphors can be visualized?
**Dramatic Moments**
- What are the breakthrough/revelation moments?
- Where are the emotional peaks?
- What creates tension and release?
### 6. Adaptation Considerations
**What to Keep**
- Essential facts and ideas
- Key quotes or moments
- Core emotional beats
**What to Simplify**
- Complex explanations
- Dense technical details
- Lengthy descriptions
**What to Expand**
- Brief mentions that deserve more attention
- Implied emotions or relationships
- Visual details not in source
**What to Omit**
- Tangential information
- Redundant examples
- Content that doesn't serve the narrative
## Output Format
Analysis results should be saved to `analysis.md` with:
1. **YAML Front Matter**: Metadata (title, topic, time_span, languages, aspect_ratio, page_count)
2. **Target Audience**: Primary, secondary, tertiary audiences with their needs
3. **Value Proposition**: What readers will gain (knowledge, emotional, practical)
4. **Core Themes**: Table with theme, narrative potential, visual opportunity
5. **Key Figures & Story Arcs**: Character profiles with arcs, visual identity, key moments
6. **Content Signals**: Style and layout recommendations based on content type
7. **Recommended Approaches**: Narrative approaches ranked by suitability
## Analysis Checklist
Before proceeding to storyboard:
- [ ] Can I state the core message in one sentence?
- [ ] Do I know exactly who will read this comic?
- [ ] Have I identified at least 3 ways this comic provides value?
- [ ] Are there clear protagonists with compelling arcs?
- [ ] Have I found at least 5 visually powerful moments?
- [ ] Do I understand what to keep, simplify, expand, and omit?
- [ ] Have I identified the emotional peaks and valleys?
@@ -20,4 +20,4 @@ Technical explanations, complex narratives, timelines
## Best Style Pairings
tech, ohmsha, vibrant
ohmsha, vibrant
@@ -27,4 +27,4 @@ Ohmsha-style tutorials, mobile reading, step-by-step guides
## Best Style Pairings
ohmsha, tech, vibrant
ohmsha, vibrant
@@ -1,23 +1,30 @@
# Outline Template
# Storyboard Template
## Outline Document Format
## Storyboard Document Format
```markdown
# [Comic Title] - Knowledge Comic Outline
---
title: "[Comic Title]"
topic: "[topic description]"
time_span: "[e.g., 1912-1954]"
narrative_approach: "[chronological/thematic/character-focused]"
recommended_style: "[style name]"
recommended_layout: "[layout name or varies]"
aspect_ratio: "3:4" # 3:4 (portrait), 4:3 (landscape), 16:9 (widescreen)
language: "[zh/en/ja/etc.]"
page_count: [N]
generated: "YYYY-MM-DD HH:mm"
---
# [Comic Title] - Knowledge Comic Storyboard
**Topic**: [topic description]
**Time Span**: [e.g., 1912-1954]
**Style**: [selected style]
**Default Layout**: [selected layout or "varies"]
**Page Count**: Cover + N pages
**Character Reference**: characters/characters.png
**Generated**: YYYY-MM-DD HH:mm
---
## Cover
**Filename**: 00-cover.png
**Filename**: 00-cover-[slug].png
**Core Message**: [one-liner]
**Visual Design**:
@@ -33,7 +40,7 @@
## Page 1 / N
**Filename**: 01-page.png
**Filename**: 01-page-[slug].png
**Layout**: [standard/cinematic/dense/splash/mixed]
**Narrative Layer**: [Main narrative / Narrator layer / Mixed]
**Core Message**: [What this page conveys]
@@ -0,0 +1,66 @@
# realistic
Full-color realistic manga style with digital painting techniques
## Style Guidelines
### Line Work
- Clean, precise outlines with clear contours
- Uniform line weight for character definition
- No excessive hatching - rely on color for depth
- Smooth curves and realistic anatomical lines
- Ligne Claire influence: clean but not overly simplified
### Character Design
- Realistic human proportions (7-8 head heights)
- Anatomically accurate features and expressions
- Detailed facial structure without exaggeration
- Natural poses and body language
- Consistent character appearance across all panels
- Subtle expressions rather than manga-style exaggeration
### Rendering Style
- Full-color digital painting with rich gradients
- Soft shadow transitions on skin and fabric
- Realistic material textures (glass, liquid, fabric, wood)
- Detailed hair with natural shine and volume
- Environmental lighting affects all elements
- NOT flat cel-shading - use smooth color blending
### Background Treatment
- Highly detailed, realistic environments
- Accurate perspective and spatial depth
- Atmospheric lighting (warm indoor, cool outdoor)
- Professional settings rendered with precision
- Props and objects with realistic textures
## Color Palette
- Skin: Natural warm tones (#F5D6C6, #E8C4B0)
- Hair: Rich browns and blacks with highlights
- Environment: Warm wood tones (#8B7355), cool stone (#9CA3AF)
- Accent: Wine red (#722F37), gold (#D4AF37)
- Lighting: Warm amber (#FFB347), cool blue (#B0C4DE)
## Color Approach
- Rich gradients for depth and volume
- Realistic lighting with warm/cool contrast
- Material-specific rendering (glass transparency, liquid reflection)
- Subtle color temperature shifts for atmosphere
- Professional, sophisticated palette
## Quality Markers
A good realistic manga page exhibits:
- ✓ Anatomically accurate character proportions
- ✓ Smooth color gradients (not flat fills)
- ✓ Realistic material textures
- ✓ Detailed, atmospheric backgrounds
- ✓ Natural lighting with soft shadows
- ✓ Expressive but subtle facial expressions
- ✓ Professional, sophisticated aesthetic
- ✓ Clean speech bubbles with clear typography
## Best For
Professional topics (wine, food, business), lifestyle content, adult-oriented narratives, educational guides for mature audiences, documentary-style storytelling
@@ -1,34 +0,0 @@
# tech
Modern, digital-age narratives
## Style Guidelines
### Line Work
- 2px, precise geometric undertones
- Clean, technical precision
- Circuit-like patterns in backgrounds
### Character Design
- Contemporary clothing, focused expressions
- Modern tech accessories
- Clean, precise features
### Background Treatment
- Digital elements, screens, circuit motifs
- Grid patterns
- Glowing interface elements
## Color Palette
- Primary: Cyan (#00D4FF), deep blue (#1A365D), white
- Accents: Neon green (#00FF88), electric purple (#805AD5)
- Background: Dark gray (#1A202C), grid patterns
## Mood
Innovation, digital, contemporary
## Best For
Computing history, AI stories, modern tech
@@ -0,0 +1,54 @@
# wuxia
Hong Kong martial arts comic style (港漫武侠风格)
## Style Guidelines
### Line Work
- 2-3px dynamic brush strokes with varying weight
- Ink wash effects, traditional Chinese brush feel
- Speed lines and impact effects for action scenes
- Bold, confident strokes with sharp edges
### Character Design
- Realistic human proportions (7.5-8 head heights)
- Defined musculature, dynamic warrior poses
- Flowing hair and clothing in motion
- Intense, determined facial expressions
- Traditional Chinese martial arts attire (robes, armor)
### Background Treatment
- Dramatic landscapes: mountains, waterfalls, temples
- Ink wash atmospheric effects
- Flying debris, dust clouds during combat
- Energy/qi effects with flowing lines
- High contrast silhouettes
## Color Palette
- Primary: Deep black ink, crimson red (#8B0000), gold (#D4AF37)
- Skin: Natural tan with dramatic shadows
- Background: Misty grays, earth tones, ink wash gradients
- Effects: Glowing qi energy (blue, gold, white)
## Special Effects
- **Combat impact**: Radiating lines, shockwaves, debris
- **Qi/energy**: Flowing aura, glowing effects around characters
- **Movement**: Extreme speed lines, motion blur
- **Atmosphere**: Ink wash clouds, mist, flying leaves/petals
## Quality Markers
A good wuxia comic page exhibits:
- ✓ Dynamic action poses with sense of motion
- ✓ Ink brush aesthetic in line work
- ✓ High contrast dramatic lighting
- ✓ Atmospheric backgrounds with Chinese elements
- ✓ Energy/qi effects that feel powerful
- ✓ Flowing fabric and hair movement
- ✓ Impactful combat moments
## Best For
Martial arts stories, Chinese historical fiction, action-heavy narratives, wuxia/xianxia adaptations
+116
View File
@@ -0,0 +1,116 @@
import { existsSync, readdirSync, readFileSync } from "fs";
import { join, basename } from "path";
import { PDFDocument } from "pdf-lib";
interface PageInfo {
filename: string;
path: string;
index: number;
promptPath?: string;
}
function parseArgs(): { dir: string; output?: string } {
const args = process.argv.slice(2);
let dir = "";
let output: string | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--output" || args[i] === "-o") {
output = args[++i];
} else if (!args[i].startsWith("-")) {
dir = args[i];
}
}
if (!dir) {
console.error("Usage: bun merge-to-pdf.ts <comic-dir> [--output filename.pdf]");
process.exit(1);
}
return { dir, output };
}
function findComicPages(dir: string): PageInfo[] {
if (!existsSync(dir)) {
console.error(`Directory not found: ${dir}`);
process.exit(1);
}
const files = readdirSync(dir);
const pagePattern = /^(\d+)-(cover|page)(-[\w-]+)?\.(png|jpg|jpeg)$/i;
const promptsDir = join(dir, "prompts");
const hasPrompts = existsSync(promptsDir);
const pages: PageInfo[] = files
.filter((f) => pagePattern.test(f))
.map((f) => {
const match = f.match(pagePattern);
const baseName = f.replace(/\.(png|jpg|jpeg)$/i, "");
const promptPath = hasPrompts ? join(promptsDir, `${baseName}.md`) : undefined;
return {
filename: f,
path: join(dir, f),
index: parseInt(match![1], 10),
promptPath: promptPath && existsSync(promptPath) ? promptPath : undefined,
};
})
.sort((a, b) => a.index - b.index);
if (pages.length === 0) {
console.error(`No comic pages found in: ${dir}`);
console.error("Expected format: 00-cover-slug.png, 01-page-slug.png, etc.");
process.exit(1);
}
return pages;
}
async function createPdf(pages: PageInfo[], outputPath: string) {
const pdfDoc = await PDFDocument.create();
pdfDoc.setAuthor("baoyu-comic");
pdfDoc.setSubject("Generated Comic");
for (const page of pages) {
const imageData = readFileSync(page.path);
const ext = page.filename.toLowerCase();
const image = ext.endsWith(".png")
? await pdfDoc.embedPng(imageData)
: await pdfDoc.embedJpg(imageData);
const { width, height } = image;
const pdfPage = pdfDoc.addPage([width, height]);
pdfPage.drawImage(image, {
x: 0,
y: 0,
width,
height,
});
console.log(`Added: ${page.filename}${page.promptPath ? " (prompt available)" : ""}`);
}
const pdfBytes = await pdfDoc.save();
await Bun.write(outputPath, pdfBytes);
console.log(`\nCreated: ${outputPath}`);
console.log(`Total pages: ${pages.length}`);
}
async function main() {
const { dir, output } = parseArgs();
const pages = findComicPages(dir);
const dirName = basename(dir) === "comic" ? basename(join(dir, "..")) : basename(dir);
const outputPath = output || join(dir, `${dirName}.pdf`);
console.log(`Found ${pages.length} pages in: ${dir}\n`);
await createPdf(pages, outputPath);
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});
+188
View File
@@ -0,0 +1,188 @@
---
name: baoyu-compress-image
description: Cross-platform image compression skill. Converts images to WebP by default with PNG-to-PNG support. Uses system tools (sips, cwebp, ImageMagick) with Sharp fallback.
---
# Image Compressor
Cross-platform image compression with WebP default output, PNG-to-PNG support, preferring system tools with Sharp fallback.
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | CLI entry point for image compression |
## Quick Start
```bash
# Compress to WebP (default)
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png
# Keep original format (PNG → PNG)
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png --format png
# Custom quality
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png -q 75
# Process directory
npx -y bun ${SKILL_DIR}/scripts/main.ts ./images/ -r
```
## Commands
### Single File Compression
```bash
# Basic (converts to WebP, replaces original)
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png
# Custom output path
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png -o compressed.webp
# Keep original file
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png --keep
# Custom quality (0-100, default: 80)
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png -q 75
# Keep original format
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png -f png
```
### Directory Processing
```bash
# Process all images in directory
npx -y bun ${SKILL_DIR}/scripts/main.ts ./images/
# Recursive processing
npx -y bun ${SKILL_DIR}/scripts/main.ts ./images/ -r
# With custom quality
npx -y bun ${SKILL_DIR}/scripts/main.ts ./images/ -r -q 75
```
### Output Formats
```bash
# Plain text (default)
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png
# JSON output
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png --json
```
## Options
| Option | Short | Description | Default |
|--------|-------|-------------|---------|
| `<input>` | | Input file or directory | Required |
| `--output <path>` | `-o` | Output path | Same path, new extension |
| `--format <fmt>` | `-f` | webp, png, jpeg | webp |
| `--quality <n>` | `-q` | Quality 0-100 | 80 |
| `--keep` | `-k` | Keep original file | false |
| `--recursive` | `-r` | Process directories recursively | false |
| `--json` | | JSON output | false |
| `--help` | `-h` | Show help | |
## Compressor Selection
Priority order (auto-detected):
1. **sips** (macOS built-in, WebP support since macOS 11)
2. **cwebp** (Google's official WebP tool)
3. **ImageMagick** (`convert` command)
4. **Sharp** (npm package, auto-installed by Bun)
The skill automatically selects the best available compressor.
## Output Format
### Text Mode (default)
```
image.png → image.webp (245KB → 89KB, 64% reduction)
```
### JSON Mode
```json
{
"input": "image.png",
"output": "image.webp",
"inputSize": 250880,
"outputSize": 91136,
"ratio": 0.36,
"compressor": "sips"
}
```
### Directory JSON Mode
```json
{
"files": [...],
"summary": {
"totalFiles": 10,
"totalInputSize": 2508800,
"totalOutputSize": 911360,
"ratio": 0.36,
"compressor": "sips"
}
}
```
## Examples
### Compress single image
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts photo.png
# photo.png → photo.webp (1.2MB → 340KB, 72% reduction)
```
### Compress with custom quality
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts photo.png -q 60
# photo.png → photo.webp (1.2MB → 280KB, 77% reduction)
```
### Keep original format
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts screenshot.png -f png --keep
# screenshot.png → screenshot-compressed.png (500KB → 380KB, 24% reduction)
```
### Process entire directory
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts ./screenshots/ -r
# Processed 15 files: 12.5MB → 4.2MB (66% reduction)
```
### Get JSON for scripting
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png --json | jq '.ratio'
```
## Extension Support
Custom configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-compress-image/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-compress-image/EXTEND.md` (user)
If found, load before workflow. Extension content overrides defaults.
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env bun
import { existsSync, statSync, readdirSync, unlinkSync, renameSync } from "fs";
import { basename, dirname, extname, join, resolve } from "path";
import { spawn } from "child_process";
type Compressor = "sips" | "cwebp" | "imagemagick" | "sharp";
type Format = "webp" | "png" | "jpeg";
interface Options {
input: string;
output?: string;
format: Format;
quality: number;
keep: boolean;
recursive: boolean;
json: boolean;
}
interface Result {
input: string;
output: string;
inputSize: number;
outputSize: number;
ratio: number;
compressor: Compressor;
}
const SUPPORTED_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".tiff"];
async function commandExists(cmd: string): Promise<boolean> {
try {
const proc = spawn("which", [cmd], { stdio: "pipe" });
return new Promise((res) => {
proc.on("close", (code) => res(code === 0));
proc.on("error", () => res(false));
});
} catch {
return false;
}
}
async function detectCompressor(format: Format): Promise<Compressor> {
if (format === "webp") {
if (await commandExists("cwebp")) return "cwebp";
if (await commandExists("convert")) return "imagemagick";
return "sharp";
}
if (process.platform === "darwin") return "sips";
if (await commandExists("convert")) return "imagemagick";
return "sharp";
}
function runCmd(cmd: string, args: string[]): Promise<{ code: number; stderr: string }> {
return new Promise((res) => {
const proc = spawn(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
let stderr = "";
proc.stderr?.on("data", (d) => (stderr += d.toString()));
proc.on("close", (code) => res({ code: code ?? 1, stderr }));
proc.on("error", (e) => res({ code: 1, stderr: e.message }));
});
}
async function compressWithSips(input: string, output: string, format: Format, quality: number): Promise<void> {
const fmt = format === "jpeg" ? "jpeg" : format;
const args = ["-s", "format", fmt, "-s", "formatOptions", String(quality), input, "--out", output];
const { code, stderr } = await runCmd("sips", args);
if (code !== 0) throw new Error(`sips failed: ${stderr}`);
}
async function compressWithCwebp(input: string, output: string, quality: number): Promise<void> {
const args = ["-q", String(quality), input, "-o", output];
const { code, stderr } = await runCmd("cwebp", args);
if (code !== 0) throw new Error(`cwebp failed: ${stderr}`);
}
async function compressWithImagemagick(input: string, output: string, quality: number): Promise<void> {
const args = [input, "-quality", String(quality), output];
const { code, stderr } = await runCmd("convert", args);
if (code !== 0) throw new Error(`convert failed: ${stderr}`);
}
async function compressWithSharp(input: string, output: string, format: Format, quality: number): Promise<void> {
const sharp = (await import("sharp")).default;
let pipeline = sharp(input);
if (format === "webp") pipeline = pipeline.webp({ quality });
else if (format === "png") pipeline = pipeline.png({ quality });
else if (format === "jpeg") pipeline = pipeline.jpeg({ quality });
await pipeline.toFile(output);
}
async function compress(
compressor: Compressor,
input: string,
output: string,
format: Format,
quality: number
): Promise<void> {
switch (compressor) {
case "sips":
await compressWithSips(input, output, format, quality);
break;
case "cwebp":
if (format !== "webp") {
await compressWithSharp(input, output, format, quality);
} else {
await compressWithCwebp(input, output, quality);
}
break;
case "imagemagick":
await compressWithImagemagick(input, output, quality);
break;
case "sharp":
await compressWithSharp(input, output, format, quality);
break;
}
}
function getOutputPath(input: string, format: Format, keep: boolean, customOutput?: string): string {
if (customOutput) return resolve(customOutput);
const dir = dirname(input);
const base = basename(input, extname(input));
const ext = format === "jpeg" ? ".jpg" : `.${format}`;
if (keep && extname(input).toLowerCase() === ext) {
return join(dir, `${base}-compressed${ext}`);
}
return join(dir, `${base}${ext}`);
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
async function processFile(
compressor: Compressor,
input: string,
opts: Options
): Promise<Result> {
const absInput = resolve(input);
const inputSize = statSync(absInput).size;
const output = getOutputPath(absInput, opts.format, opts.keep, opts.output);
const tempOutput = output + ".tmp";
await compress(compressor, absInput, tempOutput, opts.format, opts.quality);
const outputSize = statSync(tempOutput).size;
if (!opts.keep && absInput !== output) {
unlinkSync(absInput);
}
renameSync(tempOutput, output);
return {
input: absInput,
output,
inputSize,
outputSize,
ratio: outputSize / inputSize,
compressor,
};
}
function collectFiles(dir: string, recursive: boolean): string[] {
const files: string[] = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory() && recursive) {
files.push(...collectFiles(full, recursive));
} else if (entry.isFile() && SUPPORTED_EXTS.includes(extname(entry.name).toLowerCase())) {
files.push(full);
}
}
return files;
}
function printHelp() {
console.log(`Usage: bun main.ts <input> [options]
Options:
-o, --output <path> Output path
-f, --format <fmt> Output format: webp, png, jpeg (default: webp)
-q, --quality <n> Quality 0-100 (default: 80)
-k, --keep Keep original file
-r, --recursive Process directories recursively
--json JSON output
-h, --help Show help`);
}
function parseArgs(args: string[]): Options | null {
const opts: Options = {
input: "",
format: "webp",
quality: 80,
keep: false,
recursive: false,
json: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
printHelp();
process.exit(0);
} else if (arg === "-o" || arg === "--output") {
opts.output = args[++i];
} else if (arg === "-f" || arg === "--format") {
const fmt = args[++i]?.toLowerCase();
if (fmt === "webp" || fmt === "png" || fmt === "jpeg" || fmt === "jpg") {
opts.format = fmt === "jpg" ? "jpeg" : (fmt as Format);
} else {
console.error(`Invalid format: ${fmt}`);
return null;
}
} else if (arg === "-q" || arg === "--quality") {
const q = parseInt(args[++i], 10);
if (isNaN(q) || q < 0 || q > 100) {
console.error(`Invalid quality: ${args[i]}`);
return null;
}
opts.quality = q;
} else if (arg === "-k" || arg === "--keep") {
opts.keep = true;
} else if (arg === "-r" || arg === "--recursive") {
opts.recursive = true;
} else if (arg === "--json") {
opts.json = true;
} else if (!arg.startsWith("-") && !opts.input) {
opts.input = arg;
}
}
if (!opts.input) {
console.error("Error: Input file or directory required");
printHelp();
return null;
}
return opts;
}
async function main() {
const args = process.argv.slice(2);
const opts = parseArgs(args);
if (!opts) process.exit(1);
const input = resolve(opts.input);
if (!existsSync(input)) {
console.error(`Error: ${input} not found`);
process.exit(1);
}
const compressor = await detectCompressor(opts.format);
const isDir = statSync(input).isDirectory();
if (isDir) {
const files = collectFiles(input, opts.recursive);
if (files.length === 0) {
console.error("No supported images found");
process.exit(1);
}
const results: Result[] = [];
for (const file of files) {
try {
const r = await processFile(compressor, file, { ...opts, output: undefined });
results.push(r);
if (!opts.json) {
const reduction = Math.round((1 - r.ratio) * 100);
console.log(`${r.input}${r.output} (${formatSize(r.inputSize)}${formatSize(r.outputSize)}, ${reduction}% reduction)`);
}
} catch (e) {
if (!opts.json) console.error(`Error processing ${file}: ${(e as Error).message}`);
}
}
if (opts.json) {
const totalInput = results.reduce((s, r) => s + r.inputSize, 0);
const totalOutput = results.reduce((s, r) => s + r.outputSize, 0);
console.log(
JSON.stringify({
files: results,
summary: {
totalFiles: results.length,
totalInputSize: totalInput,
totalOutputSize: totalOutput,
ratio: totalInput > 0 ? totalOutput / totalInput : 0,
compressor,
},
}, null, 2)
);
} else {
const totalInput = results.reduce((s, r) => s + r.inputSize, 0);
const totalOutput = results.reduce((s, r) => s + r.outputSize, 0);
const reduction = Math.round((1 - totalOutput / totalInput) * 100);
console.log(`\nProcessed ${results.length} files: ${formatSize(totalInput)}${formatSize(totalOutput)} (${reduction}% reduction)`);
}
} else {
try {
const r = await processFile(compressor, input, opts);
if (opts.json) {
console.log(JSON.stringify(r, null, 2));
} else {
const reduction = Math.round((1 - r.ratio) * 100);
console.log(`${r.input}${r.output} (${formatSize(r.inputSize)}${formatSize(r.outputSize)}, ${reduction}% reduction)`);
}
} catch (e) {
console.error(`Error: ${(e as Error).message}`);
process.exit(1);
}
}
}
main();
+144 -45
View File
@@ -14,9 +14,9 @@ Generate hand-drawn style cover images for articles with multiple style options.
/baoyu-cover-image path/to/article.md
# Specify a style
/baoyu-cover-image path/to/article.md --style tech
/baoyu-cover-image path/to/article.md --style blueprint
/baoyu-cover-image path/to/article.md --style warm
/baoyu-cover-image path/to/article.md --style bold
/baoyu-cover-image path/to/article.md --style dark-atmospheric
# Without title text
/baoyu-cover-image path/to/article.md --no-title
@@ -38,6 +38,8 @@ Generate hand-drawn style cover images for articles with multiple style options.
| Option | Description |
|--------|-------------|
| `--style <name>` | Specify cover style (see Style Gallery below) |
| `--aspect <ratio>` | Aspect ratio: 2.35:1 (cinematic, default), 16:9 (widescreen), 1:1 (social) |
| `--lang <code>` | Output language for title text (en, zh, ja, etc.) |
| `--no-title` | Generate cover without title text (visual only) |
## Style Gallery
@@ -45,13 +47,25 @@ Generate hand-drawn style cover images for articles with multiple style options.
| Style | Description |
|-------|-------------|
| `elegant` (Default) | Refined, sophisticated, understated |
| `tech` | Modern, clean, futuristic |
| `warm` | Friendly, approachable, human-centered |
| `blueprint` | Technical schematics, engineering precision |
| `bold` | High contrast, attention-grabbing, energetic |
| `bold-editorial` | Magazine cover impact, dramatic typography |
| `chalkboard` | Black chalkboard, colorful chalk drawings |
| `dark-atmospheric` | Cinematic dark mode, glowing accents |
| `editorial-infographic` | Magazine explainer, visual storytelling |
| `fantasy-animation` | Ghibli/Disney inspired, whimsical charm |
| `intuition-machine` | Technical briefing, bilingual labels |
| `minimal` | Ultra-clean, zen-like, focused |
| `playful` | Fun, creative, whimsical |
| `nature` | Organic, calm, earthy |
| `retro` | Vintage, nostalgic, classic |
| `notion` | Clean SaaS dashboard, productivity styling |
| `pixel-art` | Retro 8-bit, nostalgic gaming aesthetic |
| `playful` | Fun, creative, whimsical |
| `retro` | Halftone dots, vintage badges, classic |
| `sketch-notes` | Hand-drawn, educational, warm |
| `vector-illustration` | Flat vector, black outlines, retro colors |
| `vintage` | Aged paper, historical, expedition style |
| `warm` | Friendly, approachable, human-centered |
| `watercolor` | Soft hand-painted, natural warmth |
Detailed style definitions: `references/styles/<style>.md`
@@ -61,58 +75,125 @@ When no `--style` is specified, the system analyzes content to select the best s
| Content Signals | Selected Style |
|----------------|----------------|
| AI, coding, tech, digital, algorithm | `tech` |
| Architecture, system design, engineering | `blueprint` |
| Product launch, keynote, marketing, brand | `bold-editorial` |
| Education, classroom, tutorial, teaching | `chalkboard` |
| Entertainment, creative, premium, cinematic | `dark-atmospheric` |
| Technology explainer, science, research | `editorial-infographic` |
| Storytelling, children, fantasy, magical | `fantasy-animation` |
| Technical docs, academic, bilingual | `intuition-machine` |
| Personal story, emotion, growth, life | `warm` |
| Controversial, urgent, must-read, warning | `bold` |
| Simple, zen, focus, essential | `minimal` |
| Fun, easy, beginner, casual, tutorial | `playful` |
| Fun, easy, beginner, casual | `playful` |
| Nature, eco, wellness, health, organic | `nature` |
| History, classic, vintage, old, traditional | `retro` |
| Pop culture, 80s/90s nostalgia, badges | `retro` |
| Product, SaaS, dashboard, productivity | `notion` |
| Gaming, retro tech, developer, 8-bit | `pixel-art` |
| Educational, tutorial, knowledge sharing | `sketch-notes` |
| Creative proposals, brand, toy-like | `vector-illustration` |
| History, exploration, heritage, biography | `vintage` |
| Lifestyle, travel, food, personal | `watercolor` |
| Business, professional, strategy, analysis | `elegant` |
## File Management
### With Article Path
### Output Directory
Save to `imgs/` subdirectory in the same folder as the article:
Each session creates an independent directory named by content slug:
```
path/to/
├── article.md
── imgs/
── prompts/
│ └── cover.md
└── cover.png
```
### Without Article Path
Save to current working directory:
```
./
├── cover-prompt.md
cover-image/{topic-slug}/
├── source-{slug}.{ext} # Source files (text, images, etc.)
── prompts/
── cover.md
└── cover.png
```
**Slug Generation**:
1. Extract main topic from content (2-4 words, kebab-case)
2. Example: "The Future of AI" → `future-of-ai`
### Conflict Resolution
If `cover-image/{topic-slug}/` already exists:
- Append timestamp: `{topic-slug}-YYYYMMDD-HHMMSS`
- Example: `ai-future` exists → `ai-future-20260118-143052`
### Source Files
Copy all sources with naming `source-{slug}.{ext}`:
- `source-article.md` (main text content)
- `source-logo.png` (image from conversation)
Multiple sources supported: text, images, files from conversation.
## Workflow
### Step 1: Analyze Content
Extract key information:
- **Main topic**: What is the article about?
- **Core message**: What's the key takeaway?
- **Tone**: Serious, playful, inspiring, educational?
- **Keywords**: Identify style-signaling words
1. **Save source content** (if not already a file):
- If user provides a file path: use as-is
- If user pastes content: save to `source.md` in target directory
### Step 2: Select Style
2. **Extract key information**:
- **Main topic**: What is the article about?
- **Core message**: What's the key takeaway?
- **Tone**: Serious, playful, inspiring, educational?
- **Keywords**: Identify style-signaling words
If `--style` specified, use that style. Otherwise:
1. Scan content for style signals (see Auto Style Selection table)
2. Match signals to most appropriate style
3. Default to `elegant` if no clear signals
3. **Language detection**:
- Detect **source language** from content
- Detect **user language** from conversation context
- Note if source_language ≠ user_language (will ask in Step 3)
### Step 3: Generate Cover Concept
### Step 2: Determine Options
1. **Style selection**:
- If `--style` specified, use that style
- Otherwise, scan content for style signals and auto-select 3 candidates
- Default to `elegant` if no clear signals
2. **Aspect ratio**:
- If `--aspect` specified, use that ratio
- Otherwise, prepare options: 2.35:1 (cinematic), 16:9 (widescreen), 1:1 (social)
### Step 3: Confirm Options
**Purpose**: Let user confirm all options in a single step before generation.
**IMPORTANT**: Present ALL options in a single confirmation step using AskUserQuestion. Do NOT interrupt workflow with multiple separate confirmations.
**Determine which questions to ask**:
| Question | When to Ask |
|----------|-------------|
| Style | Always (required) |
| Aspect ratio | Always (offer common options) |
| Language | Only if `source_language ≠ user_language` |
**Present options** (use AskUserQuestion with all applicable questions):
**Question 1 (Style)** - always:
- Style A (recommended): [style name] - [brief description]
- Style B: [style name] - [brief description]
- Style C: [style name] - [brief description]
- Custom: Provide custom style reference
**Question 2 (Aspect)** - always:
- 2.35:1 Cinematic (Recommended) - ultra-wide, dramatic
- 16:9 Widescreen - standard video/presentation
- 1:1 Square - social media optimized
**Question 3 (Language)** - only if source ≠ user language:
- [Source language] (matches content)
- [User language] (your preference)
**Language handling**:
- If source language = user language: Just inform user (e.g., "Title will be in Chinese")
- If different: Ask which language to use for title text
### Step 4: Generate Cover Concept
Create a cover image concept based on selected style:
@@ -126,21 +207,26 @@ Create a cover image concept based on selected style:
- 1-2 symbolic elements representing the topic
- Metaphors or analogies that fit the style
### Step 4: Create Prompt File
### Step 5: Create Prompt File
Save prompt to `prompts/cover.md` with confirmed options.
**All prompts are written in the user's confirmed language preference.**
**Prompt Format**:
```markdown
Cover theme: [topic in 2-3 words]
Style: [selected style name]
Aspect ratio: [confirmed aspect ratio]
[If title included:]
Title text: [8 characters or less, in content language]
Subtitle: [optional, in content language]
Title text: [8 characters or less, in confirmed language]
Subtitle: [optional, in confirmed language]
Visual composition:
- Main visual: [description matching style]
- Layout: [positioning based on title inclusion]
- Layout: [positioning based on title inclusion and aspect ratio]
- Decorative elements: [style-appropriate elements]
Color scheme:
@@ -154,23 +240,25 @@ Style notes: [specific style characteristics to emphasize]
Note: No title text, pure visual illustration only.
```
### Step 5: Generate Image
### Step 6: Generate Image
**Image Generation Skill Selection**:
1. Check available image generation skills
2. If multiple skills available, ask user to choose
**Generation**:
Call selected image generation skill with prompt file and output path.
Call selected image generation skill with prompt file, output path, and confirmed aspect ratio.
### Step 6: Output Summary
### Step 7: Output Summary
```
Cover Image Generated!
Topic: [topic]
Style: [style name]
Aspect: [aspect ratio]
Title: [cover title] (or "No title - visual only")
Language: [confirmed language]
Location: [output path]
Preview the image to verify it matches your expectations.
@@ -183,4 +271,15 @@ Preview the image to verify it matches your expectations.
- Visual metaphors work better than literal representations
- Maintain style consistency throughout the cover
- Image generation typically takes 10-30 seconds
- Title text language should match content language
- Title text uses user's confirmed language preference
- Aspect ratio: 2.35:1 for cinematic/dramatic, 16:9 for widescreen, 1:1 for social media
## Extension Support
Custom styles and configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-cover-image/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-cover-image/EXTEND.md` (user)
If found, load before Step 1. Extension content overrides defaults.
@@ -0,0 +1,25 @@
# blueprint
Precise technical blueprint style with engineering aesthetic
## Color Palette
- Primary: Engineering Blue (#2563EB), Navy Blue (#1E3A5F)
- Background: Blueprint Off-White (#FAF8F5), subtle grid overlay
- Accents: Amber (#F59E0B), Light Blue (#BFDBFE)
## Visual Elements
- Precise lines with consistent stroke weights
- Technical schematics and clean vector graphics
- Dimension lines and measurement indicators
- Grid alignment for all elements
- Geometric precision for all shapes
## Typography
- Clean sans-serif hand lettering, technical and authoritative
## Best For
Technical architecture, system design, data analysis, engineering documentation
@@ -0,0 +1,26 @@
# bold-editorial
High-impact magazine editorial with bold visual expression
## Color Palette
- Primary: Pure White (#FFFFFF), Electric Blue (#3B82F6), Bright Orange (#FB923C)
- Background: Deep Black (#0A0A0A), Deep Blue (#0F172A)
- Accents: Magenta (#EC4899), Neon Green (#22C55E), Violet (#8B5CF6)
## Visual Elements
- Strong typography as visual element itself
- Geometric shapes and bold color blocks
- Full-bleed solid color backgrounds
- Dynamic diagonal lines and angles
- Dramatic lighting effects on text
- Minimal decoration, maximum impact
## Typography
- Bold condensed typeface, oversized headlines, all-caps, tight letter-spacing
## Best For
Product launches, marketing, keynote speeches, brand showcases, investor pitches
@@ -0,0 +1,26 @@
# chalkboard
Black chalkboard background with colorful chalk drawing style
## Color Palette
- Primary: Chalk White (#F5F5F5), Chalk Yellow (#FFE566), Chalk Pink (#FF9999)
- Background: Chalkboard Black (#1A1A1A), Dark Green-Black (#1C2B1C)
- Accents: Chalk Blue (#66B3FF), Chalk Green (#90EE90), Chalk Orange (#FFB366)
## Visual Elements
- Chalkboard texture with subtle dust and smudges
- Hand-drawn chalk illustrations with sketchy lines
- Chalk dust effects around text and drawings
- Imperfect lines with visible texture
- Doodles, arrows, underlines, stars
- Eraser marks and chalk residue
## Typography
- Hand-drawn chalk lettering, slightly uneven, with chalk texture
## Best For
Educational content, tutorials, classroom themes, back-to-school, teaching materials
@@ -0,0 +1,25 @@
# dark-atmospheric
Dark moody aesthetic with glowing accent elements
## Color Palette
- Primary: Electric Purple (#8B5CF6), Cyan Blue (#06B6D4), Magenta Pink (#EC4899)
- Background: Deep Purple-Black (#0D0D1A), Rich Navy (#1A1A2E)
- Accents: Amber (#F59E0B), Pure White (#FFFFFF)
## Visual Elements
- Glowing accent elements and borders
- Subtle gradient backgrounds
- Atmospheric fog or particle effects
- Neon-style highlights on key elements
- Silhouettes with backlit edges
## Typography
- Elegant serif or refined sans-serif in light/white with subtle glow
## Best For
Entertainment, creative industries, premium brands, cinematic storytelling
@@ -0,0 +1,26 @@
# editorial-infographic
Modern magazine-style editorial with clear visual storytelling
## Color Palette
- Primary: Near Black (#1A1A1A), Editorial Blue (#2563EB)
- Background: Pure White (#FFFFFF), Light Gray (#F8F9FA)
- Accents: Coral (#F97316), Emerald (#10B981), Amber (#F59E0B)
## Visual Elements
- Clean flat illustrations (not photos)
- Structured multi-section layouts
- Callout boxes for key insights
- Icon-based data visualization
- Visual metaphors for abstract concepts
- Pull quotes and highlight boxes
## Typography
- Bold display serif or modern sans-serif, editorial sophistication
## Best For
Technology explainers, science communication, research summaries, thought leadership
@@ -0,0 +1,25 @@
# fantasy-animation
Whimsical hand-drawn animation style inspired by Ghibli and Disney
## Color Palette
- Primary: Golden Yellow (#F4D03F), Rose Pink (#E8A0BF), Deep Forest (#2D5A3D)
- Background: Soft Sky Blue (#E8F4FC), Warm Cream (#FFF8E7)
- Accents: Sage Green (#87A96B), Sky Blue (#7EC8E3), Coral (#F08080)
## Visual Elements
- Central illustrated character (friendly, expressive)
- Magical floating objects (books, orbs, sparkles)
- Storybook-style environment backgrounds
- Soft shadows and gentle highlights
- Decorative elements: stars, sparkles, flowers, leaves
## Typography
- Whimsical serif or decorative hand-lettered style, organic feel
## Best For
Educational content, storytelling, creative workshops, fantasy/gaming content
@@ -0,0 +1,26 @@
# intuition-machine
Technical briefing style with aged paper and bilingual labels
## Color Palette
- Primary: Dark Maroon (#5D3A3A), Teal (#2F7373)
- Background: Aged Cream (#F5F0E6), subtle paper texture with light creases
- Accents: Warm Brown (#8B7355), Maroon (#722F37), Deep Charcoal (#2D2D2D)
## Visual Elements
- Isometric 3D technical illustrations or flat 2D diagrams
- Bilingual callout labels (English + Chinese)
- Faded thematic background patterns
- Clean black outlines on all elements
- Split or triptych layouts
- Key quote box at bottom
## Typography
- Bold display font in dark maroon, ALL CAPS for titles, bilingual labels
## Best For
Technical explanations, academic presentations, bilingual audiences, knowledge docs
@@ -0,0 +1,25 @@
# notion
Clean SaaS dashboard aesthetic with productivity tool styling
## Color Palette
- Primary: Near Black (#1F1F1F), Notion Blue (#2383E2)
- Background: Light Gray (#F7F7F5), Pure White (#FFFFFF)
- Accents: Success Green (#0F7B6C), Alert Red (#E03E3E), Warning Yellow (#DFAB01)
## Visual Elements
- Card-based layouts with subtle borders
- Clean data visualizations
- Tag and label chips
- Icon-based navigation hints
- Progress bars and metric displays
## Typography
- System UI or Inter style, clean and functional
## Best For
Product demos, SaaS presentations, productivity content, B2B topics
@@ -0,0 +1,26 @@
# pixel-art
Retro 8-bit pixel art aesthetic with nostalgic gaming style
## Color Palette
- Primary: Dark Navy (#1A1A2E), Pixel Green (#00FF00)
- Background: Light Blue (#87CEEB), Soft Lavender (#E6E6FA)
- Accents: Pixel Red (#FF0000), Pixel Yellow (#FFFF00), Pixel Cyan (#00FFFF)
## Visual Elements
- All elements with visible pixel structure
- Simple iconography: stars, hearts, arrows
- Text bubbles with pixel borders
- Progress bars with chunky segments
- Dithering patterns for gradients
- Limited 16-32 color palette
## Typography
- Pixelated bitmap font style, chunky blocky letterforms
## Best For
Gaming content, tech tutorials, developer talks, retro-themed topics
@@ -0,0 +1,25 @@
# sketch-notes
Soft hand-drawn illustration style with fresh minimalist aesthetic
## Color Palette
- Primary: Deep Charcoal (#2C3E50), Soft Orange (#F4A261)
- Background: Warm Off-White (#FAF8F0), subtle paper grain
- Accents: Mustard Yellow (#E9C46A), Sage Green (#87A96B), Light Blue (#7EC8E3)
## Visual Elements
- Connection lines with hand-drawn wavy feel
- Conceptual abstract icons illustrating ideas
- Color fills with hand-painted casual feel
- Doodle-style decorative elements
- Arrows and pointers with sketchy style
## Typography
- Bold hand-written marker font for headlines, casual handwriting for body
## Best For
Educational content, knowledge sharing, technical explanations, tutorials
@@ -1,23 +0,0 @@
# tech
Modern, clean, futuristic
## Color Palette
- Primary: Deep blue (#1A365D), electric cyan (#00D4FF), purple (#6B46C1)
- Background: Dark gray (#1A202C), near-black (#0D1117)
- Accents: Neon green (#00FF88), bright white
## Visual Elements
- Circuit patterns, data nodes
- Geometric grids, code snippets
- Glowing effects
## Typography
- Monospace-style hand lettering, glowing effects
## Best For
AI, programming, technology, digital transformation
@@ -0,0 +1,25 @@
# vector-illustration
Flat vector illustration with black outlines and retro soft colors
## Color Palette
- Primary: Coral Red (#E07A5F), Mint Green (#81B29A), Mustard Yellow (#F2CC8F)
- Background: Cream Off-White (#F5F0E6), subtle paper texture
- Accents: Burnt Orange (#D4764A), Rock Blue (#577590), Deep Charcoal (#2D2D2D) outlines
## Visual Elements
- All objects have closed black outlines (coloring book style)
- Rounded line endings, avoid sharp corners
- Simplified geometric shapes
- 2.5D perspective with layering
- Decorative elements: sunbursts, pill clouds, dots, stars
## Typography
- Large bold retro serif for titles, clean geometric sans-serif for body
## Best For
Educational content, creative proposals, brand showcases, explainer content
@@ -0,0 +1,26 @@
# vintage
Vintage aged-paper aesthetic with historical document styling
## Color Palette
- Primary: Dark Brown (#3D2914), Forest Green (#2D5A3D), Navy Blue (#1E3A5F)
- Background: Aged Parchment (#F5E6D3), Sepia Cream (#FFF8DC)
- Accents: Burgundy (#722F37), Gold (#C9A227), Sepia Black (#3D3D3D)
## Visual Elements
- Antique maps with route lines and landmarks
- Compass roses and nautical elements
- Specimen drawings (flora, fauna)
- Handwritten-style annotations
- Rope, leather, brass decorative motifs
- Aged texture with subtle creases
## Typography
- Classic serif with historical character, elegant flourishes
## Best For
Historical content, travel, heritage storytelling, biography, scientific discovery
@@ -0,0 +1,25 @@
# watercolor
Soft watercolor illustration style with hand-painted textures
## Color Palette
- Primary: Soft Coral (#F4A261), Dusty Rose (#E8A0A0)
- Background: Warm Off-White (#FAF8F0), Soft Cream (#FFF9E6)
- Accents: Sage Green (#87A96B), Sky Blue (#7EC8E3), Soft Lavender (#C5B4E3)
## Visual Elements
- Watercolor washes as backgrounds
- Visible brush strokes and textures
- Natural elements: leaves, flowers, organic shapes
- Color bleeds and soft edges
- Hand-drawn arrows and connections
## Typography
- Elegant handwritten or brush script, organic letterforms
## Best For
Lifestyle content, wellness, travel guides, personal stories, creative topics
+267
View File
@@ -0,0 +1,267 @@
---
name: baoyu-danger-gemini-web
description: Image generation skill using Gemini Web. Generates images from text prompts via Google Gemini. Also supports text generation. Use as the image generation backend for other skills like cover-image, xhs-images, article-illustrator.
---
# Gemini Web Client
Supports:
- Text generation
- Image generation (download + save)
- Reference images for vision input (attach local images)
- Multi-turn conversations via persisted `--sessionId`
## Script Directory
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
**Agent Execution Instructions**:
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
3. Replace all `${SKILL_DIR}` in this document with the actual path
**Script Reference**:
| Script | Purpose |
|--------|---------|
| `scripts/main.ts` | CLI entry point for text/image generation |
| `scripts/gemini-webapi/*` | TypeScript port of `gemini_webapi` (GeminiClient, types, utils) |
## ⚠️ Disclaimer (REQUIRED)
**Before using this skill**, the consent check MUST be performed.
### Consent Check Flow
**Step 1**: Check consent file
```bash
# macOS
cat ~/Library/Application\ Support/baoyu-skills/gemini-web/consent.json 2>/dev/null
# Linux
cat ~/.local/share/baoyu-skills/gemini-web/consent.json 2>/dev/null
# Windows (PowerShell)
Get-Content "$env:APPDATA\baoyu-skills\gemini-web\consent.json" 2>$null
```
**Step 2**: If consent exists and `accepted: true` with matching `disclaimerVersion: "1.0"`:
Print warning and proceed:
```
⚠️ Warning: Using reverse-engineered Gemini Web API (not official). Accepted on: <acceptedAt date>
```
**Step 3**: If consent file doesn't exist or `disclaimerVersion` mismatch:
Display disclaimer and ask user:
```
⚠️ DISCLAIMER
This tool uses a reverse-engineered Gemini Web API, NOT an official Google API.
Risks:
- May break without notice if Google changes their API
- No official support or guarantees
- Use at your own risk
Do you accept these terms and wish to continue?
```
Use `AskUserQuestion` tool with options:
- **Yes, I accept** - Continue and save consent
- **No, I decline** - Exit immediately
**Step 4**: On acceptance, create consent file:
```bash
# macOS
mkdir -p ~/Library/Application\ Support/baoyu-skills/gemini-web
cat > ~/Library/Application\ Support/baoyu-skills/gemini-web/consent.json << 'EOF'
{
"version": 1,
"accepted": true,
"acceptedAt": "<ISO timestamp>",
"disclaimerVersion": "1.0"
}
EOF
# Linux
mkdir -p ~/.local/share/baoyu-skills/gemini-web
cat > ~/.local/share/baoyu-skills/gemini-web/consent.json << 'EOF'
{
"version": 1,
"accepted": true,
"acceptedAt": "<ISO timestamp>",
"disclaimerVersion": "1.0"
}
EOF
```
**Step 5**: On decline, output message and stop:
```
User declined the disclaimer. Exiting.
```
---
## Quick start
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello, Gemini"
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Explain quantum computing"
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cute cat" --image cat.png
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
# Multi-turn conversation (agent generates unique sessionId)
npx -y bun ${SKILL_DIR}/scripts/main.ts "Remember this: 42" --sessionId my-unique-id-123
npx -y bun ${SKILL_DIR}/scripts/main.ts "What number?" --sessionId my-unique-id-123
```
## Commands
### Text generation
```bash
# Simple prompt (positional)
npx -y bun ${SKILL_DIR}/scripts/main.ts "Your prompt here"
# Explicit prompt flag
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt here"
npx -y bun ${SKILL_DIR}/scripts/main.ts -p "Your prompt here"
# With model selection
npx -y bun ${SKILL_DIR}/scripts/main.ts -p "Hello" -m gemini-2.5-pro
# Pipe from stdin
echo "Summarize this" | npx -y bun ${SKILL_DIR}/scripts/main.ts
```
### Image generation
```bash
# Generate image with default path (./generated.png)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A sunset over mountains" --image
# Generate image with custom path
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cute robot" --image robot.png
# Shorthand
npx -y bun ${SKILL_DIR}/scripts/main.ts "A dragon" --image=dragon.png
```
### Vision input (reference images)
```bash
# Text + image -> text
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Describe this image" --reference a.png
# Text + image -> image
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Generate a variation" --reference a.png --image out.png
```
### Output formats
```bash
# Plain text (default)
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello"
# JSON output
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --json
```
## Options
| Option | Description |
|--------|-------------|
| `--prompt <text>`, `-p` | Prompt text |
| `--promptfiles <files...>` | Read prompt from files (concatenated in order) |
| `--model <id>`, `-m` | Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash |
| `--image [path]` | Generate image, save to path (default: generated.png) |
| `--reference <files...>`, `--ref <files...>` | Reference images for vision input |
| `--sessionId <id>` | Session ID for multi-turn conversation (agent generates unique ID) |
| `--list-sessions` | List saved sessions (max 100, sorted by update time) |
| `--json` | Output as JSON |
| `--login` | Refresh cookies only, then exit |
| `--cookie-path <path>` | Custom cookie file path |
| `--profile-dir <path>` | Chrome profile directory |
| `--help`, `-h` | Show help |
CLI note: `scripts/main.ts` supports text generation, image generation, reference images (`--reference/--ref`), and multi-turn conversations via `--sessionId`.
## Models
- `gemini-3-pro` - Default, latest model
- `gemini-2.5-pro` - Previous generation pro
- `gemini-2.5-flash` - Fast, lightweight
## Authentication
First run opens Chrome to authenticate with Google. Cookies are cached for subsequent runs.
```bash
# Force cookie refresh
npx -y bun ${SKILL_DIR}/scripts/main.ts --login
```
## Environment variables
| Variable | Description |
|----------|-------------|
| `GEMINI_WEB_DATA_DIR` | Data directory |
| `GEMINI_WEB_COOKIE_PATH` | Cookie file path |
| `GEMINI_WEB_CHROME_PROFILE_DIR` | Chrome profile directory |
| `GEMINI_WEB_CHROME_PATH` | Chrome executable path |
## Examples
### Generate text response
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts "What is the capital of France?"
```
### Generate image
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts "A photorealistic image of a golden retriever puppy" --image puppy.png
```
### Get JSON output for parsing
```bash
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --json | jq '.text'
```
### Generate image from prompt files
```bash
# Concatenate system.md + content.md as prompt
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image output.png
```
### Multi-turn conversation
```bash
# Start a session with unique ID (agent generates this)
npx -y bun ${SKILL_DIR}/scripts/main.ts "You are a helpful math tutor." --sessionId task-abc123
# Continue the conversation (remembers context)
npx -y bun ${SKILL_DIR}/scripts/main.ts "What is 2+2?" --sessionId task-abc123
npx -y bun ${SKILL_DIR}/scripts/main.ts "Now multiply that by 10" --sessionId task-abc123
# List recent sessions (max 100, sorted by update time)
npx -y bun ${SKILL_DIR}/scripts/main.ts --list-sessions
```
Session files are stored in `~/Library/Application Support/baoyu-skills/gemini-web/sessions/<id>.json` and contain:
- `id`: Session ID
- `metadata`: Gemini chat metadata for continuation
- `messages`: Array of `{role, content, timestamp, error?}`
- `createdAt`, `updatedAt`: Timestamps
## Extension Support
Custom configurations via EXTEND.md.
**Check paths** (priority order):
1. `.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md` (project)
2. `~/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md` (user)
If found, load before workflow. Extension content overrides defaults.
@@ -0,0 +1,627 @@
import { Endpoint, ErrorCode, Headers, Model } from './constants.js';
import { GemMixin } from './components/gem-mixin.js';
import {
APIError,
AuthError,
GeminiError,
ImageGenerationError,
ModelInvalid,
TemporarilyBlocked,
TimeoutError,
UsageLimitExceeded,
} from './exceptions.js';
import { Candidate, Gem, GeneratedImage, ModelOutput, RPCData, WebImage } from './types/index.js';
import {
extract_json_from_response,
get_access_token,
get_nested_value,
logger,
parse_file_name,
rotate_1psidts,
rotate_tasks,
fetch_with_timeout,
sleep,
upload_file,
write_cookie_file,
resolveGeminiWebCookiePath,
} from './utils/index.js';
type InitOptions = {
timeout?: number;
auto_close?: boolean;
close_delay?: number;
auto_refresh?: boolean;
refresh_interval?: number;
verbose?: boolean;
};
type RequestKwargs = RequestInit & { timeout_ms?: number };
function normalize_headers(h?: HeadersInit): Record<string, string> {
if (!h) return {};
if (Array.isArray(h)) return Object.fromEntries(h.map(([k, v]) => [k, v]));
if (h instanceof Headers) {
const out: Record<string, string> = {};
h.forEach((v, k) => {
out[k] = v;
});
return out;
}
return { ...(h as Record<string, string>) };
}
function collect_strings(root: unknown, accept: (s: string) => boolean, limit: number = 20): string[] {
const out: string[] = [];
const seen = new Set<string>();
const stack: unknown[] = [root];
while (stack.length > 0 && out.length < limit) {
const v = stack.pop();
if (typeof v === 'string') {
if (accept(v) && !seen.has(v)) {
seen.add(v);
out.push(v);
}
continue;
}
if (Array.isArray(v)) {
for (let i = 0; i < v.length; i++) stack.push(v[i]);
continue;
}
if (v && typeof v === 'object') {
for (const val of Object.values(v as Record<string, unknown>)) stack.push(val);
}
}
return out;
}
export class GeminiClient extends GemMixin {
public cookies: Record<string, string> = {};
public proxy: string | null = null;
public _running: boolean = false;
public access_token: string | null = null;
public timeout: number = 300;
public auto_close: boolean = false;
public close_delay: number = 300;
public auto_refresh: boolean = true;
public refresh_interval: number = 540;
public kwargs: RequestInit;
private close_timer: ReturnType<typeof setTimeout> | null = null;
private refresh_abort: AbortController | null = null;
constructor(
secure_1psid: string | null = null,
secure_1psidts: string | null = null,
proxy: string | null = null,
kwargs: RequestInit = {},
) {
super();
this.proxy = proxy;
this.kwargs = kwargs;
if (secure_1psid) {
this.cookies['__Secure-1PSID'] = secure_1psid;
if (secure_1psidts) this.cookies['__Secure-1PSIDTS'] = secure_1psidts;
}
}
async init(
timeoutOrOpts: number | InitOptions = 300,
auto_close: boolean = false,
close_delay: number = 300,
auto_refresh: boolean = true,
refresh_interval: number = 540,
verbose: boolean = true,
): Promise<void> {
const opts: InitOptions =
typeof timeoutOrOpts === 'object'
? timeoutOrOpts
: { timeout: timeoutOrOpts, auto_close, close_delay, auto_refresh, refresh_interval, verbose };
const timeout = opts.timeout ?? 300;
const ac = opts.auto_close ?? false;
const cd = opts.close_delay ?? 300;
const ar = opts.auto_refresh ?? true;
const ri = opts.refresh_interval ?? 540;
const vb = opts.verbose ?? true;
try {
const [token, valid] = await get_access_token(this.cookies, this.proxy, vb);
this.access_token = token;
this.cookies = valid;
this._running = true;
this.timeout = timeout;
this.auto_close = ac;
this.close_delay = cd;
if (this.auto_close) await this.reset_close_task();
this.auto_refresh = ar;
this.refresh_interval = ri;
const sid = this.cookies['__Secure-1PSID'];
if (sid) {
const existing = rotate_tasks.get(sid);
if (existing && existing instanceof AbortController) existing.abort();
rotate_tasks.delete(sid);
}
if (this.auto_refresh && sid) {
const ctl = new AbortController();
this.refresh_abort?.abort();
this.refresh_abort = ctl;
rotate_tasks.set(sid, ctl);
void this.start_auto_refresh(ctl.signal);
}
await write_cookie_file(this.cookies, resolveGeminiWebCookiePath(), 'client').catch(() => {});
if (vb) logger.success('Gemini client initialized successfully.');
} catch (e) {
await this.close();
throw e;
}
}
async close(delay: number = 0): Promise<void> {
if (delay > 0) await sleep(delay * 1000);
this._running = false;
if (this.close_timer) {
clearTimeout(this.close_timer);
this.close_timer = null;
}
this.refresh_abort?.abort();
this.refresh_abort = null;
const sid = this.cookies['__Secure-1PSID'];
const t = sid ? rotate_tasks.get(sid) : null;
if (t && t instanceof AbortController) t.abort();
if (sid) rotate_tasks.delete(sid);
}
async reset_close_task(): Promise<void> {
if (this.close_timer) {
clearTimeout(this.close_timer);
this.close_timer = null;
}
this.close_timer = setTimeout(() => {
void this.close(0);
}, this.close_delay * 1000);
this.close_timer.unref?.();
}
async start_auto_refresh(signal: AbortSignal): Promise<void> {
while (!signal.aborted) {
let newTs: string | null = null;
try {
newTs = await rotate_1psidts(this.cookies, this.proxy);
} catch (e) {
if (e instanceof AuthError) {
logger.warning('AuthError: Failed to refresh cookies. Auto refresh task canceled.');
return;
}
logger.warning(`Unexpected error while refreshing cookies: ${e instanceof Error ? e.message : String(e)}`);
}
if (newTs) {
this.cookies['__Secure-1PSIDTS'] = newTs;
await write_cookie_file(this.cookies, resolveGeminiWebCookiePath(), 'refresh').catch(() => {});
logger.debug('Cookies refreshed. New __Secure-1PSIDTS applied.');
}
await sleep(this.refresh_interval * 1000, signal);
}
}
protected async _run<T>(fn: () => Promise<T>, retry: number): Promise<T> {
try {
if (!this._running) {
await this.init({
timeout: this.timeout,
auto_close: this.auto_close,
close_delay: this.close_delay,
auto_refresh: this.auto_refresh,
refresh_interval: this.refresh_interval,
verbose: false,
});
if (!this._running) {
throw new APIError('Client initialization failed.');
}
}
return await fn();
} catch (e) {
let r = retry;
if (e instanceof ImageGenerationError) r = Math.min(1, r);
if (e instanceof APIError && r > 0) {
await sleep(1000);
return await this._run(fn, r - 1);
}
throw e;
}
}
async generate_content(
prompt: string,
files: string[] | null = null,
model: Model | string | Record<string, unknown> = Model.UNSPECIFIED,
gem: Gem | string | null = null,
chat: ChatSession | null = null,
kwargs: RequestKwargs = {},
): Promise<ModelOutput> {
return await this._run(async () => {
if (!prompt) throw new Error('Prompt cannot be empty.');
let mdl: Model;
if (typeof model === 'string') mdl = Model.from_name(model);
else if (model instanceof Model) mdl = model;
else if (model && typeof model === 'object') mdl = Model.from_dict(model);
else throw new TypeError(`'model' must be a Model instance, string, or dictionary; got ${typeof model}`);
const gem_id = gem instanceof Gem ? gem.id : gem;
if (this.auto_close) await this.reset_close_task();
if (!this.access_token) throw new APIError('Missing access token.');
const f = files?.length ? files : null;
const uploaded =
f &&
(await Promise.all(
f.map(async (p) => [[await upload_file(p, this.proxy)], parse_file_name(p)] as [string[], string]),
));
const first = uploaded ? [prompt, 0, null, uploaded] : [prompt];
const inner: unknown[] = [first, null, chat ? chat.metadata : null];
if (gem_id) {
for (let i = 0; i < 16; i++) inner.push(null);
inner.push(gem_id);
}
const f_req = JSON.stringify([null, JSON.stringify(inner)]);
const body = new URLSearchParams({ at: this.access_token, 'f.req': f_req }).toString();
const h0 = { ...Headers.GEMINI, ...mdl.model_header, Cookie: Object.entries(this.cookies).map(([k, v]) => `${k}=${v}`).join('; ') };
const h1 = { ...h0, ...normalize_headers(kwargs.headers) };
let res: Response;
try {
const timeout_ms = typeof kwargs.timeout_ms === 'number' ? kwargs.timeout_ms : this.timeout * 1000;
const { timeout_ms: _t, ...rest } = kwargs;
res = await fetch_with_timeout(Endpoint.GENERATE, {
method: 'POST',
headers: h1,
body,
redirect: 'follow',
...this.kwargs,
...rest,
timeout_ms,
});
} catch (e) {
throw new TimeoutError(
`Generate content request timed out, please try again. If the problem persists, consider setting a higher 'timeout' value when initializing GeminiClient. (${e instanceof Error ? e.message : String(e)})`,
);
}
if (res.status !== 200) {
await this.close();
throw new APIError(`Failed to generate contents. Request failed with status code ${res.status}`);
}
const txt = await res.text();
const response_json = extract_json_from_response(txt);
let body_json: unknown[] | null = null;
let body_index = 0;
try {
if (!Array.isArray(response_json)) throw new Error('Invalid JSON');
for (let part_index = 0; part_index < response_json.length; part_index++) {
const part = response_json[part_index];
if (!Array.isArray(part)) continue;
const part_body = get_nested_value<string | null>(part, [2], null);
if (!part_body) continue;
try {
const part_json = JSON.parse(part_body) as unknown[];
if (get_nested_value(part_json, [4], null)) {
body_index = part_index;
body_json = part_json;
break;
}
} catch {}
}
if (!body_json) throw new Error('No body');
} catch {
await this.close();
try {
const code = get_nested_value<number>(response_json, [0, 5, 2, 0, 1, 0], -1);
if (code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
throw new UsageLimitExceeded(
`Failed to generate contents. Usage limit of ${mdl.model_name} model has exceeded. Please try switching to another model.`,
);
}
if (code === ErrorCode.MODEL_INCONSISTENT) {
throw new ModelInvalid(
'Failed to generate contents. The specified model is inconsistent with the chat history. Please make sure to pass the same `model` parameter when starting a chat session with previous metadata.',
);
}
if (code === ErrorCode.MODEL_HEADER_INVALID) {
throw new ModelInvalid(
'Failed to generate contents. The specified model is not available. Please update gemini_webapi to the latest version. If the error persists and is caused by the package, please report it on GitHub.',
);
}
if (code === ErrorCode.IP_TEMPORARILY_BLOCKED) {
throw new TemporarilyBlocked(
'Failed to generate contents. Your IP address is temporarily blocked by Google. Please try using a proxy or waiting for a while.',
);
}
} catch (e) {
if (e instanceof GeminiError) throw e;
}
logger.debug(`Invalid response: ${txt.slice(0, 500)}`);
throw new APIError('Failed to generate contents. Invalid response data received. Client will try to re-initialize on next request.');
}
try {
const candidate_list = get_nested_value<unknown[]>(body_json, [4], []);
const out: Candidate[] = [];
for (let candidate_index = 0; candidate_index < candidate_list.length; candidate_index++) {
const candidate = candidate_list[candidate_index];
if (!Array.isArray(candidate)) continue;
const rcid = get_nested_value<string | null>(candidate, [0], null);
if (!rcid) continue;
let text = String(get_nested_value(candidate, [1, 0], ''));
if (/^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(text)) {
text = String(get_nested_value(candidate, [22, 0], text));
}
const thoughts = get_nested_value<string | null>(candidate, [37, 0, 0], null);
const web_images: WebImage[] = [];
for (const w of get_nested_value<unknown[]>(candidate, [12, 1], [])) {
if (!Array.isArray(w)) continue;
const url = get_nested_value<string | null>(w, [0, 0, 0], null);
if (!url) continue;
web_images.push(new WebImage(url, String(get_nested_value(w, [7, 0], '')), String(get_nested_value(w, [0, 4], '')), this.proxy));
}
const generated_images: GeneratedImage[] = [];
const wants_generated =
get_nested_value(candidate, [12, 7, 0], null) != null ||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
if (wants_generated) {
let img_body: unknown[] | null = null;
for (let part_index = body_index; part_index < (response_json as unknown[]).length; part_index++) {
const part = (response_json as unknown[])[part_index];
if (!Array.isArray(part)) continue;
const part_body = get_nested_value<string | null>(part, [2], null);
if (!part_body) continue;
try {
const part_json = JSON.parse(part_body) as unknown[];
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
if (!cand) continue;
const urls = collect_strings(cand, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 1);
if (urls.length > 0) {
img_body = part_json;
break;
}
} catch {}
}
if (!img_body) {
throw new ImageGenerationError(
'Failed to parse generated images. Please update gemini_webapi to the latest version. If the error persists and is caused by the package, please report it on GitHub.',
);
}
const img_candidate = get_nested_value<unknown[]>(img_body, [4, candidate_index], []);
const finished = get_nested_value<string | null>(img_candidate, [1, 0], null);
if (finished) {
text = finished.replace(/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/g, '').trimEnd();
}
const gen = get_nested_value<unknown[]>(img_candidate, [12, 7, 0], []);
for (let img_index = 0; img_index < gen.length; img_index++) {
const g = gen[img_index];
if (!Array.isArray(g)) continue;
const url = get_nested_value<string | null>(g, [0, 3, 3], null);
if (!url) continue;
const img_num = get_nested_value<number | null>(g, [3, 6], null);
const title = img_num ? `[Generated Image ${img_num}]` : '[Generated Image]';
const alt_list = get_nested_value<unknown[]>(g, [3, 5], []);
const alt =
(typeof alt_list[img_index] === 'string' ? (alt_list[img_index] as string) : null) ??
(typeof alt_list[0] === 'string' ? (alt_list[0] as string) : '') ??
'';
generated_images.push(new GeneratedImage(url, title, alt, this.proxy, this.cookies));
}
if (generated_images.length === 0) {
const urls = collect_strings(img_candidate, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 4);
for (const url of urls) {
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', this.proxy, this.cookies));
}
}
}
out.push(new Candidate({ rcid, text, thoughts, web_images, generated_images }));
}
if (out.length === 0) {
throw new GeminiError('Failed to generate contents. No output data found in response.');
}
const metadata = get_nested_value<string[]>(body_json, [1], []);
const output = new ModelOutput({ metadata, candidates: out });
if (chat instanceof ChatSession) chat.last_output = output;
return output;
} catch (e) {
if (e instanceof GeminiError || e instanceof APIError) throw e;
throw new APIError('Failed to parse response body. Data structure is invalid.');
}
}, 2);
}
async generateContent(
prompt: string,
files?: string[] | null,
model?: Model | string | Record<string, unknown>,
gem?: Gem | string | null,
chat?: ChatSession | null,
kwargs?: RequestKwargs,
): Promise<ModelOutput> {
return await this.generate_content(prompt, files ?? null, model ?? Model.UNSPECIFIED, gem ?? null, chat ?? null, kwargs ?? {});
}
start_chat(opts?: ConstructorParameters<typeof ChatSession>[1]): ChatSession {
return new ChatSession(this, opts);
}
startChat(opts?: ConstructorParameters<typeof ChatSession>[1]): ChatSession {
return this.start_chat(opts);
}
protected async _batch_execute(payloads: RPCData[], opts: RequestInit = {}): Promise<Response> {
if (!this.access_token) throw new APIError('Missing access token.');
const f_req = JSON.stringify([payloads.map((p) => p.serialize())]);
const body = new URLSearchParams({ at: this.access_token, 'f.req': f_req }).toString();
const h0 = { ...Headers.GEMINI, Cookie: Object.entries(this.cookies).map(([k, v]) => `${k}=${v}`).join('; ') };
const h1 = { ...h0, ...normalize_headers(opts.headers) };
const res = await fetch_with_timeout(Endpoint.BATCH_EXEC, {
method: 'POST',
headers: h1,
body,
redirect: 'follow',
...this.kwargs,
...opts,
timeout_ms: this.timeout * 1000,
});
if (res.status !== 200) {
await this.close();
throw new APIError(`Batch execution failed with status code ${res.status}`);
}
return res;
}
}
export class ChatSession {
private __metadata: Array<string | null> = [null, null, null];
public geminiclient: GeminiClient;
private _last_output: ModelOutput | null = null;
public model: Model | string | Record<string, unknown>;
public gem: Gem | string | null;
constructor(
geminiclient: GeminiClient,
opts: {
metadata?: Array<string | null>;
cid?: string | null;
rid?: string | null;
rcid?: string | null;
model?: Model | string | Record<string, unknown>;
gem?: Gem | string | null;
} = {},
) {
this.geminiclient = geminiclient;
this.model = opts.model ?? Model.UNSPECIFIED;
this.gem = opts.gem ?? null;
if (opts.metadata) this.metadata = opts.metadata;
if (opts.cid) this.cid = opts.cid;
if (opts.rid) this.rid = opts.rid;
if (opts.rcid) this.rcid = opts.rcid;
}
toString(): string {
return `ChatSession(cid='${this.cid}', rid='${this.rid}', rcid='${this.rcid}')`;
}
get last_output(): ModelOutput | null {
return this._last_output;
}
set last_output(v: ModelOutput | null) {
this._last_output = v;
if (v) {
this.metadata = (v.metadata ?? []) as Array<string | null>;
this.rcid = v.rcid;
}
}
async send_message(prompt: string, files: string[] | null = null, kwargs: RequestKwargs = {}): Promise<ModelOutput> {
return await this.geminiclient.generate_content(prompt, files, this.model, this.gem, this, kwargs);
}
async sendMessage(prompt: string, files?: string[] | null, kwargs?: RequestKwargs): Promise<ModelOutput> {
return await this.send_message(prompt, files ?? null, kwargs ?? {});
}
choose_candidate(index: number): ModelOutput {
if (!this.last_output) throw new Error('No previous output data found in this chat session.');
if (index >= this.last_output.candidates.length) {
throw new Error(`Index ${index} exceeds the number of candidates in last model output.`);
}
this.last_output.chosen = index;
this.rcid = this.last_output.rcid;
return this.last_output;
}
chooseCandidate(index: number): ModelOutput {
return this.choose_candidate(index);
}
get metadata(): Array<string | null> {
return this.__metadata;
}
set metadata(v: Array<string | null>) {
if (v.length > 3) throw new Error('metadata cannot exceed 3 elements');
this.__metadata = [null, null, null];
for (let i = 0; i < v.length; i++) this.__metadata[i] = v[i] ?? null;
}
get cid(): string | null {
return this.__metadata[0];
}
set cid(v: string | null) {
this.__metadata[0] = v;
}
get rid(): string | null {
return this.__metadata[1];
}
set rid(v: string | null) {
this.__metadata[1] = v;
}
get rcid(): string | null {
return this.__metadata[2];
}
set rcid(v: string | null) {
this.__metadata[2] = v;
}
}
@@ -0,0 +1,179 @@
import { GRPC } from '../constants.js';
import { APIError } from '../exceptions.js';
import { Gem, GemJar, RPCData } from '../types/index.js';
import { logger } from '../utils/logger.js';
import { extract_json_from_response, get_nested_value } from '../utils/parsing.js';
export abstract class GemMixin {
protected _gems: GemJar | null = null;
protected abstract _run<T>(fn: () => Promise<T>, retry: number): Promise<T>;
protected abstract _batch_execute(payloads: RPCData[], opts?: RequestInit): Promise<Response>;
protected abstract close(delay?: number): Promise<void>;
get gems(): GemJar {
if (this._gems == null) {
throw new Error(
'Gems not fetched yet. Call `GeminiClient.fetch_gems()` method to fetch gems from gemini.google.com.',
);
}
return this._gems;
}
async fetch_gems(include_hidden: boolean = false, opts?: RequestInit): Promise<GemJar> {
return await this._run(async () => {
const res = await this._batch_execute(
[
new RPCData(GRPC.LIST_GEMS, include_hidden ? '[4]' : '[3]', 'system'),
new RPCData(GRPC.LIST_GEMS, '[2]', 'custom'),
],
opts,
);
let response_json: unknown;
try {
response_json = extract_json_from_response(await res.text());
if (!Array.isArray(response_json)) throw new Error('Invalid response');
} catch {
await this.close();
throw new APIError('Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request.');
}
let predefined: unknown[] = [];
let custom: unknown[] = [];
try {
for (const part of response_json as unknown[]) {
if (!Array.isArray(part)) continue;
const ident = part[part.length - 1];
const body = get_nested_value<string | null>(part, [2], null);
if (!body) continue;
if (ident === 'system') {
const parsed = JSON.parse(body) as unknown[];
predefined = (Array.isArray(parsed) ? (parsed[2] as unknown[]) : []) ?? [];
} else if (ident === 'custom') {
const parsed = JSON.parse(body) as unknown[] | null;
if (parsed) custom = (parsed[2] as unknown[]) ?? [];
}
}
if (predefined.length === 0 && custom.length === 0) throw new Error('No gems');
} catch {
await this.close();
logger.debug('Invalid response while parsing gems');
throw new APIError('Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request.');
}
const entries: [string, Gem][] = [];
for (const gem of predefined) {
if (!Array.isArray(gem)) continue;
const id = String(get_nested_value(gem, [0], ''));
if (!id) continue;
entries.push([
id,
new Gem(
id,
String(get_nested_value(gem, [1, 0], '')),
get_nested_value<string | null>(gem, [1, 1], null),
get_nested_value<string | null>(gem, [2, 0], null),
true,
),
]);
}
for (const gem of custom) {
if (!Array.isArray(gem)) continue;
const id = String(get_nested_value(gem, [0], ''));
if (!id) continue;
entries.push([
id,
new Gem(
id,
String(get_nested_value(gem, [1, 0], '')),
get_nested_value<string | null>(gem, [1, 1], null),
get_nested_value<string | null>(gem, [2, 0], null),
false,
),
]);
}
this._gems = new GemJar(entries);
return this._gems;
}, 2);
}
async create_gem(name: string, prompt: string, description: string = ''): Promise<Gem> {
return await this._run(async () => {
const payload = JSON.stringify([
[
name,
description,
prompt,
null,
null,
null,
null,
null,
0,
null,
1,
null,
null,
null,
[],
],
]);
const res = await this._batch_execute([new RPCData(GRPC.CREATE_GEM, payload)]);
try {
const response_json = extract_json_from_response(await res.text()) as unknown[];
const gem_id = JSON.parse(String((response_json[0] as unknown[])[2]))[0] as string;
return new Gem(gem_id, name, description, prompt, false);
} catch {
await this.close();
throw new APIError('Failed to create gem. Invalid response data received. Client will try to re-initialize on next request.');
}
}, 2);
}
async update_gem(gem: Gem | string, name: string, prompt: string, description: string = ''): Promise<Gem> {
return await this._run(async () => {
const gem_id = typeof gem === 'string' ? gem : gem.id;
const payload = JSON.stringify([
gem_id,
[
name,
description,
prompt,
null,
null,
null,
null,
null,
0,
null,
1,
null,
null,
null,
[],
0,
],
]);
await this._batch_execute([new RPCData(GRPC.UPDATE_GEM, payload)]);
return new Gem(gem_id, name, description, prompt, false);
}, 2);
}
async delete_gem(gem: Gem | string, opts?: RequestInit): Promise<void> {
return await this._run(async () => {
const gem_id = typeof gem === 'string' ? gem : gem.id;
const payload = JSON.stringify([gem_id]);
await this._batch_execute([new RPCData(GRPC.DELETE_GEM, payload)], opts);
}, 2);
}
}
@@ -0,0 +1,2 @@
export { GemMixin } from './gem-mixin.js';
@@ -0,0 +1,107 @@
export const Endpoint = {
GOOGLE: 'https://www.google.com',
INIT: 'https://gemini.google.com/app',
GENERATE:
'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate',
ROTATE_COOKIES: 'https://accounts.google.com/RotateCookies',
UPLOAD: 'https://content-push.googleapis.com/upload',
BATCH_EXEC: 'https://gemini.google.com/_/BardChatUi/data/batchexecute',
} as const;
export const GRPC = {
LIST_CHATS: 'MaZiqc',
READ_CHAT: 'hNvQHb',
LIST_GEMS: 'CNgdBe',
CREATE_GEM: 'oMH3Zd',
UPDATE_GEM: 'kHv0Vd',
DELETE_GEM: 'UXcSJb',
} as const;
export const Headers = {
GEMINI: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
Host: 'gemini.google.com',
Origin: 'https://gemini.google.com',
Referer: 'https://gemini.google.com/',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'X-Same-Domain': '1',
},
ROTATE_COOKIES: {
'Content-Type': 'application/json',
},
UPLOAD: {
'Push-ID': 'feeds/mcudyrk2a4khkz',
},
} as const;
export const ErrorCode = {
TEMPORARY_ERROR_1013: 1013,
USAGE_LIMIT_EXCEEDED: 1037,
MODEL_INCONSISTENT: 1050,
MODEL_HEADER_INVALID: 1052,
IP_TEMPORARILY_BLOCKED: 1060,
} as const;
export class Model {
static readonly UNSPECIFIED = new Model('unspecified', {}, false);
static readonly G_3_0_PRO = new Model(
'gemini-3.0-pro',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]' },
false,
);
static readonly G_2_5_PRO = new Model(
'gemini-2.5-pro',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]' },
false,
);
static readonly G_2_5_FLASH = new Model(
'gemini-2.5-flash',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]' },
false,
);
constructor(
public readonly model_name: string,
public readonly model_header: Record<string, string>,
public readonly advanced_only: boolean,
) {}
static from_name(name: string): Model {
for (const model of [Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_2_5_PRO, Model.G_2_5_FLASH]) {
if (model.model_name === name) return model;
}
throw new Error(
`Unknown model name: ${name}. Available models: ${[Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_2_5_PRO, Model.G_2_5_FLASH]
.map((m) => m.model_name)
.join(', ')}`,
);
}
static from_dict(model_dict: { model_name?: unknown; model_header?: unknown }): Model {
if (!model_dict || typeof model_dict !== 'object') {
throw new Error("When passing a custom model as a dictionary, 'model_name' and 'model_header' keys must be provided.");
}
if (!('model_name' in model_dict) || !('model_header' in model_dict)) {
throw new Error("When passing a custom model as a dictionary, 'model_name' and 'model_header' keys must be provided.");
}
if (typeof model_dict.model_name !== 'string' || !model_dict.model_name.trim()) {
throw new Error("When passing a custom model as a dictionary, 'model_name' must be a non-empty string.");
}
if (!model_dict.model_header || typeof model_dict.model_header !== 'object') {
throw new Error("When passing a custom model as a dictionary, 'model_header' must be a dictionary containing valid header strings.");
}
const header: Record<string, string> = {};
for (const [k, v] of Object.entries(model_dict.model_header as Record<string, unknown>)) {
if (typeof v === 'string') header[k] = v;
}
return new Model(model_dict.model_name, header, false);
}
}
@@ -0,0 +1,56 @@
export class AuthError extends Error {
constructor(message = 'AuthError') {
super(message);
this.name = 'AuthError';
}
}
export class APIError extends Error {
constructor(message = 'APIError') {
super(message);
this.name = 'APIError';
}
}
export class ImageGenerationError extends APIError {
constructor(message = 'ImageGenerationError') {
super(message);
this.name = 'ImageGenerationError';
}
}
export class GeminiError extends Error {
constructor(message = 'GeminiError') {
super(message);
this.name = 'GeminiError';
}
}
export class TimeoutError extends GeminiError {
constructor(message = 'TimeoutError') {
super(message);
this.name = 'TimeoutError';
}
}
export class UsageLimitExceeded extends GeminiError {
constructor(message = 'UsageLimitExceeded') {
super(message);
this.name = 'UsageLimitExceeded';
}
}
export class ModelInvalid extends GeminiError {
constructor(message = 'ModelInvalid') {
super(message);
this.name = 'ModelInvalid';
}
}
export class TemporarilyBlocked extends GeminiError {
constructor(message = 'TemporarilyBlocked') {
super(message);
this.name = 'TemporarilyBlocked';
}
}
@@ -0,0 +1,8 @@
export { GeminiClient, ChatSession } from './client.js';
export * from './exceptions.js';
export * from './types/index.js';
export * from './constants.js';
export { logger, set_log_level, setLogLevel } from './utils/logger.js';
export * as utils from './utils/index.js';
@@ -0,0 +1,45 @@
import { GeneratedImage, type Image, WebImage } from './image.js';
function decode_html(s: string | null | undefined): string | null | undefined {
if (s == null) return s;
return s
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, ' ')
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)));
}
export class Candidate {
public rcid: string;
public text: string;
public thoughts: string | null;
public web_images: WebImage[];
public generated_images: GeneratedImage[];
constructor(params: {
rcid: string;
text: string;
thoughts?: string | null;
web_images?: WebImage[];
generated_images?: GeneratedImage[];
}) {
this.rcid = params.rcid;
this.text = decode_html(params.text) ?? '';
this.thoughts = decode_html(params.thoughts) ?? null;
this.web_images = params.web_images ?? [];
this.generated_images = params.generated_images ?? [];
}
toString(): string {
return this.text;
}
get images(): Image[] {
return [...this.web_images, ...this.generated_images];
}
}
@@ -0,0 +1,75 @@
export class Gem {
constructor(
public id: string,
public name: string,
public description: string | null,
public prompt: string | null,
public predefined: boolean,
) {}
toString(): string {
return `Gem(id='${this.id}', name='${this.name}', description='${this.description}', prompt='${this.prompt}', predefined=${this.predefined})`;
}
}
export class GemJar implements Iterable<Gem> {
private m = new Map<string, Gem>();
constructor(entries?: Iterable<[string, Gem]>) {
if (entries) for (const [id, gem] of entries) this.m.set(id, gem);
}
[Symbol.iterator](): Iterator<Gem> {
return this.m.values();
}
entries(): IterableIterator<[string, Gem]> {
return this.m.entries();
}
values(): IterableIterator<Gem> {
return this.m.values();
}
has(id: string): boolean {
return this.m.has(id);
}
set(id: string, gem: Gem): this {
this.m.set(id, gem);
return this;
}
get(id?: string | null, name?: string | null, def: Gem | null = null): Gem | null {
if (id == null && name == null) {
throw new Error('At least one of gem id or name must be provided.');
}
if (id != null) {
const g = this.m.get(id) ?? null;
if (!g) return def;
if (name != null) return g.name === name ? g : def;
return g;
}
if (name != null) {
for (const g of this.m.values()) {
if (g.name === name) return g;
}
return def;
}
return def;
}
filter(predefined: boolean | null = null, name: string | null = null): GemJar {
const out: [string, Gem][] = [];
for (const [id, gem] of this.m.entries()) {
if (predefined != null && gem.predefined !== predefined) continue;
if (name != null && gem.name !== name) continue;
out.push([id, gem]);
}
return new GemJar(out);
}
}
@@ -0,0 +1,16 @@
export class RPCData {
constructor(
public rpcid: string,
public payload: string,
public identifier: string = 'generic',
) {}
toString(): string {
return `GRPC(rpcid='${this.rpcid}', payload='${this.payload}', identifier='${this.identifier}')`;
}
serialize(): unknown[] {
return [this.rpcid, this.payload, null, this.identifier];
}
}
@@ -0,0 +1,114 @@
import path from 'node:path';
import { mkdir, writeFile } from 'node:fs/promises';
import { logger } from '../utils/logger.js';
import { cookie_header, fetch_with_timeout } from '../utils/http.js';
export class Image {
constructor(
public url: string,
public title = '[Image]',
public alt = '',
public proxy: string | null = null,
) {}
toString(): string {
const u = this.url.length <= 20 ? this.url : `${this.url.slice(0, 8)}...${this.url.slice(-12)}`;
return `Image(title='${this.title}', alt='${this.alt}', url='${u}')`;
}
async save(
p: string = 'temp',
filename: string | null = null,
cookies: Record<string, string> | null = null,
verbose: boolean = false,
skip_invalid_filename: boolean = false,
): Promise<string | null> {
filename = filename ?? this.url.split('/').pop()?.split('?')[0] ?? 'image';
const m = filename.match(/^(.*\.\w+)/);
if (m) filename = m[1]!;
else {
if (verbose) logger.warning(`Invalid filename: ${filename}`);
if (skip_invalid_filename) return null;
}
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0',
Accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
Referer: 'https://gemini.google.com/',
};
if (cookies) headers.Cookie = cookie_header(cookies);
let url = this.url;
let res: Response | null = null;
for (let i = 0; i < 10; i++) {
res = await fetch_with_timeout(url, {
method: 'GET',
headers,
redirect: 'manual',
timeout_ms: 30_000,
});
if (res.status >= 300 && res.status < 400) {
const loc = res.headers.get('location');
if (!loc) break;
url = new URL(loc, url).toString();
continue;
}
break;
}
if (!res) throw new Error('Image download failed: no response');
if (!res.ok) {
throw new Error(`Error downloading image: ${res.status} ${res.statusText}`);
}
const ct = res.headers.get('content-type');
if (ct && !ct.includes('image')) {
logger.warning(`Content type of ${filename} is not image, but ${ct}.`);
}
const dir = path.resolve(p);
await mkdir(dir, { recursive: true });
const dest = path.join(dir, filename);
const buf = Buffer.from(await res.arrayBuffer());
await writeFile(dest, buf);
if (verbose) logger.info(`Image saved as ${dest}`);
return dest;
}
}
export class WebImage extends Image {}
export class GeneratedImage extends Image {
constructor(
url: string,
title: string,
alt: string,
proxy: string | null,
public cookies: Record<string, string>,
) {
super(url, title, alt, proxy);
if (!cookies || Object.keys(cookies).length === 0) {
throw new Error('GeneratedImage is designed to be initialized with same cookies as GeminiClient.');
}
}
async save(
p: string = 'temp',
filename: string | null = null,
cookies: Record<string, string> | null = null,
verbose: boolean = false,
skip_invalid_filename: boolean = false,
full_size: boolean = true,
): Promise<string | null> {
const u = full_size ? `${this.url}=s2048` : this.url;
const f = filename ?? `${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}_${u.slice(-10)}.png`;
const img = new Image(u, this.title, this.alt, this.proxy);
return await img.save(p, f, cookies ?? this.cookies, verbose, skip_invalid_filename);
}
}
@@ -0,0 +1,6 @@
export { Candidate } from './candidate.js';
export { Gem, GemJar } from './gem.js';
export { RPCData } from './grpc.js';
export { GeneratedImage, Image, WebImage } from './image.js';
export { ModelOutput } from './modeloutput.js';
@@ -0,0 +1,35 @@
import type { Image } from './image.js';
import type { Candidate } from './candidate.js';
export class ModelOutput {
public metadata: string[];
public candidates: Candidate[];
public chosen: number;
constructor(params: { metadata: string[]; candidates: Candidate[]; chosen?: number }) {
this.metadata = params.metadata;
this.candidates = params.candidates;
this.chosen = params.chosen ?? 0;
}
toString(): string {
return this.text;
}
get text(): string {
return this.candidates[this.chosen]?.text ?? '';
}
get thoughts(): string | null {
return this.candidates[this.chosen]?.thoughts ?? null;
}
get images(): Image[] {
return this.candidates[this.chosen]?.images ?? [];
}
get rcid(): string {
return this.candidates[this.chosen]?.rcid ?? '';
}
}
@@ -0,0 +1,82 @@
import fs from 'node:fs';
import path from 'node:path';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolveGeminiWebCookiePath } from './paths.js';
export type CookieMap = Record<string, string>;
export type CookieFileData =
| {
cookies: CookieMap;
updated_at: number;
source?: string;
}
| {
version: number;
updatedAt: string;
cookieMap: CookieMap;
source?: string;
};
export async function read_cookie_file(p: string = resolveGeminiWebCookiePath()): Promise<CookieMap | null> {
try {
if (!fs.existsSync(p) || !fs.statSync(p).isFile()) return null;
const raw = await readFile(p, 'utf8');
const data = JSON.parse(raw) as unknown;
if (data && typeof data === 'object' && 'cookies' in (data as any)) {
const cookies = (data as any).cookies as unknown;
if (cookies && typeof cookies === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(cookies as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return out;
}
}
if (data && typeof data === 'object' && 'cookieMap' in (data as any)) {
const cookies = (data as any).cookieMap as unknown;
if (cookies && typeof cookies === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(cookies as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return Object.keys(out).length > 0 ? out : null;
}
}
if (data && typeof data === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return Object.keys(out).length > 0 ? out : null;
}
return null;
} catch {
return null;
}
}
export async function write_cookie_file(
cookies: CookieMap,
p: string = resolveGeminiWebCookiePath(),
source?: string,
): Promise<void> {
const dir = path.dirname(p);
await mkdir(dir, { recursive: true });
const payload: CookieFileData = {
version: 1,
updatedAt: new Date().toISOString(),
cookieMap: cookies,
source,
};
await writeFile(p, JSON.stringify(payload, null, 2), 'utf8');
}
export const readCookieFile = read_cookie_file;
export const writeCookieFile = write_cookie_file;
@@ -0,0 +1,34 @@
import { APIError, ImageGenerationError } from '../exceptions.js';
import { sleep } from './http.js';
export function running(retry: number = 0) {
return <TArgs extends unknown[], TResult>(
fn: (client: any, ...args: TArgs) => Promise<TResult>,
): ((client: any, ...args: TArgs) => Promise<TResult>) => {
const wrap = async (client: any, ...args: TArgs): Promise<TResult> => {
try {
if (!client?._running) {
await client.init?.({
timeout: client.timeout,
auto_close: client.auto_close,
close_delay: client.close_delay,
auto_refresh: client.auto_refresh,
refresh_interval: client.refresh_interval,
verbose: false,
});
}
return await fn(client, ...args);
} catch (e) {
let r = retry;
if (e instanceof ImageGenerationError) r = Math.min(1, r);
if (e instanceof APIError && r > 0) {
await sleep(1000);
return await running(r - 1)(fn)(client, ...args);
}
throw e;
}
};
return wrap;
};
}

Some files were not shown because too many files have changed in this diff Show More