Compare commits

...

110 Commits

Author SHA1 Message Date
Jim Liu 宝玉 40f9f05c22 chore: release v1.81.0 2026-03-24 20:59:56 -05:00
Jim Liu 宝玉 09ce80357f feat(baoyu-youtube-transcript): add yt-dlp fallback and modularize codebase
Retry with alternate InnerTube client identities when YouTube returns
anti-bot responses, then fall back to yt-dlp when available. Split
monolithic main.ts into typed modules (youtube, transcript, storage,
shared, types) and add unit tests.
2026-03-24 20:59:04 -05:00
Jim Liu 宝玉 7c995fcc24 chore: release v1.80.1 2026-03-24 20:06:02 -05:00
Jim Liu 宝玉 151f1ec2a8 fix(baoyu-image-gen): use correct prompt field name for Jimeng API 2026-03-24 20:04:21 -05:00
Jim Liu 宝玉 12e207dc3f chore: release v1.80.0 2026-03-24 19:27:57 -05:00
Jim Liu 宝玉 00e74ab071 feat(baoyu-image-gen): improve Azure OpenAI provider with flexible endpoint parsing and deployment resolution 2026-03-24 19:19:49 -05:00
优弧 1653b8544b feat(baoyu-image-gen): add Azure OpenAI as independent image generation provider (#111)
Azure OpenAI differs from standard OpenAI in two ways:
1. Auth via api-key header instead of Authorization: Bearer
2. URL requires ?api-version query param with deployment path

Changes:
- New file: scripts/providers/azure.ts (generations + edits, reuses openai utilities)
- types.ts: add "azure" to Provider type and default_model
- main.ts: register azure across rate limits, CLI args, auto-detection,
  provider loading, model resolution, help text, ref validation,
  EXTEND.md parsing, and batch logging

Env vars: AZURE_OPENAI_API_KEY, AZURE_OPENAI_BASE_URL (required),
AZURE_API_VERSION, AZURE_OPENAI_IMAGE_MODEL (optional)

Co-authored-by: CatFly <zw.catfly@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:04:34 -05:00
Jim Liu 宝玉 dad8f3a800 chore: release v1.79.2 2026-03-23 22:39:14 -05:00
Jim Liu 宝玉 35298d7c9d fix(baoyu-post-to-weibo): add no-theme rule for article markdown-to-HTML conversion 2026-03-23 22:38:30 -05:00
Jim Liu 宝玉 f22374ab62 fix(baoyu-cover-image): simplify reference image handling based on model capability 2026-03-23 22:38:27 -05:00
Jim Liu 宝玉 d4e80b1bc3 Fix Node-compatible parser tests (#107)
* Fix Node-compatible parser tests

* Add parser test dependencies to root test env
2026-03-23 15:30:42 -05:00
Jim Liu 宝玉 a5761dc71a chore: release v1.79.1 2026-03-23 12:02:44 -05:00
Jim Liu 宝玉 a5189dff37 fix(baoyu-xhs-images): remove opacity from watermark prompt and fix CJK spacing 2026-03-23 12:01:03 -05:00
Jim Liu 宝玉 39fe872bf3 fix(baoyu-comic): fix Doraemon naming spacing and remove opacity from watermark prompt 2026-03-23 12:01:00 -05:00
Jim Liu 宝玉 52813504f8 fix(baoyu-article-illustrator): remove opacity parameter from watermark prompt 2026-03-23 12:00:53 -05:00
Jim Liu 宝玉 a4d4108cd1 docs(project): update documentation to reflect single-plugin architecture 2026-03-23 12:00:38 -05:00
Yizhou Qian 钱亦舟 d7e763f1f5 fix: consolidate to single plugin to prevent duplicate skill registration (#106)
Merge the three plugins (content-skills, ai-generation-skills,
utility-skills) into one plugin entry. Since all three shared the same
source ("./"), Claude Code cached every skill three times. A single
plugin with one source keeps the flat skills/ layout while ensuring
each skill is registered exactly once.
2026-03-23 09:46:50 -05:00
Jim Liu 宝玉 097c09c59b chore: release v1.79.0 2026-03-22 15:42:33 -05:00
Jim Liu 宝玉 e4cd8bfefc feat(baoyu-post-to-wechat): improve credential loading with multi-source resolution and diagnostics 2026-03-22 15:42:08 -05:00
Jim Liu 宝玉 3dc5f2e06f chore: release v1.78.0 2026-03-22 15:19:29 -05:00
Jim Liu 宝玉 e5d6c8ec68 feat(baoyu-url-to-markdown): add URL-specific parser layer for X/Twitter and archive.ph
- New parsers/ module with pluggable rule system for site-specific HTML extraction
- X status parser: extract tweet text, media, quotes, author from data-testid elements
- X article parser: extract long-form article content with inline media
- archive.ph parser: restore original URL and prefer #CONTENT container
- Improved slug generation with stop words and content-aware slugs
- Output path uses subdirectory structure (domain/slug/slug.md)
- Fix: preserve anchor elements containing media in legacy converter
- Fix: smarter title deduplication in markdown document builder
2026-03-22 15:18:46 -05:00
Jim Liu 宝玉 6a4b312146 chore: release v1.77.0 2026-03-22 15:17:12 -05:00
Jim Liu 宝玉 2d6fe533eb Merge pull request #105 from jzOcb/feat/chapter-end-times
feat(youtube-transcript): add end times to chapter data
2026-03-22 15:13:44 -05:00
jzocb f53af25e65 fix: address review feedback on chapter end times
- Guard last chapter end against duration=0: use Math.max(duration, ch.start)
- Remove unnecessary 'as any' cast in backfill
- Check all chapters for missing end (not just first) via .some()
- Skip backfill when needsFetch is true (about to refetch anyway)
- Wrap backfill writeFileSync in try/catch (best-effort persistence)
2026-03-22 16:07:05 -04:00
jzocb c7e32b4590 fix: backfill chapter end times for cached videos
Videos cached before the chapter end-time change would silently
lack the 'end' field when loaded from cache. This adds a migration
that detects missing 'end' fields on cache hit, computes them from
adjacent chapters, and persists the updated meta.json.

This ensures consistent output regardless of whether the data was
freshly fetched or loaded from cache.
2026-03-22 15:58:13 -04:00
jzocb 8d973f2bc5 feat(youtube-transcript): add end times to chapter data
Add 'end' field to Chapter interface and parseChapters output.
Each chapter's end is derived from the next chapter's start time,
with the last chapter ending at the video's total duration.

This makes chapter data complete and ready for downstream consumers
(e.g. video clipping with ffmpeg) without requiring them to compute
end times from adjacent chapters.

Before: { title: 'Overview', start: 0 }
After:  { title: 'Overview', start: 0, end: 21 }
2026-03-22 15:52:30 -04:00
Jim Liu 宝玉 ba20cf89f2 fix(sync-clawhub): skip failed skills instead of aborting 2026-03-21 23:25:45 -05:00
Jim Liu 宝玉 1827be9234 chore: release v1.76.1 2026-03-21 23:17:04 -05:00
Jim Liu 宝玉 93c98dfc3c docs(baoyu-youtube-transcript): fix zsh glob issue for YouTube URLs 2026-03-21 23:16:51 -05:00
Jim Liu 宝玉 fbd9f9b622 chore: release v1.76.0 2026-03-21 23:08:21 -05:00
Jim Liu 宝玉 b6e293d059 fix(baoyu-markdown-to-html): use process.execPath and tsx import in test runner 2026-03-21 23:07:46 -05:00
Jim Liu 宝玉 bb78aab095 feat(baoyu-youtube-transcript): add title heading, description summary, and cover image to markdown output 2026-03-21 23:07:44 -05:00
Jim Liu 宝玉 5071a1d0d0 chore: release v1.75.0 2026-03-21 22:44:00 -05:00
Jim Liu 宝玉 e413ade164 feat(baoyu-youtube-transcript): add sentence segmentation and improve caching 2026-03-21 22:42:43 -05:00
Jim Liu 宝玉 e52f92b193 new skill 2026-03-21 21:03:06 -05:00
Jim Liu 宝玉 603cabaef4 chore: release v1.74.1 2026-03-21 00:03:51 -05:00
Jim Liu 宝玉 7d12526e90 fix(baoyu-image-gen): broaden OpenRouter model detection and aspect ratio validation 2026-03-21 00:03:20 -05:00
Jim Liu 宝玉 e7f9764a49 Merge pull request #101 from cwandev/fix/openrouter
fix(baoyu-image-gen): align `OpenRouter` image generation with current API
2026-03-20 23:32:26 -05:00
Jim Liu 宝玉 e55f91b0ea chore: release v1.74.0 2026-03-20 23:17:14 -05:00
Jim Liu 宝玉 fe3b3d9125 feat(baoyu-markdown-to-html): pass through all rendering options from CLI
- CLI now supports --color, --font-family, --font-size, --code-theme, --mac-code-block, --line-number, --count, --legend
- convertMarkdown accepts full CliOptions instead of limited subset
- Dynamic help text showing available theme/color/font options
2026-03-20 23:17:10 -05:00
Jim Liu 宝玉 105339cf3f fix(baoyu-md): fix CSS custom property regex for quoted values and add theme layering
- Remove quotes from CSS custom property regex character class so values containing quotes are fully stripped
- grace/simple themes now layer default CSS before their own rules
- Add tests for quoted property stripping and theme layering
2026-03-20 23:17:04 -05:00
Jim Liu 宝玉 dcfd9033ae chore: release v1.73.3 2026-03-20 18:46:01 -05:00
Jim Liu 宝玉 eb416d174c fix(baoyu-post-to-wechat): prevent placeholder regex from matching longer numbered variants 2026-03-20 18:45:05 -05:00
Jim Liu 宝玉 83afacb00e chore: release v1.73.2 2026-03-20 14:46:06 -05:00
Jim Liu 宝玉 0e6bfbcabd refactor: 提取图片处理模块,本地转换不支持的格式而非回退到material接口
将WeChat正文图片的格式转换和压缩逻辑提取到独立的wechat-image-processor模块,
使用jimp和@jsquash/webp在本地将WebP/BMP/GIF等不支持的格式转换为JPEG/PNG并
压缩到1MB以内,避免依赖material接口的回退策略。同时简化了news类型封面兜底逻辑。
2026-03-20 14:43:49 -05:00
Jim Liu 宝玉 7b67ff44b8 Merge pull request #100 from AICreator-Wind/fix-body-image-upload
fix: 正文图片上传使用 media/uploadimg 接口
2026-03-20 14:00:14 -05:00
浪不能停 79c289ca92 fix: 处理WebP等不支持的格式并优化回退逻辑
- 添加WebP、BMP、TIFF等格式到不支持列表
- 对于需要回退到material接口的情况,同时调用两个接口获取URL和media_id
- 提取uploadToWechat函数避免代码重复
2026-03-20 18:18:01 +08:00
浪不能停 fc5ad4b762 fix: 处理media/uploadimg接口的文件格式和大小限制
- 添加文件大小和格式检查常量
- GIF图片或大于1MB的图片自动回退使用material接口
- 确保向后兼容现有工作流
2026-03-20 14:25:48 +08:00
浪不能停 747977416d fix: 修复newspic类型图片上传和news类型封面兜底逻辑
- uploadImagesInHtml 函数添加 articleType 参数支持
- 为 newspic 类型正文图片额外调用 material 接口收集 media_id
- 返回 firstImageSource 用于 news 类型封面兜底逻辑
- 恢复 news 类型没有显式封面时使用第一张正文图作为封面的逻辑
2026-03-20 13:57:59 +08:00
cwandev e43eec260a fix(baoyu-image-gen): narrow OpenRouter Gemini ratios 2026-03-19 22:09:23 +08:00
cwandev 96ef6e2251 fix(baoyu-image-gen): harden OpenRouter image support 2026-03-19 21:59:41 +08:00
cwandev efb7a1917a fix(baoyu-image-gen): require OpenRouter image parameters 2026-03-19 21:26:19 +08:00
cwandev 1af984a64f fix(baoyu-image-gen): align OpenRouter image generation with current API 2026-03-19 17:36:03 +08:00
Jim Liu 宝玉 60363fc2df chore: release v1.73.1 2026-03-18 22:00:27 -05:00
Jim Liu 宝玉 28ec1053f6 refactor(baoyu-danger-x-to-markdown): migrate tests from bun:test to node:test 2026-03-18 21:59:49 -05:00
Jim Liu 宝玉 5a6afd576f chore: release v1.73.0 2026-03-18 21:56:14 -05:00
Jim Liu 宝玉 ebc74a10ad feat(baoyu-danger-x-to-markdown): add video media support for X articles 2026-03-18 21:54:21 -05:00
浪不能停 e79a42fd94 fix: 正文图片上传使用 media/uploadimg 接口
- 区分正文图片和封面图片的上传接口
- 正文图片使用 media/uploadimg (返回 URL)
- 封面图片使用 material/add_material (返回 media_id)
- 添加 uploadType 参数支持两种上传方式
- 优化错误提示,告知用户 news 类型需要封面图
2026-03-18 17:23:48 +08:00
Jim Liu 宝玉 ea84f21439 chore: release v1.72.0 2026-03-18 02:25:15 -05:00
Jim Liu 宝玉 0b9e51d6cc feat(baoyu-danger-x-to-markdown): add MARKDOWN entity support for X articles 2026-03-18 02:24:15 -05:00
Jim Liu 宝玉 7cc9c92722 chore: release v1.71.0 2026-03-17 11:41:28 -05:00
Jim Liu 宝玉 fc2f0d042a feat(baoyu-image-gen): add Seedream reference image support and model-specific validation 2026-03-17 11:40:24 -05:00
Jim Liu 宝玉 cf76a0d4d5 chore: release v1.70.0 2026-03-17 01:59:13 -05:00
Jim Liu 宝玉 4a25bc2651 feat(baoyu-format-markdown): optimize title generation and auto-generate dual summaries 2026-03-17 01:59:10 -05:00
Jim Liu 宝玉 f407c950c3 docs(gemini-web): clarify CDP session reuse 2026-03-16 20:01:09 -05:00
Jim Liu 宝玉 93e54a7b86 chore: release v1.69.1 2026-03-16 12:59:35 -05:00
Jim Liu 宝玉 c1f8a9ad07 chore: sync vendored baoyu-chrome-cdp copies 2026-03-16 12:57:39 -05:00
Jim Liu 宝玉 8b8ecf61a6 fix(baoyu-chrome-cdp): tighten chrome auto-connect 2026-03-16 12:55:29 -05:00
Jim Liu 宝玉 4be6f3682a chore: sync vendored baoyu-chrome-cdp across all skills 2026-03-16 12:54:36 -05:00
Jim Liu 宝玉 b89ef02221 chore(baoyu-post-to-wechat): sync vendored baoyu-chrome-cdp 2026-03-16 12:53:51 -05:00
Jim Liu 宝玉 55356c8820 chore: release v1.69.0 2026-03-16 12:53:25 -05:00
Jim Liu 宝玉 f23d4eebc3 docs(baoyu-cover-image): clarify people reference image workflow and interactive confirmation 2026-03-16 12:52:16 -05:00
Jim Liu 宝玉 3b795b6ef3 Merge pull request #92 from bviews/main
feat(baoyu-chrome-cdp): support Chrome 146 auto-connect
2026-03-16 12:40:22 -05:00
Jim Liu 宝玉 c62d9d5a1b Merge pull request #94 from cfh-7598/fix/wechat-article-reliability
fix(baoyu-post-to-wechat): improve browser publishing reliability
2026-03-16 12:32:49 -05:00
cfh 84c56b0da3 fix(baoyu-post-to-wechat): improve browser publishing reliability
- Add retry logic (5 attempts with progressive backoff) to clickMenuByText
  to handle slow-loading home page menus
- Increase post-login wait from 2s to 5s and menu timeout from 20s to 40s
- Replace fixed 3s sleep after editor tab opens with waitForElement polling
  for #title (30s) and .ProseMirror (15s) to reliably wait for full load
- Improve title/author filling with focus() and change event dispatch
  for more reliable value setting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 00:43:39 +08:00
Alec Jiang 462d080a0e fix(gemini-web): also respect BAOYU_CHROME_PROFILE_DIR in auto-discovery skip
Both GEMINI_WEB_CHROME_PROFILE_DIR and BAOYU_CHROME_PROFILE_DIR are
valid profile overrides (see resolveGeminiWebChromeProfileDir). Skip
existing Chrome auto-discovery when either is set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 19:02:58 +08:00
Alec Jiang bd01c370c9 fix(gemini-web): respect explicit profile config over auto-discovery
Skip existing Chrome auto-discovery when GEMINI_WEB_CHROME_PROFILE_DIR
is explicitly set, to avoid binding to the wrong browser profile/account.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:39:53 +08:00
Alec Jiang b7bcc8c094 fix(baoyu-chrome-cdp): keep HTTP validation in findExistingChromeDebugPort
findExistingChromeDebugPort callers (fetch_google_cookies_via_cdp) depend
on /json/version being available. TCP-only check could misclassify
non-CDP listeners as reusable, causing waitForChromeDebugPort to timeout
instead of falling back to launching a new Chrome. Restore isDebugPortReady
(HTTP) as the validator for this function; TCP-only check remains in
discoverRunningChromeDebugPort for Chrome 146 approval mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:11:37 +08:00
Alec Jiang d9f9da639d fix(baoyu-chrome-cdp): support Chrome 146 native remote debugging (approval mode)
Chrome 146+ blocks all HTTP endpoints (/json/version) in approval mode.
Read DevToolsActivePort ws path directly and use TCP port check instead
of HTTP discovery. Add WebSocket connect retry loop for approval dialog.
Unify findExistingChromeDebugPort to use the same mechanism.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:00:58 +08:00
bviews 126b3040e6 fix(gemini-web): reuse openPageSession and fix orphaned tab leak
- Replace manual CDP target orchestration with openPageSession, keeping
  behavior consistent with fetch_google_cookies_via_cdp
- Move created-tab cleanup into finally block so tabs are always closed
  even when Target.attachToTarget or Network.enable throws

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:44:53 +08:00
bviews 682888cc95 feat(baoyu-chrome-cdp): support connecting to existing Chrome session
Add ability to discover and connect to an already-running Chrome browser
(v144+) for cookie extraction, avoiding the need to launch a new window
and re-login. Uses Chrome's DevToolsActivePort from default user data
directories and process scanning as discovery mechanisms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 15:51:57 +08:00
Jim Liu 宝玉 e31294415d chore: release v1.68.0 2026-03-14 23:08:00 -05:00
Jim Liu 宝玉 926f74e9c9 feat(baoyu-cover-image): add character preservation from reference images 2026-03-14 23:07:56 -05:00
Jim Liu 宝玉 339990e87e feat(baoyu-article-illustrator): add configurable output directory support 2026-03-14 23:07:56 -05:00
Jim Liu 宝玉 3c5c3e006d Merge pull request #89 from shixy96/fix/markdown-to-html-inline-code-in-emphasis
fix(markdown-to-html): preserve inline code inside bold/emphasis
2026-03-14 22:27:14 -05:00
shixy 2aa9790789 fix: preserve inline code in cjk emphasis 2026-03-14 17:12:03 +08:00
shixy 38fc733b99 fix(markdown-to-html): preserve inline code inside bold/emphasis
The `extractText()` helper in `preprocessCjkEmphasis()` only handled
`text` nodes and nodes with `children`. `inlineCode` AST nodes (which
have a `value` but no `children`) fell through to the default empty-
string return, silently dropping their content.

For example `**算出 \`logits\`**` rendered as `<strong>算出 </strong>`
with the code span completely lost.

Add an `inlineCode` branch that wraps the node value in backticks so
the downstream `marked` pass can turn it into a proper `<code>` element.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:13:02 +08:00
Jim Liu 宝玉 4d2b95d1d1 chore: release v1.67.0 2026-03-13 19:12:47 -05:00
JianJang2017 ac2ce0b8b6 Add qwen-image-2.0-pro support for baoyu-image-gen 2026-03-13 19:09:54 -05:00
Jim Liu 宝玉 de7dc85361 chore: sync shared skill package vendor test files 2026-03-13 17:56:53 -05:00
Jim Liu 宝玉 0c02b81885 chore: release v1.66.1 2026-03-13 17:56:30 -05:00
Jim Liu 宝玉 774ad784d8 test: migrate tests from centralized mjs to colocated TypeScript
Move test files from tests/ directory to colocate with source code,
convert from .mjs to .ts using tsx runner, add workspaces and npm
cache to CI workflow.
2026-03-13 16:36:06 -05:00
Jim Liu 宝玉 484b00109f chore: release v1.66.0 2026-03-13 16:20:38 -05:00
Jim Liu 宝玉 ac217d5402 test: add test infrastructure with CI workflow and image-gen unit tests 2026-03-13 16:17:01 -05:00
Jim Liu 宝玉 70d9f63727 docs(baoyu-image-gen): add Jimeng and Seedream provider documentation 2026-03-13 16:16:57 -05:00
Jim Liu 宝玉 3398509d9e refactor(baoyu-image-gen): export functions for testability and add module entry guard 2026-03-13 16:16:50 -05:00
Jim Liu 宝玉 a11613c11b Merge pull request #88 from JimLiu/lindaifeng/main
fix(image-gen): tighten Jimeng provider behavior
2026-03-13 15:58:07 -05:00
Jim Liu 宝玉 cb17cb9cca fix(image-gen): tighten Jimeng provider behavior 2026-03-13 15:39:46 -05:00
Jim Liu 宝玉 88d6e09472 Merge pull request #87 from lindaifeng/main
Integrate DreamGraph and DoubaoGraph
2026-03-13 15:35:15 -05:00
Jim Liu 宝玉 004236682d chore: release v1.65.1 2026-03-13 15:29:52 -05:00
Jim Liu 宝玉 7e07c1bb84 refactor(baoyu-translate): replace remark/unified with markdown-it for chunk parsing
Simplifies dependencies and adds main.ts CLI entry point with exported
functions for programmatic reuse.
2026-03-13 15:27:25 -05:00
lindaifeng c151f33775 Merge branch 'JimLiu:main' into main 2026-03-14 00:39:44 +08:00
ldf 32003da694 集成即梦生图和豆包生图 2026-03-14 00:37:46 +08:00
Jim Liu 宝玉 6f38724163 chore: release v1.65.0 2026-03-13 10:58:13 -05:00
Jim Liu 宝玉 374a6b28fd feat(baoyu-post-to-wechat): add placeholder image upload and fix frontmatter parsing 2026-03-13 10:58:13 -05:00
Jim Liu 宝玉 95970480c8 Merge pull request #86 from JimLiu/codex/baoyu-md-shared
refactor: share markdown renderer via baoyu-md
2026-03-13 10:45:34 -05:00
Jim Liu 宝玉 fc324319d8 fix: preserve weibo image metadata in shared md package 2026-03-13 10:44:23 -05:00
Jim Liu 宝玉 056fa06c3b refactor: share markdown renderer via baoyu-md 2026-03-13 10:20:28 -05:00
Jim Liu 宝玉 c62cda4424 chore: release v1.64.0 2026-03-13 09:20:41 -05:00
Jim Liu 宝玉 12b43e166d feat(baoyu-image-gen): add OpenRouter provider support 2026-03-13 09:18:34 -05:00
551 changed files with 26142 additions and 3331 deletions
+16 -31
View File
@@ -6,48 +6,33 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.63.0"
"version": "1.81.0"
},
"plugins": [
{
"name": "content-skills",
"description": "Content generation and publishing skills",
"name": "baoyu-skills",
"description": "Content generation, AI backends, and utility tools for daily work efficiency",
"source": "./",
"strict": true,
"skills": [
"./skills/baoyu-xhs-images",
"./skills/baoyu-post-to-x",
"./skills/baoyu-post-to-wechat",
"./skills/baoyu-post-to-weibo",
"./skills/baoyu-article-illustrator",
"./skills/baoyu-cover-image",
"./skills/baoyu-slide-deck",
"./skills/baoyu-comic",
"./skills/baoyu-infographic"
]
},
{
"name": "ai-generation-skills",
"description": "AI-powered generation backends",
"source": "./",
"strict": true,
"skills": [
"./skills/baoyu-danger-gemini-web",
"./skills/baoyu-image-gen"
]
},
{
"name": "utility-skills",
"description": "Utility tools for content processing",
"source": "./",
"strict": true,
"skills": [
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-compress-image",
"./skills/baoyu-url-to-markdown",
"./skills/baoyu-cover-image",
"./skills/baoyu-danger-gemini-web",
"./skills/baoyu-danger-x-to-markdown",
"./skills/baoyu-format-markdown",
"./skills/baoyu-image-gen",
"./skills/baoyu-infographic",
"./skills/baoyu-markdown-to-html",
"./skills/baoyu-translate"
"./skills/baoyu-post-to-weibo",
"./skills/baoyu-post-to-wechat",
"./skills/baoyu-post-to-x",
"./skills/baoyu-slide-deck",
"./skills/baoyu-translate",
"./skills/baoyu-url-to-markdown",
"./skills/baoyu-xhs-images",
"./skills/baoyu-youtube-transcript"
]
}
]
+25
View File
@@ -0,0 +1,25 @@
name: Test
on:
push:
pull_request:
workflow_dispatch:
jobs:
node-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
+1
View File
@@ -166,3 +166,4 @@ posts/
.clawdhub/
.release-artifacts/
.worktrees/
youtube-transcript/
+209
View File
@@ -2,6 +2,215 @@
English | [中文](./CHANGELOG.zh.md)
## 1.81.0 - 2026-03-24
### Features
- `baoyu-youtube-transcript`: add yt-dlp fallback when YouTube blocks direct InnerTube API, with alternate client identity retry and cookie support via `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER` env var
### Refactor
- `baoyu-youtube-transcript`: split monolithic script into typed modules (youtube, transcript, storage, shared, types) and add unit tests
## 1.80.1 - 2026-03-24
### Fixes
- `baoyu-image-gen`: use correct `prompt` field name for Jimeng API request
## 1.80.0 - 2026-03-24
### Features
- `baoyu-image-gen`: add Azure OpenAI as independent image generation provider with flexible endpoint parsing, deployment-name resolution, quality mapping, and reference image validation
## 1.79.2 - 2026-03-23
### Fixes
- `baoyu-cover-image`: simplify reference image handling — use `--ref` when model supports it, only create description files for models without reference image support
- `baoyu-post-to-weibo`: add no-theme rule for article markdown-to-HTML conversion
### Tests
- Fix Node-compatible parser tests and add parser test dependencies
## 1.79.1 - 2026-03-23
### Fixes
- Consolidate to single plugin to prevent duplicate skill registration (by @TyrealQ)
- `baoyu-article-illustrator`: remove opacity parameter from watermark prompt
- `baoyu-comic`: fix Doraemon naming spacing and remove opacity from watermark prompt
- `baoyu-xhs-images`: remove opacity from watermark prompt and fix CJK spacing
### Documentation
- Update project documentation to reflect single-plugin architecture
## 1.79.0 - 2026-03-22
### Features
- `baoyu-post-to-wechat`: improve credential loading with multi-source resolution, priority ordering, and diagnostics for skipped incomplete sources
## 1.78.0 - 2026-03-22
### Features
- `baoyu-url-to-markdown`: add URL-specific parser layer for X/Twitter and archive.ph sites
- `baoyu-url-to-markdown`: improved slug generation with stop words removal and subdirectory output structure
### Fixes
- `baoyu-url-to-markdown`: preserve anchor elements containing media in legacy converter
- `baoyu-url-to-markdown`: smarter title deduplication to avoid redundant headings
## 1.77.0 - 2026-03-22
### Features
- `baoyu-youtube-transcript`: add end times to chapter data (by @jzOcb)
### Fixes
- `sync-clawhub`: skip failed skills instead of aborting
## 1.76.1 - 2026-03-21
### Documentation
- `baoyu-youtube-transcript`: fix zsh glob issue — always single-quote YouTube URLs when running the script
## 1.76.0 - 2026-03-21
### Features
- `baoyu-youtube-transcript`: add title heading, description summary, and cover image to markdown output
### Fixes
- `baoyu-markdown-to-html`: use process.execPath and tsx import in test runner
## 1.75.0 - 2026-03-21
### Features
- `baoyu-youtube-transcript`: new skill — download YouTube video transcripts/subtitles and cover images with multi-language, chapters, and speaker identification support
## 1.74.1 - 2026-03-21
### Fixes
- `baoyu-image-gen`: align OpenRouter image generation with current API, harden image support, and narrow Gemini aspect ratios (by @cwandev)
- `baoyu-image-gen`: broaden OpenRouter model detection and aspect ratio validation
## 1.74.0 - 2026-03-20
### Features
- `baoyu-markdown-to-html`: CLI now supports all rendering options — color, font-family, font-size, code-theme, mac-code-block, line-number, count, legend
### Fixes
- `baoyu-markdown-to-html`: fix CSS custom property regex to handle quoted values; grace/simple themes now layer default CSS
## 1.73.3 - 2026-03-20
### Fixes
- `baoyu-post-to-wechat`: fix placeholder replacement to avoid shorter placeholders matching longer numbered variants
## 1.73.2 - 2026-03-20
### Fixes
- `baoyu-post-to-wechat`: fix body image upload to correctly use media/uploadimg API with format and size validation (by @AICreator-Wind)
### Refactor
- `baoyu-post-to-wechat`: extract image processor module for local format conversion (WebP/BMP/GIF → JPEG/PNG) instead of material API fallback
## 1.73.1 - 2026-03-18
### Refactor
- `baoyu-danger-x-to-markdown`: migrate tests from bun:test to node:test
## 1.73.0 - 2026-03-18
### Features
- `baoyu-danger-x-to-markdown`: add video media support for X articles with poster image and video link rendering
## 1.72.0 - 2026-03-18
### Features
- `baoyu-danger-x-to-markdown`: add MARKDOWN entity support for rendering embedded markdown/code blocks in X articles
## 1.71.0 - 2026-03-17
### Features
- `baoyu-image-gen`: add Seedream reference image support for 5.0/4.5/4.0 models with model-specific size validation
## 1.70.0 - 2026-03-17
### Features
- `baoyu-format-markdown`: optimize title generation with formula-based recommendations and straightforward alternatives
- `baoyu-format-markdown`: auto-generate dual summaries (`summary` + `description`) in frontmatter
## 1.69.1 - 2026-03-16
### Fixes
- `baoyu-chrome-cdp`: tighten chrome auto-connect logic to reduce false positives
## 1.69.0 - 2026-03-16
### Features
- `baoyu-chrome-cdp`: support connecting to existing Chrome session (by @bviews)
### Fixes
- `baoyu-chrome-cdp`: support Chrome 146 native remote debugging in approval mode (by @bviews)
- `baoyu-chrome-cdp`: keep HTTP validation in findExistingChromeDebugPort (by @bviews)
- `baoyu-danger-gemini-web`: reuse openPageSession and fix orphaned tab leak (by @bviews)
- `baoyu-danger-gemini-web`: respect explicit profile config over auto-discovery (by @bviews)
- `baoyu-danger-gemini-web`: respect BAOYU_CHROME_PROFILE_DIR in auto-discovery skip (by @bviews)
- `baoyu-post-to-wechat`: improve browser publishing reliability (by @cfh-7598)
### Documentation
- `baoyu-cover-image`: clarify people reference image workflow and interactive confirmation
## 1.68.0 - 2026-03-14
### Features
- `baoyu-article-illustrator`: add configurable output directory (`default_output_dir`) with 4 options — `imgs-subdir`, `same-dir`, `illustrations-subdir`, `independent`
- `baoyu-cover-image`: add character preservation from reference images — use `usage: direct` to pass people references to model for stylized likeness
## 1.67.0 - 2026-03-13
### Features
- `baoyu-image-gen`: add qwen-image-2.0-pro model support for DashScope provider with free-form sizes and text rendering (by @JianJang2017)
## 1.66.1 - 2026-03-13
### Tests
- Migrate test files from centralized `tests/` directory to colocate with source code
- Convert tests from `.mjs` to TypeScript (`.test.ts`) with `tsx` runner
- Add npm workspaces configuration and npm cache to CI workflow
## 1.66.0 - 2026-03-13
### Features
- `baoyu-image-gen`: add Jimeng (即梦) and Seedream (豆包) image generation providers (by @lindaifeng)
### Fixes
- `baoyu-image-gen`: tighten Jimeng provider behavior
### Refactor
- `baoyu-image-gen`: export functions for testability and add module entry guard
### Documentation
- `baoyu-image-gen`: add Jimeng and Seedream provider documentation to SKILL.md and READMEs
### Tests
- Add test infrastructure with CI workflow and image-gen unit tests
## 1.65.1 - 2026-03-13
### Refactor
- `baoyu-translate`: replace remark/unified with markdown-it for chunk parsing, add main.ts CLI entry point
## 1.65.0 - 2026-03-13
### Features
- `baoyu-post-to-wechat`: add placeholder image upload support with deduplication for markdown-embedded images
### Fixes
- `baoyu-post-to-wechat`: fix frontmatter parsing to allow leading whitespace and optional trailing newline
### Refactor
- `baoyu-post-to-wechat`: replace `renderMarkdownToHtml` with `renderMarkdownWithPlaceholders` for structured output
## 1.64.0 - 2026-03-13
### Features
- `baoyu-image-gen`: add OpenRouter provider with support for image generation, reference images, and configurable models
## 1.63.0 - 2026-03-13
### Features
+209
View File
@@ -2,6 +2,215 @@
[English](./CHANGELOG.md) | 中文
## 1.81.0 - 2026-03-24
### 新功能
- `baoyu-youtube-transcript`YouTube 封锁直连 InnerTube API 时自动回退到 yt-dlp,支持备用客户端身份重试及通过 `YOUTUBE_TRANSCRIPT_COOKIES_FROM_BROWSER` 环境变量传递浏览器 Cookie
### 重构
- `baoyu-youtube-transcript`:将单体脚本拆分为类型化模块(youtube、transcript、storage、shared、types)并添加单元测试
## 1.80.1 - 2026-03-24
### 修复
- `baoyu-image-gen`:修正即梦 API 请求中的 `prompt` 字段名
## 1.80.0 - 2026-03-24
### 新功能
- `baoyu-image-gen`:新增 Azure OpenAI 作为独立图像生成服务商,支持灵活的端点解析、部署名称推断、质量映射及参考图片格式校验
## 1.79.2 - 2026-03-23
### 修复
- `baoyu-cover-image`:简化参考图片处理流程 — 模型支持 `--ref` 时直接传递,仅在模型不支持参考图时创建描述文件
- `baoyu-post-to-weibo`:文章 Markdown 转 HTML 时不传递 --theme 参数
### 测试
- 修复 Node 兼容的解析器测试,添加解析器测试依赖
## 1.79.1 - 2026-03-23
### 修复
- 合并为单一插件,防止 skill 重复注册 (by @TyrealQ)
- `baoyu-article-illustrator`:移除水印提示词中的不透明度参数
- `baoyu-comic`:修正哆啦 A 梦命名间距,移除水印不透明度参数
- `baoyu-xhs-images`:移除水印不透明度参数,修正中英文间距
### 文档
- 更新项目文档以反映单一插件架构
## 1.79.0 - 2026-03-22
### 新功能
- `baoyu-post-to-wechat`:改进凭据加载机制,支持多来源优先级解析,并提供不完整凭据来源的诊断信息
## 1.78.0 - 2026-03-22
### 新功能
- `baoyu-url-to-markdown`:新增 URL 专用解析层,支持 X/Twitter 和 archive.ph 站点的定制化 HTML 提取
- `baoyu-url-to-markdown`:改进 slug 生成算法,去除停用词并采用子目录输出结构
### 修复
- `baoyu-url-to-markdown`:旧版转换器保留包含媒体元素的锚标签
- `baoyu-url-to-markdown`:更智能的标题去重,避免重复添加标题
## 1.77.0 - 2026-03-22
### 新功能
- `baoyu-youtube-transcript`:为章节数据添加结束时间 (by @jzOcb)
### 修复
- `sync-clawhub`:跳过失败的技能而不是中止同步
## 1.76.1 - 2026-03-21
### 文档
- `baoyu-youtube-transcript`:修复 zsh glob 问题 — 运行脚本时始终对 YouTube URL 使用单引号
## 1.76.0 - 2026-03-21
### 新功能
- `baoyu-youtube-transcript`:Markdown 输出中新增标题、描述摘要和封面图片
### 修复
- `baoyu-markdown-to-html`:测试运行器改用 process.execPath 和 tsx import
## 1.75.0 - 2026-03-21
### 新功能
- `baoyu-youtube-transcript`:新技能 — 下载 YouTube 视频字幕/转录文本和封面图片,支持多语言、章节分段和说话人识别
## 1.74.1 - 2026-03-21
### 修复
- `baoyu-image-gen`:对齐 OpenRouter 图像生成与当前 API,增强图像支持,收窄 Gemini 宽高比范围 (by @cwandev)
- `baoyu-image-gen`:扩展 OpenRouter 模型检测和宽高比验证
## 1.74.0 - 2026-03-20
### 新功能
- `baoyu-markdown-to-html`:CLI 支持全部渲染选项 — color、font-family、font-size、code-theme、mac-code-block、line-number、count、legend
### 修复
- `baoyu-markdown-to-html`:修复 CSS 自定义属性正则无法处理带引号值的问题;grace/simple 主题现在会叠加 default 主题 CSS
## 1.73.3 - 2026-03-20
### 修复
- `baoyu-post-to-wechat`:修复占位符替换时短占位符错误匹配更长编号变体的问题
## 1.73.2 - 2026-03-20
### 修复
- `baoyu-post-to-wechat`:修复正文图片上传,正确使用 media/uploadimg 接口并处理格式和大小限制 (by @AICreator-Wind)
### 重构
- `baoyu-post-to-wechat`:提取图片处理模块,本地转换不支持的格式(WebP/BMP/GIF → JPEG/PNG)而非回退到 material 接口
## 1.73.1 - 2026-03-18
### 重构
- `baoyu-danger-x-to-markdown`:测试从 bun:test 迁移至 node:test
## 1.73.0 - 2026-03-18
### 新功能
- `baoyu-danger-x-to-markdown`:支持 X 文章中的视频媒体,渲染封面图和视频链接
## 1.72.0 - 2026-03-18
### 新功能
- `baoyu-danger-x-to-markdown`:支持渲染 X 文章中嵌入的 MARKDOWN 实体(代码块等)
## 1.71.0 - 2026-03-17
### 新功能
- `baoyu-image-gen`:为 Seedream 5.0/4.5/4.0 模型添加参考图支持,并增加模型特定的尺寸校验
## 1.70.0 - 2026-03-17
### 新功能
- `baoyu-format-markdown`:优化标题生成,基于公式智能推荐并提供平实风格备选
- `baoyu-format-markdown`:自动生成双版本摘要(`summary` + `description`),写入 frontmatter
## 1.69.1 - 2026-03-16
### 修复
- `baoyu-chrome-cdp`:收紧 Chrome 自动连接逻辑,减少误连接
## 1.69.0 - 2026-03-16
### 新功能
- `baoyu-chrome-cdp`:支持连接到已有的 Chrome 会话 (by @bviews)
### 修复
- `baoyu-chrome-cdp`:支持 Chrome 146 原生远程调试(审批模式)(by @bviews)
- `baoyu-chrome-cdp`:保留 findExistingChromeDebugPort 中的 HTTP 验证 (by @bviews)
- `baoyu-danger-gemini-web`:复用 openPageSession 并修复孤立标签页泄漏 (by @bviews)
- `baoyu-danger-gemini-web`:显式配置优先于自动发现 (by @bviews)
- `baoyu-danger-gemini-web`:自动发现跳过时也遵循 BAOYU_CHROME_PROFILE_DIR (by @bviews)
- `baoyu-post-to-wechat`:提升浏览器发布可靠性 (by @cfh-7598)
### 文档
- `baoyu-cover-image`:完善人物参考图片工作流和交互式确认说明
## 1.68.0 - 2026-03-14
### 新功能
- `baoyu-article-illustrator`:新增可配置输出目录(`default_output_dir`),支持 4 种选项——`imgs-subdir``same-dir``illustrations-subdir``independent`
- `baoyu-cover-image`:新增参考图片人物保留功能——当参考图包含人物时使用 `usage: direct` 传递给模型,风格化保留人物特征
## 1.67.0 - 2026-03-13
### 新功能
- `baoyu-image-gen`:新增 DashScope qwen-image-2.0-pro 模型支持,支持自由尺寸和文字渲染 (by @JianJang2017)
## 1.66.1 - 2026-03-13
### 测试
- 将测试文件从集中式 `tests/` 目录迁移至与源码同级
- 将测试从 `.mjs` 转换为 TypeScript`.test.ts`),使用 `tsx` 运行器
- 新增 npm workspaces 配置,CI 工作流添加 npm 缓存
## 1.66.0 - 2026-03-13
### 新功能
- `baoyu-image-gen`:新增即梦(Jimeng)和豆包(Seedream)图像生成服务商 (by @lindaifeng)
### 修复
- `baoyu-image-gen`:收紧即梦服务商行为
### 重构
- `baoyu-image-gen`:导出函数以支持测试,新增模块入口守卫
### 文档
- `baoyu-image-gen`:在 SKILL.md 和 README 中添加即梦和豆包服务商文档
### 测试
- 新增测试基础设施,包含 CI 工作流和 image-gen 单元测试
## 1.65.1 - 2026-03-13
### 重构
- `baoyu-translate`:将 chunk 解析从 remark/unified 替换为 markdown-it,新增 main.ts CLI 入口
## 1.65.0 - 2026-03-13
### 新功能
- `baoyu-post-to-wechat`:新增占位符图片上传支持,自动去重 Markdown 内嵌图片
### 修复
- `baoyu-post-to-wechat`:修复 frontmatter 解析,允许前导空白和可选的尾随换行
### 重构
- `baoyu-post-to-wechat`:将 `renderMarkdownToHtml` 重构为 `renderMarkdownWithPlaceholders`,输出结构化结果
## 1.64.0 - 2026-03-13
### 新功能
- `baoyu-image-gen`:新增 OpenRouter 服务商,支持图像生成、参考图和可配置模型
## 1.63.0 - 2026-03-13
### 新功能
+8 -8
View File
@@ -1,16 +1,16 @@
# CLAUDE.md
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.63.0**.
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.81.0**.
## Architecture
Skills organized into three categories in `.claude-plugin/marketplace.json` (defines plugin metadata, version, and skill paths):
Skills are exposed through the single `baoyu-skills` plugin in `.claude-plugin/marketplace.json` (which defines plugin metadata, version, and skill paths). The repo docs still group them into three logical areas:
| Category | Description |
|----------|-------------|
| `content-skills` | Generate or publish content (images, slides, comics, posts) |
| `ai-generation-skills` | AI generation backends |
| `utility-skills` | Content processing (conversion, compression, translation) |
| Group | Description |
|-------|-------------|
| Content Skills | Generate or publish content (images, slides, comics, posts) |
| AI Generation Skills | AI generation backends |
| Utility Skills | Content processing (conversion, compression, translation) |
Each skill contains `SKILL.md` (YAML front matter + docs), optional `scripts/`, `references/`, `prompts/`.
@@ -31,7 +31,7 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
- **Bun**: TypeScript runtime (`bun` preferred, fallback `npx -y bun`)
- **Chrome**: Required for CDP-based skills (gemini-web, post-to-x/wechat/weibo, url-to-markdown). All CDP skills share a single profile, override via `BAOYU_CHROME_PROFILE_DIR` env var. Platform paths: [docs/chrome-profile.md](docs/chrome-profile.md)
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Google, DashScope, or Replicate) configured in EXTEND.md
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, or Replicate) configured in EXTEND.md
- **Gemini Web auth**: Browser cookies (first run opens Chrome for login, `--login` to refresh)
## Security
+101 -18
View File
@@ -52,16 +52,14 @@ Run the following command in Claude Code:
1. Select **Browse and install plugins**
2. Select **baoyu-skills**
3. Select the plugin(s) you want to install
3. Select the **baoyu-skills** plugin
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
# Install the marketplace's single plugin
/plugin install baoyu-skills@baoyu-skills
```
**Option 3: Ask the Agent**
@@ -70,13 +68,13 @@ Simply tell Claude Code:
> Please install Skills from github.com/JimLiu/baoyu-skills
### Available Plugins
### Available Plugin
| Plugin | Description | Skills |
|--------|-------------|--------|
| **content-skills** | Content generation and publishing | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
| **ai-generation-skills** | AI-powered generation backends | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | Utility tools for content processing | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [markdown-to-html](#baoyu-markdown-to-html), [translate](#baoyu-translate) |
The marketplace now exposes a single plugin so each skill is registered exactly once.
| Plugin | Description | Includes |
|--------|-------------|----------|
| **baoyu-skills** | Content generation, AI backends, and utility tools for daily work efficiency | All skills in this repository, organized below as Content Skills, AI Generation Skills, and Utility Skills |
## Update Skills
@@ -665,7 +663,7 @@ AI-powered generation backends.
#### baoyu-image-gen
AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyun Tongyi Wanxiang) APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), Jimeng (即梦), Seedream (豆包), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
```bash
# Basic generation (auto-detect provider)
@@ -680,10 +678,25 @@ AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyu
# Specific provider
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai
# Azure OpenAI (model = deployment name)
/baoyu-image-gen --prompt "A cat" --image cat.png --provider azure --model gpt-image-1.5
# OpenRouter
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openrouter
# DashScope (Aliyun Tongyi Wanxiang)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
# With reference images (Google multimodal only)
# Replicate
/baoyu-image-gen --prompt "A cat" --image cat.png --provider replicate
# Jimeng (即梦)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider jimeng
# Seedream (豆包)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
# With reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, or Seedream 5.0/4.5/4.0)
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
```
@@ -693,25 +706,39 @@ AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyu
| `--prompt`, `-p` | Prompt text |
| `--promptfiles` | Read prompt from files (concatenated) |
| `--image` | Output image path (required) |
| `--provider` | `google`, `openai` or `dashscope` (default: google) |
| `--provider` | `google`, `openai`, `openrouter`, `dashscope`, `jimeng`, `seedream` or `replicate` (default: auto-detect; prefers google) |
| `--model`, `-m` | Model ID |
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
| `--size` | Size (e.g., `1024x1024`) |
| `--quality` | `normal` or `2k` (default: normal) |
| `--ref` | Reference images (Google multimodal only) |
| `--quality` | `normal` or `2k` (default: `2k`) |
| `--ref` | Reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 5.0/4.5/4.0) |
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
| Variable | Description | Default |
|----------|-------------|---------|
| `OPENAI_API_KEY` | OpenAI API key | - |
| `OPENROUTER_API_KEY` | OpenRouter API key | - |
| `GOOGLE_API_KEY` | Google API key | - |
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
| `REPLICATE_API_TOKEN` | Replicate API token | - |
| `JIMENG_ACCESS_KEY_ID` | Jimeng Volcengine access key | - |
| `JIMENG_SECRET_ACCESS_KEY` | Jimeng Volcengine secret key | - |
| `ARK_API_KEY` | Seedream Volcengine ARK API key | - |
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model | `google/gemini-3.1-flash-image-preview` |
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `z-image-turbo` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `qwen-image-2.0-pro` |
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-pro` |
| `JIMENG_IMAGE_MODEL` | Jimeng model | `jimeng_t2i_v40` |
| `SEEDREAM_IMAGE_MODEL` | Seedream model | `doubao-seedream-5-0-260128` |
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
| `OPENROUTER_BASE_URL` | Custom OpenRouter endpoint | `https://openrouter.ai/api/v1` |
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
| `REPLICATE_BASE_URL` | Custom Replicate endpoint | - |
| `JIMENG_BASE_URL` | Custom Jimeng endpoint | `https://visual.volcengineapi.com` |
| `JIMENG_REGION` | Jimeng region | `cn-north-1` |
| `SEEDREAM_BASE_URL` | Custom Seedream endpoint | `https://ark.cn-beijing.volces.com/api/v3` |
**Provider Auto-Selection**:
1. If `--provider` specified → use it
@@ -740,6 +767,40 @@ Interacts with Gemini Web to generate text and images.
Utility tools for content processing.
#### baoyu-youtube-transcript
Download YouTube video transcripts/subtitles and cover images. Supports multiple languages, translation, chapters, and speaker identification. Caches raw data for fast re-formatting.
```bash
# Default: markdown with timestamps
/baoyu-youtube-transcript https://www.youtube.com/watch?v=VIDEO_ID
# Specify languages (priority order)
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --languages zh,en,ja
# With chapters and speaker identification
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --chapters --speakers
# SRT subtitle format
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --format srt
# List available transcripts
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --list
```
**Options**:
| Option | Description | Default |
|--------|-------------|---------|
| `<url-or-id>` | YouTube URL or video ID | Required |
| `--languages <codes>` | Language codes, comma-separated | `en` |
| `--format <fmt>` | Output format: `text`, `srt` | `text` |
| `--translate <code>` | Translate to specified language | |
| `--chapters` | Chapter segmentation from video description | |
| `--speakers` | Speaker identification (requires AI post-processing) | |
| `--no-timestamps` | Disable timestamps | |
| `--list` | List available transcripts | |
| `--refresh` | Force re-fetch, ignore cache | |
#### baoyu-url-to-markdown
Fetch any URL via Chrome CDP and convert to clean markdown. Saves rendered HTML snapshot alongside the markdown, and automatically falls back to a legacy extractor when Defuddle fails.
@@ -958,6 +1019,11 @@ OPENAI_API_KEY=sk-xxx
OPENAI_IMAGE_MODEL=gpt-image-1.5
# OPENAI_BASE_URL=https://api.openai.com/v1
# OpenRouter
OPENROUTER_API_KEY=sk-or-xxx
OPENROUTER_IMAGE_MODEL=google/gemini-3.1-flash-image-preview
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
# Google
GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
@@ -965,8 +1031,25 @@ GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# DashScope (Aliyun Tongyi Wanxiang)
DASHSCOPE_API_KEY=sk-xxx
DASHSCOPE_IMAGE_MODEL=z-image-turbo
DASHSCOPE_IMAGE_MODEL=qwen-image-2.0-pro
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
# Replicate
REPLICATE_API_TOKEN=r8_xxx
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
# REPLICATE_BASE_URL=https://api.replicate.com
# Jimeng (即梦)
JIMENG_ACCESS_KEY_ID=xxx
JIMENG_SECRET_ACCESS_KEY=xxx
JIMENG_IMAGE_MODEL=jimeng_t2i_v40
# JIMENG_BASE_URL=https://visual.volcengineapi.com
# JIMENG_REGION=cn-north-1
# Seedream (豆包)
ARK_API_KEY=xxx
SEEDREAM_IMAGE_MODEL=doubao-seedream-5-0-260128
# SEEDREAM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
EOF
```
+99 -16
View File
@@ -52,16 +52,14 @@ clawhub install baoyu-markdown-to-html
1. 选择 **Browse and install plugins**
2. 选择 **baoyu-skills**
3. 选择要安装的插件
3. 选择 **baoyu-skills** 插件
4. 选择 **Install now**
**方式二:直接安装**
```bash
# 安装指定插件
/plugin install content-skills@baoyu-skills
/plugin install ai-generation-skills@baoyu-skills
/plugin install utility-skills@baoyu-skills
# 安装 marketplace 中唯一的插件
/plugin install baoyu-skills@baoyu-skills
```
**方式三:告诉 Agent**
@@ -72,11 +70,11 @@ clawhub install baoyu-markdown-to-html
### 可用插件
| 插件 | 说明 | 包含技能 |
现在 marketplace 只暴露一个插件,这样每个 skill 只会注册一次。
| 插件 | 说明 | 包含内容 |
|------|------|----------|
| **content-skills** | 内容生成和发布 | [xhs-images](#baoyu-xhs-images), [infographic](#baoyu-infographic), [cover-image](#baoyu-cover-image), [slide-deck](#baoyu-slide-deck), [comic](#baoyu-comic), [article-illustrator](#baoyu-article-illustrator), [post-to-x](#baoyu-post-to-x), [post-to-wechat](#baoyu-post-to-wechat), [post-to-weibo](#baoyu-post-to-weibo) |
| **ai-generation-skills** | AI 生成后端 | [image-gen](#baoyu-image-gen), [danger-gemini-web](#baoyu-danger-gemini-web) |
| **utility-skills** | 内容处理工具 | [url-to-markdown](#baoyu-url-to-markdown), [danger-x-to-markdown](#baoyu-danger-x-to-markdown), [compress-image](#baoyu-compress-image), [format-markdown](#baoyu-format-markdown), [markdown-to-html](#baoyu-markdown-to-html), [translate](#baoyu-translate) |
| **baoyu-skills** | 提供内容生成、AI 后端和日常效率工具技能 | 仓库中的全部 skills,仍按下方的内容技能、AI 生成技能、工具技能三个分类展示 |
## 更新技能
@@ -665,7 +663,7 @@ AI 驱动的生成后端。
#### baoyu-image-gen
基于 AI SDK 的图像生成,使用官方 OpenAI、GoogleDashScope(阿里通义万相)API。支持文生图、参考图、宽高比和质量预设。
基于 AI SDK 的图像生成,支持 OpenAI、Azure OpenAI、Google、OpenRouter、DashScope(阿里通义万相)、即梦(Jimeng)、豆包(Seedream)和 Replicate API。支持文生图、参考图、宽高比和质量预设。
```bash
# 基础生成(自动检测服务商)
@@ -680,10 +678,25 @@ AI 驱动的生成后端。
# 指定服务商
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
# Azure OpenAImodel 为部署名称)
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider azure --model gpt-image-1.5
# OpenRouter
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openrouter
# DashScope(阿里通义万相)
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
# 带参考图(仅 Google 多模态支持)
# Replicate
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider replicate
# 即梦(Jimeng
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider jimeng
# 豆包(Seedream
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
# 带参考图(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate 或 Seedream 5.0/4.5/4.0
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
```
@@ -693,25 +706,39 @@ AI 驱动的生成后端。
| `--prompt`, `-p` | 提示词文本 |
| `--promptfiles` | 从文件读取提示词(多文件拼接) |
| `--image` | 输出图片路径(必需) |
| `--provider` | `google``openai``dashscope`(默认:google |
| `--provider` | `google``openai``openrouter``dashscope``jimeng``seedream``replicate`(默认:自动检测,优先 google |
| `--model`, `-m` | 模型 ID |
| `--ar` | 宽高比(如 `16:9``1:1``4:3` |
| `--size` | 尺寸(如 `1024x1024` |
| `--quality` | `normal``2k`(默认:normal |
| `--ref` | 参考图片(Google 多模态支持 |
| `--quality` | `normal``2k`(默认:`2k` |
| `--ref` | 参考图片(Google、OpenAI、OpenRouter、Replicate 或 Seedream 5.0/4.5/4.0 |
**环境变量**(配置方法见[环境配置](#环境配置)):
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `OPENAI_API_KEY` | OpenAI API 密钥 | - |
| `OPENROUTER_API_KEY` | OpenRouter API 密钥 | - |
| `GOOGLE_API_KEY` | Google API 密钥 | - |
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
| `REPLICATE_API_TOKEN` | Replicate API Token | - |
| `JIMENG_ACCESS_KEY_ID` | 即梦火山引擎 Access Key | - |
| `JIMENG_SECRET_ACCESS_KEY` | 即梦火山引擎 Secret Key | - |
| `ARK_API_KEY` | 豆包火山引擎 ARK API 密钥 | - |
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
| `OPENROUTER_IMAGE_MODEL` | OpenRouter 模型 | `google/gemini-3.1-flash-image-preview` |
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `z-image-turbo` |
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `qwen-image-2.0-pro` |
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-pro` |
| `JIMENG_IMAGE_MODEL` | 即梦模型 | `jimeng_t2i_v40` |
| `SEEDREAM_IMAGE_MODEL` | 豆包模型 | `doubao-seedream-5-0-260128` |
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
| `OPENROUTER_BASE_URL` | 自定义 OpenRouter 端点 | `https://openrouter.ai/api/v1` |
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
| `REPLICATE_BASE_URL` | 自定义 Replicate 端点 | - |
| `JIMENG_BASE_URL` | 自定义即梦端点 | `https://visual.volcengineapi.com` |
| `JIMENG_REGION` | 即梦区域 | `cn-north-1` |
| `SEEDREAM_BASE_URL` | 自定义豆包端点 | `https://ark.cn-beijing.volces.com/api/v3` |
**服务商自动选择**
1. 如果指定了 `--provider` → 使用指定的
@@ -740,6 +767,40 @@ AI 驱动的生成后端。
内容处理工具。
#### baoyu-youtube-transcript
下载 YouTube 视频字幕/转录文本和封面图片。支持多语言、翻译、章节分段和说话人识别。缓存原始数据以便快速重新格式化。
```bash
# 默认:带时间戳的 Markdown
/baoyu-youtube-transcript https://www.youtube.com/watch?v=VIDEO_ID
# 指定语言(按优先级排列)
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --languages zh,en,ja
# 章节分段 + 说话人识别
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --chapters --speakers
# SRT 字幕格式
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --format srt
# 列出可用字幕
/baoyu-youtube-transcript https://youtu.be/VIDEO_ID --list
```
**选项**
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `<url-or-id>` | YouTube URL 或视频 ID | 必填 |
| `--languages <codes>` | 语言代码,逗号分隔 | `en` |
| `--format <fmt>` | 输出格式:`text``srt` | `text` |
| `--translate <code>` | 翻译为指定语言 | |
| `--chapters` | 根据视频描述进行章节分段 | |
| `--speakers` | 说话人识别(需 AI 后处理) | |
| `--no-timestamps` | 禁用时间戳 | |
| `--list` | 列出可用字幕 | |
| `--refresh` | 强制重新获取,忽略缓存 | |
#### baoyu-url-to-markdown
通过 Chrome CDP 抓取任意 URL 并转换为 Markdown。同时保存渲染后的 HTML 快照,Defuddle 失败时自动回退到旧版提取器。
@@ -958,6 +1019,11 @@ OPENAI_API_KEY=sk-xxx
OPENAI_IMAGE_MODEL=gpt-image-1.5
# OPENAI_BASE_URL=https://api.openai.com/v1
# OpenRouter
OPENROUTER_API_KEY=sk-or-xxx
OPENROUTER_IMAGE_MODEL=google/gemini-3.1-flash-image-preview
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
# Google
GOOGLE_API_KEY=xxx
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
@@ -965,8 +1031,25 @@ GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
# DashScope(阿里通义万相)
DASHSCOPE_API_KEY=sk-xxx
DASHSCOPE_IMAGE_MODEL=z-image-turbo
DASHSCOPE_IMAGE_MODEL=qwen-image-2.0-pro
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
# Replicate
REPLICATE_API_TOKEN=r8_xxx
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
# REPLICATE_BASE_URL=https://api.replicate.com
# 即梦(Jimeng
JIMENG_ACCESS_KEY_ID=xxx
JIMENG_SECRET_ACCESS_KEY=xxx
JIMENG_IMAGE_MODEL=jimeng_t2i_v40
# JIMENG_BASE_URL=https://visual.volcengineapi.com
# JIMENG_REGION=cn-north-1
# 豆包(Seedream
ARK_API_KEY=xxx
SEEDREAM_IMAGE_MODEL=doubao-seedream-5-0-260128
# SEEDREAM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
EOF
```
+11 -9
View File
@@ -34,20 +34,22 @@ metadata:
1. Create `skills/baoyu-<name>/SKILL.md` with YAML front matter
2. Add TypeScript in `skills/baoyu-<name>/scripts/` (if applicable)
3. Add prompt templates in `skills/baoyu-<name>/prompts/` if needed
4. Register in `marketplace.json` under appropriate category
4. Register the skill in `.claude-plugin/marketplace.json` under the `baoyu-skills` plugin entry
5. Add Script Directory section to SKILL.md if skill has scripts
6. Add openclaw metadata to frontmatter
## Category Selection
## Skill Grouping
| If your skill... | Use category |
|------------------|--------------|
| Generates visual content (images, slides, comics) | `content-skills` |
| Publishes to platforms (X, WeChat, Weibo) | `content-skills` |
| Provides AI generation backend | `ai-generation-skills` |
| Converts or processes content | `utility-skills` |
All skills are registered under the single `baoyu-skills` plugin. Use these logical groups when deciding where the skill should appear in the docs:
New category: add plugin object to `marketplace.json` with `name`, `description`, `skills[]`.
| If your skill... | Use group |
|------------------|-----------|
| Generates visual content (images, slides, comics) | Content Skills |
| Publishes to platforms (X, WeChat, Weibo) | Content Skills |
| Provides AI generation backend | AI Generation Skills |
| Converts or processes content | Utility Skills |
If you add a new logical group, update the docs that present grouped skills, but keep the skill registered under the single `baoyu-skills` plugin entry.
## Writing Descriptions
+3 -1
View File
@@ -27,7 +27,9 @@ Release hooks are configured via `.releaserc.yml`. This repo does not stage a se
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
Current package: `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`).
Current packages:
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`)
- `baoyu-md` (shared Markdown rendering and placeholder pipeline), consumed by 3 skills (`baoyu-markdown-to-html`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`)
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
+73
View File
@@ -0,0 +1,73 @@
# Testing Strategy
This repository has many scripts, but they do not share a single runtime or dependency graph. The lowest-risk testing strategy is to start from stable Node-based library code, then expand outward to CLI and skill-specific smoke tests.
## Current Baseline
- Root test runner: `node:test`
- Entry point: `npm test`
- Coverage command: `npm run test:coverage`
- CI trigger: GitHub Actions on `push`, `pull_request`, and manual dispatch
This avoids introducing Jest/Vitest across a repo that already mixes plain Node scripts, Bun-based skill packages, vendored code, and browser automation.
## Rollout Plan
### Phase 1: Stable library coverage
Focus on pure functions under `scripts/lib/` first.
- `scripts/lib/release-files.mjs`
- `scripts/lib/shared-skill-packages.mjs`
Goals:
- Validate file filtering and release packaging rules
- Catch regressions in package vendoring and dependency rewriting
- Keep tests deterministic and free of network, Bun, or browser requirements
### Phase 2: Root CLI integration tests
Add temp-directory integration tests for root CLIs that already support dry-run or local-only flows.
- `scripts/sync-shared-skill-packages.mjs`
- `scripts/publish-skill.mjs --dry-run`
- `scripts/sync-clawhub.mjs` argument handling and local skill discovery
Goals:
- Assert exit codes and stdout for common flows
- Cover CLI argument parsing without hitting external services
### Phase 3: Skill script smoke tests
Add opt-in smoke tests for selected `skills/*/scripts/` packages, starting with those that:
- accept local input files
- have deterministic output
- do not require authenticated browser sessions
Examples:
- markdown transforms
- file conversion helpers
- local content analyzers
Keep browser automation, login flows, and live API publishing scripts outside the default CI path unless they are explicitly mocked.
### Phase 4: Coverage gates
After the stable Node path has enough breadth, add coverage thresholds in CI for the tested root modules.
Recommended order:
1. Start with reporting only
2. Add line/function thresholds for `scripts/lib/**`
3. Expand include patterns once skill-level smoke tests are reliable
## Conventions For New Tests
- Prefer temp directories over committed fixtures unless the fixture is reused heavily
- Test exported functions before testing CLI wrappers
- Avoid network, browser, and credential dependencies in default CI
- Keep tests isolated so they can run with plain `node --test`
@@ -1,20 +1,478 @@
{
"name": "md",
"name": "baoyu-skills",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"fflate": "^0.8.2",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"juice": "^11.0.1",
"marked": "^15.0.6",
"reading-time": "^1.5.0",
"remark-cjk-friendly": "^1.1.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.5"
"name": "baoyu-skills",
"workspaces": [
"packages/*"
],
"devDependencies": {
"@mozilla/readability": "^0.6.0",
"linkedom": "^0.18.12",
"tsx": "^4.20.5",
"turndown": "^7.2.2",
"turndown-plugin-gfm": "^1.0.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/@mozilla/readability": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
"integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@types/debug": {
@@ -75,6 +533,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/baoyu-chrome-cdp": {
"resolved": "packages/baoyu-chrome-cdp",
"link": true
},
"node_modules/baoyu-md": {
"resolved": "packages/baoyu-md",
"link": true
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -170,6 +636,13 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -314,6 +787,48 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.4",
"@esbuild/android-arm": "0.27.4",
"@esbuild/android-arm64": "0.27.4",
"@esbuild/android-x64": "0.27.4",
"@esbuild/darwin-arm64": "0.27.4",
"@esbuild/darwin-x64": "0.27.4",
"@esbuild/freebsd-arm64": "0.27.4",
"@esbuild/freebsd-x64": "0.27.4",
"@esbuild/linux-arm": "0.27.4",
"@esbuild/linux-arm64": "0.27.4",
"@esbuild/linux-ia32": "0.27.4",
"@esbuild/linux-loong64": "0.27.4",
"@esbuild/linux-mips64el": "0.27.4",
"@esbuild/linux-ppc64": "0.27.4",
"@esbuild/linux-riscv64": "0.27.4",
"@esbuild/linux-s390x": "0.27.4",
"@esbuild/linux-x64": "0.27.4",
"@esbuild/netbsd-arm64": "0.27.4",
"@esbuild/netbsd-x64": "0.27.4",
"@esbuild/openbsd-arm64": "0.27.4",
"@esbuild/openbsd-x64": "0.27.4",
"@esbuild/openharmony-arm64": "0.27.4",
"@esbuild/sunos-x64": "0.27.4",
"@esbuild/win32-arm64": "0.27.4",
"@esbuild/win32-ia32": "0.27.4",
"@esbuild/win32-x64": "0.27.4"
}
},
"node_modules/escape-goat": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz",
@@ -360,6 +875,21 @@
"js-yaml": "^3.13.1"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-east-asian-width": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
@@ -372,6 +902,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.6",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
@@ -381,6 +924,13 @@
"node": ">=12.0.0"
}
},
"node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
"dev": true,
"license": "MIT"
},
"node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
@@ -469,6 +1019,51 @@
"node": ">=18.17"
}
},
"node_modules/linkedom": {
"version": "0.18.12",
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz",
"integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==",
"dev": true,
"license": "ISC",
"dependencies": {
"css-select": "^5.1.0",
"cssom": "^0.5.0",
"html-escaper": "^3.0.3",
"htmlparser2": "^10.0.0",
"uhyphen": "^0.2.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"canvas": ">= 2"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/linkedom/node_modules/htmlparser2": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -1192,6 +1787,16 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1223,10 +1828,54 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/turndown": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
"integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
}
},
"node_modules/turndown-plugin-gfm": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz",
"integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==",
"dev": true,
"license": "MIT"
},
"node_modules/uhyphen": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
"dev": true,
"license": "ISC"
},
"node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"version": "6.24.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz",
"integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -1390,6 +2039,24 @@
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"packages/baoyu-chrome-cdp": {
"version": "0.1.0"
},
"packages/baoyu-md": {
"version": "0.1.0",
"dependencies": {
"fflate": "^0.8.2",
"front-matter": "^4.0.2",
"highlight.js": "^11.11.1",
"juice": "^11.0.1",
"marked": "^15.0.6",
"reading-time": "^1.5.0",
"remark-cjk-friendly": "^1.1.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.5"
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "baoyu-skills",
"private": true,
"type": "module",
"workspaces": [
"packages/*"
],
"scripts": {
"test": "node --import tsx --test",
"test:coverage": "node --import tsx --experimental-test-coverage --test"
},
"devDependencies": {
"@mozilla/readability": "^0.6.0",
"linkedom": "^0.18.12",
"turndown": "^7.2.2",
"turndown-plugin-gfm": "^1.0.2",
"tsx": "^4.20.5"
}
}
+307
View File
@@ -0,0 +1,307 @@
import assert from "node:assert/strict";
import { spawn, type ChildProcess } from "node:child_process";
import fs from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import test, { type TestContext } from "node:test";
import {
discoverRunningChromeDebugPort,
findChromeExecutable,
findExistingChromeDebugPort,
getFreePort,
openPageSession,
resolveSharedChromeProfileDir,
waitForChromeDebugPort,
} from "./index.ts";
function useEnv(
t: TestContext,
values: Record<string, string | null>,
): void {
const previous = new Map<string, string | undefined>();
for (const [key, value] of Object.entries(values)) {
previous.set(key, process.env[key]);
if (value == null) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
t.after(() => {
for (const [key, value] of previous.entries()) {
if (value == null) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
}
async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
async function startDebugServer(port: number): Promise<http.Server> {
const server = http.createServer((req, res) => {
if (req.url === "/json/version") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
}));
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => resolve());
});
return server;
}
async function closeServer(server: http.Server): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
}
function shellPathForPlatform(): string | null {
if (process.platform === "win32") return null;
return "/bin/bash";
}
async function startFakeChromiumProcess(port: number): Promise<ChildProcess | null> {
const shell = shellPathForPlatform();
if (!shell) return null;
const child = spawn(
shell,
[
"-lc",
`exec -a chromium-mock ${JSON.stringify(process.execPath)} -e 'setInterval(() => {}, 1000)' -- --remote-debugging-port=${port}`,
],
{ stdio: "ignore" },
);
await new Promise((resolve) => setTimeout(resolve, 250));
return child;
}
async function stopProcess(child: ChildProcess | null): Promise<void> {
if (!child) return;
if (child.exitCode !== null || child.signalCode !== null) return;
child.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 100));
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
if (child.exitCode !== null || child.signalCode !== null) return;
await new Promise((resolve) => child.once("exit", resolve));
}
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
useEnv(t, { TEST_FIXED_PORT: "45678" });
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
const dynamicPort = await getFreePort();
assert.ok(Number.isInteger(dynamicPort));
assert.ok(dynamicPort > 0);
});
test("findChromeExecutable prefers env overrides and falls back to candidate paths", async (t) => {
const root = await makeTempDir("baoyu-chrome-bin-");
t.after(() => fs.rm(root, { recursive: true, force: true }));
const envChrome = path.join(root, "env-chrome");
const fallbackChrome = path.join(root, "fallback-chrome");
await fs.writeFile(envChrome, "");
await fs.writeFile(fallbackChrome, "");
useEnv(t, { BAOYU_CHROME_PATH: envChrome });
assert.equal(
findChromeExecutable({
envNames: ["BAOYU_CHROME_PATH"],
candidates: { default: [fallbackChrome] },
}),
envChrome,
);
useEnv(t, { BAOYU_CHROME_PATH: null });
assert.equal(
findChromeExecutable({
envNames: ["BAOYU_CHROME_PATH"],
candidates: { default: [fallbackChrome] },
}),
fallbackChrome,
);
});
test("resolveSharedChromeProfileDir supports env overrides, WSL paths, and default suffixes", (t) => {
useEnv(t, { BAOYU_SHARED_PROFILE: "/tmp/custom-profile" });
assert.equal(
resolveSharedChromeProfileDir({
envNames: ["BAOYU_SHARED_PROFILE"],
appDataDirName: "demo-app",
profileDirName: "demo-profile",
}),
path.resolve("/tmp/custom-profile"),
);
useEnv(t, { BAOYU_SHARED_PROFILE: null });
assert.equal(
resolveSharedChromeProfileDir({
wslWindowsHome: "/mnt/c/Users/demo",
appDataDirName: "demo-app",
profileDirName: "demo-profile",
}),
path.join("/mnt/c/Users/demo", ".local", "share", "demo-app", "demo-profile"),
);
const fallback = resolveSharedChromeProfileDir({
appDataDirName: "demo-app",
profileDirName: "demo-profile",
});
assert.match(fallback, /demo-app[\\/]demo-profile$/);
});
test("findExistingChromeDebugPort reads DevToolsActivePort and validates it against a live endpoint", async (t) => {
const root = await makeTempDir("baoyu-cdp-profile-");
t.after(() => fs.rm(root, { recursive: true, force: true }));
const port = await getFreePort();
const server = await startDebugServer(port);
t.after(() => closeServer(server));
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
const found = await findExistingChromeDebugPort({ profileDir: root, timeoutMs: 1000 });
assert.equal(found, port);
});
test("discoverRunningChromeDebugPort reads DevToolsActivePort from the provided user-data dir", async (t) => {
const root = await makeTempDir("baoyu-cdp-user-data-");
t.after(() => fs.rm(root, { recursive: true, force: true }));
const port = await getFreePort();
const server = await startDebugServer(port);
t.after(() => closeServer(server));
await fs.writeFile(path.join(root, "DevToolsActivePort"), `${port}\n/devtools/browser/demo\n`);
const found = await discoverRunningChromeDebugPort({
userDataDirs: [root],
timeoutMs: 1000,
});
assert.deepEqual(found, {
port,
wsUrl: `ws://127.0.0.1:${port}/devtools/browser/demo`,
});
});
test("discoverRunningChromeDebugPort ignores unrelated debugging processes", async (t) => {
if (process.platform === "win32") {
t.skip("Process discovery fallback is not used on Windows.");
return;
}
const root = await makeTempDir("baoyu-cdp-user-data-");
t.after(() => fs.rm(root, { recursive: true, force: true }));
const port = await getFreePort();
const server = await startDebugServer(port);
t.after(() => closeServer(server));
const fakeChromium = await startFakeChromiumProcess(port);
t.after(async () => { await stopProcess(fakeChromium); });
const found = await discoverRunningChromeDebugPort({
userDataDirs: [root],
timeoutMs: 1000,
});
assert.equal(found, null);
});
test("openPageSession reports whether it created a new target", async () => {
const calls: string[] = [];
const cdpExisting = {
send: async <T>(method: string): Promise<T> => {
calls.push(method);
if (method === "Target.getTargets") {
return {
targetInfos: [{ targetId: "existing-target", type: "page", url: "https://gemini.google.com/app" }],
} as T;
}
if (method === "Target.attachToTarget") return { sessionId: "session-existing" } as T;
throw new Error(`Unexpected method: ${method}`);
},
};
const existing = await openPageSession({
cdp: cdpExisting as never,
reusing: false,
url: "https://gemini.google.com/app",
matchTarget: (target) => target.url.includes("gemini.google.com"),
activateTarget: false,
});
assert.deepEqual(existing, {
sessionId: "session-existing",
targetId: "existing-target",
createdTarget: false,
});
assert.deepEqual(calls, ["Target.getTargets", "Target.attachToTarget"]);
const createCalls: string[] = [];
const cdpCreated = {
send: async <T>(method: string): Promise<T> => {
createCalls.push(method);
if (method === "Target.getTargets") return { targetInfos: [] } as T;
if (method === "Target.createTarget") return { targetId: "created-target" } as T;
if (method === "Target.attachToTarget") return { sessionId: "session-created" } as T;
throw new Error(`Unexpected method: ${method}`);
},
};
const created = await openPageSession({
cdp: cdpCreated as never,
reusing: false,
url: "https://gemini.google.com/app",
matchTarget: (target) => target.url.includes("gemini.google.com"),
activateTarget: false,
});
assert.deepEqual(created, {
sessionId: "session-created",
targetId: "created-target",
createdTarget: true,
});
assert.deepEqual(createCalls, ["Target.getTargets", "Target.createTarget", "Target.attachToTarget"]);
});
test("waitForChromeDebugPort retries until the debug endpoint becomes available", async (t) => {
const port = await getFreePort();
const serverPromise = (async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
const server = await startDebugServer(port);
t.after(() => closeServer(server));
})();
const websocketUrl = await waitForChromeDebugPort(port, 4000, {
includeLastError: true,
});
await serverPromise;
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
});
+123 -8
View File
@@ -43,6 +43,19 @@ type FindExistingChromeDebugPortOptions = {
timeoutMs?: number;
};
export type ChromeChannel = "stable" | "beta" | "canary" | "dev";
export type DiscoveredChrome = {
port: number;
wsUrl: string;
};
type DiscoverRunningChromeOptions = {
channels?: ChromeChannel[];
userDataDirs?: string[];
timeoutMs?: number;
};
type LaunchChromeOptions = {
chromePath: string;
profileDir: string;
@@ -73,6 +86,7 @@ type OpenPageSessionOptions = {
export type PageSession = {
sessionId: string;
targetId: string;
createdTarget: boolean;
};
export function sleep(ms: number): Promise<void> {
@@ -173,16 +187,32 @@ async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise<boolea
}
}
function isPortListening(port: number, timeoutMs = 3_000): Promise<boolean> {
return new Promise((resolve) => {
const socket = new net.Socket();
const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeoutMs);
socket.once("connect", () => { clearTimeout(timer); socket.destroy(); resolve(true); });
socket.once("error", () => { clearTimeout(timer); resolve(false); });
socket.connect(port, "127.0.0.1");
});
}
function parseDevToolsActivePort(filePath: string): { port: number; wsPath: string } | null {
try {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split(/\r?\n/);
const port = Number.parseInt(lines[0]?.trim() ?? "", 10);
const wsPath = lines[1]?.trim();
if (port > 0 && wsPath) return { port, wsPath };
} catch {}
return null;
}
export async function findExistingChromeDebugPort(options: FindExistingChromeDebugPortOptions): Promise<number | null> {
const timeoutMs = options.timeoutMs ?? 3_000;
const portFile = path.join(options.profileDir, "DevToolsActivePort");
const parsed = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));
try {
const content = fs.readFileSync(portFile, "utf-8");
const [portLine] = content.split(/\r?\n/);
const port = Number.parseInt(portLine?.trim() ?? "", 10);
if (port > 0 && await isDebugPortReady(port, timeoutMs)) return port;
} catch {}
if (parsed && parsed.port > 0 && await isDebugPortReady(parsed.port, timeoutMs)) return parsed.port;
if (process.platform === "win32") return null;
@@ -204,6 +234,88 @@ export async function findExistingChromeDebugPort(options: FindExistingChromeDeb
return null;
}
export function getDefaultChromeUserDataDirs(channels: ChromeChannel[] = ["stable"]): string[] {
const home = os.homedir();
const dirs: string[] = [];
const channelDirs: Record<string, { darwin: string; linux: string; win32: string }> = {
stable: {
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome"),
linux: path.join(home, ".config", "google-chrome"),
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data"),
},
beta: {
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Beta"),
linux: path.join(home, ".config", "google-chrome-beta"),
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Beta", "User Data"),
},
canary: {
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Canary"),
linux: path.join(home, ".config", "google-chrome-canary"),
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome SxS", "User Data"),
},
dev: {
darwin: path.join(home, "Library", "Application Support", "Google", "Chrome Dev"),
linux: path.join(home, ".config", "google-chrome-dev"),
win32: path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome Dev", "User Data"),
},
};
const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "win32" : "linux";
for (const ch of channels) {
const entry = channelDirs[ch];
if (entry) dirs.push(entry[platform]);
}
return dirs;
}
// Best-effort reuse of an already-running local CDP session discovered from
// known Chrome user-data dirs. This is distinct from Chrome DevTools MCP's
// prompt-based --autoConnect flow.
export async function discoverRunningChromeDebugPort(options: DiscoverRunningChromeOptions = {}): Promise<DiscoveredChrome | null> {
const channels = options.channels ?? ["stable", "beta", "canary", "dev"];
const timeoutMs = options.timeoutMs ?? 3_000;
const userDataDirs = (options.userDataDirs ?? getDefaultChromeUserDataDirs(channels))
.map((dir) => path.resolve(dir));
for (const dir of userDataDirs) {
const parsed = parseDevToolsActivePort(path.join(dir, "DevToolsActivePort"));
if (!parsed) continue;
if (await isPortListening(parsed.port, timeoutMs)) {
return { port: parsed.port, wsUrl: `ws://127.0.0.1:${parsed.port}${parsed.wsPath}` };
}
}
if (process.platform !== "win32") {
try {
const result = spawnSync("ps", ["aux"], { encoding: "utf-8", timeout: 5_000 });
if (result.status === 0 && result.stdout) {
const lines = result.stdout
.split("\n")
.filter((line) =>
line.includes("--remote-debugging-port=") &&
userDataDirs.some((dir) => line.includes(dir))
);
for (const line of lines) {
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
const port = Number.parseInt(portMatch?.[1] ?? "", 10);
if (port > 0 && await isDebugPortReady(port, timeoutMs)) {
try {
const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`, { timeoutMs });
if (version.webSocketDebuggerUrl) return { port, wsUrl: version.webSocketDebuggerUrl };
} catch {}
}
}
}
} catch {}
}
return null;
}
export async function waitForChromeDebugPort(
port: number,
timeoutMs: number,
@@ -376,10 +488,12 @@ export function killChrome(chrome: ChildProcess): void {
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
let targetId: string;
let createdTarget = false;
if (options.reusing) {
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
targetId = created.targetId;
createdTarget = true;
} else {
const targets = await options.cdp.send<{ targetInfos: ChromeTargetInfo[] }>("Target.getTargets");
const existing = targets.targetInfos.find(options.matchTarget);
@@ -388,6 +502,7 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
} else {
const created = await options.cdp.send<{ targetId: string }>("Target.createTarget", { url: options.url });
targetId = created.targetId;
createdTarget = true;
}
}
@@ -404,5 +519,5 @@ export async function openPageSession(options: OpenPageSessionOptions): Promise<
if (options.enableDom) await options.cdp.send("DOM.enable", {}, { sessionId });
if (options.enableNetwork) await options.cdp.send("Network.enable", {}, { sessionId });
return { sessionId, targetId };
return { sessionId, targetId, createdTarget };
}
@@ -1,4 +1,11 @@
{
"name": "baoyu-md",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"fflate": "^0.8.2",
"front-matter": "^4.0.2",
@@ -12,7 +12,7 @@ export function printUsage(): void {
console.error(
[
"Usage:",
" npx tsx src/md/render.ts <markdown_file> [options]",
" npx tsx render.ts <markdown_file> [options]",
"",
"Options:",
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
+93
View File
@@ -0,0 +1,93 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
extractSummaryFromBody,
extractTitleFromMarkdown,
parseFrontmatter,
pickFirstString,
serializeFrontmatter,
stripWrappingQuotes,
toFrontmatterString,
} from "./content.ts";
test("parseFrontmatter extracts YAML fields and strips wrapping quotes", () => {
const input = `---
title: "Hello World"
author: Baoyu
summary: plain text
---
# Heading
Body`;
const result = parseFrontmatter(input);
assert.deepEqual(result.frontmatter, {
title: "Hello World",
author: "Baoyu",
summary: "plain text",
});
assert.match(result.body, /^# Heading/);
});
test("parseFrontmatter returns original content when no frontmatter exists", () => {
const input = "# No frontmatter";
assert.deepEqual(parseFrontmatter(input), {
frontmatter: {},
body: input,
});
});
test("serializeFrontmatter renders YAML only when fields exist", () => {
assert.equal(serializeFrontmatter({}), "");
assert.equal(
serializeFrontmatter({ title: "Hello", author: "Baoyu" }),
"---\ntitle: Hello\nauthor: Baoyu\n---\n",
);
});
test("quote and frontmatter string helpers normalize mixed scalar values", () => {
assert.equal(stripWrappingQuotes(`" quoted "`), "quoted");
assert.equal(stripWrappingQuotes("“ 中文标题 ”"), "中文标题");
assert.equal(stripWrappingQuotes("plain"), "plain");
assert.equal(toFrontmatterString("'hello'"), "hello");
assert.equal(toFrontmatterString(42), "42");
assert.equal(toFrontmatterString(false), "false");
assert.equal(toFrontmatterString({}), undefined);
assert.equal(
pickFirstString({ summary: 123, title: "" }, ["title", "summary"]),
"123",
);
});
test("markdown title and summary extraction skip non-body content and clean formatting", () => {
const markdown = `
![cover](cover.png)
## My Title
Body paragraph
`;
assert.equal(extractTitleFromMarkdown(markdown), "My Title");
const summary = extractSummaryFromBody(
`
# Heading
> quote
- list
1. ordered
\`\`\`
code
\`\`\`
This is **the first paragraph** with [a link](https://example.com) and \`inline code\` that should be summarized cleanly.
`,
70,
);
assert.equal(
summary,
"This is the first paragraph with a link and inline code that should...",
);
});
+105
View File
@@ -0,0 +1,105 @@
import { Lexer } from "marked";
export type FrontmatterFields = Record<string, string>;
export function parseFrontmatter(content: string): {
frontmatter: FrontmatterFields;
body: string;
} {
const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) {
return { frontmatter: {}, body: content };
}
const frontmatter: FrontmatterFields = {};
const lines = match[1]!.split("\n");
for (const line of lines) {
const colonIdx = line.indexOf(":");
if (colonIdx <= 0) continue;
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim();
frontmatter[key] = stripWrappingQuotes(value);
}
return { frontmatter, body: match[2]! };
}
export function serializeFrontmatter(frontmatter: FrontmatterFields): string {
const entries = Object.entries(frontmatter);
if (entries.length === 0) return "";
return `---\n${entries.map(([key, value]) => `${key}: ${value}`).join("\n")}\n---\n`;
}
export function stripWrappingQuotes(value: string): string {
if (!value) return value;
const doubleQuoted = value.startsWith('"') && value.endsWith('"');
const singleQuoted = value.startsWith("'") && value.endsWith("'");
const cjkDoubleQuoted = value.startsWith("\u201c") && value.endsWith("\u201d");
const cjkSingleQuoted = value.startsWith("\u2018") && value.endsWith("\u2019");
if (doubleQuoted || singleQuoted || cjkDoubleQuoted || cjkSingleQuoted) {
return value.slice(1, -1).trim();
}
return value.trim();
}
export function toFrontmatterString(value: unknown): string | undefined {
if (typeof value === "string") {
return stripWrappingQuotes(value);
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return undefined;
}
export function pickFirstString(
frontmatter: Record<string, unknown>,
keys: string[],
): string | undefined {
for (const key of keys) {
const value = toFrontmatterString(frontmatter[key]);
if (value) return value;
}
return undefined;
}
export function extractTitleFromMarkdown(markdown: string): string {
const tokens = Lexer.lex(markdown, { gfm: true, breaks: true });
for (const token of tokens) {
if (token.type !== "heading" || (token.depth !== 1 && token.depth !== 2)) continue;
return stripWrappingQuotes(token.text);
}
return "";
}
export function extractSummaryFromBody(body: string, maxLen: number): string {
const lines = body.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
if (trimmed.startsWith("#")) continue;
if (trimmed.startsWith("![")) continue;
if (trimmed.startsWith(">")) continue;
if (trimmed.startsWith("-") || trimmed.startsWith("*")) continue;
if (/^\d+\./.test(trimmed)) continue;
if (trimmed.startsWith("```")) continue;
const cleanText = trimmed
.replace(/\*\*(.+?)\*\*/g, "$1")
.replace(/\*(.+?)\*/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/`([^`]+)`/g, "$1");
if (cleanText.length > 20) {
if (cleanText.length <= maxLen) return cleanText;
return `${cleanText.slice(0, maxLen - 3)}...`;
}
}
return "";
}
+174
View File
@@ -0,0 +1,174 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import test, { type TestContext } from "node:test";
import { COLOR_PRESETS, FONT_FAMILY_MAP } from "./constants.ts";
import {
buildMarkdownDocumentMeta,
formatTimestamp,
renderMarkdownDocument,
resolveColorToken,
resolveFontFamilyToken,
resolveMarkdownStyle,
resolveRenderOptions,
} from "./document.ts";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`);
}
function findInlineStyle(html: string, tagName: string, text: string): string {
const pattern = new RegExp(
`<${tagName}[^>]*style="([^"]*)"[^>]*>${escapeRegExp(text)}</${tagName}>`,
);
const match = html.match(pattern);
assert.ok(match, `Expected inline style for <${tagName}>${text}</${tagName}>`);
return match![1]!;
}
function useCwd(t: TestContext, cwd: string): void {
const previous = process.cwd();
process.chdir(cwd);
t.after(() => {
process.chdir(previous);
});
}
async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
test("document token resolvers map known presets and allow passthrough values", () => {
assert.equal(resolveColorToken("green"), COLOR_PRESETS.green);
assert.equal(resolveColorToken("#123456"), "#123456");
assert.equal(resolveColorToken(), undefined);
assert.equal(resolveFontFamilyToken("mono"), FONT_FAMILY_MAP.mono);
assert.equal(resolveFontFamilyToken("Custom Font"), "Custom Font");
assert.equal(resolveFontFamilyToken(), undefined);
});
test("formatTimestamp uses compact sortable datetime output", () => {
const date = new Date("2026-03-13T21:04:05.000Z");
const pad = (value: number) => String(value).padStart(2, "0");
const expected = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
date.getDate(),
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
assert.equal(formatTimestamp(date), expected);
});
test("buildMarkdownDocumentMeta prefers frontmatter and falls back to markdown title and summary", () => {
const metaFromYaml = buildMarkdownDocumentMeta(
"# Markdown Title\n\nBody summary paragraph that should be ignored.",
{
title: `" YAML Title "`,
author: "'Baoyu'",
summary: `" YAML Summary "`,
},
"fallback",
);
assert.deepEqual(metaFromYaml, {
title: "YAML Title",
author: "Baoyu",
description: "YAML Summary",
});
const metaFromMarkdown = buildMarkdownDocumentMeta(
`## “Markdown Title”\n\nThis is the first body paragraph that should become the summary because it is long enough.`,
{},
"fallback",
);
assert.equal(metaFromMarkdown.title, "Markdown Title");
assert.match(metaFromMarkdown.description ?? "", /^This is the first body paragraph/);
});
test("resolveMarkdownStyle merges theme defaults with explicit overrides", () => {
const style = resolveMarkdownStyle({
theme: "modern",
primaryColor: "#112233",
fontFamily: "Custom Sans",
});
assert.equal(style.primaryColor, "#112233");
assert.equal(style.fontFamily, "Custom Sans");
assert.equal(style.fontSize, "15px");
assert.equal(style.containerBg, "rgba(250, 249, 245, 1)");
});
test("resolveRenderOptions loads workspace EXTEND settings and lets explicit options win", async (t) => {
const root = await makeTempDir("baoyu-md-render-options-");
useCwd(t, root);
const extendPath = path.join(
root,
".baoyu-skills",
"baoyu-markdown-to-html",
"EXTEND.md",
);
await fs.mkdir(path.dirname(extendPath), { recursive: true });
await fs.writeFile(
extendPath,
`---
default_theme: modern
default_color: green
default_font_family: mono
default_font_size: 17
default_code_theme: nord
mac_code_block: false
show_line_number: true
cite: true
count: true
legend: title-alt
keep_title: true
---
`,
);
const fromExtend = resolveRenderOptions();
assert.equal(fromExtend.theme, "modern");
assert.equal(fromExtend.primaryColor, COLOR_PRESETS.green);
assert.equal(fromExtend.fontFamily, FONT_FAMILY_MAP.mono);
assert.equal(fromExtend.fontSize, "17px");
assert.equal(fromExtend.codeTheme, "nord");
assert.equal(fromExtend.isMacCodeBlock, false);
assert.equal(fromExtend.isShowLineNumber, true);
assert.equal(fromExtend.citeStatus, true);
assert.equal(fromExtend.countStatus, true);
assert.equal(fromExtend.legend, "title-alt");
assert.equal(fromExtend.keepTitle, true);
const explicit = resolveRenderOptions({
theme: "simple",
fontSize: "18px",
keepTitle: false,
});
assert.equal(explicit.theme, "simple");
assert.equal(explicit.fontSize, "18px");
assert.equal(explicit.keepTitle, false);
});
test("renderMarkdownDocument layers default rules into grace theme before CSS inlining", async () => {
const { html } = await renderMarkdownDocument(
`## Section\n\nParagraph with **bold** text.`,
{ keepTitle: true, theme: "grace" },
);
const h2Style = findInlineStyle(html, "h2", "Section");
assert.match(h2Style, /background: #92617E/);
assert.match(h2Style, /box-shadow: 0 4px 6px rgba\(0, 0, 0, 0\.1\)/);
const pMatch = html.match(/<p[^>]*style="([^"]*)"[^>]*>/);
assert.ok(pMatch, "Expected inline style on <p> tag");
assert.match(pMatch![1]!, /color:/);
const strongPattern = /<strong[^>]*style="([^"]*)"[^>]*>bold<\/strong>/;
const strongMatch = html.match(strongPattern);
assert.ok(strongMatch, "Expected inline style for <strong>bold</strong>");
assert.match(strongMatch![1]!, /font-weight:/);
});
+206
View File
@@ -0,0 +1,206 @@
import fs from "node:fs";
import path from "node:path";
import type { ReadTimeResults } from "reading-time";
import {
COLOR_PRESETS,
DEFAULT_STYLE,
FONT_FAMILY_MAP,
THEME_STYLE_DEFAULTS,
} from "./constants.js";
import {
extractSummaryFromBody,
extractTitleFromMarkdown,
pickFirstString,
stripWrappingQuotes,
} from "./content.js";
import { loadExtendConfig } from "./extend-config.js";
import {
buildCss,
buildHtmlDocument,
inlineCss,
loadCodeThemeCss,
modifyHtmlStructure,
normalizeInlineCss,
removeFirstHeading,
} from "./html-builder.js";
import { initRenderer, postProcessHtml, renderMarkdown } from "./renderer.js";
import { loadThemeCss, normalizeThemeCss } from "./themes.js";
import type { HtmlDocumentMeta, IOpts, StyleConfig, ThemeName } from "./types.js";
export interface RenderMarkdownDocumentOptions {
codeTheme?: string;
countStatus?: boolean;
citeStatus?: boolean;
defaultTitle?: string;
fontFamily?: string;
fontSize?: string;
isMacCodeBlock?: boolean;
isShowLineNumber?: boolean;
keepTitle?: boolean;
legend?: string;
primaryColor?: string;
theme?: ThemeName;
themeMode?: IOpts["themeMode"];
}
export interface RenderMarkdownDocumentResult {
contentHtml: string;
html: string;
meta: HtmlDocumentMeta;
readingTime: ReadTimeResults;
style: StyleConfig;
yamlData: Record<string, unknown>;
}
export function resolveColorToken(value?: string): string | undefined {
if (!value) return undefined;
return COLOR_PRESETS[value] ?? value;
}
export function resolveFontFamilyToken(value?: string): string | undefined {
if (!value) return undefined;
return FONT_FAMILY_MAP[value] ?? value;
}
export function formatTimestamp(date = new Date()): string {
const pad = (value: number) => String(value).padStart(2, "0");
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
date.getDate(),
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}
export function buildMarkdownDocumentMeta(
markdown: string,
yamlData: Record<string, unknown>,
defaultTitle = "document",
): HtmlDocumentMeta {
const title = pickFirstString(yamlData, ["title"])
|| extractTitleFromMarkdown(markdown)
|| defaultTitle;
const author = pickFirstString(yamlData, ["author"]);
const description = pickFirstString(yamlData, ["description", "summary"])
|| extractSummaryFromBody(markdown, 120);
return {
title: stripWrappingQuotes(title),
author: author ? stripWrappingQuotes(author) : undefined,
description: description ? stripWrappingQuotes(description) : undefined,
};
}
export function resolveMarkdownStyle(options: RenderMarkdownDocumentOptions = {}): StyleConfig {
const theme = options.theme ?? "default";
const themeDefaults = THEME_STYLE_DEFAULTS[theme] ?? {};
return {
...DEFAULT_STYLE,
...themeDefaults,
...(options.primaryColor !== undefined ? { primaryColor: options.primaryColor } : {}),
...(options.fontFamily !== undefined ? { fontFamily: options.fontFamily } : {}),
...(options.fontSize !== undefined ? { fontSize: options.fontSize } : {}),
};
}
export function resolveRenderOptions(
options: RenderMarkdownDocumentOptions = {},
): RenderMarkdownDocumentOptions {
const extendConfig = loadExtendConfig();
return {
codeTheme: options.codeTheme ?? extendConfig.default_code_theme ?? "github",
countStatus: options.countStatus ?? extendConfig.count ?? false,
citeStatus: options.citeStatus ?? extendConfig.cite ?? false,
defaultTitle: options.defaultTitle,
fontFamily: options.fontFamily ?? resolveFontFamilyToken(extendConfig.default_font_family ?? undefined),
fontSize: options.fontSize ?? extendConfig.default_font_size ?? undefined,
isMacCodeBlock: options.isMacCodeBlock ?? extendConfig.mac_code_block ?? true,
isShowLineNumber: options.isShowLineNumber ?? extendConfig.show_line_number ?? false,
keepTitle: options.keepTitle ?? extendConfig.keep_title ?? false,
legend: options.legend ?? extendConfig.legend ?? "alt",
primaryColor: options.primaryColor ?? resolveColorToken(extendConfig.default_color ?? undefined),
theme: options.theme ?? extendConfig.default_theme ?? "default",
themeMode: options.themeMode,
};
}
export async function renderMarkdownDocument(
markdown: string,
options: RenderMarkdownDocumentOptions = {},
): Promise<RenderMarkdownDocumentResult> {
const resolvedOptions = resolveRenderOptions(options);
const theme = resolvedOptions.theme ?? "default";
const codeTheme = resolvedOptions.codeTheme ?? "github";
const style = resolveMarkdownStyle(resolvedOptions);
const { baseCss, themeCss } = loadThemeCss(theme);
const css = normalizeThemeCss(buildCss(baseCss, themeCss, style));
const codeThemeCss = loadCodeThemeCss(codeTheme);
const renderer = initRenderer({
citeStatus: resolvedOptions.citeStatus ?? false,
countStatus: resolvedOptions.countStatus ?? false,
isMacCodeBlock: resolvedOptions.isMacCodeBlock ?? true,
isShowLineNumber: resolvedOptions.isShowLineNumber ?? false,
legend: resolvedOptions.legend ?? "alt",
themeMode: resolvedOptions.themeMode,
});
const { yamlData, markdownContent, readingTime } = renderer.parseFrontMatterAndContent(markdown);
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(markdown, renderer);
let contentHtml = postProcessHtml(baseHtml, readingTimeResult, renderer);
if (!(resolvedOptions.keepTitle ?? false)) {
contentHtml = removeFirstHeading(contentHtml);
}
const meta = buildMarkdownDocumentMeta(
markdownContent,
yamlData as Record<string, unknown>,
resolvedOptions.defaultTitle,
);
const html = buildHtmlDocument(meta, css, contentHtml, codeThemeCss);
const inlinedHtml = normalizeInlineCss(await inlineCss(html), style);
return {
contentHtml,
html: modifyHtmlStructure(inlinedHtml),
meta,
readingTime,
style,
yamlData: yamlData as Record<string, unknown>,
};
}
export async function renderMarkdownFileToHtml(
inputPath: string,
options: RenderMarkdownDocumentOptions = {},
): Promise<RenderMarkdownDocumentResult & {
backupPath?: string;
outputPath: string;
}> {
const markdown = fs.readFileSync(inputPath, "utf-8");
const outputPath = path.resolve(
path.dirname(inputPath),
`${path.basename(inputPath, path.extname(inputPath))}.html`,
);
const result = await renderMarkdownDocument(markdown, {
...options,
defaultTitle: options.defaultTitle ?? path.basename(outputPath, ".html"),
});
let backupPath: string | undefined;
if (fs.existsSync(outputPath)) {
backupPath = `${outputPath}.bak-${formatTimestamp()}`;
fs.renameSync(outputPath, backupPath);
}
fs.writeFileSync(outputPath, result.html, "utf-8");
return {
...result,
backupPath,
outputPath,
};
}

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