Compare commits

..

4 Commits

Author SHA1 Message Date
Jim Liu 宝玉 c5c54e26da chore: release v1.90.1 2026-04-05 13:55:58 -05:00
Jim Liu 宝玉 2a0bba6161 fix(baoyu-post-to-wechat): detect actual image format from buffer magic bytes
CDNs may serve WebP for URLs with .png extension. Detect real format
from magic bytes and correct content-type/extension before upload.
Also treat .webp as PNG-preferred for transparency handling.
2026-04-05 13:36:51 -05:00
Jim Liu 宝玉 c44a524fa6 chore: release v1.90.0 2026-04-03 23:45:44 -05:00
Jim Liu 宝玉 826535abe4 feat(baoyu-xhs-images): add --yes non-interactive mode for automation 2026-04-03 23:45:10 -05:00
9 changed files with 98 additions and 22 deletions
+1 -1
View File
@@ -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.89.2" "version": "1.90.1"
}, },
"plugins": [ "plugins": [
{ {
+5
View File
@@ -2,6 +2,11 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.90.1 - 2026-04-05
### Fixes
- `baoyu-post-to-wechat`: detect actual image format from buffer magic bytes to fix CDN content-type mismatches (e.g. WebP served for .png URLs); treat WebP as PNG-preferred for transparency handling
## 1.89.1 - 2026-04-01 ## 1.89.1 - 2026-04-01
### Features ### Features
+5
View File
@@ -2,6 +2,11 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.90.1 - 2026-04-05
### 修复
- `baoyu-post-to-wechat`:通过 magic bytes 检测实际图片格式,修复 CDN 返回与 URL 扩展名不一致的 content-type 问题(如 .png URL 实际返回 WebP);WebP 格式按 PNG 策略处理以保留透明度
## 1.89.1 - 2026-04-01 ## 1.89.1 - 2026-04-01
### 新功能 ### 新功能
+1 -1
View File
@@ -1,6 +1,6 @@
# CLAUDE.md # CLAUDE.md
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.89.2**. Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.90.1**.
## Architecture ## Architecture
+4
View File
@@ -116,6 +116,10 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
# Direct content input # Direct content input
/baoyu-xhs-images 今日星座运势 /baoyu-xhs-images 今日星座运势
# Non-interactive (skip all confirmations, for scheduled tasks)
/baoyu-xhs-images posts/ai-future/article.md --yes
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
``` ```
**Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard` **Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard`
+4
View File
@@ -116,6 +116,10 @@ clawhub install baoyu-markdown-to-html
# 直接输入内容 # 直接输入内容
/baoyu-xhs-images 今日星座运势 /baoyu-xhs-images 今日星座运势
# 非交互模式(跳过所有确认,适用于定时任务)
/baoyu-xhs-images posts/ai-future/article.md --yes
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
``` ```
**风格**(视觉美学):`cute`(默认)、`fresh``warm``bold``minimal``retro``pop``notion``chalkboard` **风格**(视觉美学):`cute`(默认)、`fresh``warm``bold``minimal``retro``pop``notion``chalkboard`
@@ -7,6 +7,7 @@ import {
type WechatUploadAsset, type WechatUploadAsset,
prepareWechatBodyImageUpload, prepareWechatBodyImageUpload,
needsWechatBodyImageProcessing, needsWechatBodyImageProcessing,
detectImageFormatFromBuffer,
} from "./wechat-image-processor.ts"; } from "./wechat-image-processor.ts";
interface AccessTokenResponse { interface AccessTokenResponse {
@@ -138,6 +139,16 @@ async function loadUploadAsset(
contentType = mimeTypes[fileExt] || "image/jpeg"; contentType = mimeTypes[fileExt] || "image/jpeg";
} }
// Detect actual format from magic bytes to fix extension/content-type mismatches
// (e.g. CDNs serving WebP for URLs with .png extension)
const detected = detectImageFormatFromBuffer(fileBuffer);
if (detected && detected.contentType !== contentType) {
console.error(`[wechat-api] Format mismatch: ${filename} declared as ${contentType}, actual ${detected.contentType}`);
contentType = detected.contentType;
fileExt = detected.fileExt;
filename = `${path.basename(filename, path.extname(filename))}${detected.fileExt}`;
}
return { return {
buffer: fileBuffer, buffer: fileBuffer,
filename, filename,
@@ -52,6 +52,39 @@ const MIME_TO_EXT: Record<string, string> = {
const JPEG_QUALITY_STEPS = [82, 74, 66, 58, 50, 42, 34]; const JPEG_QUALITY_STEPS = [82, 74, 66, 58, 50, 42, 34];
const MAX_WIDTH_STEPS = [2560, 2048, 1600, 1280, 1024, 800, 640, 480]; const MAX_WIDTH_STEPS = [2560, 2048, 1600, 1280, 1024, 800, 640, 480];
/**
* Detect actual image format from buffer magic bytes.
* Returns corrected { contentType, fileExt } or null if unknown.
*/
export function detectImageFormatFromBuffer(buffer: Buffer): { contentType: string; fileExt: string } | null {
if (buffer.length < 12) return null;
// WebP: RIFF....WEBP
if (
buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50
) {
return { contentType: "image/webp", fileExt: ".webp" };
}
// PNG: 89 50 4E 47
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
return { contentType: "image/png", fileExt: ".png" };
}
// JPEG: FF D8 FF
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
return { contentType: "image/jpeg", fileExt: ".jpg" };
}
// GIF: GIF8
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
return { contentType: "image/gif", fileExt: ".gif" };
}
// BMP: BM
if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
return { contentType: "image/bmp", fileExt: ".bmp" };
}
return null;
}
let webpDecoderReady: Promise<void> | undefined; let webpDecoderReady: Promise<void> | undefined;
type JimpImage = Awaited<ReturnType<typeof Jimp.read>>; type JimpImage = Awaited<ReturnType<typeof Jimp.read>>;
@@ -209,7 +242,8 @@ export async function prepareWechatBodyImageUpload(
const image = await loadImageForProcessing(asset); const image = await loadImageForProcessing(asset);
const widths = buildCandidateWidths(image.bitmap.width); const widths = buildCandidateWidths(image.bitmap.width);
const preferPng = imageHasTransparency(image) || ensureFileExt(asset) === ".png"; const ext = ensureFileExt(asset);
const preferPng = imageHasTransparency(image) || ext === ".png" || ext === ".webp";
const processingNotes = buildProcessingNotes(asset); const processingNotes = buildProcessingNotes(asset);
for (const width of widths) { for (const width of widths) {
+32 -19
View File
@@ -39,6 +39,10 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
# Direct input with options # Direct input with options
/baoyu-xhs-images --style bold --layout comparison /baoyu-xhs-images --style bold --layout comparison
[paste content] [paste content]
# Non-interactive (for scheduled tasks / automation)
/baoyu-xhs-images posts/ai-future/article.md --yes
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
``` ```
## Options ## Options
@@ -48,6 +52,7 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
| `--style <name>` | Visual style (see Style Gallery) | | `--style <name>` | Visual style (see Style Gallery) |
| `--layout <name>` | Information layout (see Layout Gallery) | | `--layout <name>` | Information layout (see Layout Gallery) |
| `--preset <name>` | Style + layout shorthand (see [Style Presets](references/style-presets.md)) | | `--preset <name>` | Style + layout shorthand (see [Style Presets](references/style-presets.md)) |
| `--yes` | Non-interactive mode: skip all confirmations. Uses EXTEND.md preferences if found, otherwise uses defaults (no watermark, auto style/layout). Auto-confirms recommended plan (Path A). Suitable for scheduled tasks and automation. |
## Two Dimensions ## Two Dimensions
@@ -237,11 +242,11 @@ Copy and track progress:
``` ```
XHS Infographic Progress: XHS Infographic Progress:
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING - [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING (--yes: use defaults if not found)
- [ ] Found → load preferences → continue - [ ] Found → load preferences → continue
- [ ] Not found → run first-time setup → MUST complete before Step 1 - [ ] Not found → run first-time setup → MUST complete before Step 1 (--yes: skip setup, use defaults)
- [ ] Step 1: Analyze content → analysis.md - [ ] Step 1: Analyze content → analysis.md
- [ ] Step 2: Smart Confirm ⚠️ REQUIRED - [ ] Step 2: Smart Confirm ⚠️ REQUIRED (--yes: auto-confirm Path A)
- [ ] Path A: Quick confirm → generate recommended outline - [ ] Path A: Quick confirm → generate recommended outline
- [ ] Path B: Customize → adjust then generate outline - [ ] Path B: Customize → adjust then generate outline
- [ ] Path C: Detailed → 3 outlines → second confirm → generate outline - [ ] Path C: Detailed → 3 outlines → second confirm → generate outline
@@ -252,26 +257,30 @@ XHS Infographic Progress:
### Flow ### Flow
``` ```
Input → [Step 0: Preferences] ─┬─ Found → Continue Input → [--yes?] ─┬─ Yes → [Step 0: Load or defaults] → Analyze → Auto-confirm → Generate → Complete
└─ Not found → First-Time Setup ⛔ BLOCKING └─ No → [Step 0: Preferences] ─┬─ Found → Continue
└─ Complete setup → Save EXTEND.md → Continue └─ Not found → First-Time Setup ⛔ BLOCKING
┌───────────────────────────────────────────────────────────────────────────┘ └─ Complete setup → Save EXTEND.md → Continue
Analyze → [Smart Confirm] ─┬─ Quick: confirm recommended → outline.md → Generate → Complete ┌─────────────────────────────────────────────────────────────────────────────────────┘
├─ Customize: adjust options → outline.md → Generate → Complete Analyze → [Smart Confirm] ─┬─ Quick: confirm recommended → outline.md → Generate → Complete
└─ Detailed: 3 outlines → [Confirm 2] → outline.md → Generate → Complete ├─ Customize: adjust options → outline.md → Generate → Complete
└─ Detailed: 3 outlines → [Confirm 2] → outline.md → Generate → Complete
``` ```
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING ### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
**Purpose**: Load user preferences or run first-time setup. **Purpose**: Load user preferences or run first-time setup.
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to content analysis, do NOT ask about style, do NOT ask about layout — ONLY complete the preferences setup first. **`--yes` mode**: If EXTEND.md found → load it. If not found → use built-in defaults (no watermark, style/layout auto-select, language from content). Do NOT run first-time setup, do NOT create EXTEND.md, do NOT ask any questions. Proceed directly to Step 1.
**CRITICAL** (interactive mode only): If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to content analysis, do NOT ask about style, do NOT ask about layout — ONLY complete the preferences setup first.
Check EXTEND.md existence (priority order): Check EXTEND.md existence (priority order):
@@ -340,7 +349,11 @@ Read source content, save it if needed, and perform deep analysis.
### Step 2: Smart Confirm ⚠️ ### Step 2: Smart Confirm ⚠️
**Purpose**: Present auto-recommended plan, let user confirm or adjust. **Do NOT skip.** **Purpose**: Present auto-recommended plan, let user confirm or adjust.
**`--yes` mode**: Skip this entire step. Use auto-recommended strategy + style + layout from Step 1 analysis (or `--style`/`--layout`/`--preset` if provided). Generate outline directly using Path A logic → save to `outline.md` → proceed to Step 3. No AskUserQuestion calls.
**Interactive mode**: Do NOT skip.
**Auto-Recommendation Logic**: **Auto-Recommendation Logic**:
1. Use Auto Selection table to match content signals → best strategy + style + layout 1. Use Auto Selection table to match content signals → best strategy + style + layout
@@ -491,7 +504,7 @@ Reference: `references/config/watermark-guide.md`
**Image Generation Skill Selection**: **Image Generation Skill Selection**:
- Check available image generation skills - Check available image generation skills
- If multiple skills available, ask user preference - If multiple skills available: ask user preference (interactive) or use first available skill (`--yes` mode)
**Session Management**: **Session Management**:
If image generation skill supports `--sessionId`: If image generation skill supports `--sessionId`: