mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 22:09:48 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 004236682d | |||
| 7e07c1bb84 | |||
| 6f38724163 | |||
| 374a6b28fd | |||
| 95970480c8 | |||
| fc324319d8 | |||
| 056fa06c3b | |||
| c62cda4424 | |||
| 12b43e166d | |||
| c1e1526c84 | |||
| 0279fa403d | |||
| 994e47d1be | |||
| 6a71d5080d | |||
| 1e6e6637ac | |||
| 873b60aee5 | |||
| b88ac59133 | |||
| d889c04084 | |||
| 5276fae6bd | |||
| c0941f8089 | |||
| 42b8b1fc99 |
@@ -6,14 +6,14 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.60.0"
|
||||
"version": "1.65.1"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "content-skills",
|
||||
"description": "Content generation and publishing skills",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"skills": [
|
||||
"./skills/baoyu-xhs-images",
|
||||
"./skills/baoyu-post-to-x",
|
||||
@@ -30,7 +30,7 @@
|
||||
"name": "ai-generation-skills",
|
||||
"description": "AI-powered generation backends",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"skills": [
|
||||
"./skills/baoyu-danger-gemini-web",
|
||||
"./skills/baoyu-image-gen"
|
||||
@@ -40,7 +40,7 @@
|
||||
"name": "utility-skills",
|
||||
"description": "Utility tools for content processing",
|
||||
"source": "./",
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"skills": [
|
||||
"./skills/baoyu-danger-x-to-markdown",
|
||||
"./skills/baoyu-compress-image",
|
||||
|
||||
@@ -2,6 +2,61 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 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
|
||||
- `baoyu-url-to-markdown`: add hosted `defuddle.md` API fallback when local browser capture fails
|
||||
- `baoyu-url-to-markdown`: extract YouTube transcript/caption text into markdown output
|
||||
- `baoyu-url-to-markdown`: materialize shadow DOM content for better web-component page conversion
|
||||
- `baoyu-url-to-markdown`: include language hint in markdown front matter when available
|
||||
|
||||
### Refactor
|
||||
- `baoyu-url-to-markdown`: split monolithic converter into defuddle, legacy, and shared modules
|
||||
|
||||
### Documentation
|
||||
- Fix Claude Code marketplace repo casing in READMEs
|
||||
|
||||
## 1.62.0 - 2026-03-12
|
||||
|
||||
### Features
|
||||
- `baoyu-infographic`: support flexible aspect ratios with custom W:H values (e.g., 3:4, 4:3, 2.35:1) in addition to named presets
|
||||
|
||||
### Fixes
|
||||
- Set strict mode on plugins to prevent duplicated slash commands
|
||||
|
||||
### Documentation
|
||||
- `baoyu-post-to-wechat`: replace credential-like placeholders
|
||||
|
||||
## 1.61.0 - 2026-03-11
|
||||
|
||||
### Features
|
||||
- `baoyu-post-to-wechat`: add multi-account support with `--account` CLI arg, EXTEND.md accounts block, isolated Chrome profiles, and credential resolution chain
|
||||
|
||||
### Fixes
|
||||
- Exclude `out/dist/build` dirs and `bun.lockb` from skill release files
|
||||
- Use proper MIME types in skill publish to fix ClawhHub rejection
|
||||
|
||||
## 1.60.0 - 2026-03-11
|
||||
|
||||
### Features
|
||||
|
||||
@@ -2,6 +2,61 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 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
|
||||
|
||||
### 新功能
|
||||
- `baoyu-url-to-markdown`:本地浏览器抓取失败时自动回退到 `defuddle.md` 托管 API
|
||||
- `baoyu-url-to-markdown`:将 YouTube 字幕/文字记录提取到 Markdown 输出中
|
||||
- `baoyu-url-to-markdown`:转换前展开 Shadow DOM 内容,提升 Web Component 页面的转换质量
|
||||
- `baoyu-url-to-markdown`:Markdown front matter 中包含语言标识(如有)
|
||||
|
||||
### 重构
|
||||
- `baoyu-url-to-markdown`:将单体转换器拆分为 defuddle、legacy 和 shared 三个模块
|
||||
|
||||
### 文档
|
||||
- 修复 README 中 Claude Code marketplace 仓库名大小写
|
||||
|
||||
## 1.62.0 - 2026-03-12
|
||||
|
||||
### 新功能
|
||||
- `baoyu-infographic`:支持灵活宽高比,可使用自定义 W:H 值(如 3:4、4:3、2.35:1),同时保留预设名称
|
||||
|
||||
### 修复
|
||||
- 设置插件严格模式,防止重复注册斜杠命令
|
||||
|
||||
### 文档
|
||||
- `baoyu-post-to-wechat`:替换类似凭证的占位符
|
||||
|
||||
## 1.61.0 - 2026-03-11
|
||||
|
||||
### 新功能
|
||||
- `baoyu-post-to-wechat`:新增多账号支持,通过 `--account` 参数选择账号,EXTEND.md 支持 accounts 配置块,每个账号独立 Chrome 配置目录和凭证解析链
|
||||
|
||||
### 修复
|
||||
- 排除 `out/dist/build` 目录和 `bun.lockb` 文件,避免打包到技能发布文件中
|
||||
- 修复技能发布时 MIME 类型不正确导致 ClawhHub 拒绝的问题
|
||||
|
||||
## 1.60.0 - 2026-03-11
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.60.0**.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.65.1**.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -31,7 +31,7 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
|
||||
|
||||
- **Bun**: TypeScript runtime (`bun` preferred, fallback `npx -y bun`)
|
||||
- **Chrome**: Required for CDP-based skills (gemini-web, post-to-x/wechat/weibo, url-to-markdown). All CDP skills share a single profile, override via `BAOYU_CHROME_PROFILE_DIR` env var. Platform paths: [docs/chrome-profile.md](docs/chrome-profile.md)
|
||||
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Google, DashScope, or Replicate) configured in EXTEND.md
|
||||
- **Image generation APIs**: `baoyu-image-gen` requires API key (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
|
||||
|
||||
@@ -43,7 +43,7 @@ Publishing to ClawHub releases the published skill under `MIT-0`, per ClawHub's
|
||||
Run the following command in Claude Code:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add jimliu/baoyu-skills
|
||||
/plugin marketplace add JimLiu/baoyu-skills
|
||||
```
|
||||
|
||||
### Install Skills
|
||||
@@ -169,8 +169,9 @@ Generate professional infographics with 20 layout types and 17 visual styles. An
|
||||
# Specify both
|
||||
/baoyu-infographic path/to/content.md --layout funnel --style corporate-memphis
|
||||
|
||||
# With aspect ratio
|
||||
# With aspect ratio (named preset or custom W:H)
|
||||
/baoyu-infographic path/to/content.md --aspect portrait
|
||||
/baoyu-infographic path/to/content.md --aspect 3:4
|
||||
```
|
||||
|
||||
**Options**:
|
||||
@@ -178,7 +179,7 @@ Generate professional infographics with 20 layout types and 17 visual styles. An
|
||||
|--------|-------------|
|
||||
| `--layout <name>` | Information layout (20 options) |
|
||||
| `--style <name>` | Visual style (17 options, default: craft-handmade) |
|
||||
| `--aspect <ratio>` | landscape (16:9), portrait (9:16), square (1:1) |
|
||||
| `--aspect <ratio>` | Named: landscape (16:9), portrait (9:16), square (1:1). Custom: any W:H ratio (e.g., 3:4, 4:3, 2.35:1) |
|
||||
| `--lang <code>` | Output language (en, zh, ja, etc.) |
|
||||
|
||||
**Layouts** (information structure):
|
||||
@@ -581,6 +582,47 @@ To obtain credentials:
|
||||
|
||||
**Browser Method** (no API setup needed): Requires Google Chrome. First run opens browser for QR code login (session preserved).
|
||||
|
||||
**Multi-Account Support**: Manage multiple WeChat Official Accounts via `EXTEND.md`:
|
||||
|
||||
```bash
|
||||
mkdir -p .baoyu-skills/baoyu-post-to-wechat
|
||||
```
|
||||
|
||||
Create `.baoyu-skills/baoyu-post-to-wechat/EXTEND.md`:
|
||||
|
||||
```yaml
|
||||
# Global settings (shared across all accounts)
|
||||
default_theme: default
|
||||
default_color: blue
|
||||
|
||||
# Account list
|
||||
accounts:
|
||||
- name: My Tech Blog
|
||||
alias: tech-blog
|
||||
default: false
|
||||
default_publish_method: api
|
||||
default_author: Author Name
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
app_id: your_wechat_app_id
|
||||
app_secret: your_wechat_app_secret
|
||||
- name: AI Newsletter
|
||||
alias: ai-news
|
||||
default_publish_method: browser
|
||||
default_author: AI Newsletter
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
```
|
||||
|
||||
| Accounts configured | Behavior |
|
||||
|---------------------|----------|
|
||||
| No `accounts` block | Single-account mode (backward compatible) |
|
||||
| 1 account | Auto-select, no prompt |
|
||||
| 2+ accounts | Prompt to select, or use `--account <alias>` |
|
||||
| 1 account has `default: true` | Pre-selected as default |
|
||||
|
||||
Each account gets an isolated Chrome profile for independent login sessions (browser method). API credentials can be set inline in EXTEND.md or via `.env` with alias-prefixed keys (e.g., `WECHAT_TECH_BLOG_APP_ID`).
|
||||
|
||||
#### baoyu-post-to-weibo
|
||||
|
||||
Post content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input. Uses real Chrome with CDP to bypass anti-automation.
|
||||
@@ -623,7 +665,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, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
|
||||
|
||||
```bash
|
||||
# Basic generation (auto-detect provider)
|
||||
@@ -638,10 +680,16 @@ 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
|
||||
|
||||
# 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
|
||||
|
||||
# With reference images (Google, OpenAI, OpenRouter, or Replicate)
|
||||
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
||||
```
|
||||
|
||||
@@ -651,25 +699,31 @@ 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` 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 or Replicate) |
|
||||
|
||||
**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 | - |
|
||||
| `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` |
|
||||
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-pro` |
|
||||
| `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 | - |
|
||||
|
||||
**Provider Auto-Selection**:
|
||||
1. If `--provider` specified → use it
|
||||
@@ -916,6 +970,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
|
||||
@@ -925,6 +984,11 @@ GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
||||
DASHSCOPE_API_KEY=sk-xxx
|
||||
DASHSCOPE_IMAGE_MODEL=z-image-turbo
|
||||
# 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
|
||||
EOF
|
||||
```
|
||||
|
||||
|
||||
+72
-8
@@ -43,7 +43,7 @@ clawhub install baoyu-markdown-to-html
|
||||
在 Claude Code 中运行:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add jimliu/baoyu-skills
|
||||
/plugin marketplace add JimLiu/baoyu-skills
|
||||
```
|
||||
|
||||
### 安装技能
|
||||
@@ -169,8 +169,9 @@ clawhub install baoyu-markdown-to-html
|
||||
# 同时指定布局和风格
|
||||
/baoyu-infographic path/to/content.md --layout funnel --style corporate-memphis
|
||||
|
||||
# 指定比例
|
||||
# 指定比例(预设名称或自定义 W:H)
|
||||
/baoyu-infographic path/to/content.md --aspect portrait
|
||||
/baoyu-infographic path/to/content.md --aspect 3:4
|
||||
```
|
||||
|
||||
**选项**:
|
||||
@@ -178,7 +179,7 @@ clawhub install baoyu-markdown-to-html
|
||||
|------|------|
|
||||
| `--layout <name>` | 信息布局(20 种选项) |
|
||||
| `--style <name>` | 视觉风格(17 种选项,默认:craft-handmade) |
|
||||
| `--aspect <ratio>` | landscape (16:9)、portrait (9:16)、square (1:1) |
|
||||
| `--aspect <ratio>` | 预设:landscape (16:9)、portrait (9:16)、square (1:1)。自定义:任意 W:H 比例(如 3:4、4:3、2.35:1) |
|
||||
| `--lang <code>` | 输出语言(en、zh、ja 等) |
|
||||
|
||||
**布局**(信息结构):
|
||||
@@ -581,6 +582,47 @@ WECHAT_APP_SECRET=你的AppSecret
|
||||
|
||||
**浏览器方式**(无需 API 配置):需已安装 Google Chrome,首次运行需扫码登录(登录状态会保存)
|
||||
|
||||
**多账号支持**:通过 `EXTEND.md` 管理多个微信公众号:
|
||||
|
||||
```bash
|
||||
mkdir -p .baoyu-skills/baoyu-post-to-wechat
|
||||
```
|
||||
|
||||
创建 `.baoyu-skills/baoyu-post-to-wechat/EXTEND.md`:
|
||||
|
||||
```yaml
|
||||
# 全局设置(所有账号共享)
|
||||
default_theme: default
|
||||
default_color: blue
|
||||
|
||||
# 账号列表
|
||||
accounts:
|
||||
- name: 宝玉的技术分享
|
||||
alias: baoyu
|
||||
default: false
|
||||
default_publish_method: api
|
||||
default_author: 宝玉
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
app_id: 你的微信AppID
|
||||
app_secret: 你的微信AppSecret
|
||||
- name: AI 工具集
|
||||
alias: ai-tools
|
||||
default_publish_method: browser
|
||||
default_author: AI 工具集
|
||||
need_open_comment: 1
|
||||
only_fans_can_comment: 0
|
||||
```
|
||||
|
||||
| 账号配置情况 | 行为 |
|
||||
|-------------|------|
|
||||
| 无 `accounts` 块 | 单账号模式(向后兼容) |
|
||||
| 1 个账号 | 自动选择,无需提示 |
|
||||
| 2+ 个账号 | 提示选择,或使用 `--account <别名>` |
|
||||
| 某账号设置 `default: true` | 预选为默认账号 |
|
||||
|
||||
每个账号拥有独立的 Chrome 配置目录,保证浏览器方式下的登录会话互不干扰。API 凭证可在 EXTEND.md 中直接配置,也可通过 `.env` 文件使用别名前缀的环境变量(如 `WECHAT_BAOYU_APP_ID`)。
|
||||
|
||||
#### baoyu-post-to-weibo
|
||||
|
||||
发布内容到微博。支持文字、图片、视频发布和头条文章(长篇 Markdown)。使用真实 Chrome + CDP 绕过反自动化检测。
|
||||
@@ -623,7 +665,7 @@ AI 驱动的生成后端。
|
||||
|
||||
#### baoyu-image-gen
|
||||
|
||||
基于 AI SDK 的图像生成,使用官方 OpenAI、Google 和 DashScope(阿里通义万相)API。支持文生图、参考图、宽高比和质量预设。
|
||||
基于 AI SDK 的图像生成,支持 OpenAI、Google、OpenRouter、DashScope(阿里通义万相)和 Replicate API。支持文生图、参考图、宽高比和质量预设。
|
||||
|
||||
```bash
|
||||
# 基础生成(自动检测服务商)
|
||||
@@ -638,10 +680,16 @@ AI 驱动的生成后端。
|
||||
# 指定服务商
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
|
||||
|
||||
# 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
|
||||
|
||||
# 带参考图(Google、OpenAI、OpenRouter 或 Replicate)
|
||||
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
||||
```
|
||||
|
||||
@@ -651,25 +699,31 @@ AI 驱动的生成后端。
|
||||
| `--prompt`, `-p` | 提示词文本 |
|
||||
| `--promptfiles` | 从文件读取提示词(多文件拼接) |
|
||||
| `--image` | 输出图片路径(必需) |
|
||||
| `--provider` | `google`、`openai` 或 `dashscope`(默认:google) |
|
||||
| `--provider` | `google`、`openai`、`openrouter`、`dashscope` 或 `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) |
|
||||
|
||||
**环境变量**(配置方法见[环境配置](#环境配置)):
|
||||
| 变量 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `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 | - |
|
||||
| `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` |
|
||||
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-pro` |
|
||||
| `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 端点 | - |
|
||||
|
||||
**服务商自动选择**:
|
||||
1. 如果指定了 `--provider` → 使用指定的
|
||||
@@ -916,6 +970,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
|
||||
@@ -925,6 +984,11 @@ GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
||||
DASHSCOPE_API_KEY=sk-xxx
|
||||
DASHSCOPE_IMAGE_MODEL=z-image-turbo
|
||||
# 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
|
||||
EOF
|
||||
```
|
||||
|
||||
|
||||
+3
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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(", ")})`,
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import path from "node:path";
|
||||
|
||||
export interface ImagePlaceholder {
|
||||
originalPath: string;
|
||||
placeholder: string;
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedImageInfo extends ImagePlaceholder {
|
||||
localPath: string;
|
||||
}
|
||||
|
||||
export function replaceMarkdownImagesWithPlaceholders(
|
||||
markdown: string,
|
||||
placeholderPrefix: string,
|
||||
): {
|
||||
images: ImagePlaceholder[];
|
||||
markdown: string;
|
||||
} {
|
||||
const images: ImagePlaceholder[] = [];
|
||||
let imageCounter = 0;
|
||||
|
||||
const rewritten = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => {
|
||||
const placeholder = `${placeholderPrefix}${++imageCounter}`;
|
||||
images.push({
|
||||
alt,
|
||||
originalPath: src,
|
||||
placeholder,
|
||||
});
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
return { images, markdown: rewritten };
|
||||
}
|
||||
|
||||
export function getImageExtension(urlOrPath: string): string {
|
||||
const match = urlOrPath.match(/\.(jpg|jpeg|png|gif|webp)(\?|$)/i);
|
||||
return match ? match[1]!.toLowerCase() : "png";
|
||||
}
|
||||
|
||||
export async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith("https://") ? https : http;
|
||||
const file = fs.createWriteStream(destPath);
|
||||
|
||||
const request = protocol.get(url, { headers: { "User-Agent": "Mozilla/5.0" } }, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl) {
|
||||
file.close();
|
||||
fs.unlinkSync(destPath);
|
||||
void downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
file.close();
|
||||
fs.unlinkSync(destPath);
|
||||
reject(new Error(`Failed to download: ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
response.pipe(file);
|
||||
file.on("finish", () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", (error) => {
|
||||
file.close();
|
||||
fs.unlink(destPath, () => {});
|
||||
reject(error);
|
||||
});
|
||||
|
||||
request.setTimeout(30_000, () => {
|
||||
request.destroy();
|
||||
reject(new Error("Download timeout"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveImagePath(
|
||||
imagePath: string,
|
||||
baseDir: string,
|
||||
tempDir: string,
|
||||
logLabel = "baoyu-md",
|
||||
): Promise<string> {
|
||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||
const hash = createHash("md5").update(imagePath).digest("hex").slice(0, 8);
|
||||
const ext = getImageExtension(imagePath);
|
||||
const localPath = path.join(tempDir, `remote_${hash}.${ext}`);
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
console.error(`[${logLabel}] Downloading: ${imagePath}`);
|
||||
await downloadFile(imagePath, localPath);
|
||||
}
|
||||
return localPath;
|
||||
}
|
||||
|
||||
const resolved = path.isAbsolute(imagePath)
|
||||
? imagePath
|
||||
: path.resolve(baseDir, imagePath);
|
||||
return resolveLocalWithFallback(resolved, logLabel);
|
||||
}
|
||||
|
||||
export async function resolveContentImages(
|
||||
images: ImagePlaceholder[],
|
||||
baseDir: string,
|
||||
tempDir: string,
|
||||
logLabel = "baoyu-md",
|
||||
): Promise<ResolvedImageInfo[]> {
|
||||
const resolved: ResolvedImageInfo[] = [];
|
||||
|
||||
for (const image of images) {
|
||||
resolved.push({
|
||||
...image,
|
||||
localPath: await resolveImagePath(image.originalPath, baseDir, tempDir, logLabel),
|
||||
});
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveLocalWithFallback(resolved: string, logLabel: string): string {
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const ext = path.extname(resolved);
|
||||
const base = ext ? resolved.slice(0, -ext.length) : resolved;
|
||||
const alternatives = [
|
||||
`${base}.webp`,
|
||||
`${base}.jpg`,
|
||||
`${base}.jpeg`,
|
||||
`${base}.png`,
|
||||
`${base}.gif`,
|
||||
`${base}_original.png`,
|
||||
`${base}_original.jpg`,
|
||||
].filter((candidate) => candidate !== resolved);
|
||||
|
||||
for (const alternative of alternatives) {
|
||||
if (!fs.existsSync(alternative)) continue;
|
||||
console.error(
|
||||
`[${logLabel}] Image fallback: ${path.basename(resolved)} -> ${path.basename(alternative)}`,
|
||||
);
|
||||
return alternative;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from "./cli.js";
|
||||
export * from "./constants.js";
|
||||
export * from "./content.js";
|
||||
export * from "./document.js";
|
||||
export * from "./extend-config.js";
|
||||
export * from "./html-builder.js";
|
||||
export * from "./images.js";
|
||||
export * from "./renderer.js";
|
||||
export * from "./themes.js";
|
||||
export * from "./types.js";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user