mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-14 06:39:48 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83afacb00e | |||
| 0e6bfbcabd | |||
| 7b67ff44b8 | |||
| 79c289ca92 | |||
| fc5ad4b762 | |||
| 747977416d | |||
| 60363fc2df | |||
| 28ec1053f6 | |||
| 5a6afd576f | |||
| ebc74a10ad | |||
| e79a42fd94 | |||
| ea84f21439 | |||
| 0b9e51d6cc | |||
| 7cc9c92722 | |||
| fc2f0d042a |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.70.0"
|
"version": "1.73.2"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 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
|
## 1.70.0 - 2026-03-17
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 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
|
## 1.70.0 - 2026-03-17
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.70.0**.
|
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.73.2**.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -695,7 +695,7 @@ AI SDK-based image generation using OpenAI, Google, OpenRouter, DashScope (Aliyu
|
|||||||
# Seedream (豆包)
|
# Seedream (豆包)
|
||||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||||
|
|
||||||
# With reference images (Google, OpenAI, OpenRouter, or Replicate)
|
# With reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 5.0/4.5/4.0)
|
||||||
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -710,7 +710,7 @@ AI SDK-based image generation using OpenAI, Google, OpenRouter, DashScope (Aliyu
|
|||||||
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||||
| `--size` | Size (e.g., `1024x1024`) |
|
| `--size` | Size (e.g., `1024x1024`) |
|
||||||
| `--quality` | `normal` or `2k` (default: `2k`) |
|
| `--quality` | `normal` or `2k` (default: `2k`) |
|
||||||
| `--ref` | Reference images (Google, OpenAI, OpenRouter or Replicate) |
|
| `--ref` | Reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 5.0/4.5/4.0) |
|
||||||
|
|
||||||
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
|
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|
|||||||
+2
-2
@@ -695,7 +695,7 @@ AI 驱动的生成后端。
|
|||||||
# 豆包(Seedream)
|
# 豆包(Seedream)
|
||||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||||
|
|
||||||
# 带参考图(Google、OpenAI、OpenRouter 或 Replicate)
|
# 带参考图(Google、OpenAI、OpenRouter、Replicate 或 Seedream 5.0/4.5/4.0)
|
||||||
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -710,7 +710,7 @@ AI 驱动的生成后端。
|
|||||||
| `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) |
|
| `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) |
|
||||||
| `--size` | 尺寸(如 `1024x1024`) |
|
| `--size` | 尺寸(如 `1024x1024`) |
|
||||||
| `--quality` | `normal` 或 `2k`(默认:`2k`) |
|
| `--quality` | `normal` 或 `2k`(默认:`2k`) |
|
||||||
| `--ref` | 参考图片(Google、OpenAI、OpenRouter 或 Replicate) |
|
| `--ref` | 参考图片(Google、OpenAI、OpenRouter、Replicate 或 Seedream 5.0/4.5/4.0) |
|
||||||
|
|
||||||
**环境变量**(配置方法见[环境配置](#环境配置)):
|
**环境变量**(配置方法见[环境配置](#环境配置)):
|
||||||
| 变量 | 说明 | 默认值 |
|
| 变量 | 说明 | 默认值 |
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { formatArticleMarkdown } from "./markdown.js";
|
||||||
|
|
||||||
|
test("formatArticleMarkdown renders MARKDOWN entities from atomic blocks", () => {
|
||||||
|
const article = {
|
||||||
|
title: "Atomic Markdown Example",
|
||||||
|
content_state: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "unstyled",
|
||||||
|
text: "Before the snippet.",
|
||||||
|
entityRanges: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "atomic",
|
||||||
|
text: " ",
|
||||||
|
entityRanges: [{ key: 0, offset: 0, length: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "unstyled",
|
||||||
|
text: "After the snippet.",
|
||||||
|
entityRanges: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
entityMap: {
|
||||||
|
"0": {
|
||||||
|
key: "5",
|
||||||
|
value: {
|
||||||
|
type: "MARKDOWN",
|
||||||
|
mutability: "Mutable",
|
||||||
|
data: {
|
||||||
|
markdown: "```python\nprint('hello from x article')\n```\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { markdown } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
assert.ok(markdown.includes("Before the snippet."));
|
||||||
|
assert.ok(markdown.includes("```python\nprint('hello from x article')\n```"));
|
||||||
|
assert.ok(markdown.includes("After the snippet."));
|
||||||
|
assert.strictEqual(markdown, `# Atomic Markdown Example
|
||||||
|
|
||||||
|
Before the snippet.
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
print('hello from x article')
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
After the snippet.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatArticleMarkdown renders article video media as poster plus video link", () => {
|
||||||
|
const posterUrl = "https://pbs.twimg.com/amplify_video_thumb/123/img/poster.jpg";
|
||||||
|
const videoUrl = "https://video.twimg.com/amplify_video/123/vid/avc1/720x720/demo.mp4?tag=21";
|
||||||
|
const article = {
|
||||||
|
title: "Video Example",
|
||||||
|
content_state: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "unstyled",
|
||||||
|
text: "Intro text.",
|
||||||
|
entityRanges: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "atomic",
|
||||||
|
text: " ",
|
||||||
|
entityRanges: [{ key: 0, offset: 0, length: 1 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
entityMap: {
|
||||||
|
"0": {
|
||||||
|
key: "0",
|
||||||
|
value: {
|
||||||
|
type: "MEDIA",
|
||||||
|
mutability: "Immutable",
|
||||||
|
data: {
|
||||||
|
caption: "Demo reel",
|
||||||
|
mediaItems: [{ mediaId: "vid-1" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
media_entities: [
|
||||||
|
{
|
||||||
|
media_id: "vid-1",
|
||||||
|
media_info: {
|
||||||
|
__typename: "ApiVideo",
|
||||||
|
preview_image: {
|
||||||
|
original_img_url: posterUrl,
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
content_type: "video/mp4",
|
||||||
|
bit_rate: 256000,
|
||||||
|
url: videoUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { markdown } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
assert.ok(markdown.includes("Intro text."));
|
||||||
|
assert.ok(markdown.includes(``));
|
||||||
|
assert.ok(markdown.includes(`[video](${videoUrl})`));
|
||||||
|
assert.ok(!markdown.includes(``));
|
||||||
|
assert.ok(!markdown.includes("## Media"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatArticleMarkdown renders unused article videos in trailing media section", () => {
|
||||||
|
const posterUrl = "https://pbs.twimg.com/amplify_video_thumb/456/img/poster.jpg";
|
||||||
|
const videoUrl = "https://video.twimg.com/amplify_video/456/vid/avc1/1080x1080/demo.mp4?tag=21";
|
||||||
|
const article = {
|
||||||
|
title: "Trailing Media Example",
|
||||||
|
plain_text: "Body text.",
|
||||||
|
media_entities: [
|
||||||
|
{
|
||||||
|
media_id: "vid-2",
|
||||||
|
media_info: {
|
||||||
|
__typename: "ApiVideo",
|
||||||
|
preview_image: {
|
||||||
|
original_img_url: posterUrl,
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
content_type: "video/mp4",
|
||||||
|
bit_rate: 832000,
|
||||||
|
url: videoUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { markdown, coverUrl } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
assert.strictEqual(coverUrl, null);
|
||||||
|
assert.ok(markdown.includes("## Media"));
|
||||||
|
assert.ok(markdown.includes(``));
|
||||||
|
assert.ok(markdown.includes(`[video](${videoUrl})`));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatArticleMarkdown keeps coverUrl as preview image for video cover media", () => {
|
||||||
|
const posterUrl = "https://pbs.twimg.com/amplify_video_thumb/789/img/poster.jpg";
|
||||||
|
const videoUrl = "https://video.twimg.com/amplify_video/789/vid/avc1/720x720/demo.mp4?tag=21";
|
||||||
|
const article = {
|
||||||
|
title: "Video Cover Example",
|
||||||
|
plain_text: "Body text.",
|
||||||
|
cover_media: {
|
||||||
|
media_info: {
|
||||||
|
__typename: "ApiVideo",
|
||||||
|
preview_image: {
|
||||||
|
original_img_url: posterUrl,
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
content_type: "video/mp4",
|
||||||
|
bit_rate: 1280000,
|
||||||
|
url: videoUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { coverUrl } = formatArticleMarkdown(article);
|
||||||
|
|
||||||
|
assert.strictEqual(coverUrl, posterUrl);
|
||||||
|
});
|
||||||
@@ -18,6 +18,17 @@ export type FormatArticleOptions = {
|
|||||||
referencedTweets?: Map<string, ReferencedTweetInfo>;
|
referencedTweets?: Map<string, ReferencedTweetInfo>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ResolvedMediaAsset =
|
||||||
|
| {
|
||||||
|
kind: "image";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "video";
|
||||||
|
url: string;
|
||||||
|
posterUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function coerceArticleEntity(value: unknown): ArticleEntity | null {
|
function coerceArticleEntity(value: unknown): ArticleEntity | null {
|
||||||
if (!value || typeof value !== "object") return null;
|
if (!value || typeof value !== "object") return null;
|
||||||
const candidate = value as ArticleEntity;
|
const candidate = value as ArticleEntity;
|
||||||
@@ -109,58 +120,127 @@ function resolveEntityEntry(
|
|||||||
return entityMap[String(entityKey)];
|
return entityMap[String(entityKey)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMediaUrl(info?: ArticleMediaInfo): string | undefined {
|
function resolveVideoUrl(info?: ArticleMediaInfo): string | undefined {
|
||||||
if (!info) return undefined;
|
if (!info) return undefined;
|
||||||
if (info.original_img_url) return info.original_img_url;
|
|
||||||
if (info.preview_image?.original_img_url) return info.preview_image.original_img_url;
|
|
||||||
const variants = info.variants ?? [];
|
const variants = info.variants ?? [];
|
||||||
const mp4 = variants
|
const mp4 = variants
|
||||||
.filter((variant) => variant?.content_type?.includes("video"))
|
.filter((variant) => variant?.content_type?.includes("video"))
|
||||||
.sort((a, b) => (b.bit_rate ?? 0) - (a.bit_rate ?? 0))[0];
|
.sort((a, b) => (b.bit_rate ?? 0) - (a.bit_rate ?? 0))[0];
|
||||||
return mp4?.url ?? variants[0]?.url;
|
return mp4?.url ?? variants.find((variant) => typeof variant?.url === "string")?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMediaById(article: ArticleEntity): Map<string, string> {
|
function resolveMediaAsset(info?: ArticleMediaInfo): ResolvedMediaAsset | undefined {
|
||||||
const map = new Map<string, string>();
|
if (!info) return undefined;
|
||||||
|
|
||||||
|
const posterUrl = info.preview_image?.original_img_url ?? info.original_img_url;
|
||||||
|
const videoUrl = resolveVideoUrl(info);
|
||||||
|
if (videoUrl) {
|
||||||
|
return {
|
||||||
|
kind: "video",
|
||||||
|
url: videoUrl,
|
||||||
|
posterUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = info.original_img_url ?? info.preview_image?.original_img_url;
|
||||||
|
if (imageUrl) {
|
||||||
|
return {
|
||||||
|
kind: "image",
|
||||||
|
url: imageUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFallbackMediaAsset(rawUrl?: string): ResolvedMediaAsset | undefined {
|
||||||
|
if (!rawUrl) return undefined;
|
||||||
|
|
||||||
|
if (/^https:\/\/video\.twimg\.com\//i.test(rawUrl) || /\.(mp4|m4v|mov|webm)(?:$|[?#])/i.test(rawUrl)) {
|
||||||
|
return {
|
||||||
|
kind: "video",
|
||||||
|
url: rawUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "image",
|
||||||
|
url: rawUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCoverUrl(info?: ArticleMediaInfo): string | undefined {
|
||||||
|
if (!info) return undefined;
|
||||||
|
return info.original_img_url ?? info.preview_image?.original_img_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMediaIdentity(asset: ResolvedMediaAsset): string {
|
||||||
|
return asset.kind === "video"
|
||||||
|
? `video:${asset.url}:${asset.posterUrl ?? ""}`
|
||||||
|
: `image:${asset.url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMediaLines(
|
||||||
|
asset: ResolvedMediaAsset,
|
||||||
|
altText: string,
|
||||||
|
usedUrls: Set<string>
|
||||||
|
): string[] {
|
||||||
|
if (asset.kind === "video") {
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (asset.posterUrl && !usedUrls.has(asset.posterUrl)) {
|
||||||
|
usedUrls.add(asset.posterUrl);
|
||||||
|
lines.push(``);
|
||||||
|
}
|
||||||
|
if (!usedUrls.has(asset.url)) {
|
||||||
|
usedUrls.add(asset.url);
|
||||||
|
lines.push(`[video](${asset.url})`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedUrls.has(asset.url)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
usedUrls.add(asset.url);
|
||||||
|
return [``];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMediaById(article: ArticleEntity): Map<string, ResolvedMediaAsset> {
|
||||||
|
const map = new Map<string, ResolvedMediaAsset>();
|
||||||
for (const entity of article.media_entities ?? []) {
|
for (const entity of article.media_entities ?? []) {
|
||||||
if (!entity?.media_id) continue;
|
if (!entity?.media_id) continue;
|
||||||
const url = resolveMediaUrl(entity.media_info);
|
const asset = resolveMediaAsset(entity.media_info);
|
||||||
if (url) {
|
if (asset) {
|
||||||
map.set(entity.media_id, url);
|
map.set(entity.media_id, asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectMediaUrls(
|
function collectMediaAssets(article: ArticleEntity): ResolvedMediaAsset[] {
|
||||||
article: ArticleEntity,
|
const assets: ResolvedMediaAsset[] = [];
|
||||||
usedUrls: Set<string>,
|
const seen = new Set<string>();
|
||||||
excludeUrl?: string
|
const addAsset = (asset?: ResolvedMediaAsset) => {
|
||||||
): string[] {
|
if (!asset) return;
|
||||||
const urls: string[] = [];
|
const identity = buildMediaIdentity(asset);
|
||||||
const addUrl = (url?: string) => {
|
if (seen.has(identity)) return;
|
||||||
if (!url) return;
|
seen.add(identity);
|
||||||
if (excludeUrl && url === excludeUrl) {
|
assets.push(asset);
|
||||||
usedUrls.add(url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (usedUrls.has(url)) return;
|
|
||||||
usedUrls.add(url);
|
|
||||||
urls.push(url);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const entity of article.media_entities ?? []) {
|
for (const entity of article.media_entities ?? []) {
|
||||||
addUrl(resolveMediaUrl(entity?.media_info));
|
addAsset(resolveMediaAsset(entity?.media_info));
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveEntityMediaLines(
|
function resolveEntityMediaLines(
|
||||||
entityKey: number | undefined,
|
entityKey: number | undefined,
|
||||||
entityMap: ArticleContentState["entityMap"] | undefined,
|
entityMap: ArticleContentState["entityMap"] | undefined,
|
||||||
entityLookup: EntityLookup,
|
entityLookup: EntityLookup,
|
||||||
mediaById: Map<string, string>,
|
mediaById: Map<string, ResolvedMediaAsset>,
|
||||||
usedUrls: Set<string>
|
usedUrls: Set<string>
|
||||||
): string[] {
|
): string[] {
|
||||||
if (entityKey === undefined) return [];
|
if (entityKey === undefined) return [];
|
||||||
@@ -182,17 +262,16 @@ function resolveEntityMediaLines(
|
|||||||
: typeof item?.media_id === "string"
|
: typeof item?.media_id === "string"
|
||||||
? item.media_id
|
? item.media_id
|
||||||
: undefined;
|
: undefined;
|
||||||
const url = mediaId ? mediaById.get(mediaId) : undefined;
|
const asset = mediaId ? mediaById.get(mediaId) : undefined;
|
||||||
if (url && !usedUrls.has(url)) {
|
if (asset) {
|
||||||
usedUrls.add(url);
|
lines.push(...renderMediaLines(asset, altText, usedUrls));
|
||||||
lines.push(``);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackUrl = typeof value.data?.url === "string" ? value.data.url : undefined;
|
const fallbackUrl = typeof value.data?.url === "string" ? value.data.url : undefined;
|
||||||
if (fallbackUrl && !usedUrls.has(fallbackUrl)) {
|
const fallbackAsset = resolveFallbackMediaAsset(fallbackUrl);
|
||||||
usedUrls.add(fallbackUrl);
|
if (fallbackAsset) {
|
||||||
lines.push(``);
|
lines.push(...renderMediaLines(fallbackAsset, altText, usedUrls));
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
@@ -237,6 +316,22 @@ function resolveEntityTweetLines(
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveEntityMarkdownLines(
|
||||||
|
entityKey: number | undefined,
|
||||||
|
entityMap: ArticleContentState["entityMap"] | undefined,
|
||||||
|
entityLookup: EntityLookup
|
||||||
|
): string[] {
|
||||||
|
if (entityKey === undefined) return [];
|
||||||
|
const entry = resolveEntityEntry(entityKey, entityMap, entityLookup);
|
||||||
|
const value = entry?.value;
|
||||||
|
if (!value || value.type !== "MARKDOWN") return [];
|
||||||
|
|
||||||
|
const markdown = typeof value.data?.markdown === "string" ? value.data.markdown : "";
|
||||||
|
const normalized = markdown.replace(/\r\n/g, "\n").trimEnd();
|
||||||
|
if (!normalized) return [];
|
||||||
|
return normalized.split("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function buildMediaLinkMap(
|
function buildMediaLinkMap(
|
||||||
entityMap: ArticleContentState["entityMap"] | undefined
|
entityMap: ArticleContentState["entityMap"] | undefined
|
||||||
): Map<number, string> {
|
): Map<number, string> {
|
||||||
@@ -330,7 +425,7 @@ function renderContentBlocks(
|
|||||||
blocks: ArticleBlock[],
|
blocks: ArticleBlock[],
|
||||||
entityMap: ArticleContentState["entityMap"] | undefined,
|
entityMap: ArticleContentState["entityMap"] | undefined,
|
||||||
entityLookup: EntityLookup,
|
entityLookup: EntityLookup,
|
||||||
mediaById: Map<string, string>,
|
mediaById: Map<string, ResolvedMediaAsset>,
|
||||||
usedUrls: Set<string>,
|
usedUrls: Set<string>,
|
||||||
mediaLinkMap: Map<number, string>,
|
mediaLinkMap: Map<number, string>,
|
||||||
referencedTweets?: Map<string, ReferencedTweetInfo>
|
referencedTweets?: Map<string, ReferencedTweetInfo>
|
||||||
@@ -397,6 +492,16 @@ function renderContentBlocks(
|
|||||||
return [...new Set(linkLines)];
|
return [...new Set(linkLines)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const collectMarkdownLines = (block: ArticleBlock): string[] => {
|
||||||
|
const ranges = Array.isArray(block.entityRanges) ? block.entityRanges : [];
|
||||||
|
const markdownLines: string[] = [];
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (typeof range?.key !== "number") continue;
|
||||||
|
markdownLines.push(...resolveEntityMarkdownLines(range.key, entityMap, entityLookup));
|
||||||
|
}
|
||||||
|
return markdownLines;
|
||||||
|
};
|
||||||
|
|
||||||
const pushTrailingMedia = (mediaLines: string[]) => {
|
const pushTrailingMedia = (mediaLines: string[]) => {
|
||||||
if (mediaLines.length > 0) {
|
if (mediaLines.length > 0) {
|
||||||
pushBlock(mediaLines, "media");
|
pushBlock(mediaLines, "media");
|
||||||
@@ -441,6 +546,11 @@ function renderContentBlocks(
|
|||||||
pushBlock(tweetLines, "quote");
|
pushBlock(tweetLines, "quote");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const markdownLines = collectMarkdownLines(block);
|
||||||
|
if (markdownLines.length > 0) {
|
||||||
|
pushBlock(markdownLines, "text");
|
||||||
|
}
|
||||||
|
|
||||||
const mediaLines = collectMediaLines(block);
|
const mediaLines = collectMediaLines(block);
|
||||||
if (mediaLines.length > 0) {
|
if (mediaLines.length > 0) {
|
||||||
pushBlock(mediaLines, "media");
|
pushBlock(mediaLines, "media");
|
||||||
@@ -571,7 +681,7 @@ export function formatArticleMarkdown(
|
|||||||
lines.push(`# ${title}`);
|
lines.push(`# ${title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverUrl = resolveMediaUrl(candidate.cover_media?.media_info) ?? null;
|
const coverUrl = resolveCoverUrl(candidate.cover_media?.media_info) ?? null;
|
||||||
if (coverUrl) {
|
if (coverUrl) {
|
||||||
usedUrls.add(coverUrl);
|
usedUrls.add(coverUrl);
|
||||||
}
|
}
|
||||||
@@ -602,12 +712,13 @@ export function formatArticleMarkdown(
|
|||||||
lines.push(candidate.preview_text.trim());
|
lines.push(candidate.preview_text.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaUrls = collectMediaUrls(candidate, usedUrls, coverUrl ?? undefined);
|
const trailingMediaLines: string[] = [];
|
||||||
if (mediaUrls.length > 0) {
|
for (const asset of collectMediaAssets(candidate)) {
|
||||||
|
trailingMediaLines.push(...renderMediaLines(asset, "", usedUrls));
|
||||||
|
}
|
||||||
|
if (trailingMediaLines.length > 0) {
|
||||||
lines.push("", "## Media", "");
|
lines.push("", "## Media", "");
|
||||||
for (const url of mediaUrls) {
|
lines.push(...trailingMediaLines);
|
||||||
lines.push(``);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { markdown: lines.join("\n").trimEnd(), coverUrl };
|
return { markdown: lines.join("\n").trimEnd(), coverUrl };
|
||||||
|
|||||||
@@ -202,6 +202,13 @@ function toHighResUrl(rawUrl: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlausibleMediaUrl(rawUrl: string): boolean {
|
||||||
|
const ext = resolveExtensionFromUrl(rawUrl);
|
||||||
|
if (ext && (IMAGE_EXTENSIONS.has(ext) || VIDEO_EXTENSIONS.has(ext))) return true;
|
||||||
|
if (resolveKindFromHostname(rawUrl) !== undefined) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[] {
|
function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[] {
|
||||||
const candidates: MarkdownLinkCandidate[] = [];
|
const candidates: MarkdownLinkCandidate[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -221,10 +228,12 @@ function collectMarkdownLinkCandidates(markdown: string): MarkdownLinkCandidate[
|
|||||||
const label = match[1] ?? "";
|
const label = match[1] ?? "";
|
||||||
const rawUrl = match[3] ?? "";
|
const rawUrl = match[3] ?? "";
|
||||||
if (!rawUrl || seen.has(rawUrl)) continue;
|
if (!rawUrl || seen.has(rawUrl)) continue;
|
||||||
|
const isImage = label.startsWith("![");
|
||||||
|
if (!isImage && !isPlausibleMediaUrl(rawUrl)) continue;
|
||||||
seen.add(rawUrl);
|
seen.add(rawUrl);
|
||||||
candidates.push({
|
candidates.push({
|
||||||
url: rawUrl,
|
url: rawUrl,
|
||||||
hint: label.startsWith("![") ? "image" : "unknown",
|
hint: isImage ? "image" : "unknown",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type ArticleEntityMapEntry = {
|
|||||||
mutability?: string;
|
mutability?: string;
|
||||||
data?: {
|
data?: {
|
||||||
caption?: string;
|
caption?: string;
|
||||||
|
markdown?: string;
|
||||||
mediaItems?: ArticleEntityMapMediaItem[];
|
mediaItems?: ArticleEntityMapMediaItem[];
|
||||||
url?: string;
|
url?: string;
|
||||||
tweetId?: string;
|
tweetId?: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-image-gen
|
name: baoyu-image-gen
|
||||||
description: AI image generation with OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
|
description: AI image generation with OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
|
||||||
version: 1.56.2
|
version: 1.56.3
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-image-gen
|
homepage: https://github.com/JimLiu/baoyu-skills#baoyu-image-gen
|
||||||
@@ -74,7 +74,7 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
|
|||||||
# From prompt files
|
# From prompt files
|
||||||
${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||||
|
|
||||||
# With reference images (Google, OpenAI, OpenRouter, or Replicate)
|
# With reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 4.0/4.5/5.0)
|
||||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
||||||
|
|
||||||
# With reference images (explicit provider/model)
|
# With reference images (explicit provider/model)
|
||||||
@@ -153,7 +153,7 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
|
|||||||
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
||||||
| `--quality normal\|2k` | Quality preset (default: `2k`) |
|
| `--quality normal\|2k` | Quality preset (default: `2k`) |
|
||||||
| `--imageSize 1K\|2K\|4K` | Image size for Google/OpenRouter (default: from quality) |
|
| `--imageSize 1K\|2K\|4K` | Image size for Google/OpenRouter (default: from quality) |
|
||||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, OpenRouter multimodal models, and Replicate. Not supported by Jimeng or Seedream |
|
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, OpenRouter multimodal models, Replicate, and Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, or removed SeedEdit 3.0 |
|
||||||
| `--n <count>` | Number of images |
|
| `--n <count>` | Number of images |
|
||||||
| `--json` | JSON output |
|
| `--json` | JSON output |
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,39 @@ test("detectProvider selects an available ref-capable provider for reference-ima
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("detectProvider infers Seedream from model id and allows Seedream reference-image workflows", (t) => {
|
||||||
|
useEnv(t, {
|
||||||
|
GOOGLE_API_KEY: null,
|
||||||
|
OPENAI_API_KEY: null,
|
||||||
|
OPENROUTER_API_KEY: null,
|
||||||
|
DASHSCOPE_API_KEY: null,
|
||||||
|
REPLICATE_API_TOKEN: null,
|
||||||
|
JIMENG_ACCESS_KEY_ID: null,
|
||||||
|
JIMENG_SECRET_ACCESS_KEY: null,
|
||||||
|
ARK_API_KEY: "ark-key",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
detectProvider(
|
||||||
|
makeArgs({
|
||||||
|
model: "doubao-seedream-4-5-251128",
|
||||||
|
referenceImages: ["ref.png"],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
"seedream",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
detectProvider(
|
||||||
|
makeArgs({
|
||||||
|
provider: "seedream",
|
||||||
|
referenceImages: ["ref.png"],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
"seedream",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("batch worker and provider-rate-limit configuration prefer env over EXTEND config", (t) => {
|
test("batch worker and provider-rate-limit configuration prefer env over EXTEND config", (t) => {
|
||||||
useEnv(t, {
|
useEnv(t, {
|
||||||
BAOYU_IMAGE_GEN_MAX_WORKERS: "12",
|
BAOYU_IMAGE_GEN_MAX_WORKERS: "12",
|
||||||
@@ -294,6 +327,7 @@ test("loadBatchTasks and createTaskArgs resolve batch-relative paths", async (t)
|
|||||||
|
|
||||||
test("path normalization, worker count, and retry classification follow expected rules", () => {
|
test("path normalization, worker count, and retry classification follow expected rules", () => {
|
||||||
assert.match(normalizeOutputImagePath("out/sample"), /out[\\/]+sample\.png$/);
|
assert.match(normalizeOutputImagePath("out/sample"), /out[\\/]+sample\.png$/);
|
||||||
|
assert.match(normalizeOutputImagePath("out/sample", ".jpg"), /out[\\/]+sample\.jpg$/);
|
||||||
assert.match(normalizeOutputImagePath("out/sample.webp"), /out[\\/]+sample\.webp$/);
|
assert.match(normalizeOutputImagePath("out/sample.webp"), /out[\\/]+sample\.webp$/);
|
||||||
|
|
||||||
assert.equal(getWorkerCount(8, null, 3), 3);
|
assert.equal(getWorkerCount(8, null, 3), 3);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
type ProviderModule = {
|
type ProviderModule = {
|
||||||
getDefaultModel: () => string;
|
getDefaultModel: () => string;
|
||||||
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
|
generateImage: (prompt: string, model: string, args: CliArgs) => Promise<Uint8Array>;
|
||||||
|
validateArgs?: (model: string, args: CliArgs) => void;
|
||||||
|
getDefaultOutputExtension?: (model: string, args: CliArgs) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreparedTask = {
|
type PreparedTask = {
|
||||||
@@ -78,7 +80,7 @@ Options:
|
|||||||
--size <WxH> Size (e.g., 1024x1024)
|
--size <WxH> Size (e.g., 1024x1024)
|
||||||
--quality normal|2k Quality preset (default: 2k)
|
--quality normal|2k Quality preset (default: 2k)
|
||||||
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
|
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
|
||||||
--ref <files...> Reference images (Google multimodal, OpenAI GPT Image edits, OpenRouter multimodal, or Replicate)
|
--ref <files...> Reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 4.0/4.5/5.0)
|
||||||
--n <count> Number of images for the current task (default: 1)
|
--n <count> Number of images for the current task (default: 1)
|
||||||
--json JSON output
|
--json JSON output
|
||||||
-h, --help Show help
|
-h, --help Show help
|
||||||
@@ -560,11 +562,17 @@ async function readPromptFromStdin(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeOutputImagePath(p: string): string {
|
export function normalizeOutputImagePath(p: string, defaultExtension = ".png"): string {
|
||||||
const full = path.resolve(p);
|
const full = path.resolve(p);
|
||||||
const ext = path.extname(full);
|
const ext = path.extname(full);
|
||||||
if (ext) return full;
|
if (ext) return full;
|
||||||
return `${full}.png`;
|
return `${full}${defaultExtension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferProviderFromModel(model: string | null): Provider | null {
|
||||||
|
if (!model) return null;
|
||||||
|
if (model.includes("seedream") || model.includes("seededit")) return "seedream";
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function detectProvider(args: CliArgs): Provider {
|
export function detectProvider(args: CliArgs): Provider {
|
||||||
@@ -574,10 +582,11 @@ export function detectProvider(args: CliArgs): Provider {
|
|||||||
args.provider !== "google" &&
|
args.provider !== "google" &&
|
||||||
args.provider !== "openai" &&
|
args.provider !== "openai" &&
|
||||||
args.provider !== "openrouter" &&
|
args.provider !== "openrouter" &&
|
||||||
args.provider !== "replicate"
|
args.provider !== "replicate" &&
|
||||||
|
args.provider !== "seedream"
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider openrouter (OpenRouter multimodal), or --provider replicate."
|
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider openrouter (OpenRouter multimodal), --provider replicate, or --provider seedream for supported Seedream models."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,14 +599,23 @@ export function detectProvider(args: CliArgs): Provider {
|
|||||||
const hasReplicate = !!process.env.REPLICATE_API_TOKEN;
|
const hasReplicate = !!process.env.REPLICATE_API_TOKEN;
|
||||||
const hasJimeng = !!(process.env.JIMENG_ACCESS_KEY_ID && process.env.JIMENG_SECRET_ACCESS_KEY);
|
const hasJimeng = !!(process.env.JIMENG_ACCESS_KEY_ID && process.env.JIMENG_SECRET_ACCESS_KEY);
|
||||||
const hasSeedream = !!process.env.ARK_API_KEY;
|
const hasSeedream = !!process.env.ARK_API_KEY;
|
||||||
|
const modelProvider = inferProviderFromModel(args.model);
|
||||||
|
|
||||||
|
if (modelProvider === "seedream") {
|
||||||
|
if (!hasSeedream) {
|
||||||
|
throw new Error("Model looks like a Volcengine ARK image model, but ARK_API_KEY is not set.");
|
||||||
|
}
|
||||||
|
return "seedream";
|
||||||
|
}
|
||||||
|
|
||||||
if (args.referenceImages.length > 0) {
|
if (args.referenceImages.length > 0) {
|
||||||
if (hasGoogle) return "google";
|
if (hasGoogle) return "google";
|
||||||
if (hasOpenai) return "openai";
|
if (hasOpenai) return "openai";
|
||||||
if (hasOpenrouter) return "openrouter";
|
if (hasOpenrouter) return "openrouter";
|
||||||
if (hasReplicate) return "replicate";
|
if (hasReplicate) return "replicate";
|
||||||
|
if (hasSeedream) return "seedream";
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Reference images require Google, OpenAI, OpenRouter, or Replicate. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, or REPLICATE_API_TOKEN, or remove --ref."
|
"Reference images require Google, OpenAI, OpenRouter, Replicate, or supported Seedream models. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, REPLICATE_API_TOKEN, or ARK_API_KEY, or remove --ref."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +719,8 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConf
|
|||||||
const provider = detectProvider(args);
|
const provider = detectProvider(args);
|
||||||
const providerModule = await loadProviderModule(provider);
|
const providerModule = await loadProviderModule(provider);
|
||||||
const model = getModelForProvider(provider, args.model, extendConfig, providerModule);
|
const model = getModelForProvider(provider, args.model, extendConfig, providerModule);
|
||||||
|
providerModule.validateArgs?.(model, args);
|
||||||
|
const defaultOutputExtension = providerModule.getDefaultOutputExtension?.(model, args) ?? ".png";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: "single",
|
id: "single",
|
||||||
@@ -708,7 +728,7 @@ async function prepareSingleTask(args: CliArgs, extendConfig: Partial<ExtendConf
|
|||||||
args,
|
args,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
outputPath: normalizeOutputImagePath(args.imagePath),
|
outputPath: normalizeOutputImagePath(args.imagePath, defaultOutputExtension),
|
||||||
providerModule,
|
providerModule,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -784,13 +804,15 @@ async function prepareBatchTasks(
|
|||||||
const provider = detectProvider(taskArgs);
|
const provider = detectProvider(taskArgs);
|
||||||
const providerModule = await loadProviderModule(provider);
|
const providerModule = await loadProviderModule(provider);
|
||||||
const model = getModelForProvider(provider, taskArgs.model, extendConfig, providerModule);
|
const model = getModelForProvider(provider, taskArgs.model, extendConfig, providerModule);
|
||||||
|
providerModule.validateArgs?.(model, taskArgs);
|
||||||
|
const defaultOutputExtension = providerModule.getDefaultOutputExtension?.(model, taskArgs) ?? ".png";
|
||||||
prepared.push({
|
prepared.push({
|
||||||
id: task.id || `task-${String(i + 1).padStart(2, "0")}`,
|
id: task.id || `task-${String(i + 1).padStart(2, "0")}`,
|
||||||
prompt,
|
prompt,
|
||||||
args: taskArgs,
|
args: taskArgs,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
outputPath: normalizeOutputImagePath(taskArgs.imagePath),
|
outputPath: normalizeOutputImagePath(taskArgs.imagePath, defaultOutputExtension),
|
||||||
providerModule,
|
providerModule,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test, { type TestContext } from "node:test";
|
||||||
|
|
||||||
|
import type { CliArgs } from "../types.ts";
|
||||||
|
import {
|
||||||
|
buildImageInput,
|
||||||
|
buildRequestBody,
|
||||||
|
generateImage,
|
||||||
|
getDefaultOutputExtension,
|
||||||
|
resolveSeedreamSize,
|
||||||
|
validateArgs,
|
||||||
|
} from "./seedream.ts";
|
||||||
|
|
||||||
|
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||||
|
return {
|
||||||
|
prompt: null,
|
||||||
|
promptFiles: [],
|
||||||
|
imagePath: null,
|
||||||
|
provider: null,
|
||||||
|
model: null,
|
||||||
|
aspectRatio: null,
|
||||||
|
size: null,
|
||||||
|
quality: null,
|
||||||
|
imageSize: null,
|
||||||
|
referenceImages: [],
|
||||||
|
n: 1,
|
||||||
|
batchFile: null,
|
||||||
|
jobs: null,
|
||||||
|
json: false,
|
||||||
|
help: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 makeTempPng(t: TestContext, name: string): Promise<string> {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "seedream-test-"));
|
||||||
|
t.after(() => fs.rm(dir, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const filePath = path.join(dir, name);
|
||||||
|
const png1x1 =
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a7m0AAAAASUVORK5CYII=";
|
||||||
|
await fs.writeFile(filePath, Buffer.from(png1x1, "base64"));
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Seedream request body and default extensions follow official model capabilities", () => {
|
||||||
|
const five = buildRequestBody(
|
||||||
|
"A robot illustrator",
|
||||||
|
"doubao-seedream-5-0-260128",
|
||||||
|
makeArgs(),
|
||||||
|
);
|
||||||
|
assert.equal(five.size, "2K");
|
||||||
|
assert.equal(five.response_format, "url");
|
||||||
|
assert.equal(five.output_format, "png");
|
||||||
|
assert.equal(getDefaultOutputExtension("doubao-seedream-5-0-260128"), ".png");
|
||||||
|
|
||||||
|
const fourFive = buildRequestBody(
|
||||||
|
"A robot illustrator",
|
||||||
|
"doubao-seedream-4-5-251128",
|
||||||
|
makeArgs(),
|
||||||
|
);
|
||||||
|
assert.equal(fourFive.size, "2K");
|
||||||
|
assert.equal(fourFive.response_format, "url");
|
||||||
|
assert.ok(!("output_format" in fourFive));
|
||||||
|
assert.equal(getDefaultOutputExtension("doubao-seedream-4-5-251128"), ".jpg");
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
buildRequestBody(
|
||||||
|
"Change the bubbles into hearts",
|
||||||
|
"doubao-seededit-3-0-i2i-250628",
|
||||||
|
makeArgs({ referenceImages: ["ref.png"] }),
|
||||||
|
"data:image/png;base64,AAAA",
|
||||||
|
),
|
||||||
|
/no longer supported/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Seedream size selection validates model-specific presets", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveSeedreamSize("doubao-seedream-4-0-250828", makeArgs({ quality: "normal" })),
|
||||||
|
"1K",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveSeedreamSize("doubao-seedream-3-0-t2i-250415", makeArgs({ quality: "2k" })),
|
||||||
|
"2048x2048",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
resolveSeedreamSize("doubao-seedream-5-0-260128", makeArgs({ size: "4K" })),
|
||||||
|
/only supports 2K, 3K/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
resolveSeedreamSize("doubao-seedream-3-0-t2i-250415", makeArgs({ imageSize: "2K" })),
|
||||||
|
/only supports explicit WxH sizes/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
resolveSeedreamSize("doubao-seededit-3-0-i2i-250628", makeArgs({ size: "1024x1024" })),
|
||||||
|
/no longer supported/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Seedream reference-image support is model-specific", () => {
|
||||||
|
assert.doesNotThrow(() =>
|
||||||
|
validateArgs(
|
||||||
|
"doubao-seedream-5-0-260128",
|
||||||
|
makeArgs({ referenceImages: ["a.png", "b.png"] }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateArgs(
|
||||||
|
"doubao-seedream-3-0-t2i-250415",
|
||||||
|
makeArgs({ referenceImages: ["a.png"] }),
|
||||||
|
),
|
||||||
|
/does not support reference images/,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateArgs(
|
||||||
|
"doubao-seededit-3-0-i2i-250628",
|
||||||
|
makeArgs(),
|
||||||
|
),
|
||||||
|
/no longer supported/,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateArgs(
|
||||||
|
"ep-20260315171508-t8br2",
|
||||||
|
makeArgs({ referenceImages: ["a.png"] }),
|
||||||
|
),
|
||||||
|
/require a known model ID/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Seedream image input encodes local references as data URLs", async (t) => {
|
||||||
|
const refOne = await makeTempPng(t, "one.png");
|
||||||
|
const refTwo = await makeTempPng(t, "two.png");
|
||||||
|
|
||||||
|
const single = await buildImageInput("doubao-seedream-4-5-251128", [refOne]);
|
||||||
|
assert.match(String(single), /^data:image\/png;base64,/);
|
||||||
|
|
||||||
|
const multiple = await buildImageInput("doubao-seedream-5-0-260128", [refOne, refTwo]);
|
||||||
|
assert.ok(Array.isArray(multiple));
|
||||||
|
assert.equal(multiple.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Seedream generateImage posts the documented response_format and downloads the returned URL", async (t) => {
|
||||||
|
useEnv(t, { ARK_API_KEY: "test-key", SEEDREAM_BASE_URL: null });
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
t.after(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calls: Array<{
|
||||||
|
input: string;
|
||||||
|
init?: RequestInit;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
calls.push({
|
||||||
|
input: String(input),
|
||||||
|
init,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (calls.length === 1) {
|
||||||
|
return Response.json({
|
||||||
|
model: "doubao-seedream-4-5-251128",
|
||||||
|
created: 1740000000,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
url: "https://example.com/generated-image",
|
||||||
|
size: "2048x2048",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
generated_images: 1,
|
||||||
|
output_tokens: 1,
|
||||||
|
total_tokens: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(Uint8Array.from([7, 8, 9]), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "image/jpeg" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const image = await generateImage(
|
||||||
|
"A robot illustrator",
|
||||||
|
"doubao-seedream-4-5-251128",
|
||||||
|
makeArgs(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual([...image], [7, 8, 9]);
|
||||||
|
assert.equal(calls.length, 2);
|
||||||
|
assert.equal(
|
||||||
|
calls[0]?.input,
|
||||||
|
"https://ark.cn-beijing.volces.com/api/v3/images/generations",
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestBody = JSON.parse(String(calls[0]?.init?.body)) as Record<string, unknown>;
|
||||||
|
assert.equal(requestBody.model, "doubao-seedream-4-5-251128");
|
||||||
|
assert.equal(requestBody.size, "2K");
|
||||||
|
assert.equal(requestBody.response_format, "url");
|
||||||
|
assert.ok(!("output_format" in requestBody));
|
||||||
|
assert.equal(calls[1]?.input, "https://example.com/generated-image");
|
||||||
|
});
|
||||||
@@ -1,5 +1,50 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
import type { CliArgs } from "../types";
|
import type { CliArgs } from "../types";
|
||||||
|
|
||||||
|
export type SeedreamModelFamily =
|
||||||
|
| "seedream5"
|
||||||
|
| "seedream45"
|
||||||
|
| "seedream40"
|
||||||
|
| "seedream30"
|
||||||
|
| "unknown";
|
||||||
|
|
||||||
|
type SeedreamRequestImage = string | string[];
|
||||||
|
|
||||||
|
type SeedreamRequestBody = {
|
||||||
|
model: string;
|
||||||
|
prompt: string;
|
||||||
|
size: string;
|
||||||
|
response_format: "url";
|
||||||
|
watermark: boolean;
|
||||||
|
image?: SeedreamRequestImage;
|
||||||
|
output_format?: "png";
|
||||||
|
};
|
||||||
|
|
||||||
|
type SeedreamImageResponse = {
|
||||||
|
model?: string;
|
||||||
|
created?: number;
|
||||||
|
data?: Array<{
|
||||||
|
url?: string;
|
||||||
|
b64_json?: string;
|
||||||
|
size?: string;
|
||||||
|
error?: {
|
||||||
|
code?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
usage?: {
|
||||||
|
generated_images: number;
|
||||||
|
output_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
};
|
||||||
|
error?: {
|
||||||
|
code?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function getDefaultModel(): string {
|
export function getDefaultModel(): string {
|
||||||
return process.env.SEEDREAM_IMAGE_MODEL || "doubao-seedream-5-0-260128";
|
return process.env.SEEDREAM_IMAGE_MODEL || "doubao-seedream-5-0-260128";
|
||||||
}
|
}
|
||||||
@@ -12,46 +57,252 @@ function getBaseUrl(): string {
|
|||||||
return process.env.SEEDREAM_BASE_URL || "https://ark.cn-beijing.volces.com/api/v3";
|
return process.env.SEEDREAM_BASE_URL || "https://ark.cn-beijing.volces.com/api/v3";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function parsePixelSize(value: string): { width: number; height: number } | null {
|
||||||
* Convert aspect ratio to Seedream size format
|
const match = value.trim().match(/^(\d+)\s*[xX]\s*(\d+)$/);
|
||||||
* Seedream API accepts: "2k" (default), "3k", or WIDTHxHEIGHT format
|
if (!match) return null;
|
||||||
* Note: API uses lowercase "2k"/"3k", not "2K"/"3K"
|
|
||||||
*/
|
const width = parseInt(match[1]!, 10);
|
||||||
function getSeedreamSize(ar: string | null, quality: CliArgs["quality"], imageSize?: string | null): string {
|
const height = parseInt(match[2]!, 10);
|
||||||
// If explicit size is provided
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||||
if (imageSize) {
|
return null;
|
||||||
const upper = imageSize.toUpperCase();
|
|
||||||
if (upper === "2K" || upper === "3K") {
|
|
||||||
return upper.toLowerCase(); // API expects "2k" or "3k"
|
|
||||||
}
|
|
||||||
// For widthxheight format, pass through as-is
|
|
||||||
if (imageSize.includes("x")) {
|
|
||||||
return imageSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to 2k (smallest option supported by API)
|
return { width, height };
|
||||||
return "2k";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SeedreamImageResponse = {
|
function normalizePixelSize(value: string): string | null {
|
||||||
model: string;
|
const parsed = parsePixelSize(value);
|
||||||
created: number;
|
if (!parsed) return null;
|
||||||
data: Array<{
|
return `${parsed.width}x${parsed.height}`;
|
||||||
url: string;
|
}
|
||||||
size: string;
|
|
||||||
}>;
|
function normalizeSizePreset(value: string): string | null {
|
||||||
usage: {
|
const upper = value.trim().toUpperCase();
|
||||||
generated_images: number;
|
if (upper === "ADAPTIVE") return "adaptive";
|
||||||
output_tokens: number;
|
if (upper === "1K" || upper === "2K" || upper === "3K" || upper === "4K") return upper;
|
||||||
total_tokens: number;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSizeValue(value: string): string | null {
|
||||||
|
return normalizeSizePreset(value) ?? normalizePixelSize(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMimeType(filename: string): string {
|
||||||
|
const ext = path.extname(filename).toLowerCase();
|
||||||
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
||||||
|
if (ext === ".webp") return "image/webp";
|
||||||
|
if (ext === ".gif") return "image/gif";
|
||||||
|
if (ext === ".bmp") return "image/bmp";
|
||||||
|
if (ext === ".tiff" || ext === ".tif") return "image/tiff";
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readImageAsDataUrl(filePath: string): Promise<string> {
|
||||||
|
const bytes = await readFile(filePath);
|
||||||
|
return `data:${getMimeType(filePath)};base64,${bytes.toString("base64")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModelFamily(model: string): SeedreamModelFamily {
|
||||||
|
const normalized = model.trim();
|
||||||
|
if (/^doubao-seedream-5-0(?:-lite)?-\d+$/.test(normalized)) return "seedream5";
|
||||||
|
if (/^doubao-seedream-4-5-\d+$/.test(normalized)) return "seedream45";
|
||||||
|
if (/^doubao-seedream-4-0-\d+$/.test(normalized)) return "seedream40";
|
||||||
|
if (/^doubao-seedream-3-0-t2i-\d+$/.test(normalized)) return "seedream30";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRemovedSeededitModel(model: string): boolean {
|
||||||
|
return /^doubao-seededit-3-0-i2i-\d+$/.test(model.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertSupportedModel(model: string): void {
|
||||||
|
if (isRemovedSeededitModel(model)) {
|
||||||
|
throw new Error(
|
||||||
|
`${model} is no longer supported. SeedEdit 3.0 support has been removed from this tool; use Seedream 5.0/4.5/4.0/3.0 instead.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsReferenceImages(model: string): boolean {
|
||||||
|
const family = getModelFamily(model);
|
||||||
|
return family === "seedream5" || family === "seedream45" || family === "seedream40";
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsOutputFormat(model: string): boolean {
|
||||||
|
return getModelFamily(model) === "seedream5";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultOutputExtension(model: string): ".png" | ".jpg" {
|
||||||
|
assertSupportedModel(model);
|
||||||
|
return supportsOutputFormat(model) ? ".png" : ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultSeedreamSize(model: string, args: CliArgs): string {
|
||||||
|
assertSupportedModel(model);
|
||||||
|
const family = getModelFamily(model);
|
||||||
|
|
||||||
|
if (family === "seedream5") return "2K";
|
||||||
|
if (family === "seedream45") return "2K";
|
||||||
|
if (family === "seedream40") return args.quality === "normal" ? "1K" : "2K";
|
||||||
|
if (family === "seedream30") return args.quality === "2k" ? "2048x2048" : "1024x1024";
|
||||||
|
return "2K";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSeedreamSize(model: string, args: CliArgs): string {
|
||||||
|
assertSupportedModel(model);
|
||||||
|
const family = getModelFamily(model);
|
||||||
|
const requested = args.size || args.imageSize || null;
|
||||||
|
const normalized = requested ? normalizeSizeValue(requested) : null;
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
return getDefaultSeedreamSize(model, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === "seedream30") {
|
||||||
|
const pixelSize = normalizePixelSize(normalized);
|
||||||
|
if (!pixelSize) {
|
||||||
|
throw new Error("Seedream 3.0 only supports explicit WxH sizes such as 1024x1024.");
|
||||||
|
}
|
||||||
|
return pixelSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === "seedream5") {
|
||||||
|
if (normalized === "4K" || normalized === "1K" || normalized === "adaptive") {
|
||||||
|
throw new Error("Seedream 5.0 only supports 2K, 3K, or explicit WxH sizes.");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === "seedream45") {
|
||||||
|
if (normalized === "1K" || normalized === "3K" || normalized === "adaptive") {
|
||||||
|
throw new Error("Seedream 4.5 only supports 2K, 4K, or explicit WxH sizes.");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === "seedream40") {
|
||||||
|
if (normalized === "3K" || normalized === "adaptive") {
|
||||||
|
throw new Error("Seedream 4.0 only supports 1K, 2K, 4K, or explicit WxH sizes.");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === "adaptive") {
|
||||||
|
throw new Error("Adaptive size is not supported by Seedream image generation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === "1K" || normalized === "3K" || normalized === "4K") {
|
||||||
|
throw new Error(
|
||||||
|
"Unknown Seedream model ID. Use a documented model ID or pass an explicit WxH size instead of preset imageSize."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateArgs(model: string, args: CliArgs): void {
|
||||||
|
assertSupportedModel(model);
|
||||||
|
const family = getModelFamily(model);
|
||||||
|
const refCount = args.referenceImages.length;
|
||||||
|
|
||||||
|
if (refCount === 0) {
|
||||||
|
resolveSeedreamSize(model, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === "unknown") {
|
||||||
|
throw new Error(
|
||||||
|
"Reference images with Seedream require a known model ID. Use Seedream 5.0/4.5/4.0 model IDs instead of an endpoint ID."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportsReferenceImages(model)) {
|
||||||
|
throw new Error(`${model} does not support reference images.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((family === "seedream5" || family === "seedream45" || family === "seedream40") && refCount > 14) {
|
||||||
|
throw new Error(`${model} supports at most 14 reference images.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveSeedreamSize(model, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildImageInput(
|
||||||
|
model: string,
|
||||||
|
referenceImages: string[],
|
||||||
|
): Promise<SeedreamRequestImage | undefined> {
|
||||||
|
if (referenceImages.length === 0) return undefined;
|
||||||
|
assertSupportedModel(model);
|
||||||
|
|
||||||
|
const encoded = await Promise.all(referenceImages.map((refPath) => readImageAsDataUrl(refPath)));
|
||||||
|
|
||||||
|
return encoded.length === 1 ? encoded[0]! : encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRequestBody(
|
||||||
|
prompt: string,
|
||||||
|
model: string,
|
||||||
|
args: CliArgs,
|
||||||
|
imageInput?: SeedreamRequestImage,
|
||||||
|
): SeedreamRequestBody {
|
||||||
|
validateArgs(model, args);
|
||||||
|
|
||||||
|
const requestBody: SeedreamRequestBody = {
|
||||||
|
model,
|
||||||
|
prompt,
|
||||||
|
size: resolveSeedreamSize(model, args),
|
||||||
|
response_format: "url",
|
||||||
|
watermark: false,
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
if (imageInput) {
|
||||||
|
requestBody.image = imageInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsOutputFormat(model)) {
|
||||||
|
requestBody.output_format = "png";
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(url: string): Promise<Uint8Array> {
|
||||||
|
const imgResponse = await fetch(url);
|
||||||
|
if (!imgResponse.ok) {
|
||||||
|
throw new Error(`Failed to download image from ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await imgResponse.arrayBuffer();
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractImageFromResponse(result: SeedreamImageResponse): Promise<Uint8Array> {
|
||||||
|
const first = result.data?.find((item) => item.url || item.b64_json || item.error);
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
throw new Error("No image data in Seedream response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first.error) {
|
||||||
|
throw new Error(first.error.message || "Seedream returned an image generation error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first.b64_json) {
|
||||||
|
return Uint8Array.from(Buffer.from(first.b64_json, "base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first.url) {
|
||||||
|
console.error(`Downloading image from ${first.url}...`);
|
||||||
|
return downloadImage(first.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No image URL or base64 data in Seedream response");
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateImage(
|
export async function generateImage(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
model: string,
|
model: string,
|
||||||
args: CliArgs
|
args: CliArgs,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const apiKey = getApiKey();
|
const apiKey = getApiKey();
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
@@ -61,20 +312,13 @@ export async function generateImage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = getBaseUrl();
|
validateArgs(model, args);
|
||||||
const size = getSeedreamSize(args.aspectRatio, args.quality, args.imageSize);
|
const imageInput = await buildImageInput(model, args.referenceImages);
|
||||||
|
const requestBody = buildRequestBody(prompt, model, args, imageInput);
|
||||||
|
|
||||||
console.error(`Calling Seedream API (${model}) with size: ${size}`);
|
console.error(`Calling Seedream API (${model}) with size: ${requestBody.size}`);
|
||||||
|
|
||||||
const requestBody = {
|
const response = await fetch(`${getBaseUrl()}/images/generations`, {
|
||||||
model,
|
|
||||||
prompt,
|
|
||||||
size,
|
|
||||||
output_format: "png",
|
|
||||||
watermark: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/images/generations`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -89,23 +333,9 @@ export async function generateImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = (await response.json()) as SeedreamImageResponse;
|
const result = (await response.json()) as SeedreamImageResponse;
|
||||||
|
if (result.error) {
|
||||||
if (!result.data || result.data.length === 0) {
|
throw new Error(result.error.message || "Seedream API returned an error");
|
||||||
throw new Error("No image data in Seedream response");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = result.data[0].url;
|
return extractImageFromResponse(result);
|
||||||
if (!imageUrl) {
|
|
||||||
throw new Error("No image URL in Seedream response");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download image from URL
|
|
||||||
console.error(`Downloading image from ${imageUrl}...`);
|
|
||||||
const imgResponse = await fetch(imageUrl);
|
|
||||||
if (!imgResponse.ok) {
|
|
||||||
throw new Error(`Failed to download image from ${imageUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await imgResponse.arrayBuffer();
|
|
||||||
return new Uint8Array(buffer);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,32 +4,108 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "baoyu-post-to-wechat-scripts",
|
"name": "baoyu-post-to-wechat-scripts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jsquash/webp": "^1.5.0",
|
||||||
"baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
|
"baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
|
||||||
"baoyu-md": "file:./vendor/baoyu-md",
|
"baoyu-md": "file:./vendor/baoyu-md",
|
||||||
|
"jimp": "^1.6.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="],
|
||||||
|
|
||||||
|
"@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="],
|
||||||
|
|
||||||
|
"@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="],
|
||||||
|
|
||||||
|
"@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="],
|
||||||
|
|
||||||
|
"@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="],
|
||||||
|
|
||||||
|
"@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="],
|
||||||
|
|
||||||
|
"@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="],
|
||||||
|
|
||||||
|
"@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="],
|
||||||
|
|
||||||
|
"@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="],
|
||||||
|
|
||||||
|
"@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="],
|
||||||
|
|
||||||
|
"@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
|
||||||
|
|
||||||
|
"@jsquash/webp": ["@jsquash/webp@1.5.0", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw=="],
|
||||||
|
|
||||||
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||||
|
|
||||||
|
"any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="],
|
||||||
|
|
||||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||||
|
|
||||||
|
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||||
|
|
||||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
"baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
|
"baoyu-chrome-cdp": ["baoyu-chrome-cdp@file:vendor/baoyu-chrome-cdp", {}],
|
||||||
|
|
||||||
"baoyu-md": ["baoyu-md@file:vendor/baoyu-md", { "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" } }],
|
"baoyu-md": ["baoyu-md@file:vendor/baoyu-md", { "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" } }],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="],
|
||||||
|
|
||||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||||
|
|
||||||
"cheerio": ["cheerio@1.0.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "encoding-sniffer": "^0.2.0", "htmlparser2": "^9.1.0", "parse5": "^7.1.2", "parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-parser-stream": "^7.1.2", "undici": "^6.19.5", "whatwg-mimetype": "^4.0.0" } }, "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww=="],
|
"cheerio": ["cheerio@1.0.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "encoding-sniffer": "^0.2.0", "htmlparser2": "^9.1.0", "parse5": "^7.1.2", "parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-parser-stream": "^7.1.2", "undici": "^6.19.5", "whatwg-mimetype": "^4.0.0" } }, "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww=="],
|
||||||
@@ -66,22 +142,40 @@
|
|||||||
|
|
||||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
|
|
||||||
|
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||||
|
|
||||||
|
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||||
|
|
||||||
|
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
|
||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||||
|
|
||||||
|
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||||
|
|
||||||
"front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="],
|
"front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="],
|
||||||
|
|
||||||
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
||||||
|
|
||||||
|
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||||
|
|
||||||
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||||
|
|
||||||
"htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
|
"htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
|
"jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
|
||||||
|
|
||||||
|
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||||
|
|
||||||
"juice": ["juice@11.1.1", "", { "dependencies": { "cheerio": "1.0.0", "commander": "^12.1.0", "entities": "^7.0.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^8.0.0" }, "bin": { "juice": "bin/juice" } }, "sha512-4SBfZqKcc6DrIS+5b/WiGoWaZsdUPBH+e6SbRlNjJpaIRtfoBhYReAtobIEW6mcLeFFDXLBJMuZwkJLkBJjs2w=="],
|
"juice": ["juice@11.1.1", "", { "dependencies": { "cheerio": "1.0.0", "commander": "^12.1.0", "entities": "^7.0.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^8.0.0" }, "bin": { "juice": "bin/juice" } }, "sha512-4SBfZqKcc6DrIS+5b/WiGoWaZsdUPBH+e6SbRlNjJpaIRtfoBhYReAtobIEW6mcLeFFDXLBJMuZwkJLkBJjs2w=="],
|
||||||
@@ -146,18 +240,40 @@
|
|||||||
|
|
||||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
"mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
|
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||||
|
|
||||||
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
|
"parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="],
|
||||||
|
|
||||||
|
"parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="],
|
||||||
|
|
||||||
|
"parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="],
|
||||||
|
|
||||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
||||||
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
|
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
|
||||||
|
|
||||||
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
|
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
|
||||||
|
|
||||||
|
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
|
||||||
|
|
||||||
|
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
|
||||||
|
|
||||||
|
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
||||||
|
|
||||||
|
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||||
|
|
||||||
|
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
|
||||||
|
|
||||||
"reading-time": ["reading-time@1.5.0", "", {}, "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg=="],
|
"reading-time": ["reading-time@1.5.0", "", {}, "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg=="],
|
||||||
|
|
||||||
"remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="],
|
"remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="],
|
||||||
@@ -166,12 +282,26 @@
|
|||||||
|
|
||||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||||
|
|
||||||
|
"simple-xml-to-json": ["simple-xml-to-json@1.2.4", "", {}, "sha512-3MY16e0ocMHL7N1ufpdObURGyX+lCo0T/A+y6VCwosLdH1HSda4QZl1Sdt/O+2qWp48WFi26XEp5rF0LoaL0Dg=="],
|
||||||
|
|
||||||
"slick": ["slick@1.12.2", "", {}, "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A=="],
|
"slick": ["slick@1.12.2", "", {}, "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
|
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
||||||
|
|
||||||
|
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||||
|
|
||||||
|
"token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
|
||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
"undici": ["undici@6.24.0", "", {}, "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA=="],
|
"undici": ["undici@6.24.0", "", {}, "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA=="],
|
||||||
@@ -186,18 +316,30 @@
|
|||||||
|
|
||||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||||
|
|
||||||
|
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
|
||||||
|
|
||||||
"valid-data-url": ["valid-data-url@3.0.1", "", {}, "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA=="],
|
"valid-data-url": ["valid-data-url@3.0.1", "", {}, "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
|
"wasm-feature-detect": ["wasm-feature-detect@1.8.0", "", {}, "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ=="],
|
||||||
|
|
||||||
"web-resource-inliner": ["web-resource-inliner@8.0.0", "", { "dependencies": { "ansi-colors": "^4.1.1", "escape-goat": "^3.0.0", "htmlparser2": "^9.1.0", "mime": "^2.4.6", "valid-data-url": "^3.0.0" } }, "sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA=="],
|
"web-resource-inliner": ["web-resource-inliner@8.0.0", "", { "dependencies": { "ansi-colors": "^4.1.1", "escape-goat": "^3.0.0", "htmlparser2": "^9.1.0", "mime": "^2.4.6", "valid-data-url": "^3.0.0" } }, "sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA=="],
|
||||||
|
|
||||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||||
|
|
||||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||||
|
|
||||||
|
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
||||||
|
|
||||||
|
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||||
|
|
||||||
|
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
@@ -205,5 +347,9 @@
|
|||||||
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
|
|
||||||
|
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||||
|
|
||||||
|
"web-resource-inliner/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jsquash/webp": "^1.5.0",
|
||||||
"baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
|
"baoyu-chrome-cdp": "file:./vendor/baoyu-chrome-cdp",
|
||||||
"baoyu-md": "file:./vendor/baoyu-md"
|
"baoyu-md": "file:./vendor/baoyu-md",
|
||||||
|
"jimp": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import path from "node:path";
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { loadWechatExtendConfig, resolveAccount, loadCredentials } from "./wechat-extend-config.ts";
|
import { loadWechatExtendConfig, resolveAccount, loadCredentials } from "./wechat-extend-config.ts";
|
||||||
|
import {
|
||||||
|
type WechatUploadAsset,
|
||||||
|
prepareWechatBodyImageUpload,
|
||||||
|
needsWechatBodyImageProcessing,
|
||||||
|
} from "./wechat-image-processor.ts";
|
||||||
|
|
||||||
interface AccessTokenResponse {
|
interface AccessTokenResponse {
|
||||||
access_token?: string;
|
access_token?: string;
|
||||||
@@ -52,10 +57,10 @@ interface ArticleOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
const TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
||||||
const UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";
|
const UPLOAD_BODY_IMG_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg";
|
||||||
|
const UPLOAD_MATERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material";
|
||||||
const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";
|
const DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add";
|
||||||
|
|
||||||
|
|
||||||
async function fetchAccessToken(appId: string, appSecret: string): Promise<string> {
|
async function fetchAccessToken(appId: string, appSecret: string): Promise<string> {
|
||||||
const url = `${TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
const url = `${TOKEN_URL}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
@@ -72,14 +77,20 @@ async function fetchAccessToken(appId: string, appSecret: string): Promise<strin
|
|||||||
return data.access_token;
|
return data.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadImage(
|
function toHttpsUrl(url: string | undefined): string {
|
||||||
|
if (!url) return "";
|
||||||
|
return url.startsWith("http://") ? url.replace(/^http:\/\//i, "https://") : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUploadAsset(
|
||||||
imagePath: string,
|
imagePath: string,
|
||||||
accessToken: string,
|
baseDir?: string,
|
||||||
baseDir?: string
|
): Promise<WechatUploadAsset> {
|
||||||
): Promise<UploadResponse> {
|
|
||||||
let fileBuffer: Buffer;
|
let fileBuffer: Buffer;
|
||||||
let filename: string;
|
let filename: string;
|
||||||
let contentType: string;
|
let contentType: string;
|
||||||
|
let fileSize = 0;
|
||||||
|
let fileExt = "";
|
||||||
|
|
||||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||||
const response = await fetch(imagePath);
|
const response = await fetch(imagePath);
|
||||||
@@ -91,8 +102,10 @@ async function uploadImage(
|
|||||||
throw new Error(`Remote image is empty: ${imagePath}`);
|
throw new Error(`Remote image is empty: ${imagePath}`);
|
||||||
}
|
}
|
||||||
fileBuffer = Buffer.from(buffer);
|
fileBuffer = Buffer.from(buffer);
|
||||||
|
fileSize = buffer.byteLength;
|
||||||
const urlPath = imagePath.split("?")[0];
|
const urlPath = imagePath.split("?")[0];
|
||||||
filename = path.basename(urlPath) || "image.jpg";
|
filename = path.basename(urlPath) || "image.jpg";
|
||||||
|
fileExt = path.extname(filename).toLowerCase();
|
||||||
contentType = response.headers.get("content-type") || "image/jpeg";
|
contentType = response.headers.get("content-type") || "image/jpeg";
|
||||||
} else {
|
} else {
|
||||||
const resolvedPath = path.isAbsolute(imagePath)
|
const resolvedPath = path.isAbsolute(imagePath)
|
||||||
@@ -106,19 +119,85 @@ async function uploadImage(
|
|||||||
if (stats.size === 0) {
|
if (stats.size === 0) {
|
||||||
throw new Error(`Local image is empty: ${resolvedPath}`);
|
throw new Error(`Local image is empty: ${resolvedPath}`);
|
||||||
}
|
}
|
||||||
|
fileSize = stats.size;
|
||||||
fileBuffer = fs.readFileSync(resolvedPath);
|
fileBuffer = fs.readFileSync(resolvedPath);
|
||||||
filename = path.basename(resolvedPath);
|
filename = path.basename(resolvedPath);
|
||||||
const ext = path.extname(filename).toLowerCase();
|
fileExt = path.extname(filename).toLowerCase();
|
||||||
const mimeTypes: Record<string, string> = {
|
const mimeTypes: Record<string, string> = {
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
".jpeg": "image/jpeg",
|
".jpeg": "image/jpeg",
|
||||||
".png": "image/png",
|
".png": "image/png",
|
||||||
".gif": "image/gif",
|
".gif": "image/gif",
|
||||||
".webp": "image/webp",
|
".webp": "image/webp",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".tiff": "image/tiff",
|
||||||
|
".tif": "image/tiff",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".ico": "image/x-icon",
|
||||||
};
|
};
|
||||||
contentType = mimeTypes[ext] || "image/jpeg";
|
contentType = mimeTypes[fileExt] || "image/jpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: fileBuffer,
|
||||||
|
filename,
|
||||||
|
contentType,
|
||||||
|
fileExt,
|
||||||
|
fileSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(
|
||||||
|
imagePath: string,
|
||||||
|
accessToken: string,
|
||||||
|
baseDir?: string,
|
||||||
|
uploadType: "body" | "material" = "body"
|
||||||
|
): Promise<UploadResponse> {
|
||||||
|
const asset = await loadUploadAsset(imagePath, baseDir);
|
||||||
|
let uploadAsset = asset;
|
||||||
|
|
||||||
|
if (uploadType === "body" && needsWechatBodyImageProcessing(asset)) {
|
||||||
|
const prepared = await prepareWechatBodyImageUpload(asset);
|
||||||
|
uploadAsset = {
|
||||||
|
...asset,
|
||||||
|
buffer: prepared.buffer,
|
||||||
|
filename: prepared.filename,
|
||||||
|
contentType: prepared.contentType,
|
||||||
|
fileExt: path.extname(prepared.filename).toLowerCase(),
|
||||||
|
fileSize: prepared.buffer.length,
|
||||||
|
};
|
||||||
|
const note = prepared.processingNotes.join(", ");
|
||||||
|
console.error(`[wechat-api] Processed ${asset.filename} for body upload: ${note}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await uploadToWechat(
|
||||||
|
uploadAsset.buffer,
|
||||||
|
uploadAsset.filename,
|
||||||
|
uploadAsset.contentType,
|
||||||
|
accessToken,
|
||||||
|
uploadType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// media/uploadimg 接口只返回 URL,material/add_material 返回 media_id
|
||||||
|
if (uploadType === "body") {
|
||||||
|
return {
|
||||||
|
url: toHttpsUrl(result.url),
|
||||||
|
media_id: "",
|
||||||
|
} as UploadResponse;
|
||||||
|
} else {
|
||||||
|
result.url = toHttpsUrl(result.url);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实际的微信上传函数
|
||||||
|
async function uploadToWechat(
|
||||||
|
fileBuffer: Buffer,
|
||||||
|
filename: string,
|
||||||
|
contentType: string,
|
||||||
|
accessToken: string,
|
||||||
|
uploadType: "body" | "material"
|
||||||
|
): Promise<UploadResponse> {
|
||||||
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;
|
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;
|
||||||
const header = [
|
const header = [
|
||||||
`--${boundary}`,
|
`--${boundary}`,
|
||||||
@@ -133,7 +212,8 @@ async function uploadImage(
|
|||||||
const footerBuffer = Buffer.from(footer, "utf-8");
|
const footerBuffer = Buffer.from(footer, "utf-8");
|
||||||
const body = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]);
|
const body = Buffer.concat([headerBuffer, fileBuffer, footerBuffer]);
|
||||||
|
|
||||||
const url = `${UPLOAD_URL}?access_token=${accessToken}&type=image`;
|
const uploadUrl = uploadType === "body" ? UPLOAD_BODY_IMG_URL : UPLOAD_MATERIAL_URL;
|
||||||
|
const url = `${uploadUrl}?type=image&access_token=${accessToken}`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -147,10 +227,6 @@ async function uploadImage(
|
|||||||
throw new Error(`Upload failed ${data.errcode}: ${data.errmsg}`);
|
throw new Error(`Upload failed ${data.errcode}: ${data.errmsg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.url?.startsWith("http://")) {
|
|
||||||
data.url = data.url.replace(/^http:\/\//i, "https://");
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,17 +235,19 @@ async function uploadImagesInHtml(
|
|||||||
accessToken: string,
|
accessToken: string,
|
||||||
baseDir: string,
|
baseDir: string,
|
||||||
contentImages: ImageInfo[] = [],
|
contentImages: ImageInfo[] = [],
|
||||||
): Promise<{ html: string; firstMediaId: string; allMediaIds: string[] }> {
|
articleType: ArticleType = "news",
|
||||||
|
collectNewsCoverFallback: boolean = false,
|
||||||
|
): Promise<{ html: string; firstCoverMediaId: string; imageMediaIds: string[] }> {
|
||||||
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
||||||
const matches = [...html.matchAll(imgRegex)];
|
const matches = [...html.matchAll(imgRegex)];
|
||||||
|
|
||||||
if (matches.length === 0 && contentImages.length === 0) {
|
if (matches.length === 0 && contentImages.length === 0) {
|
||||||
return { html, firstMediaId: "", allMediaIds: [] };
|
return { html, firstCoverMediaId: "", imageMediaIds: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstMediaId = "";
|
let firstCoverMediaId = "";
|
||||||
let updatedHtml = html;
|
let updatedHtml = html;
|
||||||
const allMediaIds: string[] = [];
|
const imageMediaIds: string[] = [];
|
||||||
const uploadedBySource = new Map<string, UploadResponse>();
|
const uploadedBySource = new Map<string, UploadResponse>();
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
@@ -177,8 +255,13 @@ async function uploadImagesInHtml(
|
|||||||
if (!src) continue;
|
if (!src) continue;
|
||||||
|
|
||||||
if (src.startsWith("https://mmbiz.qpic.cn")) {
|
if (src.startsWith("https://mmbiz.qpic.cn")) {
|
||||||
if (!firstMediaId) {
|
if (collectNewsCoverFallback && !firstCoverMediaId) {
|
||||||
firstMediaId = src;
|
try {
|
||||||
|
const coverResp = await uploadImage(src, accessToken, baseDir, "material");
|
||||||
|
firstCoverMediaId = coverResp.media_id;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[wechat-api] Failed to reuse existing WeChat image as cover: ${src}`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -186,20 +269,31 @@ async function uploadImagesInHtml(
|
|||||||
const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
|
const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
|
||||||
const imagePath = localPathMatch ? localPathMatch[1]! : src;
|
const imagePath = localPathMatch ? localPathMatch[1]! : src;
|
||||||
|
|
||||||
console.error(`[wechat-api] Uploading image: ${imagePath}`);
|
console.error(`[wechat-api] Uploading body image: ${imagePath}`);
|
||||||
try {
|
try {
|
||||||
let resp = uploadedBySource.get(imagePath);
|
let resp = uploadedBySource.get(imagePath);
|
||||||
if (!resp) {
|
if (!resp) {
|
||||||
resp = await uploadImage(imagePath, accessToken, baseDir);
|
// 正文图片使用 media/uploadimg 接口获取 URL
|
||||||
|
resp = await uploadImage(imagePath, accessToken, baseDir, "body");
|
||||||
uploadedBySource.set(imagePath, resp);
|
uploadedBySource.set(imagePath, resp);
|
||||||
}
|
}
|
||||||
const newTag = fullTag
|
const newTag = fullTag
|
||||||
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
||||||
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
||||||
updatedHtml = updatedHtml.replace(fullTag, newTag);
|
updatedHtml = updatedHtml.replace(fullTag, newTag);
|
||||||
allMediaIds.push(resp.media_id);
|
const shouldUploadMaterial = articleType === "newspic" || (collectNewsCoverFallback && !firstCoverMediaId);
|
||||||
if (!firstMediaId) {
|
if (shouldUploadMaterial) {
|
||||||
firstMediaId = resp.media_id;
|
let materialResp = uploadedBySource.get(`${imagePath}:material`);
|
||||||
|
if (!materialResp) {
|
||||||
|
materialResp = await uploadImage(imagePath, accessToken, baseDir, "material");
|
||||||
|
uploadedBySource.set(`${imagePath}:material`, materialResp);
|
||||||
|
}
|
||||||
|
if (articleType === "newspic" && materialResp.media_id) {
|
||||||
|
imageMediaIds.push(materialResp.media_id);
|
||||||
|
}
|
||||||
|
if (collectNewsCoverFallback && !firstCoverMediaId && materialResp.media_id) {
|
||||||
|
firstCoverMediaId = materialResp.media_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[wechat-api] Failed to upload ${imagePath}:`, err);
|
console.error(`[wechat-api] Failed to upload ${imagePath}:`, err);
|
||||||
@@ -210,27 +304,38 @@ async function uploadImagesInHtml(
|
|||||||
if (!updatedHtml.includes(image.placeholder)) continue;
|
if (!updatedHtml.includes(image.placeholder)) continue;
|
||||||
|
|
||||||
const imagePath = image.localPath || image.originalPath;
|
const imagePath = image.localPath || image.originalPath;
|
||||||
console.error(`[wechat-api] Uploading placeholder image: ${imagePath}`);
|
console.error(`[wechat-api] Uploading body image: ${imagePath}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let resp = uploadedBySource.get(imagePath);
|
let resp = uploadedBySource.get(imagePath);
|
||||||
if (!resp) {
|
if (!resp) {
|
||||||
resp = await uploadImage(imagePath, accessToken, baseDir);
|
// 正文图片使用 media/uploadimg 接口获取 URL
|
||||||
|
resp = await uploadImage(imagePath, accessToken, baseDir, "body");
|
||||||
uploadedBySource.set(imagePath, resp);
|
uploadedBySource.set(imagePath, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const replacementTag = `<img src="${resp.url}" style="display: block; width: 100%; margin: 1.5em auto;">`;
|
const replacementTag = `<img src="${resp.url}" style="display: block; width: 100%; margin: 1.5em auto;">`;
|
||||||
updatedHtml = replaceAllPlaceholders(updatedHtml, image.placeholder, replacementTag);
|
updatedHtml = replaceAllPlaceholders(updatedHtml, image.placeholder, replacementTag);
|
||||||
allMediaIds.push(resp.media_id);
|
const shouldUploadMaterial = articleType === "newspic" || (collectNewsCoverFallback && !firstCoverMediaId);
|
||||||
if (!firstMediaId) {
|
if (shouldUploadMaterial) {
|
||||||
firstMediaId = resp.media_id;
|
let materialResp = uploadedBySource.get(`${imagePath}:material`);
|
||||||
|
if (!materialResp) {
|
||||||
|
materialResp = await uploadImage(imagePath, accessToken, baseDir, "material");
|
||||||
|
uploadedBySource.set(`${imagePath}:material`, materialResp);
|
||||||
|
}
|
||||||
|
if (articleType === "newspic" && materialResp.media_id) {
|
||||||
|
imageMediaIds.push(materialResp.media_id);
|
||||||
|
}
|
||||||
|
if (collectNewsCoverFallback && !firstCoverMediaId && materialResp.media_id) {
|
||||||
|
firstCoverMediaId = materialResp.media_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err);
|
console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { html: updatedHtml, firstMediaId, allMediaIds };
|
return { html: updatedHtml, firstCoverMediaId, imageMediaIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publishToDraft(
|
async function publishToDraft(
|
||||||
@@ -592,16 +697,6 @@ async function main(): Promise<void> {
|
|||||||
console.error("[wechat-api] Fetching access token...");
|
console.error("[wechat-api] Fetching access token...");
|
||||||
const accessToken = await fetchAccessToken(creds.appId, creds.appSecret);
|
const accessToken = await fetchAccessToken(creds.appId, creds.appSecret);
|
||||||
|
|
||||||
console.error("[wechat-api] Uploading images...");
|
|
||||||
const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml(
|
|
||||||
htmlContent,
|
|
||||||
accessToken,
|
|
||||||
baseDir,
|
|
||||||
contentImages,
|
|
||||||
);
|
|
||||||
htmlContent = processedHtml;
|
|
||||||
|
|
||||||
let thumbMediaId = "";
|
|
||||||
const rawCoverPath = args.cover ||
|
const rawCoverPath = args.cover ||
|
||||||
frontmatter.coverImage ||
|
frontmatter.coverImage ||
|
||||||
frontmatter.featureImage ||
|
frontmatter.featureImage ||
|
||||||
@@ -610,19 +705,31 @@ async function main(): Promise<void> {
|
|||||||
const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover
|
const coverPath = rawCoverPath && !path.isAbsolute(rawCoverPath) && args.cover
|
||||||
? path.resolve(process.cwd(), rawCoverPath)
|
? path.resolve(process.cwd(), rawCoverPath)
|
||||||
: rawCoverPath;
|
: rawCoverPath;
|
||||||
|
const needNewsCoverFallback = args.articleType === "news" && !coverPath;
|
||||||
|
|
||||||
|
console.error("[wechat-api] Uploading body images...");
|
||||||
|
const { html: processedHtml, firstCoverMediaId, imageMediaIds } = await uploadImagesInHtml(
|
||||||
|
htmlContent,
|
||||||
|
accessToken,
|
||||||
|
baseDir,
|
||||||
|
contentImages,
|
||||||
|
args.articleType,
|
||||||
|
needNewsCoverFallback,
|
||||||
|
);
|
||||||
|
htmlContent = processedHtml;
|
||||||
|
|
||||||
|
let thumbMediaId = "";
|
||||||
|
|
||||||
if (coverPath) {
|
if (coverPath) {
|
||||||
console.error(`[wechat-api] Uploading cover: ${coverPath}`);
|
console.error(`[wechat-api] Uploading cover: ${coverPath}`);
|
||||||
const coverResp = await uploadImage(coverPath, accessToken, baseDir);
|
// 封面图片使用 material/add_material 接口
|
||||||
|
const coverResp = await uploadImage(coverPath, accessToken, baseDir, "material");
|
||||||
thumbMediaId = coverResp.media_id;
|
thumbMediaId = coverResp.media_id;
|
||||||
} else if (firstMediaId) {
|
console.error(`[wechat-api] Cover uploaded successfully, media_id: ${thumbMediaId}`);
|
||||||
if (firstMediaId.startsWith("https://")) {
|
} else if (firstCoverMediaId && args.articleType === "news") {
|
||||||
console.error(`[wechat-api] Uploading first image as cover: ${firstMediaId}`);
|
// news 类型没有封面时,使用第一张正文图的 media_id 作为封面(兜底逻辑)
|
||||||
const coverResp = await uploadImage(firstMediaId, accessToken, baseDir);
|
thumbMediaId = firstCoverMediaId;
|
||||||
thumbMediaId = coverResp.media_id;
|
console.error(`[wechat-api] Using first body image as cover (fallback), media_id: ${thumbMediaId}`);
|
||||||
} else {
|
|
||||||
thumbMediaId = firstMediaId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.articleType === "news" && !thumbMediaId) {
|
if (args.articleType === "news" && !thumbMediaId) {
|
||||||
@@ -630,7 +737,7 @@ async function main(): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.articleType === "newspic" && allMediaIds.length === 0) {
|
if (args.articleType === "newspic" && imageMediaIds.length === 0) {
|
||||||
console.error("Error: newspic requires at least one image in content.");
|
console.error("Error: newspic requires at least one image in content.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -643,7 +750,7 @@ async function main(): Promise<void> {
|
|||||||
content: htmlContent,
|
content: htmlContent,
|
||||||
thumbMediaId,
|
thumbMediaId,
|
||||||
articleType: args.articleType,
|
articleType: args.articleType,
|
||||||
imageMediaIds: args.articleType === "newspic" ? allMediaIds : undefined,
|
imageMediaIds: args.articleType === "newspic" ? imageMediaIds : undefined,
|
||||||
needOpenComment: resolved.need_open_comment,
|
needOpenComment: resolved.need_open_comment,
|
||||||
onlyFansCanComment: resolved.only_fans_can_comment,
|
onlyFansCanComment: resolved.only_fans_can_comment,
|
||||||
}, accessToken);
|
}, accessToken);
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { Jimp, JimpMime } from "jimp";
|
||||||
|
import decodeWebp, { init as initWebpDecode } from "@jsquash/webp/decode.js";
|
||||||
|
|
||||||
|
export interface WechatUploadAsset {
|
||||||
|
buffer: Buffer;
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
fileExt: string;
|
||||||
|
fileSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreparedWechatUploadAsset {
|
||||||
|
buffer: Buffer;
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
wasProcessed: boolean;
|
||||||
|
processingNotes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WECHAT_BODY_IMAGE_MAX_SIZE = 1024 * 1024; // 1MB
|
||||||
|
export const WECHAT_BODY_IMAGE_UNSUPPORTED_FORMATS = new Set([
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".bmp",
|
||||||
|
".tiff",
|
||||||
|
".tif",
|
||||||
|
".svg",
|
||||||
|
".ico",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const BODY_UPLOAD_ALLOWED_MIME_TYPES = new Set([
|
||||||
|
JimpMime.jpeg,
|
||||||
|
JimpMime.png,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MIME_TO_EXT: Record<string, string> = {
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/gif": ".gif",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
"image/bmp": ".bmp",
|
||||||
|
"image/x-ms-bmp": ".bmp",
|
||||||
|
"image/tiff": ".tiff",
|
||||||
|
"image/svg+xml": ".svg",
|
||||||
|
"image/x-icon": ".ico",
|
||||||
|
"image/vnd.microsoft.icon": ".ico",
|
||||||
|
};
|
||||||
|
|
||||||
|
const JPEG_QUALITY_STEPS = [82, 74, 66, 58, 50, 42, 34];
|
||||||
|
const MAX_WIDTH_STEPS = [2560, 2048, 1600, 1280, 1024, 800, 640, 480];
|
||||||
|
|
||||||
|
let webpDecoderReady: Promise<void> | undefined;
|
||||||
|
|
||||||
|
type JimpImage = Awaited<ReturnType<typeof Jimp.read>>;
|
||||||
|
|
||||||
|
function normalizeMimeType(contentType: string): string {
|
||||||
|
return contentType.split(";")[0]!.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extFromMimeType(contentType: string): string {
|
||||||
|
return MIME_TO_EXT[normalizeMimeType(contentType)] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureFileExt(asset: WechatUploadAsset): string {
|
||||||
|
return asset.fileExt || extFromMimeType(asset.contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function basenameWithoutExt(filename: string): string {
|
||||||
|
const base = path.basename(filename, path.extname(filename));
|
||||||
|
return base || "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameWithExt(filename: string, ext: string): string {
|
||||||
|
return `${basenameWithoutExt(filename)}${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsWechatBodyImageProcessing(asset: WechatUploadAsset): boolean {
|
||||||
|
if (asset.fileSize > WECHAT_BODY_IMAGE_MAX_SIZE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedMimeType = normalizeMimeType(asset.contentType);
|
||||||
|
if (BODY_UPLOAD_ALLOWED_MIME_TYPES.has(normalizedMimeType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExt = ensureFileExt(asset);
|
||||||
|
return WECHAT_BODY_IMAGE_UNSUPPORTED_FORMATS.has(fileExt) || !fileExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureWebpDecoder(): Promise<void> {
|
||||||
|
if (!webpDecoderReady) {
|
||||||
|
webpDecoderReady = (async () => {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const wasmPath = path.resolve(__dirname, "node_modules/@jsquash/webp/codec/dec/webp_dec.wasm");
|
||||||
|
const wasmModule = await WebAssembly.compile(await fs.readFile(wasmPath));
|
||||||
|
await initWebpDecode(wasmModule, {});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
await webpDecoderReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImageForProcessing(asset: WechatUploadAsset): Promise<JimpImage> {
|
||||||
|
const fileExt = ensureFileExt(asset);
|
||||||
|
const normalizedMimeType = normalizeMimeType(asset.contentType);
|
||||||
|
|
||||||
|
if (fileExt === ".webp" || normalizedMimeType === "image/webp") {
|
||||||
|
await ensureWebpDecoder();
|
||||||
|
const decoded = await decodeWebp(asset.buffer);
|
||||||
|
return new Jimp({
|
||||||
|
data: Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength),
|
||||||
|
width: decoded.width,
|
||||||
|
height: decoded.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExt === ".svg" || fileExt === ".ico") {
|
||||||
|
throw new Error(`Cannot convert ${fileExt} image for WeChat body upload; provide a PNG or JPG instead.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Jimp.read(asset.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageHasTransparency(image: JimpImage): boolean {
|
||||||
|
const { data } = image.bitmap;
|
||||||
|
for (let i = 3; i < data.length; i += 4) {
|
||||||
|
if (data[i] !== 255) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCandidateWidths(width: number): number[] {
|
||||||
|
const candidates = new Set<number>([width]);
|
||||||
|
|
||||||
|
for (const maxWidth of MAX_WIDTH_STEPS) {
|
||||||
|
if (width > maxWidth) {
|
||||||
|
candidates.add(maxWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...candidates].sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeToWidth(image: JimpImage, width: number): JimpImage {
|
||||||
|
const cloned = image.clone();
|
||||||
|
if (width < image.bitmap.width) {
|
||||||
|
cloned.resize({ w: width });
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenOnWhite(image: JimpImage): JimpImage {
|
||||||
|
const flattened = new Jimp({
|
||||||
|
width: image.bitmap.width,
|
||||||
|
height: image.bitmap.height,
|
||||||
|
color: 0xffffffff,
|
||||||
|
});
|
||||||
|
flattened.composite(image, 0, 0);
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encodePng(image: JimpImage): Promise<Buffer> {
|
||||||
|
return image.getBuffer(JimpMime.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encodeJpeg(image: JimpImage, quality: number): Promise<Buffer> {
|
||||||
|
const jpegSource = imageHasTransparency(image) ? flattenOnWhite(image) : image;
|
||||||
|
return jpegSource.getBuffer(JimpMime.jpeg, { quality });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProcessingNotes(asset: WechatUploadAsset): string[] {
|
||||||
|
const notes: string[] = [];
|
||||||
|
const fileExt = ensureFileExt(asset);
|
||||||
|
|
||||||
|
if (fileExt && WECHAT_BODY_IMAGE_UNSUPPORTED_FORMATS.has(fileExt)) {
|
||||||
|
notes.push(`converted unsupported ${fileExt} source`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.fileSize > WECHAT_BODY_IMAGE_MAX_SIZE) {
|
||||||
|
notes.push(`compressed ${(asset.fileSize / 1024 / 1024).toFixed(2)}MB source below 1MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
notes.push("re-encoded for WeChat body upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareWechatBodyImageUpload(
|
||||||
|
asset: WechatUploadAsset,
|
||||||
|
): Promise<PreparedWechatUploadAsset> {
|
||||||
|
if (!needsWechatBodyImageProcessing(asset)) {
|
||||||
|
return {
|
||||||
|
buffer: asset.buffer,
|
||||||
|
filename: asset.filename,
|
||||||
|
contentType: asset.contentType,
|
||||||
|
wasProcessed: false,
|
||||||
|
processingNotes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await loadImageForProcessing(asset);
|
||||||
|
const widths = buildCandidateWidths(image.bitmap.width);
|
||||||
|
const preferPng = imageHasTransparency(image) || ensureFileExt(asset) === ".png";
|
||||||
|
const processingNotes = buildProcessingNotes(asset);
|
||||||
|
|
||||||
|
for (const width of widths) {
|
||||||
|
const resized = resizeToWidth(image, width);
|
||||||
|
|
||||||
|
if (preferPng) {
|
||||||
|
const pngBuffer = await encodePng(resized);
|
||||||
|
if (pngBuffer.length <= WECHAT_BODY_IMAGE_MAX_SIZE) {
|
||||||
|
return {
|
||||||
|
buffer: pngBuffer,
|
||||||
|
filename: renameWithExt(asset.filename, ".png"),
|
||||||
|
contentType: JimpMime.png,
|
||||||
|
wasProcessed: true,
|
||||||
|
processingNotes: width < image.bitmap.width
|
||||||
|
? [...processingNotes, `resized to ${width}px wide`]
|
||||||
|
: processingNotes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const quality of JPEG_QUALITY_STEPS) {
|
||||||
|
const jpegBuffer = await encodeJpeg(resized, quality);
|
||||||
|
if (jpegBuffer.length <= WECHAT_BODY_IMAGE_MAX_SIZE) {
|
||||||
|
const notes = [...processingNotes, `encoded as JPEG (${quality} quality)`];
|
||||||
|
if (width < image.bitmap.width) {
|
||||||
|
notes.push(`resized to ${width}px wide`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
buffer: jpegBuffer,
|
||||||
|
filename: renameWithExt(asset.filename, ".jpg"),
|
||||||
|
contentType: JimpMime.jpeg,
|
||||||
|
wasProcessed: true,
|
||||||
|
processingNotes: notes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unable to reduce ${asset.filename} below 1MB for WeChat body upload.`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user