mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 05:51:44 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdd9ee042d | |||
| c742bfa1af | |||
| 0faea4ecaa | |||
| 7c9ec259a1 | |||
| 24a17709a8 | |||
| a3849af0cf | |||
| cc95e6fe05 | |||
| e7d9ed7917 | |||
| 876c6332f6 | |||
| 502d2448b2 | |||
| b9e48d9483 |
@@ -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.28.2"
|
"version": "1.29.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.29.0 - 2026-02-06
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: add EXTEND.md configuration support, including schema documentation and runtime preference loading in scripts (by @kingdomad).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix duplicated title and ordered-list numbering in WeChat article publishing (by @NantesCheval).
|
||||||
|
- `baoyu-url-to-markdown`: replace regex-only conversion with multi-strategy content extraction and Turndown conversion; improve noise filtering for Substack-style pages.
|
||||||
|
|
||||||
|
## 1.28.4 - 2026-02-03
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-markdown-to-html`: add author and description meta tags to generated HTML from YAML frontmatter; strip quotes from frontmatter values (supports both English and Chinese quotation marks).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: remove extra empty lines after image paste; fix summary field timing to fill after content paste (prevents being overwritten).
|
||||||
|
|
||||||
|
## 1.28.3 - 2026-02-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix placeholder matching issue where `WECHATIMGPH_1` incorrectly matched `WECHATIMGPH_10`.
|
||||||
|
|
||||||
## 1.28.2 - 2026-02-03
|
## 1.28.2 - 2026-02-03
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.29.0 - 2026-02-06
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:新增 EXTEND.md 配置支持,补充配置 schema 文档并在脚本运行时读取偏好设置 (by @kingdomad)。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复公众号文章发布时标题和有序列表编号重复问题 (by @NantesCheval)。
|
||||||
|
- `baoyu-url-to-markdown`:将正则转换升级为多策略正文抽取 + Turndown 转换,提升 Substack 类页面的噪声过滤能力。
|
||||||
|
|
||||||
|
## 1.28.4 - 2026-02-03
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-markdown-to-html`:从 YAML frontmatter 生成 author 和 description meta 标签;自动去除 frontmatter 值两端的引号(支持中英文引号)。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:移除图片粘贴后产生的多余空行;修复摘要填充时机,改为内容粘贴后填写(避免被覆盖)。
|
||||||
|
|
||||||
|
## 1.28.3 - 2026-02-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复占位符匹配问题(`WECHATIMGPH_1` 错误匹配 `WECHATIMGPH_10`)。
|
||||||
|
|
||||||
## 1.28.2 - 2026-02-03
|
## 1.28.2 - 2026-02-03
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ test -f "$HOME/.baoyu-skills/baoyu-image-gen/EXTEND.md" && echo "user"
|
|||||||
│ Not found │ Use defaults │
|
│ Not found │ Use defaults │
|
||||||
└───────────┴───────────────────────────────────────────────────────────────────────────┘
|
└───────────┴───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio
|
**EXTEND.md Supports**: Default provider | Default quality | Default aspect ratio | Default image size | Default models
|
||||||
|
|
||||||
|
Schema: `references/config/preferences-schema.md`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -99,7 +101,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "一只可爱的猫" --image ou
|
|||||||
| `GOOGLE_BASE_URL` | Custom Google endpoint |
|
| `GOOGLE_BASE_URL` | Custom Google endpoint |
|
||||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
|
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint |
|
||||||
|
|
||||||
**Load Priority**: CLI args > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
|
**Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
|
||||||
|
|
||||||
## Provider Selection
|
## Provider Selection
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: preferences-schema
|
||||||
|
description: EXTEND.md YAML schema for baoyu-image-gen user preferences
|
||||||
|
---
|
||||||
|
|
||||||
|
# Preferences Schema
|
||||||
|
|
||||||
|
## Full Schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
default_provider: null # google|openai|dashscope|null (null = auto-detect)
|
||||||
|
|
||||||
|
default_quality: null # normal|2k|null (null = use default: 2k)
|
||||||
|
|
||||||
|
default_aspect_ratio: null # "16:9"|"1:1"|"4:3"|"3:4"|"2.35:1"|null
|
||||||
|
|
||||||
|
default_image_size: null # 1K|2K|4K|null (Google only, overrides quality)
|
||||||
|
|
||||||
|
default_model:
|
||||||
|
google: null # e.g., "gemini-3-pro-image-preview"
|
||||||
|
openai: null # e.g., "gpt-image-1.5"
|
||||||
|
dashscope: null # e.g., "z-image-turbo"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Reference
|
||||||
|
|
||||||
|
| Field | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| `version` | int | 1 | Schema version |
|
||||||
|
| `default_provider` | string\|null | null | Default provider (null = auto-detect) |
|
||||||
|
| `default_quality` | string\|null | null | Default quality (null = 2k) |
|
||||||
|
| `default_aspect_ratio` | string\|null | null | Default aspect ratio |
|
||||||
|
| `default_image_size` | string\|null | null | Google image size (overrides quality) |
|
||||||
|
| `default_model.google` | string\|null | null | Google default model |
|
||||||
|
| `default_model.openai` | string\|null | null | OpenAI default model |
|
||||||
|
| `default_model.dashscope` | string\|null | null | DashScope default model |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Minimal**:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
default_provider: google
|
||||||
|
default_quality: 2k
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full**:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
version: 1
|
||||||
|
default_provider: google
|
||||||
|
default_quality: 2k
|
||||||
|
default_aspect_ratio: "16:9"
|
||||||
|
default_image_size: 2K
|
||||||
|
default_model:
|
||||||
|
google: "gemini-3-pro-image-preview"
|
||||||
|
openai: "gpt-image-1.5"
|
||||||
|
dashscope: "z-image-turbo"
|
||||||
|
---
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
|||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import type { CliArgs, Provider } from "./types";
|
import type { CliArgs, Provider, ExtendConfig } from "./types";
|
||||||
|
|
||||||
function printUsage(): void {
|
function printUsage(): void {
|
||||||
console.log(`Usage:
|
console.log(`Usage:
|
||||||
@@ -37,7 +37,7 @@ Environment variables:
|
|||||||
GOOGLE_BASE_URL Custom Google endpoint
|
GOOGLE_BASE_URL Custom Google endpoint
|
||||||
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
||||||
|
|
||||||
Env file load order: CLI args > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
|
Env file load order: CLI args > EXTEND.md > process.env > <cwd>/.baoyu-skills/.env > ~/.baoyu-skills/.env`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv: string[]): CliArgs {
|
function parseArgs(argv: string[]): CliArgs {
|
||||||
@@ -49,7 +49,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
model: null,
|
model: null,
|
||||||
aspectRatio: null,
|
aspectRatio: null,
|
||||||
size: null,
|
size: null,
|
||||||
quality: "2k",
|
quality: null,
|
||||||
imageSize: null,
|
imageSize: null,
|
||||||
referenceImages: [],
|
referenceImages: [],
|
||||||
n: 1,
|
n: 1,
|
||||||
@@ -215,6 +215,87 @@ async function loadEnv(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractYamlFrontMatter(content: string): string | null {
|
||||||
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
||||||
|
const config: Partial<ExtendConfig> = {};
|
||||||
|
const lines = yaml.split("\n");
|
||||||
|
let currentKey: string | null = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
|
||||||
|
if (trimmed.includes(":") && !trimmed.startsWith("-")) {
|
||||||
|
const colonIdx = trimmed.indexOf(":");
|
||||||
|
const key = trimmed.slice(0, colonIdx).trim();
|
||||||
|
let value = trimmed.slice(colonIdx + 1).trim();
|
||||||
|
|
||||||
|
if (value === "null" || value === "") {
|
||||||
|
value = "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "version") {
|
||||||
|
config.version = value === "null" ? 1 : parseInt(value, 10);
|
||||||
|
} else if (key === "default_provider") {
|
||||||
|
config.default_provider = value === "null" ? null : (value as Provider);
|
||||||
|
} else if (key === "default_quality") {
|
||||||
|
config.default_quality = value === "null" ? null : (value as "normal" | "2k");
|
||||||
|
} else if (key === "default_aspect_ratio") {
|
||||||
|
const cleaned = value.replace(/['"]/g, "");
|
||||||
|
config.default_aspect_ratio = cleaned === "null" ? null : cleaned;
|
||||||
|
} else if (key === "default_image_size") {
|
||||||
|
config.default_image_size = value === "null" ? null : (value as "1K" | "2K" | "4K");
|
||||||
|
} else if (key === "default_model") {
|
||||||
|
config.default_model = { google: null, openai: null, dashscope: null };
|
||||||
|
currentKey = "default_model";
|
||||||
|
} else if (currentKey === "default_model" && (key === "google" || key === "openai" || key === "dashscope")) {
|
||||||
|
const cleaned = value.replace(/['"]/g, "");
|
||||||
|
config.default_model![key] = cleaned === "null" ? null : cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExtendConfig(): Promise<Partial<ExtendConfig>> {
|
||||||
|
const home = homedir();
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const paths = [
|
||||||
|
path.join(cwd, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
|
||||||
|
path.join(home, ".baoyu-skills", "baoyu-image-gen", "EXTEND.md"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of paths) {
|
||||||
|
try {
|
||||||
|
const content = await readFile(p, "utf8");
|
||||||
|
const yaml = extractYamlFrontMatter(content);
|
||||||
|
if (!yaml) continue;
|
||||||
|
|
||||||
|
return parseSimpleYaml(yaml);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeConfig(args: CliArgs, extend: Partial<ExtendConfig>): CliArgs {
|
||||||
|
return {
|
||||||
|
...args,
|
||||||
|
provider: args.provider ?? extend.default_provider ?? null,
|
||||||
|
quality: args.quality ?? extend.default_quality ?? null,
|
||||||
|
aspectRatio: args.aspectRatio ?? extend.default_aspect_ratio ?? null,
|
||||||
|
imageSize: args.imageSize ?? extend.default_image_size ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function readPromptFromFiles(files: string[]): Promise<string> {
|
async function readPromptFromFiles(files: string[]): Promise<string> {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
@@ -283,9 +364,13 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await loadEnv();
|
await loadEnv();
|
||||||
|
const extendConfig = await loadExtendConfig();
|
||||||
|
const mergedArgs = mergeConfig(args, extendConfig);
|
||||||
|
|
||||||
let prompt: string | null = args.prompt;
|
if (!mergedArgs.quality) mergedArgs.quality = "2k";
|
||||||
if (!prompt && args.promptFiles.length > 0) prompt = await readPromptFromFiles(args.promptFiles);
|
|
||||||
|
let prompt: string | null = mergedArgs.prompt;
|
||||||
|
if (!prompt && mergedArgs.promptFiles.length > 0) prompt = await readPromptFromFiles(mergedArgs.promptFiles);
|
||||||
if (!prompt) prompt = await readPromptFromStdin();
|
if (!prompt) prompt = await readPromptFromStdin();
|
||||||
|
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
@@ -295,24 +380,32 @@ async function main(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.imagePath) {
|
if (!mergedArgs.imagePath) {
|
||||||
console.error("Error: --image is required");
|
console.error("Error: --image is required");
|
||||||
printUsage();
|
printUsage();
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = detectProvider(args);
|
const provider = detectProvider(mergedArgs);
|
||||||
const providerModule = await loadProviderModule(provider);
|
const providerModule = await loadProviderModule(provider);
|
||||||
const model = args.model || providerModule.getDefaultModel();
|
|
||||||
const outputPath = normalizeOutputImagePath(args.imagePath);
|
let model = mergedArgs.model;
|
||||||
|
if (!model && extendConfig.default_model) {
|
||||||
|
if (provider === "google") model = extendConfig.default_model.google ?? null;
|
||||||
|
if (provider === "openai") model = extendConfig.default_model.openai ?? null;
|
||||||
|
if (provider === "dashscope") model = extendConfig.default_model.dashscope ?? null;
|
||||||
|
}
|
||||||
|
model = model || providerModule.getDefaultModel();
|
||||||
|
|
||||||
|
const outputPath = normalizeOutputImagePath(mergedArgs.imagePath);
|
||||||
|
|
||||||
let imageData: Uint8Array;
|
let imageData: Uint8Array;
|
||||||
let retried = false;
|
let retried = false;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
imageData = await providerModule.generateImage(prompt, model, args);
|
imageData = await providerModule.generateImage(prompt, model, mergedArgs);
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!retried) {
|
if (!retried) {
|
||||||
@@ -328,7 +421,7 @@ async function main(): Promise<void> {
|
|||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
await writeFile(outputPath, imageData);
|
await writeFile(outputPath, imageData);
|
||||||
|
|
||||||
if (args.json) {
|
if (mergedArgs.json) {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,10 +9,23 @@ export type CliArgs = {
|
|||||||
model: string | null;
|
model: string | null;
|
||||||
aspectRatio: string | null;
|
aspectRatio: string | null;
|
||||||
size: string | null;
|
size: string | null;
|
||||||
quality: Quality;
|
quality: Quality | null;
|
||||||
imageSize: string | null;
|
imageSize: string | null;
|
||||||
referenceImages: string[];
|
referenceImages: string[];
|
||||||
n: number;
|
n: number;
|
||||||
json: boolean;
|
json: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExtendConfig = {
|
||||||
|
version: number;
|
||||||
|
default_provider: Provider | null;
|
||||||
|
default_quality: Quality | null;
|
||||||
|
default_aspect_ratio: string | null;
|
||||||
|
default_image_size: "1K" | "2K" | "4K" | null;
|
||||||
|
default_model: {
|
||||||
|
google: string | null;
|
||||||
|
openai: string | null;
|
||||||
|
dashscope: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -126,7 +126,18 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
|
|
||||||
const { frontmatter, body } = parseFrontmatter(content);
|
const { frontmatter, body } = parseFrontmatter(content);
|
||||||
|
|
||||||
let title = options?.title ?? frontmatter.title ?? '';
|
const stripQuotes = (s?: string): string => {
|
||||||
|
if (!s) return '';
|
||||||
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
let title = options?.title ?? stripQuotes(frontmatter.title) ?? '';
|
||||||
if (!title) {
|
if (!title) {
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -138,8 +149,8 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
||||||
const author = frontmatter.author || '';
|
const author = stripQuotes(frontmatter.author);
|
||||||
let summary = frontmatter.description || frontmatter.summary || '';
|
let summary = stripQuotes(frontmatter.description) || stripQuotes(frontmatter.summary);
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
|
|||||||
@@ -708,14 +708,28 @@ function normalizeThemeCss(css: string): string {
|
|||||||
return stripOutputScope(css);
|
return stripOutputScope(css);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildHtmlDocument(title: string, css: string, html: string): string {
|
interface HtmlDocumentMeta {
|
||||||
return [
|
title: string;
|
||||||
|
author?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): string {
|
||||||
|
const lines = [
|
||||||
"<!doctype html>",
|
"<!doctype html>",
|
||||||
"<html>",
|
"<html>",
|
||||||
"<head>",
|
"<head>",
|
||||||
' <meta charset="utf-8" />',
|
' <meta charset="utf-8" />',
|
||||||
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
||||||
` <title>${title}</title>`,
|
` <title>${meta.title}</title>`,
|
||||||
|
];
|
||||||
|
if (meta.author) {
|
||||||
|
lines.push(` <meta name="author" content="${meta.author}" />`);
|
||||||
|
}
|
||||||
|
if (meta.description) {
|
||||||
|
lines.push(` <meta name="description" content="${meta.description}" />`);
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
` <style>${css}</style>`,
|
` <style>${css}</style>`,
|
||||||
"</head>",
|
"</head>",
|
||||||
"<body>",
|
"<body>",
|
||||||
@@ -723,8 +737,9 @@ function buildHtmlDocument(title: string, css: string, html: string): string {
|
|||||||
html,
|
html,
|
||||||
" </div>",
|
" </div>",
|
||||||
"</body>",
|
"</body>",
|
||||||
"</html>",
|
"</html>"
|
||||||
].join("\n");
|
);
|
||||||
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function inlineCss(html: string): Promise<string> {
|
async function inlineCss(html: string): Promise<string> {
|
||||||
@@ -814,6 +829,7 @@ async function main(): Promise<void> {
|
|||||||
const markdown = fs.readFileSync(inputPath, "utf-8");
|
const markdown = fs.readFileSync(inputPath, "utf-8");
|
||||||
|
|
||||||
const renderer = initRenderer({});
|
const renderer = initRenderer({});
|
||||||
|
const { yamlData } = renderer.parseFrontMatterAndContent(markdown);
|
||||||
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(
|
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(
|
||||||
markdown,
|
markdown,
|
||||||
renderer
|
renderer
|
||||||
@@ -823,8 +839,23 @@ async function main(): Promise<void> {
|
|||||||
content = removeFirstHeading(content);
|
content = removeFirstHeading(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = path.basename(outputPath, ".html");
|
const stripQuotes = (s?: string): string | undefined => {
|
||||||
const html = buildHtmlDocument(title, css, content);
|
if (!s) return s;
|
||||||
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
if ((s.startsWith('\u201c') && s.endsWith('\u201d')) || (s.startsWith('\u2018') && s.endsWith('\u2019'))) {
|
||||||
|
return s.slice(1, -1);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: HtmlDocumentMeta = {
|
||||||
|
title: stripQuotes(yamlData.title) || path.basename(outputPath, ".html"),
|
||||||
|
author: stripQuotes(yamlData.author),
|
||||||
|
description: stripQuotes(yamlData.description) || stripQuotes(yamlData.summary),
|
||||||
|
};
|
||||||
|
const html = buildHtmlDocument(meta, css, content);
|
||||||
const inlinedHtml = normalizeInlineCss(await inlineCss(html));
|
const inlinedHtml = normalizeInlineCss(await inlineCss(html));
|
||||||
const finalHtml = modifyHtmlStructure(inlinedHtml);
|
const finalHtml = modifyHtmlStructure(inlinedHtml);
|
||||||
|
|
||||||
|
|||||||
@@ -120,22 +120,28 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
const { frontmatter, body } = parseFrontmatter(content);
|
const { frontmatter, body } = parseFrontmatter(content);
|
||||||
|
|
||||||
let title = options?.title ?? frontmatter.title ?? '';
|
let title = options?.title ?? frontmatter.title ?? '';
|
||||||
|
let bodyWithoutTitle = body;
|
||||||
if (!title) {
|
if (!title) {
|
||||||
const lines = body.split('\n');
|
const lines = body.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
const headingMatch = trimmed.match(/^#{1,2}\s+(.+)$/);
|
const headingMatch = trimmed.match(/^#{1,2}\s+(.+)$/);
|
||||||
if (headingMatch) title = headingMatch[1]!;
|
if (headingMatch) {
|
||||||
|
title = headingMatch[1]!;
|
||||||
|
bodyWithoutTitle = body.replace(/^#{1,2}\s+.+\r?\n?/, '');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bodyWithoutTitle = body.replace(/^#{1,2}\s+.+\r?\n?/, '');
|
||||||
}
|
}
|
||||||
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
if (!title) title = path.basename(markdownPath, path.extname(markdownPath));
|
||||||
const author = frontmatter.author || '';
|
const author = frontmatter.author || '';
|
||||||
let summary = frontmatter.description || frontmatter.summary || '';
|
let summary = frontmatter.description || frontmatter.summary || '';
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
const lines = body.split('\n');
|
const lines = bodyWithoutTitle.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
@@ -161,7 +167,7 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
|
|||||||
const images: Array<{ src: string; placeholder: string }> = [];
|
const images: Array<{ src: string; placeholder: string }> = [];
|
||||||
let imageCounter = 0;
|
let imageCounter = 0;
|
||||||
|
|
||||||
const modifiedBody = body.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
const modifiedBody = bodyWithoutTitle.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||||
const placeholder = `WECHATIMGPH_${++imageCounter}`;
|
const placeholder = `WECHATIMGPH_${++imageCounter}`;
|
||||||
images.push({ src, placeholder });
|
images.push({ src, placeholder });
|
||||||
return placeholder;
|
return placeholder;
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ export function initRenderer(opts: IOpts = {}): RendererAPI {
|
|||||||
|
|
||||||
listCounters[listCounters.length - 1] = idx + 1;
|
listCounters[listCounters.length - 1] = idx + 1;
|
||||||
|
|
||||||
const prefix = ordered ? `${idx}. ` : "• ";
|
const prefix = ordered ? "" : "• ";
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -273,23 +273,32 @@ async function selectAndReplacePlaceholder(session: ChromeSession, placeholder:
|
|||||||
const editor = document.querySelector('.ProseMirror');
|
const editor = document.querySelector('.ProseMirror');
|
||||||
if (!editor) return false;
|
if (!editor) return false;
|
||||||
|
|
||||||
|
const placeholder = ${JSON.stringify(placeholder)};
|
||||||
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null, false);
|
||||||
let node;
|
let node;
|
||||||
|
|
||||||
while ((node = walker.nextNode())) {
|
while ((node = walker.nextNode())) {
|
||||||
const text = node.textContent || '';
|
const text = node.textContent || '';
|
||||||
const idx = text.indexOf(${JSON.stringify(placeholder)});
|
let searchStart = 0;
|
||||||
if (idx !== -1) {
|
let idx;
|
||||||
|
// Search for exact match (not prefix of longer placeholder like XIMGPH_1 in XIMGPH_10)
|
||||||
|
while ((idx = text.indexOf(placeholder, searchStart)) !== -1) {
|
||||||
|
const afterIdx = idx + placeholder.length;
|
||||||
|
const charAfter = text[afterIdx];
|
||||||
|
// Exact match if next char is not a digit
|
||||||
|
if (charAfter === undefined || !/\\d/.test(charAfter)) {
|
||||||
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
node.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, idx);
|
range.setStart(node, idx);
|
||||||
range.setEnd(node, idx + ${placeholder.length});
|
range.setEnd(node, idx + placeholder.length);
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
searchStart = afterIdx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})()
|
})()
|
||||||
@@ -306,6 +315,71 @@ async function pressDeleteKey(session: ChromeSession): Promise<void> {
|
|||||||
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId: session.sessionId });
|
await session.cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 }, { sessionId: session.sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<boolean> {
|
||||||
|
const removed = await evaluate<boolean>(session, `
|
||||||
|
(function() {
|
||||||
|
const editor = document.querySelector('.ProseMirror');
|
||||||
|
if (!editor) return false;
|
||||||
|
|
||||||
|
const sel = window.getSelection();
|
||||||
|
if (!sel || sel.rangeCount === 0) return false;
|
||||||
|
|
||||||
|
let node = sel.anchorNode;
|
||||||
|
if (!node) return false;
|
||||||
|
let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
||||||
|
if (!element || !editor.contains(element)) return false;
|
||||||
|
|
||||||
|
const isEmptyParagraph = (el) => {
|
||||||
|
if (!el || el.tagName !== 'P') return false;
|
||||||
|
const text = (el.textContent || '').trim();
|
||||||
|
if (text.length > 0) return false;
|
||||||
|
return el.querySelectorAll('img, figure, video, iframe').length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasImage = (el) => {
|
||||||
|
if (!el) return false;
|
||||||
|
return !!el.querySelector('img, figure img, picture img');
|
||||||
|
};
|
||||||
|
|
||||||
|
const placeCursorAfter = (el) => {
|
||||||
|
if (!el) return;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStartAfter(el);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Case 1: caret is inside an empty paragraph right after an image block.
|
||||||
|
const emptyPara = element.closest('p');
|
||||||
|
if (emptyPara && editor.contains(emptyPara) && isEmptyParagraph(emptyPara)) {
|
||||||
|
const prev = emptyPara.previousElementSibling;
|
||||||
|
if (prev && hasImage(prev)) {
|
||||||
|
emptyPara.remove();
|
||||||
|
placeCursorAfter(prev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: caret is on the image block itself; remove the next empty paragraph.
|
||||||
|
const imageBlock = element.closest('figure, p');
|
||||||
|
if (imageBlock && editor.contains(imageBlock) && hasImage(imageBlock)) {
|
||||||
|
const next = imageBlock.nextElementSibling;
|
||||||
|
if (next && isEmptyParagraph(next)) {
|
||||||
|
next.remove();
|
||||||
|
placeCursorAfter(imageBlock);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (removed) console.log('[wechat] Removed extra empty line after image.');
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
export async function postArticle(options: ArticleOptions): Promise<void> {
|
export async function postArticle(options: ArticleOptions): Promise<void> {
|
||||||
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
|
||||||
let { contentImages = [] } = options;
|
let { contentImages = [] } = options;
|
||||||
@@ -445,11 +519,6 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
|
await evaluate(session, `document.querySelector('#author').value = ${JSON.stringify(effectiveAuthor)}; document.querySelector('#author').dispatchEvent(new Event('input', { bubbles: true }));`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveSummary) {
|
|
||||||
console.log(`[wechat] Filling summary: ${effectiveSummary}`);
|
|
||||||
await evaluate(session, `document.querySelector('#js_description').value = ${JSON.stringify(effectiveSummary)}; document.querySelector('#js_description').dispatchEvent(new Event('input', { bubbles: true }));`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
if (effectiveTitle) {
|
if (effectiveTitle) {
|
||||||
@@ -461,15 +530,6 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effectiveSummary) {
|
|
||||||
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
|
|
||||||
if (actualSummary === effectiveSummary) {
|
|
||||||
console.log('[wechat] Summary verified OK.');
|
|
||||||
} else {
|
|
||||||
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[wechat] Clicking on editor...');
|
console.log('[wechat] Clicking on editor...');
|
||||||
await clickElement(session, '.ProseMirror');
|
await clickElement(session, '.ProseMirror');
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
@@ -525,6 +585,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
console.log('[wechat] Pasting image...');
|
console.log('[wechat] Pasting image...');
|
||||||
await pasteFromClipboardInEditor(session);
|
await pasteFromClipboardInEditor(session);
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
await removeExtraEmptyLineAfterImage(session);
|
||||||
}
|
}
|
||||||
console.log('[wechat] All images inserted.');
|
console.log('[wechat] All images inserted.');
|
||||||
}
|
}
|
||||||
@@ -536,6 +597,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
await sleep(500);
|
await sleep(500);
|
||||||
await pasteInEditor(session);
|
await pasteInEditor(session);
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
await removeExtraEmptyLineAfterImage(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +620,30 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (effectiveSummary) {
|
||||||
|
console.log(`[wechat] Filling summary (after content paste): ${effectiveSummary}`);
|
||||||
|
await evaluate(session, `
|
||||||
|
(function() {
|
||||||
|
const el = document.querySelector('#js_description');
|
||||||
|
if (!el) return;
|
||||||
|
el.focus();
|
||||||
|
el.select();
|
||||||
|
el.value = ${JSON.stringify(effectiveSummary)};
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||||
|
})()
|
||||||
|
`);
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
const actualSummary = await evaluate<string>(session, `document.querySelector('#js_description')?.value || ''`);
|
||||||
|
if (actualSummary === effectiveSummary) {
|
||||||
|
console.log('[wechat] Summary verified OK.');
|
||||||
|
} else {
|
||||||
|
console.warn(`[wechat] Summary verification failed. Expected: "${effectiveSummary}", got: "${actualSummary}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[wechat] Saving as draft...');
|
console.log('[wechat] Saving as draft...');
|
||||||
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
await evaluate(session, `document.querySelector('#js_submit button').click()`);
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user