mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0162bb3af | |||
| e9342a16f8 | |||
| 5cb86d8e51 | |||
| 66ed08f010 | |||
| b179166d28 | |||
| 4805526649 | |||
| 89159bf86e | |||
| b45b5d2c6e | |||
| 62866426c5 | |||
| 28a7db6129 | |||
| 7c5e81fd15 | |||
| 058d25d02c |
@@ -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.25.2"
|
"version": "1.26.1"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.26.1 - 2026-01-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-article-illustrator`, `baoyu-comic`, `baoyu-cover-image`, `baoyu-infographic`, `baoyu-slide-deck`, `baoyu-xhs-images`: add backup rules for existing files—automatically renames source, prompt, and image files with timestamp suffix before overwriting.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-xhs-images`: remove `notebook` style (10 styles remaining).
|
||||||
|
|
||||||
|
## 1.26.0 - 2026-01-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-xhs-images`: add `notebook` style (hand-drawn infographic with watercolor rendering and Morandi palette) and `study-notes` style (realistic handwritten photo aesthetic).
|
||||||
|
- `baoyu-xhs-images`: add `mindmap` (center radial) and `quadrant` (four-section grid) layouts.
|
||||||
|
|
||||||
|
## 1.25.4 - 2026-01-29
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-markdown-to-html`: generate proper `<img>` tags with `data-local-path` attribute instead of text placeholders.
|
||||||
|
- `baoyu-post-to-wechat`: fix API publishing to read image paths from `data-local-path` attribute; fix title/cover extraction from corresponding `.md` frontmatter when publishing HTML files.
|
||||||
|
- `baoyu-post-to-wechat`: fix CLI argument parsing to handle unknown parameters gracefully; add `--summary` parameter support.
|
||||||
|
- `baoyu-post-to-wechat`: fix browser publishing to convert `<img>` tags back to text placeholders before paste.
|
||||||
|
|
||||||
|
## 1.25.3 - 2026-01-28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-format-markdown`: add content type detection with user confirmation for markdown files; add CJK punctuation handling to move paired punctuation outside emphasis markers.
|
||||||
|
|
||||||
## 1.25.2 - 2026-01-28
|
## 1.25.2 - 2026-01-28
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|||||||
@@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.26.1 - 2026-01-29
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-article-illustrator`、`baoyu-comic`、`baoyu-cover-image`、`baoyu-infographic`、`baoyu-slide-deck`、`baoyu-xhs-images`:新增文件备份规则,覆盖前自动将现有源文件、提示词和图片重命名为带时间戳后缀的备份文件。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-xhs-images`:移除 `notebook` 风格(保留 10 种风格)。
|
||||||
|
|
||||||
|
## 1.26.0 - 2026-01-29
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-xhs-images`:新增 `notebook` 风格(水彩渲染手绘信息图 + 莫兰迪配色)和 `study-notes` 风格(真实手写照片美学)。
|
||||||
|
- `baoyu-xhs-images`:新增 `mindmap`(中心发散式)和 `quadrant`(四象限)布局。
|
||||||
|
|
||||||
|
## 1.25.4 - 2026-01-29
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-markdown-to-html`:生成带 `data-local-path` 属性的 `<img>` 标签,而非纯文本占位符。
|
||||||
|
- `baoyu-post-to-wechat`:修复 API 发布时从 `data-local-path` 属性读取图片路径;修复发布 HTML 文件时从对应 `.md` 的 frontmatter 提取标题和封面图。
|
||||||
|
- `baoyu-post-to-wechat`:修复命令行参数解析,正确跳过未知参数;新增 `--summary` 参数支持。
|
||||||
|
- `baoyu-post-to-wechat`:修复浏览器发布模式,粘贴前将 `<img>` 标签转换回文本占位符。
|
||||||
|
|
||||||
|
## 1.25.3 - 2026-01-28
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-format-markdown`:新增内容类型检测,对已有 markdown 格式的文件提供用户确认选项;新增 CJK 配对标点处理,将括号、引号等标点移出加粗标记外。
|
||||||
|
|
||||||
## 1.25.2 - 2026-01-28
|
## 1.25.2 - 2026-01-28
|
||||||
|
|
||||||
### 文档
|
### 文档
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ Progress:
|
|||||||
| File path | Ask user (1.2) | → 1.2 |
|
| File path | Ask user (1.2) | → 1.2 |
|
||||||
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
|
| Pasted content | `illustrations/{topic-slug}/` | → 1.4 |
|
||||||
|
|
||||||
|
**Backup rule for pasted content**: If `source.md` exists in target directory, rename to `source-backup-YYYYMMDD-HHMMSS.md` before saving.
|
||||||
|
|
||||||
**1.2 Determine Output Directory** (file path input only)
|
**1.2 Determine Output Directory** (file path input only)
|
||||||
|
|
||||||
Check `default_output_dir` in preferences:
|
Check `default_output_dir` in preferences:
|
||||||
@@ -236,6 +238,7 @@ image_count: 4
|
|||||||
**5.1 Create Prompts**
|
**5.1 Create Prompts**
|
||||||
|
|
||||||
Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
|
Follow [references/prompt-construction.md](references/prompt-construction.md). Save to `prompts/illustration-{slug}.md`.
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/illustration-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
**5.2 Select Generation Skill**
|
**5.2 Select Generation Skill**
|
||||||
|
|
||||||
@@ -247,7 +250,9 @@ Add: `Include a subtle watermark "[content]" at [position].`
|
|||||||
|
|
||||||
**5.4 Generate**
|
**5.4 Generate**
|
||||||
|
|
||||||
1. Generate sequentially
|
1. For each illustration:
|
||||||
|
- **Backup rule**: If image file exists, rename to `NN-{type}-{slug}-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
- Generate image
|
||||||
2. After each: "Generated X/N"
|
2. After each: "Generated X/N"
|
||||||
3. On failure: retry once, then log and continue
|
3. On failure: retry once, then log and continue
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ Input → Preferences → Analyze → [Check Existing?] → [Confirm: Style + Re
|
|||||||
**Character reference is MANDATORY for visual consistency.**
|
**Character reference is MANDATORY for visual consistency.**
|
||||||
|
|
||||||
**7.1 Generate character sheet first**:
|
**7.1 Generate character sheet first**:
|
||||||
|
- **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||||
```bash
|
```bash
|
||||||
# Use Reference Sheet Prompt from characters/characters.md
|
# Use Reference Sheet Prompt from characters/characters.md
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
@@ -214,6 +215,10 @@ Compress to reduce token usage when used as reference image:
|
|||||||
| Supports `--ref` | Pass `characters/characters.png` with EVERY page |
|
| Supports `--ref` | Pass `characters/characters.png` with EVERY page |
|
||||||
| No `--ref` support | Prepend character descriptions to EVERY prompt file |
|
| No `--ref` support | Prepend character descriptions to EVERY prompt file |
|
||||||
|
|
||||||
|
**Backup rules for page generation**:
|
||||||
|
- If prompt file exists: rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
- If image file exists: rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example: ALWAYS include --ref for consistency
|
# Example: ALWAYS include --ref for consistency
|
||||||
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
npx -y bun ${SKILL_DIR}/../baoyu-image-gen/scripts/main.ts \
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ Read source content, save it if needed, and perform deep analysis.
|
|||||||
1. **Save source content** (if not already a file):
|
1. **Save source content** (if not already a file):
|
||||||
- If user provides a file path: use as-is
|
- If user provides a file path: use as-is
|
||||||
- If user pastes content: save to `source.md` in target directory
|
- If user pastes content: save to `source.md` in target directory
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Read source content
|
2. Read source content
|
||||||
3. **Deep analysis** following `analysis-framework.md`:
|
3. **Deep analysis** following `analysis-framework.md`:
|
||||||
- Target audience identification
|
- Target audience identification
|
||||||
@@ -329,6 +330,7 @@ Create image generation prompts for all pages.
|
|||||||
1. Create prompt following art style + tone guidelines
|
1. Create prompt following art style + tone guidelines
|
||||||
2. Include character visual descriptions for consistency
|
2. Include character visual descriptions for consistency
|
||||||
3. Save to `prompts/NN-{cover|page}-[slug].md`
|
3. Save to `prompts/NN-{cover|page}-[slug].md`
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
**Prompt File Format**:
|
**Prompt File Format**:
|
||||||
```markdown
|
```markdown
|
||||||
@@ -405,8 +407,9 @@ With confirmed prompts from Step 5/6:
|
|||||||
### 7.1 Generate Character Reference Sheet (first)
|
### 7.1 Generate Character Reference Sheet (first)
|
||||||
|
|
||||||
1. Use Reference Sheet Prompt from `characters/characters.md`
|
1. Use Reference Sheet Prompt from `characters/characters.md`
|
||||||
2. Generate → `characters/characters.png`
|
2. **Backup rule**: If `characters/characters.png` exists, rename to `characters/characters-backup-YYYYMMDD-HHMMSS.png`
|
||||||
3. This ensures visual consistency for all subsequent pages
|
3. Generate → `characters/characters.png`
|
||||||
|
4. This ensures visual consistency for all subsequent pages
|
||||||
|
|
||||||
### 7.2 Generate Comic Pages
|
### 7.2 Generate Comic Pages
|
||||||
|
|
||||||
@@ -453,9 +456,10 @@ When skill does NOT support reference images, create combined prompt files:
|
|||||||
|
|
||||||
**For each page (cover + pages)**:
|
**For each page (cover + pages)**:
|
||||||
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
|
1. Read prompt from `prompts/NN-{cover|page}-[slug].md`
|
||||||
2. Generate image using Strategy A or B (based on skill capability)
|
2. **Backup rule**: If image file exists, rename to `NN-{cover|page}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
3. Save to `NN-{cover|page}-[slug].png`
|
3. Generate image using Strategy A or B (based on skill capability)
|
||||||
4. Report progress after each generation: "Generated X/N: [page title]"
|
4. Save to `NN-{cover|page}-[slug].png`
|
||||||
|
5. Report progress after each generation: "Generated X/N: [page title]"
|
||||||
|
|
||||||
**Session Management**:
|
**Session Management**:
|
||||||
If image generation skill supports `--sessionId`:
|
If image generation skill supports `--sessionId`:
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ Schema: [references/config/preferences-schema.md](references/config/preferences-
|
|||||||
### Step 1: Analyze Content
|
### Step 1: Analyze Content
|
||||||
|
|
||||||
1. **Save source content** (if pasted, save to `source.md` in target directory; if file path, use as-is)
|
1. **Save source content** (if pasted, save to `source.md` in target directory; if file path, use as-is)
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. **Content analysis**: Extract topic, core message, tone, keywords; identify visual metaphors; detect content type
|
2. **Content analysis**: Extract topic, core message, tone, keywords; identify visual metaphors; detect content type
|
||||||
3. **Language detection**: Detect source language, note user's input language, compare with EXTEND.md preference
|
3. **Language detection**: Detect source language, note user's input language, compare with EXTEND.md preference
|
||||||
4. **Determine output directory** per File Structure rules. If no `default_output_dir` preference + file path input, include in Step 2 Q4
|
4. **Determine output directory** per File Structure rules. If no `default_output_dir` preference + file path input, include in Step 2 Q4
|
||||||
@@ -226,6 +227,8 @@ Validate all 5 dimensions + aspect ratio. Full confirmation flow: [references/wo
|
|||||||
|
|
||||||
### Step 3: Create Prompt
|
### Step 3: Create Prompt
|
||||||
|
|
||||||
|
**Backup rule**: If `prompts/cover.md` exists, rename to `prompts/cover-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
|
Save to `prompts/cover.md`. Full template: [references/workflow/prompt-template.md](references/workflow/prompt-template.md)
|
||||||
|
|
||||||
### Step 4: Generate Image
|
### Step 4: Generate Image
|
||||||
|
|||||||
@@ -58,6 +58,55 @@ Claude performs content analysis and formatting (Steps 1-6), then runs the scrip
|
|||||||
|
|
||||||
Read the user-specified markdown or plain text file.
|
Read the user-specified markdown or plain text file.
|
||||||
|
|
||||||
|
### Step 1.5: Detect Content Type & Confirm
|
||||||
|
|
||||||
|
**Content Type Detection:**
|
||||||
|
|
||||||
|
| Indicator | Classification |
|
||||||
|
|-----------|----------------|
|
||||||
|
| Has `---` YAML frontmatter | Markdown |
|
||||||
|
| Has `#`, `##`, `###` headings | Markdown |
|
||||||
|
| Has `**bold**`, `*italic*` | Markdown |
|
||||||
|
| Has `- ` or `1. ` lists | Markdown |
|
||||||
|
| Has ``` code blocks | Markdown |
|
||||||
|
| Has `> ` blockquotes | Markdown |
|
||||||
|
| None of above | Plain text |
|
||||||
|
|
||||||
|
**Decision Flow:**
|
||||||
|
|
||||||
|
┌─────────────────┬────────────────────────────────────────────────┐
|
||||||
|
│ Content Type │ Action │
|
||||||
|
├─────────────────┼────────────────────────────────────────────────┤
|
||||||
|
│ Plain text │ Proceed to Step 2 (format to markdown) │
|
||||||
|
├─────────────────┼────────────────────────────────────────────────┤
|
||||||
|
│ Markdown │ Use AskUserQuestion to confirm optimization │
|
||||||
|
└─────────────────┴────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
**If Markdown detected, ask user:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Detected existing markdown formatting. What would you like to do?
|
||||||
|
|
||||||
|
1. Optimize formatting (Recommended)
|
||||||
|
- Add/improve frontmatter, headings, bold, lists
|
||||||
|
- Run typography script (spacing, emphasis fixes)
|
||||||
|
- Output: {filename}-formatted.md
|
||||||
|
|
||||||
|
2. Keep original formatting
|
||||||
|
- Preserve existing markdown structure
|
||||||
|
- Run typography script (spacing, emphasis fixes)
|
||||||
|
- Output: {filename}-formatted.md
|
||||||
|
|
||||||
|
3. Typography fixes only
|
||||||
|
- Run typography script on original file in-place
|
||||||
|
- No copy created, modifies original file directly
|
||||||
|
```
|
||||||
|
|
||||||
|
**Based on user choice:**
|
||||||
|
- **Optimize**: Continue to Step 2-8 (full workflow)
|
||||||
|
- **Keep original**: Skip Steps 2-5, copy file → Step 6-8 (run script on copy)
|
||||||
|
- **Typography only**: Skip Steps 2-6, run Step 7 on original file directly
|
||||||
|
|
||||||
### Step 2: Analyze Content Structure
|
### Step 2: Analyze Content Structure
|
||||||
|
|
||||||
Identify:
|
Identify:
|
||||||
@@ -138,6 +187,10 @@ Examples:
|
|||||||
- `final.md` → `final-formatted.md`
|
- `final.md` → `final-formatted.md`
|
||||||
- `draft-v1.md` → `draft-v1-formatted.md`
|
- `draft-v1.md` → `draft-v1-formatted.md`
|
||||||
|
|
||||||
|
**If user chose "Keep original formatting" (from Step 1.5):**
|
||||||
|
- Copy original file to `{filename}-formatted.md` without modifications
|
||||||
|
- Proceed to Step 7 for typography fixes only
|
||||||
|
|
||||||
**Backup existing file:**
|
**Backup existing file:**
|
||||||
|
|
||||||
If `{filename}-formatted.md` already exists, backup before overwriting:
|
If `{filename}-formatted.md` already exists, backup before overwriting:
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ export const CJK_CHAR_RE = new RegExp(`[${CJK_SCRIPTS}]`, "u");
|
|||||||
const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u;
|
const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u;
|
||||||
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
|
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
|
||||||
|
|
||||||
|
const CJK_PUNCT_PAIRS: Record<string, string> = {
|
||||||
|
"“": "”",
|
||||||
|
"‘": "’",
|
||||||
|
"(": ")",
|
||||||
|
"〔": "〕",
|
||||||
|
"〖": "〗",
|
||||||
|
"〘": "〙",
|
||||||
|
"〚": "〛",
|
||||||
|
"「": "」",
|
||||||
|
"『": "』",
|
||||||
|
"〈": "〉",
|
||||||
|
"《": "》",
|
||||||
|
"【": "】",
|
||||||
|
};
|
||||||
|
|
||||||
function findInlineCodeRanges(text: string): Array<[number, number]> {
|
function findInlineCodeRanges(text: string): Array<[number, number]> {
|
||||||
const ranges: Array<[number, number]> = [];
|
const ranges: Array<[number, number]> = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -69,8 +84,120 @@ function isPunctuationOrSymbol(ch: string | undefined): boolean {
|
|||||||
return !!ch && PUNCT_OR_SYMBOL_RE.test(ch);
|
return !!ch && PUNCT_OR_SYMBOL_RE.test(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixCjkEmphasisSpacingInBlock(block: string): string {
|
function isMatchingCjkPunct(open: string, close: string): boolean {
|
||||||
|
return CJK_PUNCT_PAIRS[open] === close;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapNonCodeSegments(
|
||||||
|
block: string,
|
||||||
|
mapper: (segment: string) => string
|
||||||
|
): string {
|
||||||
const codeRanges = findInlineCodeRanges(block);
|
const codeRanges = findInlineCodeRanges(block);
|
||||||
|
if (codeRanges.length === 0) {
|
||||||
|
return mapper(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
let lastIndex = 0;
|
||||||
|
for (const [start, end] of codeRanges) {
|
||||||
|
if (start > lastIndex) {
|
||||||
|
result += mapper(block.slice(lastIndex, start));
|
||||||
|
}
|
||||||
|
result += block.slice(start, end + 1);
|
||||||
|
lastIndex = end + 1;
|
||||||
|
}
|
||||||
|
if (lastIndex < block.length) {
|
||||||
|
result += mapper(block.slice(lastIndex));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCjkPunctuationOutsideEmphasis(block: string): string {
|
||||||
|
return mapNonCodeSegments(block, (segment) => {
|
||||||
|
const delimiterPositions: number[] = [];
|
||||||
|
let cursor = 0;
|
||||||
|
while (cursor < segment.length - 1) {
|
||||||
|
if (
|
||||||
|
segment[cursor] === "*" &&
|
||||||
|
segment[cursor + 1] === "*" &&
|
||||||
|
segment[cursor - 1] !== "*" &&
|
||||||
|
segment[cursor + 2] !== "*" &&
|
||||||
|
!isEscaped(segment, cursor)
|
||||||
|
) {
|
||||||
|
delimiterPositions.push(cursor);
|
||||||
|
cursor += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delimiterPositions.length < 2) return segment;
|
||||||
|
|
||||||
|
const stack: number[] = [];
|
||||||
|
const pairs: Array<{ open: number; close: number }> = [];
|
||||||
|
for (const pos of delimiterPositions) {
|
||||||
|
if (stack.length === 0) {
|
||||||
|
stack.push(pos);
|
||||||
|
} else {
|
||||||
|
const open = stack.pop() as number;
|
||||||
|
pairs.push({ open, close: pos });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const skip = new Set<number>();
|
||||||
|
const insertBefore = new Map<number, string>();
|
||||||
|
|
||||||
|
for (const pair of pairs) {
|
||||||
|
const openPunctPos = pair.open + 2;
|
||||||
|
const closePunctPos = pair.close - 1;
|
||||||
|
if (openPunctPos >= closePunctPos) continue;
|
||||||
|
|
||||||
|
const openPunct = segment[openPunctPos];
|
||||||
|
const closePunct = segment[closePunctPos];
|
||||||
|
if (!openPunct || !closePunct) continue;
|
||||||
|
if (!CJK_OPENING_PUNCT_RE.test(openPunct)) continue;
|
||||||
|
if (!CJK_CLOSING_PUNCT_RE.test(closePunct)) continue;
|
||||||
|
if (!isMatchingCjkPunct(openPunct, closePunct)) continue;
|
||||||
|
if (openPunctPos + 1 >= closePunctPos) continue;
|
||||||
|
|
||||||
|
const inner = segment.slice(openPunctPos + 1, closePunctPos);
|
||||||
|
if (inner.length === 0) continue;
|
||||||
|
|
||||||
|
skip.add(openPunctPos);
|
||||||
|
skip.add(closePunctPos);
|
||||||
|
|
||||||
|
insertBefore.set(pair.open, (insertBefore.get(pair.open) ?? "") + openPunct);
|
||||||
|
const afterClose = pair.close + 2;
|
||||||
|
insertBefore.set(
|
||||||
|
afterClose,
|
||||||
|
(insertBefore.get(afterClose) ?? "") + closePunct
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip.size === 0) return segment;
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
for (let idx = 0; idx < segment.length; idx += 1) {
|
||||||
|
const insert = insertBefore.get(idx);
|
||||||
|
if (insert) {
|
||||||
|
result += insert;
|
||||||
|
}
|
||||||
|
if (skip.has(idx)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result += segment[idx];
|
||||||
|
}
|
||||||
|
const tailInsert = insertBefore.get(segment.length);
|
||||||
|
if (tailInsert) {
|
||||||
|
result += tailInsert;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixCjkEmphasisSpacingInBlock(block: string): string {
|
||||||
|
const normalized = moveCjkPunctuationOutsideEmphasis(block);
|
||||||
|
const codeRanges = findInlineCodeRanges(normalized);
|
||||||
let rangeIndex = 0;
|
let rangeIndex = 0;
|
||||||
|
|
||||||
const delimiters: Array<{
|
const delimiters: Array<{
|
||||||
@@ -79,7 +206,7 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
|
|||||||
canClose: boolean;
|
canClose: boolean;
|
||||||
}> = [];
|
}> = [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
while (cursor < block.length - 1) {
|
while (cursor < normalized.length - 1) {
|
||||||
if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) {
|
if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) {
|
||||||
if (cursor <= codeRanges[rangeIndex][1]) {
|
if (cursor <= codeRanges[rangeIndex][1]) {
|
||||||
cursor = codeRanges[rangeIndex][1] + 1;
|
cursor = codeRanges[rangeIndex][1] + 1;
|
||||||
@@ -90,14 +217,14 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
block[cursor] === "*" &&
|
normalized[cursor] === "*" &&
|
||||||
block[cursor + 1] === "*" &&
|
normalized[cursor + 1] === "*" &&
|
||||||
block[cursor - 1] !== "*" &&
|
normalized[cursor - 1] !== "*" &&
|
||||||
block[cursor + 2] !== "*" &&
|
normalized[cursor + 2] !== "*" &&
|
||||||
!isEscaped(block, cursor)
|
!isEscaped(normalized, cursor)
|
||||||
) {
|
) {
|
||||||
const before = block[cursor - 1];
|
const before = normalized[cursor - 1];
|
||||||
const after = block[cursor + 2];
|
const after = normalized[cursor + 2];
|
||||||
const beforeIsSpace = isWhitespaceChar(before);
|
const beforeIsSpace = isWhitespaceChar(before);
|
||||||
const afterIsSpace = isWhitespaceChar(after);
|
const afterIsSpace = isWhitespaceChar(after);
|
||||||
const beforeIsPunct = isPunctuationOrSymbol(before);
|
const beforeIsPunct = isPunctuationOrSymbol(before);
|
||||||
@@ -140,12 +267,12 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pairs.length === 0) return block;
|
if (pairs.length === 0) return normalized;
|
||||||
|
|
||||||
const insertPositions = new Set<number>();
|
const insertPositions = new Set<number>();
|
||||||
for (const pair of pairs) {
|
for (const pair of pairs) {
|
||||||
const insideLast = block[pair.close - 1];
|
const insideLast = normalized[pair.close - 1];
|
||||||
const afterClose = block[pair.close + 2];
|
const afterClose = normalized[pair.close + 2];
|
||||||
if (!afterClose) continue;
|
if (!afterClose) continue;
|
||||||
if (
|
if (
|
||||||
CJK_CLOSING_PUNCT_RE.test(insideLast) &&
|
CJK_CLOSING_PUNCT_RE.test(insideLast) &&
|
||||||
@@ -155,16 +282,16 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (insertPositions.size === 0) return block;
|
if (insertPositions.size === 0) return normalized;
|
||||||
|
|
||||||
let result = "";
|
let result = "";
|
||||||
for (let idx = 0; idx < block.length; idx += 1) {
|
for (let idx = 0; idx < normalized.length; idx += 1) {
|
||||||
if (insertPositions.has(idx)) {
|
if (insertPositions.has(idx)) {
|
||||||
result += " ";
|
result += " ";
|
||||||
}
|
}
|
||||||
result += block[idx];
|
result += normalized[idx];
|
||||||
}
|
}
|
||||||
if (insertPositions.has(block.length)) {
|
if (insertPositions.has(normalized.length)) {
|
||||||
result += " ";
|
result += " ";
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -153,10 +153,12 @@ Schema: `references/config/preferences-schema.md`
|
|||||||
**1.2 Analyze Content → `analysis.md`**
|
**1.2 Analyze Content → `analysis.md`**
|
||||||
|
|
||||||
1. Save source content (file path or paste → `source.md`)
|
1. Save source content (file path or paste → `source.md`)
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Analyze: topic, data type, complexity, tone, audience
|
2. Analyze: topic, data type, complexity, tone, audience
|
||||||
3. Detect source language and user language
|
3. Detect source language and user language
|
||||||
4. Extract design instructions from user input
|
4. Extract design instructions from user input
|
||||||
5. Save analysis
|
5. Save analysis
|
||||||
|
- **Backup rule**: If `analysis.md` exists, rename to `analysis-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
See `references/analysis-framework.md` for detailed format.
|
See `references/analysis-framework.md` for detailed format.
|
||||||
|
|
||||||
@@ -189,6 +191,8 @@ Present all options in single confirmation:
|
|||||||
|
|
||||||
### Step 5: Generate Prompt → `prompts/infographic.md`
|
### Step 5: Generate Prompt → `prompts/infographic.md`
|
||||||
|
|
||||||
|
**Backup rule**: If `prompts/infographic.md` exists, rename to `prompts/infographic-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
Combine:
|
Combine:
|
||||||
1. Layout definition from `references/layouts/<layout>.md`
|
1. Layout definition from `references/layouts/<layout>.md`
|
||||||
2. Style definition from `references/styles/<style>.md`
|
2. Style definition from `references/styles/<style>.md`
|
||||||
@@ -199,8 +203,10 @@ Combine:
|
|||||||
### Step 6: Generate Image
|
### Step 6: Generate Image
|
||||||
|
|
||||||
1. Select available image generation skill (ask user if multiple)
|
1. Select available image generation skill (ask user if multiple)
|
||||||
2. Call with prompt file and output path
|
2. **Check for existing file**: Before generating, check if `infographic.png` exists
|
||||||
3. On failure, auto-retry once
|
- If exists: Rename to `infographic-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
3. Call with prompt file and output path
|
||||||
|
4. On failure, auto-retry once
|
||||||
|
|
||||||
### Step 7: Output Summary
|
### Step 7: Output Summary
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,6 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.copyFileSync(tempHtmlPath, finalHtmlPath);
|
fs.copyFileSync(tempHtmlPath, finalHtmlPath);
|
||||||
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
|
|
||||||
|
|
||||||
const contentImages: ImageInfo[] = [];
|
const contentImages: ImageInfo[] = [];
|
||||||
for (const img of images) {
|
for (const img of images) {
|
||||||
@@ -226,6 +225,15 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let htmlContent = fs.readFileSync(finalHtmlPath, 'utf-8');
|
||||||
|
for (const img of contentImages) {
|
||||||
|
const imgTag = `<img src="${img.placeholder}" data-local-path="${img.localPath}" style="display: block; width: 100%; margin: 1.5em auto;">`;
|
||||||
|
htmlContent = htmlContent.replace(img.placeholder, imgTag);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(finalHtmlPath, htmlContent, 'utf-8');
|
||||||
|
|
||||||
|
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
author,
|
author,
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ async function uploadImagesInHtml(
|
|||||||
accessToken: string,
|
accessToken: string,
|
||||||
baseDir: string
|
baseDir: string
|
||||||
): Promise<{ html: string; firstMediaId: string }> {
|
): Promise<{ html: string; firstMediaId: string }> {
|
||||||
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
||||||
const matches = [...html.matchAll(imgRegex)];
|
const matches = [...html.matchAll(imgRegex)];
|
||||||
|
|
||||||
if (matches.length === 0) {
|
if (matches.length === 0) {
|
||||||
@@ -188,7 +188,7 @@ async function uploadImagesInHtml(
|
|||||||
let updatedHtml = html;
|
let updatedHtml = html;
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const [, src] = match;
|
const [fullTag, src] = match;
|
||||||
if (!src) continue;
|
if (!src) continue;
|
||||||
|
|
||||||
if (src.startsWith("https://mmbiz.qpic.cn")) {
|
if (src.startsWith("https://mmbiz.qpic.cn")) {
|
||||||
@@ -198,15 +198,21 @@ async function uploadImagesInHtml(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`[wechat-api] Uploading image: ${src}`);
|
const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
|
||||||
|
const imagePath = localPathMatch ? localPathMatch[1]! : src;
|
||||||
|
|
||||||
|
console.error(`[wechat-api] Uploading image: ${imagePath}`);
|
||||||
try {
|
try {
|
||||||
const resp = await uploadImage(src, accessToken, baseDir);
|
const resp = await uploadImage(imagePath, accessToken, baseDir);
|
||||||
updatedHtml = updatedHtml.replace(src, resp.url);
|
const newTag = fullTag
|
||||||
|
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
||||||
|
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
||||||
|
updatedHtml = updatedHtml.replace(fullTag, newTag);
|
||||||
if (!firstMediaId) {
|
if (!firstMediaId) {
|
||||||
firstMediaId = resp.media_id;
|
firstMediaId = resp.media_id;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[wechat-api] Failed to upload ${src}:`, err);
|
console.error(`[wechat-api] Failed to upload ${imagePath}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +316,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--title <title> Override title
|
--title <title> Override title
|
||||||
|
--summary <text> Article summary (for future use)
|
||||||
--theme <name> Theme name for markdown (default, grace, simple). Default: default
|
--theme <name> Theme name for markdown (default, grace, simple). Default: default
|
||||||
--cover <path> Cover image path (local or URL)
|
--cover <path> Cover image path (local or URL)
|
||||||
--dry-run Parse and render only, don't publish
|
--dry-run Parse and render only, don't publish
|
||||||
@@ -337,6 +344,7 @@ interface CliArgs {
|
|||||||
filePath: string;
|
filePath: string;
|
||||||
isHtml: boolean;
|
isHtml: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
summary?: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
cover?: string;
|
cover?: string;
|
||||||
dryRun: boolean;
|
dryRun: boolean;
|
||||||
@@ -358,12 +366,16 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
const arg = argv[i]!;
|
const arg = argv[i]!;
|
||||||
if (arg === "--title" && argv[i + 1]) {
|
if (arg === "--title" && argv[i + 1]) {
|
||||||
args.title = argv[++i];
|
args.title = argv[++i];
|
||||||
|
} else if (arg === "--summary" && argv[i + 1]) {
|
||||||
|
args.summary = argv[++i];
|
||||||
} else if (arg === "--theme" && argv[i + 1]) {
|
} else if (arg === "--theme" && argv[i + 1]) {
|
||||||
args.theme = argv[++i]!;
|
args.theme = argv[++i]!;
|
||||||
} else if (arg === "--cover" && argv[i + 1]) {
|
} else if (arg === "--cover" && argv[i + 1]) {
|
||||||
args.cover = argv[++i];
|
args.cover = argv[++i];
|
||||||
} else if (arg === "--dry-run") {
|
} else if (arg === "--dry-run") {
|
||||||
args.dryRun = true;
|
args.dryRun = true;
|
||||||
|
} else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) {
|
||||||
|
i++;
|
||||||
} else if (!arg.startsWith("-")) {
|
} else if (!arg.startsWith("-")) {
|
||||||
args.filePath = arg;
|
args.filePath = arg;
|
||||||
}
|
}
|
||||||
@@ -405,6 +417,15 @@ async function main(): Promise<void> {
|
|||||||
if (args.isHtml) {
|
if (args.isHtml) {
|
||||||
htmlPath = filePath;
|
htmlPath = filePath;
|
||||||
htmlContent = extractHtmlContent(htmlPath);
|
htmlContent = extractHtmlContent(htmlPath);
|
||||||
|
const mdPath = filePath.replace(/\.html$/i, ".md");
|
||||||
|
if (fs.existsSync(mdPath)) {
|
||||||
|
const mdContent = fs.readFileSync(mdPath, "utf-8");
|
||||||
|
const parsed = parseFrontmatter(mdContent);
|
||||||
|
frontmatter = parsed.frontmatter;
|
||||||
|
if (!title && frontmatter.title) {
|
||||||
|
title = frontmatter.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!title) {
|
if (!title) {
|
||||||
title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8"));
|
title = extractHtmlTitle(fs.readFileSync(htmlPath, "utf-8"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ async function sendPaste(cdp?: CdpConnection, sessionId?: string): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Promise<void> {
|
async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string, contentImages: ImageInfo[] = []): Promise<void> {
|
||||||
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
|
const absolutePath = path.isAbsolute(htmlFilePath) ? htmlFilePath : path.resolve(process.cwd(), htmlFilePath);
|
||||||
const fileUrl = `file://${absolutePath}`;
|
const fileUrl = `file://${absolutePath}`;
|
||||||
|
|
||||||
@@ -128,6 +128,28 @@ async function copyHtmlFromBrowser(cdp: CdpConnection, htmlFilePath: string): Pr
|
|||||||
await cdp.send('Runtime.enable', {}, { sessionId });
|
await cdp.send('Runtime.enable', {}, { sessionId });
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
|
||||||
|
if (contentImages.length > 0) {
|
||||||
|
console.log('[wechat] Replacing img tags with placeholders for browser paste...');
|
||||||
|
const replacements = contentImages.map(img => ({ placeholder: img.placeholder, localPath: img.localPath }));
|
||||||
|
await cdp.send<{ result: { value: unknown } }>('Runtime.evaluate', {
|
||||||
|
expression: `
|
||||||
|
(function() {
|
||||||
|
const replacements = ${JSON.stringify(replacements)};
|
||||||
|
for (const r of replacements) {
|
||||||
|
const imgs = document.querySelectorAll('img[src="' + r.placeholder + '"], img[data-local-path="' + r.localPath + '"]');
|
||||||
|
for (const img of imgs) {
|
||||||
|
const text = document.createTextNode(r.placeholder);
|
||||||
|
img.parentNode.replaceChild(text, img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})()
|
||||||
|
`,
|
||||||
|
returnByValue: true,
|
||||||
|
}, { sessionId });
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[wechat] Selecting #output content...');
|
console.log('[wechat] Selecting #output content...');
|
||||||
await cdp.send<{ result: { value: unknown } }>('Runtime.evaluate', {
|
await cdp.send<{ result: { value: unknown } }>('Runtime.evaluate', {
|
||||||
expression: `
|
expression: `
|
||||||
@@ -176,7 +198,7 @@ async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: strin
|
|||||||
return JSON.parse(output);
|
return JSON.parse(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHtmlMeta(htmlPath: string): { title: string; author: string; summary: string } {
|
function parseHtmlMeta(htmlPath: string): { title: string; author: string; summary: string; contentImages: ImageInfo[] } {
|
||||||
const content = fs.readFileSync(htmlPath, 'utf-8');
|
const content = fs.readFileSync(htmlPath, 'utf-8');
|
||||||
|
|
||||||
let title = '';
|
let title = '';
|
||||||
@@ -203,7 +225,45 @@ function parseHtmlMeta(htmlPath: string): { title: string; author: string; summa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { title, author, summary };
|
const mdPath = htmlPath.replace(/\.html$/i, '.md');
|
||||||
|
if (fs.existsSync(mdPath)) {
|
||||||
|
const mdContent = fs.readFileSync(mdPath, 'utf-8');
|
||||||
|
const fmMatch = mdContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
|
if (fmMatch) {
|
||||||
|
const lines = fmMatch[1]!.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const colonIdx = line.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const key = line.slice(0, colonIdx).trim();
|
||||||
|
let value = line.slice(colonIdx + 1).trim();
|
||||||
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
if (key === 'title' && !title) title = value;
|
||||||
|
if (key === 'author' && !author) author = value;
|
||||||
|
if ((key === 'description' || key === 'summary') && !summary) summary = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentImages: ImageInfo[] = [];
|
||||||
|
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
||||||
|
const matches = [...content.matchAll(imgRegex)];
|
||||||
|
for (const match of matches) {
|
||||||
|
const [fullTag, src] = match;
|
||||||
|
if (!src || src.startsWith('http')) continue;
|
||||||
|
const localPathMatch = fullTag.match(/data-local-path=["']([^"']+)["']/);
|
||||||
|
if (localPathMatch) {
|
||||||
|
contentImages.push({
|
||||||
|
placeholder: src,
|
||||||
|
localPath: localPathMatch[1]!,
|
||||||
|
originalPath: src,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { title, author, summary, contentImages };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectAndReplacePlaceholder(session: ChromeSession, placeholder: string): Promise<boolean> {
|
async function selectAndReplacePlaceholder(session: ChromeSession, placeholder: string): Promise<boolean> {
|
||||||
@@ -273,9 +333,13 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
effectiveAuthor = effectiveAuthor || meta.author;
|
effectiveAuthor = effectiveAuthor || meta.author;
|
||||||
effectiveSummary = effectiveSummary || meta.summary;
|
effectiveSummary = effectiveSummary || meta.summary;
|
||||||
effectiveHtmlFile = htmlFile;
|
effectiveHtmlFile = htmlFile;
|
||||||
|
if (meta.contentImages.length > 0) {
|
||||||
|
contentImages = meta.contentImages;
|
||||||
|
}
|
||||||
console.log(`[wechat] Title: ${effectiveTitle || '(empty)'}`);
|
console.log(`[wechat] Title: ${effectiveTitle || '(empty)'}`);
|
||||||
console.log(`[wechat] Author: ${effectiveAuthor || '(empty)'}`);
|
console.log(`[wechat] Author: ${effectiveAuthor || '(empty)'}`);
|
||||||
console.log(`[wechat] Summary: ${effectiveSummary || '(empty)'}`);
|
console.log(`[wechat] Summary: ${effectiveSummary || '(empty)'}`);
|
||||||
|
console.log(`[wechat] Found ${contentImages.length} images to insert`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveTitle && effectiveTitle.length > 64) throw new Error(`Title too long: ${effectiveTitle.length} chars (max 64)`);
|
if (effectiveTitle && effectiveTitle.length > 64) throw new Error(`Title too long: ${effectiveTitle.length} chars (max 64)`);
|
||||||
@@ -416,7 +480,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
|
if (effectiveHtmlFile && fs.existsSync(effectiveHtmlFile)) {
|
||||||
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
|
console.log(`[wechat] Copying HTML content from: ${effectiveHtmlFile}`);
|
||||||
await copyHtmlFromBrowser(cdp, effectiveHtmlFile);
|
await copyHtmlFromBrowser(cdp, effectiveHtmlFile, contentImages);
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
console.log('[wechat] Pasting into editor...');
|
console.log('[wechat] Pasting into editor...');
|
||||||
await pasteFromClipboardInEditor(session);
|
await pasteFromClipboardInEditor(session);
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ Schema: `references/config/preferences-schema.md`
|
|||||||
**1.2 Analyze Content**
|
**1.2 Analyze Content**
|
||||||
|
|
||||||
1. Save source content (if pasted, save as `source.md`)
|
1. Save source content (if pasted, save as `source.md`)
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Follow `references/analysis-framework.md` for content analysis
|
2. Follow `references/analysis-framework.md` for content analysis
|
||||||
3. Analyze content signals for style recommendations
|
3. Analyze content signals for style recommendations
|
||||||
4. Detect source language
|
4. Detect source language
|
||||||
@@ -483,6 +484,7 @@ options:
|
|||||||
- Add slide-specific content
|
- Add slide-specific content
|
||||||
- If `Layout:` specified, include layout guidance from `references/layouts.md`
|
- If `Layout:` specified, include layout guidance from `references/layouts.md`
|
||||||
3. Save to `prompts/` directory
|
3. Save to `prompts/` directory
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.md`
|
||||||
|
|
||||||
**After generation**:
|
**After generation**:
|
||||||
- If `--prompts-only`, stop here and output prompt summary
|
- If `--prompts-only`, stop here and output prompt summary
|
||||||
@@ -539,7 +541,9 @@ options:
|
|||||||
**Standard flow**:
|
**Standard flow**:
|
||||||
1. Select available image generation skill
|
1. Select available image generation skill
|
||||||
2. Generate session ID: `slides-{topic-slug}-{timestamp}`
|
2. Generate session ID: `slides-{topic-slug}-{timestamp}`
|
||||||
3. Generate each slide sequentially with same session ID
|
3. For each slide:
|
||||||
|
- **Backup rule**: If image file exists, rename to `NN-slide-{slug}-backup-YYYYMMDD-HHMMSS.png`
|
||||||
|
- Generate image sequentially with same session ID
|
||||||
4. Report progress: "Generated X/N" (in user's language)
|
4. Report progress: "Generated X/N" (in user's language)
|
||||||
5. Auto-retry once on failure before reporting error
|
5. Auto-retry once on failure before reporting error
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: baoyu-xhs-images
|
name: baoyu-xhs-images
|
||||||
description: Generates Xiaohongshu (Little Red Book) infographic series with 9 visual styles and 6 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
|
description: Generates Xiaohongshu (Little Red Book) infographic series with 10 visual styles and 8 layouts. Breaks content into 1-10 cartoon-style images optimized for XHS engagement. Use when user mentions "小红书图片", "XHS images", "RedNote infographics", "小红书种草", or wants social media infographics for Chinese platforms.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Xiaohongshu Infographic Series Generator
|
# Xiaohongshu Infographic Series Generator
|
||||||
@@ -42,8 +42,8 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
|
|||||||
|
|
||||||
| Dimension | Controls | Options |
|
| Dimension | Controls | Options |
|
||||||
|-----------|----------|---------|
|
|-----------|----------|---------|
|
||||||
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard |
|
| **Style** | Visual aesthetics: colors, lines, decorations | cute, fresh, warm, bold, minimal, retro, pop, notion, chalkboard, study-notes |
|
||||||
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow |
|
| **Layout** | Information structure: density, arrangement | sparse, balanced, dense, list, comparison, flow, mindmap, quadrant |
|
||||||
|
|
||||||
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
|
Style × Layout can be freely combined. Example: `--style notion --layout dense` creates an intellectual-looking knowledge card with high information density.
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ Style × Layout can be freely combined. Example: `--style notion --layout dense`
|
|||||||
| `pop` | Vibrant, energetic, eye-catching |
|
| `pop` | Vibrant, energetic, eye-catching |
|
||||||
| `notion` | Minimalist hand-drawn line art, intellectual |
|
| `notion` | Minimalist hand-drawn line art, intellectual |
|
||||||
| `chalkboard` | Colorful chalk on black board, educational |
|
| `chalkboard` | Colorful chalk on black board, educational |
|
||||||
|
| `study-notes` | Realistic handwritten photo style, blue pen + red annotations + yellow highlighter |
|
||||||
|
|
||||||
Detailed style definitions: `references/presets/<style>.md`
|
Detailed style definitions: `references/presets/<style>.md`
|
||||||
|
|
||||||
@@ -73,6 +74,8 @@ Detailed style definitions: `references/presets/<style>.md`
|
|||||||
| `list` | Enumeration and ranking format (4-7 items) |
|
| `list` | Enumeration and ranking format (4-7 items) |
|
||||||
| `comparison` | Side-by-side contrast layout |
|
| `comparison` | Side-by-side contrast layout |
|
||||||
| `flow` | Process and timeline layout (3-6 steps) |
|
| `flow` | Process and timeline layout (3-6 steps) |
|
||||||
|
| `mindmap` | Center radial mind map layout (4-8 branches) |
|
||||||
|
| `quadrant` | Four-quadrant / circular section layout |
|
||||||
|
|
||||||
Detailed layout definitions: `references/elements/canvas.md`
|
Detailed layout definitions: `references/elements/canvas.md`
|
||||||
|
|
||||||
@@ -89,6 +92,7 @@ Detailed layout definitions: `references/elements/canvas.md`
|
|||||||
| Fun, exciting, wow, amazing | `pop` | sparse/list |
|
| Fun, exciting, wow, amazing | `pop` | sparse/list |
|
||||||
| Knowledge, concept, productivity, SaaS | `notion` | dense/list |
|
| Knowledge, concept, productivity, SaaS | `notion` | dense/list |
|
||||||
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
|
| Education, tutorial, learning, teaching, classroom | `chalkboard` | balanced/dense |
|
||||||
|
| Notes, handwritten, study guide, knowledge, realistic, photo | `study-notes` | dense/list/mindmap |
|
||||||
|
|
||||||
## Outline Strategies
|
## Outline Strategies
|
||||||
|
|
||||||
@@ -227,6 +231,7 @@ Read source content, save it if needed, and perform deep analysis.
|
|||||||
1. **Save source content** (if not already a file):
|
1. **Save source content** (if not already a file):
|
||||||
- If user provides a file path: use as-is
|
- If user provides a file path: use as-is
|
||||||
- If user pastes content: save to `source.md` in target directory
|
- If user pastes content: save to `source.md` in target directory
|
||||||
|
- **Backup rule**: If `source.md` exists, rename to `source-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Read source content
|
2. Read source content
|
||||||
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
|
3. **Deep analysis** following `references/workflows/analysis-framework.md`:
|
||||||
- Content type classification (种草/干货/测评/教程/避坑...)
|
- Content type classification (种草/干货/测评/教程/避坑...)
|
||||||
@@ -353,7 +358,9 @@ With confirmed outline + style + layout:
|
|||||||
|
|
||||||
**For each image (cover + content + ending)**:
|
**For each image (cover + content + ending)**:
|
||||||
1. Save prompt to `prompts/NN-{type}-[slug].md` (in user's preferred language)
|
1. Save prompt to `prompts/NN-{type}-[slug].md` (in user's preferred language)
|
||||||
|
- **Backup rule**: If prompt file exists, rename to `prompts/NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.md`
|
||||||
2. Generate image using confirmed style and layout
|
2. Generate image using confirmed style and layout
|
||||||
|
- **Backup rule**: If image file exists, rename to `NN-{type}-[slug]-backup-YYYYMMDD-HHMMSS.png`
|
||||||
3. Report progress after each generation
|
3. Report progress after each generation
|
||||||
|
|
||||||
**Watermark Application** (if enabled in preferences):
|
**Watermark Application** (if enabled in preferences):
|
||||||
@@ -417,17 +424,18 @@ Files:
|
|||||||
|
|
||||||
**Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well):
|
**Style × Layout Matrix** (✓✓ = highly recommended, ✓ = works well):
|
||||||
|
|
||||||
| | sparse | balanced | dense | list | comparison | flow |
|
| | sparse | balanced | dense | list | comparison | flow | mindmap | quadrant |
|
||||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ |
|
| cute | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ |
|
| fresh | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ |
|
||||||
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
|
| warm | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ |
|
||||||
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
| bold | ✓✓ | ✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ |
|
||||||
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
|
| minimal | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ |
|
| retro | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
| pop | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓ |
|
||||||
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
|
| notion | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓✓ |
|
||||||
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ |
|
| chalkboard | ✓✓ | ✓✓ | ✓✓ | ✓✓ | ✓ | ✓✓ | ✓✓ | ✓ |
|
||||||
|
| study-notes | ✗ | ✓ | ✓✓ | ✓✓ | ✓ | ✓ | ✓✓ | ✓ |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ Avoid placing critical content in these areas:
|
|||||||
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
|
| list | Vertical enumeration | 4-7 | Rankings, checklists, step guides |
|
||||||
| comparison | Left vs Right | 2 sections | Before/after, pros/cons |
|
| comparison | Left vs Right | 2 sections | Before/after, pros/cons |
|
||||||
| flow | Connected nodes | 3-6 steps | Processes, timelines, workflows |
|
| flow | Connected nodes | 3-6 steps | Processes, timelines, workflows |
|
||||||
|
| mindmap | Center radial | 4-8 branches | Concept maps, brainstorming, topic overview |
|
||||||
|
| quadrant | 4-section grid | 4 sections | SWOT analysis, priority matrix, classification |
|
||||||
|
|
||||||
## Layout by Position
|
## Layout by Position
|
||||||
|
|
||||||
@@ -106,3 +108,15 @@ For multi-element compositions:
|
|||||||
- Directional flow (top→bottom or left→right)
|
- Directional flow (top→bottom or left→right)
|
||||||
- Connected nodes with arrows
|
- Connected nodes with arrows
|
||||||
- Clear progression indicators
|
- Clear progression indicators
|
||||||
|
|
||||||
|
### Mindmap Layout
|
||||||
|
- Central topic node
|
||||||
|
- Radial branches outward
|
||||||
|
- Hierarchical sub-branches
|
||||||
|
- Organic curved connections
|
||||||
|
|
||||||
|
### Quadrant Layout
|
||||||
|
- 4-section grid (2×2)
|
||||||
|
- Clear axis labels
|
||||||
|
- Each quadrant with distinct content
|
||||||
|
- Optional circular variant for cycles
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
name: study-notes
|
||||||
|
category: realistic
|
||||||
|
---
|
||||||
|
|
||||||
|
# Study Notes Style
|
||||||
|
|
||||||
|
Realistic handwritten photo aesthetic - student notes style, dense and messy but readable.
|
||||||
|
|
||||||
|
## Element Combination
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
canvas:
|
||||||
|
ratio: portrait-3-4
|
||||||
|
grid: single
|
||||||
|
|
||||||
|
image_effects:
|
||||||
|
cutout: none
|
||||||
|
stroke: none
|
||||||
|
filter: natural-photo
|
||||||
|
|
||||||
|
typography:
|
||||||
|
decorated: none
|
||||||
|
tags: none
|
||||||
|
direction: horizontal
|
||||||
|
|
||||||
|
decorations:
|
||||||
|
emphasis: circle-mark | underline | checkmark | cross | star-simple
|
||||||
|
background: lined-paper-white
|
||||||
|
doodles: arrows-simple | margin-notes | corrections | explanatory-diagrams
|
||||||
|
frames: none
|
||||||
|
```
|
||||||
|
|
||||||
|
## Color Palette (Three-Color Annotation System)
|
||||||
|
|
||||||
|
| Role | Colors | Hex |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Primary | Blue ballpoint, Black ink | #1E3A5F, #1A1A1A |
|
||||||
|
| Highlights | Yellow highlighter | #FFFF00 (50% opacity) |
|
||||||
|
| Accents | Red pen (circles, underlines) | #CC0000 |
|
||||||
|
| Background | White lined paper | #FFFFFF |
|
||||||
|
|
||||||
|
## Visual Elements
|
||||||
|
|
||||||
|
- Realistic photo perspective: top-down view of study desk
|
||||||
|
- Hand holding blue ballpoint pen, actively underlining
|
||||||
|
- Extremely dense handwritten content, filling entire page
|
||||||
|
- Red pen annotations: circles, underlines, stars, boxes
|
||||||
|
- Yellow highlighter marking key terms
|
||||||
|
- Correction marks, cramped notes squeezed into margins
|
||||||
|
- Simple hand-drawn symbols: → * ✓ ✗ !
|
||||||
|
- Varying pen pressure creating lighter and darker strokes
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
- Authentic student handwriting
|
||||||
|
- Messy but readable, clear structure maintained
|
||||||
|
- Varying font sizes (large titles, small body, tiny margin notes)
|
||||||
|
- CJK optimized
|
||||||
|
|
||||||
|
## Content Structure
|
||||||
|
|
||||||
|
Three-section layout:
|
||||||
|
|
||||||
|
### Top Section
|
||||||
|
- Core topic (circled multiple times in red)
|
||||||
|
- First section title + 3-4 key points
|
||||||
|
- Arrow connections, red underlines
|
||||||
|
|
||||||
|
### Middle Section
|
||||||
|
- Second section title (red pen box)
|
||||||
|
- Numbered steps ①②③
|
||||||
|
- Specific methods and supplementary notes
|
||||||
|
|
||||||
|
### Bottom Section
|
||||||
|
- Third section title (red star)
|
||||||
|
- Time points / key metrics
|
||||||
|
- Key quotes / core tips (tiny corner notes)
|
||||||
|
|
||||||
|
## Best Layout Pairings
|
||||||
|
|
||||||
|
| Layout | Compatibility | Use Case |
|
||||||
|
|--------|---------------|----------|
|
||||||
|
| sparse | ✗ | Not suitable - style requires dense content |
|
||||||
|
| balanced | ✓ | When content is lighter |
|
||||||
|
| dense | ✓✓ | Best fit - knowledge notes, summaries |
|
||||||
|
| list | ✓✓ | Step checklists, rankings |
|
||||||
|
| comparison | ✓ | Comparative analysis |
|
||||||
|
| flow | ✓ | Process flows |
|
||||||
|
| mindmap | ✓✓ | Mind map notes |
|
||||||
|
| quadrant | ✓ | Quadrant analysis |
|
||||||
|
|
||||||
|
## Best For
|
||||||
|
|
||||||
|
- Study guides, exam notes
|
||||||
|
- Knowledge organization, framework summaries
|
||||||
|
- Tutorial summaries, quick notes
|
||||||
|
- "Top student notes" style content
|
||||||
|
- Knowledge sharing requiring authentic feel
|
||||||
|
|
||||||
|
## Style Rules
|
||||||
|
|
||||||
|
### DO ✓
|
||||||
|
- Keep content extremely dense
|
||||||
|
- Use simple symbols (→ * ✓ ✗ !)
|
||||||
|
- Annotate key points with red pen
|
||||||
|
- Include correction marks
|
||||||
|
- Squeeze tiny notes into margins
|
||||||
|
|
||||||
|
### DON'T ✗
|
||||||
|
- Use complex emojis
|
||||||
|
- Leave too much whitespace
|
||||||
|
- Make neat, tidy layouts
|
||||||
|
- Add colorful decorations
|
||||||
|
- Include cartoon elements
|
||||||
Reference in New Issue
Block a user