multi-publisher 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -22
- package/dist/adapters/csdn.d.ts +15 -0
- package/dist/adapters/csdn.js +136 -1
- package/dist/adapters/interface.d.ts +10 -0
- package/dist/adapters/qq.d.ts +19 -0
- package/dist/adapters/qq.js +185 -0
- package/dist/adapters/registry.js +2 -0
- package/dist/adapters/toutiao.d.ts +4 -0
- package/dist/adapters/toutiao.js +205 -40
- package/dist/adapters/wechat-publisher.d.ts +6 -1
- package/dist/adapters/wechat-publisher.js +58 -17
- package/dist/adapters/weixin.d.ts +5 -0
- package/dist/adapters/weixin.js +41 -5
- package/dist/cli/capture.d.ts +6 -0
- package/dist/cli/capture.js +49 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/login.js +3 -0
- package/dist/cli/platforms.js +2 -1
- package/dist/cli/publish-all.js +1 -1
- package/dist/cli/publish.d.ts +1 -0
- package/dist/cli/publish.js +29 -5
- package/dist/config.d.ts +8 -0
- package/dist/config.js +11 -0
- package/dist/core/parser.js +9 -1
- package/dist/core/renderer.d.ts +9 -1
- package/dist/core/renderer.js +86 -11
- package/dist/core/theme.d.ts +1 -1
- package/dist/core/theme.js +91 -336
- package/dist/runtime/browser-runtime.js +31 -2
- package/dist/tools/browser-upload.d.ts +13 -0
- package/dist/tools/browser-upload.js +349 -0
- package/dist/tools/capture.d.ts +26 -0
- package/dist/tools/capture.js +348 -0
- package/dist/tools/cover-fetcher.d.ts +10 -0
- package/dist/tools/cover-fetcher.js +100 -0
- package/dist/tools/imgbb-uploader.d.ts +18 -0
- package/dist/tools/imgbb-uploader.js +89 -0
- package/dist/tools/toutiao-upload.d.ts +10 -0
- package/dist/tools/toutiao-upload.js +166 -0
- package/package.json +1 -1
- package/themes/previews/cyberpunk.png +0 -0
- package/themes/previews/nord.png +0 -0
package/dist/cli/platforms.js
CHANGED
|
@@ -19,9 +19,10 @@ const PLATFORM_LIST = [
|
|
|
19
19
|
{ id: 'sohu', name: '搜狐号', auth: 'Cookie', capabilities: ['article', 'draft'], note: 'Cookie 登录支持', status: '🔄 待测试' },
|
|
20
20
|
{ id: 'eastmoney', name: '东方财富', auth: 'Cookie', capabilities: ['article', 'draft'], note: 'Cookie 登录支持', status: '🔄 待测试' },
|
|
21
21
|
{ id: 'cto51', name: '51CTO', auth: 'Cookie', capabilities: ['article', 'draft'], note: 'Cookie 登录支持', status: '🔄 待测试' },
|
|
22
|
+
{ id: 'qq', name: '企鹅号', auth: 'Cookie', capabilities: ['article', 'draft', 'image_upload'], note: '企鹅号平台', status: '🔄 待测试' },
|
|
22
23
|
];
|
|
23
24
|
export async function runPlatforms() {
|
|
24
|
-
console.log('\n🖥️ 支持的平台(共
|
|
25
|
+
console.log('\n🖥️ 支持的平台(共 21 个):\n');
|
|
25
26
|
console.log('状态说明:');
|
|
26
27
|
console.log(' ✅ 已验证 - 登录和发布功能已测试通过');
|
|
27
28
|
console.log(' 🔄 待测试 - Cookie 登录支持,但发布功能尚未测试\n');
|
package/dist/cli/publish-all.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function runPublishAll(options) {
|
|
|
55
55
|
cover = coverMatch[1].trim();
|
|
56
56
|
markdownContent = markdownContent.slice(frontmatterMatch[0].length);
|
|
57
57
|
}
|
|
58
|
-
// 渲染 HTML
|
|
58
|
+
// 渲染 HTML(不处理 mermaid,由各平台适配器自行处理)
|
|
59
59
|
const renderResult = await renderMarkdown(markdownContent, {
|
|
60
60
|
theme,
|
|
61
61
|
highlight,
|
package/dist/cli/publish.d.ts
CHANGED
package/dist/cli/publish.js
CHANGED
|
@@ -27,14 +27,30 @@ export async function runPublish(options, input) {
|
|
|
27
27
|
else {
|
|
28
28
|
throw new Error('请提供 -f 选项指定 Markdown 文件');
|
|
29
29
|
}
|
|
30
|
-
//
|
|
30
|
+
// 平台未显式指定主题时,按平台绑定合适的默认主题
|
|
31
|
+
const platformId = options.platform || 'weixin';
|
|
32
|
+
const PLATFORM_DEFAULT_THEME = {
|
|
33
|
+
weixin: 'wechat',
|
|
34
|
+
zhihu: 'modern',
|
|
35
|
+
toutiao: 'minimal',
|
|
36
|
+
// 其余平台回退到 default
|
|
37
|
+
};
|
|
38
|
+
const theme = options.theme || PLATFORM_DEFAULT_THEME[platformId] || 'default';
|
|
39
|
+
// 2. 渲染(不处理 mermaid,由各平台适配器自行处理)
|
|
31
40
|
const result = await renderMarkdown(content, {
|
|
32
|
-
theme
|
|
41
|
+
theme,
|
|
33
42
|
macStyle: options.macStyle !== false,
|
|
34
43
|
autoCover: options.autoCover || false,
|
|
44
|
+
coverMode: options.coverMode || 'auto',
|
|
35
45
|
});
|
|
36
46
|
// 3. 选择适配器
|
|
37
|
-
|
|
47
|
+
// 校验 title
|
|
48
|
+
if (!result.title || result.title === '无标题') {
|
|
49
|
+
console.warn(`[publish] ⚠️ 警告:文章标题为空!`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(`[publish] ✅ 文章标题: "${result.title}"`);
|
|
53
|
+
}
|
|
38
54
|
// 初始化适配器注册表
|
|
39
55
|
const runtime = createNodeRuntime();
|
|
40
56
|
await initAdapterRegistry(runtime);
|
|
@@ -50,8 +66,16 @@ export async function runPublish(options, input) {
|
|
|
50
66
|
if (options.appId) {
|
|
51
67
|
process.env.WECHAT_APP_ID = options.appId;
|
|
52
68
|
}
|
|
53
|
-
//
|
|
54
|
-
|
|
69
|
+
// 使用封面图:
|
|
70
|
+
// - coverMode=sharp/network: 强制用 autoCoverPath,忽略 front-matter 封面
|
|
71
|
+
// - coverMode=auto: 优先用 front-matter 封面,其次 autoCoverPath
|
|
72
|
+
let coverImage;
|
|
73
|
+
if (options.coverMode === 'sharp' || options.coverMode === 'network') {
|
|
74
|
+
coverImage = result.autoCoverPath;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
coverImage = result.cover || result.autoCoverPath;
|
|
78
|
+
}
|
|
55
79
|
const syncResult = await adapter.publish({
|
|
56
80
|
title: result.title,
|
|
57
81
|
markdown: content,
|
package/dist/config.d.ts
CHANGED
|
@@ -61,6 +61,9 @@ export interface SohuConfig {
|
|
|
61
61
|
export interface EastmoneyConfig {
|
|
62
62
|
cookies?: Record<string, string>;
|
|
63
63
|
}
|
|
64
|
+
export interface QQConfig {
|
|
65
|
+
cookies?: Record<string, string>;
|
|
66
|
+
}
|
|
64
67
|
export interface GlobalConfig {
|
|
65
68
|
version: number;
|
|
66
69
|
weixin?: WeixinConfig;
|
|
@@ -83,6 +86,7 @@ export interface GlobalConfig {
|
|
|
83
86
|
douban?: DoubanConfig;
|
|
84
87
|
sohu?: SohuConfig;
|
|
85
88
|
eastmoney?: EastmoneyConfig;
|
|
89
|
+
qq?: QQConfig;
|
|
86
90
|
}
|
|
87
91
|
/** 读取配置(带缓存) */
|
|
88
92
|
export declare function loadConfig(): Promise<GlobalConfig>;
|
|
@@ -172,6 +176,10 @@ export declare class ConfigStore {
|
|
|
172
176
|
static getEastmoneyCookies(): Promise<Record<string, string> | null>;
|
|
173
177
|
/** 设置东方财富 Cookie */
|
|
174
178
|
static setEastmoneyCookies(cookies: Record<string, string>): Promise<void>;
|
|
179
|
+
/** 获取企鹅号 Cookie */
|
|
180
|
+
static getQQCookies(): Promise<Record<string, string> | null>;
|
|
181
|
+
/** 设置企鹅号 Cookie */
|
|
182
|
+
static setQQCookies(cookies: Record<string, string>): Promise<void>;
|
|
175
183
|
/** 获取配置目录路径 */
|
|
176
184
|
static getDir(): string;
|
|
177
185
|
}
|
package/dist/config.js
CHANGED
|
@@ -326,6 +326,17 @@ export class ConfigStore {
|
|
|
326
326
|
c.eastmoney = { ...c.eastmoney, cookies };
|
|
327
327
|
});
|
|
328
328
|
}
|
|
329
|
+
/** 获取企鹅号 Cookie */
|
|
330
|
+
static async getQQCookies() {
|
|
331
|
+
const config = await loadConfig();
|
|
332
|
+
return config.qq?.cookies || null;
|
|
333
|
+
}
|
|
334
|
+
/** 设置企鹅号 Cookie */
|
|
335
|
+
static async setQQCookies(cookies) {
|
|
336
|
+
await updateConfig((c) => {
|
|
337
|
+
c.qq = { ...c.qq, cookies };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
329
340
|
/** 获取配置目录路径 */
|
|
330
341
|
static getDir() {
|
|
331
342
|
return getConfigDir();
|
package/dist/core/parser.js
CHANGED
|
@@ -27,9 +27,17 @@ const markedInstance = createMarked();
|
|
|
27
27
|
export function parseMarkdown(content) {
|
|
28
28
|
const parsed = fm(content);
|
|
29
29
|
const html = markedInstance.parse(parsed.body);
|
|
30
|
+
// Fallback title extraction: try to get first # heading if front-matter title is missing
|
|
31
|
+
let title = parsed.attributes.title;
|
|
32
|
+
if (!title) {
|
|
33
|
+
const headingMatch = parsed.body.match(/^#\s+(.+)/m);
|
|
34
|
+
if (headingMatch) {
|
|
35
|
+
title = headingMatch[1].trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
30
38
|
return {
|
|
31
39
|
meta: {
|
|
32
|
-
title:
|
|
40
|
+
title: title || '无标题',
|
|
33
41
|
author: parsed.attributes.author,
|
|
34
42
|
cover: parsed.attributes.cover,
|
|
35
43
|
source_url: parsed.attributes.source_url,
|
package/dist/core/renderer.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type CoverMode = 'sharp' | 'network' | 'auto';
|
|
1
2
|
export interface RenderOptions {
|
|
2
3
|
theme?: string;
|
|
3
4
|
customCss?: string;
|
|
@@ -6,6 +7,8 @@ export interface RenderOptions {
|
|
|
6
7
|
footnote?: boolean;
|
|
7
8
|
/** 是否在无封面时自动根据标题获取封面图 */
|
|
8
9
|
autoCover?: boolean;
|
|
10
|
+
/** 封面模式: sharp(SVG生成)|network(网络抓取)|auto(自动选择) */
|
|
11
|
+
coverMode?: CoverMode;
|
|
9
12
|
}
|
|
10
13
|
export interface RenderResult {
|
|
11
14
|
title: string;
|
|
@@ -17,6 +20,11 @@ export interface RenderResult {
|
|
|
17
20
|
autoCoverPath?: string;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
23
|
+
* 处理 Mermaid 代码块,将它们转换为图片
|
|
24
|
+
* 各平台适配器可调用此函数
|
|
21
25
|
*/
|
|
26
|
+
export declare function processMermaid(html: string, outputDir: string): Promise<{
|
|
27
|
+
html: string;
|
|
28
|
+
tempFiles: string[];
|
|
29
|
+
}>;
|
|
22
30
|
export declare function renderMarkdown(content: string, options?: RenderOptions): Promise<RenderResult>;
|
package/dist/core/renderer.js
CHANGED
|
@@ -6,32 +6,107 @@ import { processMath } from './mathjax.js';
|
|
|
6
6
|
import { inlineStyles } from './styler.js';
|
|
7
7
|
import { loadThemeCss, DEFAULT_CSS } from './theme.js';
|
|
8
8
|
import { generateCover } from '../tools/cover-generator.js';
|
|
9
|
+
import { fetchCoverByTitle } from '../tools/cover-fetcher.js';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs/promises';
|
|
13
|
+
import os from 'os';
|
|
9
14
|
/**
|
|
10
|
-
*
|
|
15
|
+
* 处理 Mermaid 代码块,将它们转换为图片
|
|
16
|
+
* 各平台适配器可调用此函数
|
|
11
17
|
*/
|
|
18
|
+
export async function processMermaid(html, outputDir) {
|
|
19
|
+
const mermaidBlockRegex = /<pre[^>]*><code[^>]*class="hljs language-mermaid"[^>]*>([\s\S]*?)<\/code><\/pre>/gi;
|
|
20
|
+
const matches = [...html.matchAll(mermaidBlockRegex)];
|
|
21
|
+
const tempFiles = [];
|
|
22
|
+
if (matches.length === 0)
|
|
23
|
+
return { html, tempFiles };
|
|
24
|
+
console.log(`[renderer] 发现 ${matches.length} 个 mermaid 代码块,开始转换为图片...`);
|
|
25
|
+
for (const match of matches) {
|
|
26
|
+
let mermaidCode = match[1].trim();
|
|
27
|
+
// 解码 HTML 实体
|
|
28
|
+
mermaidCode = mermaidCode
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/&/g, '&')
|
|
32
|
+
.replace(/"/g, '"');
|
|
33
|
+
const tmpMmd = path.join(os.tmpdir(), `mermaid-${Date.now()}-${Math.random()}.mmd`);
|
|
34
|
+
const outputPng = path.join(outputDir, `mermaid-${Date.now()}-${Math.random()}.png`);
|
|
35
|
+
try {
|
|
36
|
+
// 写入临时 mmd 文件
|
|
37
|
+
await fs.writeFile(tmpMmd, mermaidCode, 'utf8');
|
|
38
|
+
tempFiles.push(tmpMmd);
|
|
39
|
+
// 调用 mmdc 渲染
|
|
40
|
+
execSync(`mmdc -i "${tmpMmd}" -o "${outputPng}" -b white`, {
|
|
41
|
+
stdio: 'pipe',
|
|
42
|
+
timeout: 60000
|
|
43
|
+
});
|
|
44
|
+
// 验证图片生成
|
|
45
|
+
await fs.access(outputPng);
|
|
46
|
+
tempFiles.push(outputPng);
|
|
47
|
+
// 替换 mermaid 代码块为图片
|
|
48
|
+
const imageTag = `<img src="${outputPng.replace(/\\/g, '/')}" />`;
|
|
49
|
+
html = html.replace(match[0], imageTag);
|
|
50
|
+
console.log(`[renderer] Mermaid 图片生成成功: ${outputPng}`);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.warn(`[renderer] Mermaid 渲染失败: ${err.message},保留原代码块`);
|
|
54
|
+
await fs.unlink(tmpMmd).catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { html, tempFiles };
|
|
58
|
+
}
|
|
12
59
|
export async function renderMarkdown(content, options = {}) {
|
|
13
|
-
const { theme = 'default', customCss, macStyle = true, autoCover = false, } = options;
|
|
60
|
+
const { theme = 'default', customCss, macStyle = true, autoCover = false, coverMode = 'auto', } = options;
|
|
14
61
|
// 1. 解析 front-matter + Markdown → HTML
|
|
15
62
|
const parsed = parseMarkdown(content);
|
|
16
63
|
// 2. 处理 LaTeX 公式($...$ 和 $$...$$)
|
|
17
64
|
let html = processMath(parsed.html);
|
|
65
|
+
// 注意:Mermaid 处理移到各平台适配器的 processMermaid 钩子
|
|
66
|
+
// 各 adapter 自己决定是否转换 mermaid 代码块
|
|
18
67
|
// 3. 加载主题 CSS
|
|
19
68
|
const themeCss = customCss ?? (await loadThemeCss(theme)) ?? DEFAULT_CSS;
|
|
20
69
|
// 4. CSS 内联
|
|
21
70
|
html = inlineStyles(html, themeCss);
|
|
22
71
|
// 5. 包裹 section 标签(微信公众号要求)
|
|
23
72
|
html = `<section style="margin-left:6px;margin-right:6px;line-height:1.75em">${html}</section>`;
|
|
73
|
+
// 5.5 校验 title
|
|
74
|
+
if (!parsed.meta.title || parsed.meta.title === '无标题') {
|
|
75
|
+
console.warn(`[renderer] ⚠️ 标题为空或为默认值,将尝试从正文提取。原始 content 前200字符: ${content.substring(0, 200)}`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(`[renderer] ✅ 标题: "${parsed.meta.title}"`);
|
|
79
|
+
}
|
|
24
80
|
let autoCoverPath;
|
|
25
|
-
// 6.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
81
|
+
// 6. 自动生成封面图
|
|
82
|
+
// 条件:有 --auto-cover 且(front-matter 无封面 或 coverMode=sharp/network 强制覆盖)
|
|
83
|
+
const shouldGenerateCover = autoCover && (!parsed.meta.cover ||
|
|
84
|
+
coverMode === 'sharp' ||
|
|
85
|
+
coverMode === 'network');
|
|
86
|
+
if (shouldGenerateCover && parsed.meta.title) {
|
|
87
|
+
if (coverMode === 'sharp') {
|
|
88
|
+
// sharp 模式:强制用 SVG 生成封面
|
|
89
|
+
console.log(`[renderer] cover-mode=sharp,强制用 SVG 生成封面...`);
|
|
90
|
+
const result = await generateCover(parsed.meta.title, parsed.meta.description || '');
|
|
91
|
+
if (result.success && result.localPath) {
|
|
92
|
+
autoCoverPath = result.localPath;
|
|
93
|
+
console.log(`[renderer] SVG 封面生成成功: ${result.localPath}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.warn(`[renderer] SVG 封面生成失败: ${result.error}`);
|
|
97
|
+
}
|
|
32
98
|
}
|
|
33
|
-
else {
|
|
34
|
-
|
|
99
|
+
else if (coverMode === 'network' || (coverMode === 'auto' && !parsed.meta.cover)) {
|
|
100
|
+
// network 模式 或 auto 模式(无 front-matter 封面):从网络抓取
|
|
101
|
+
console.log(`[renderer] cover-mode=${coverMode},从网络抓取封面...`);
|
|
102
|
+
const result = await fetchCoverByTitle(parsed.meta.title);
|
|
103
|
+
if (result.success && result.localPath) {
|
|
104
|
+
autoCoverPath = result.localPath;
|
|
105
|
+
console.log(`[renderer] 网络封面获取成功: ${result.localPath}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.warn(`[renderer] 网络封面获取失败: ${result.error}`);
|
|
109
|
+
}
|
|
35
110
|
}
|
|
36
111
|
}
|
|
37
112
|
return {
|
package/dist/core/theme.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export interface ThemeInfo {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
isBuiltin: boolean;
|
|
6
6
|
}
|
|
7
|
-
export declare const DEFAULT_CSS = "\np {\n color: rgb(51, 51, 51);\n font-size: 15px;\n line-height: 1.75em;\n margin: 0 0 1em 0;\n word-wrap: break-word;\n}\nh1, h2, h3, h4, h5, h6 {\n font-weight: bold;\n margin:
|
|
7
|
+
export declare const DEFAULT_CSS = "\np {\n color: rgb(51, 51, 51);\n font-size: 15px;\n line-height: 1.75em;\n margin: 0 0 1em 0;\n word-wrap: break-word;\n letter-spacing: 0.5px;\n}\nh1, h2, h3, h4, h5, h6 {\n font-weight: bold;\n color: #2c3e50;\n margin: 1.2em 0 0.6em 0;\n}\nh1 { font-size: 1.4em; line-height: 1.4em; border-bottom: 1px solid #e8e8e8; padding-bottom: 0.3em; }\nh2 { font-size: 1.2em; border-left: 4px solid #07a35a; padding-left: 0.5em; }\nh3 { font-size: 1.05em; }\nh4, h5, h6 { font-size: 1em; }\nul, ol { margin: 0 0 1em 0; padding-left: 1.8em; }\nli { margin: 0.3em 0; line-height: 1.7em; }\nli p { margin: 0; }\npre {\n background-color: #f6f8fa;\n border: 1px solid #eaecef;\n border-radius: 6px;\n padding: 14px 16px;\n overflow-x: auto;\n font-size: 13.5px;\n line-height: 1.6;\n margin: 1em 0;\n color: #24292e;\n}\ncode {\n background-color: rgba(175, 184, 193, 0.2);\n border-radius: 3px;\n padding: 0.2em 0.4em;\n font-size: 0.9em;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n}\npre code { background: none; padding: 0; border: none; font-size: 13.5px; }\nblockquote {\n border-left: 4px solid #07a35a;\n background-color: #f6fbf8;\n padding: 0.8em 1em;\n margin: 1em 0;\n color: #555;\n border-radius: 0 4px 4px 0;\n}\nhr { border: none; border-top: 1px solid #e8e8e8; margin: 1.5em 0; }\ni, cite, em, var, address { font-style: italic; }\nb, strong { font-weight: bold; color: #1a1a1a; }\nimg { max-width: 100%; height: auto; display: block; margin: 1em auto; border-radius: 4px; }\ntable { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 14px; }\ntable th, table td { border: 1px solid #e8e8e8; padding: 8px 12px; }\ntable th { background-color: #f6f8fa; font-weight: bold; color: #2c3e50; }\ntable tr:nth-child(even) { background-color: #fafafa; }\na { color: #576b95; text-decoration: none; border-bottom: 1px solid rgba(87, 107, 149, 0.3); }\n";
|
|
8
8
|
export declare function listThemes(): Promise<ThemeInfo[]>;
|
|
9
9
|
export declare function loadThemeCss(themeId: string): Promise<string | null>;
|