multi-publisher 1.1.2 → 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.js +24 -8
- 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 +163 -49
- 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/index.js +1 -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/capture.js +28 -4
- 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/package.json +1 -1
- package/themes/previews/cyberpunk.png +0 -0
- package/themes/previews/nord.png +0 -0
|
@@ -15,6 +15,10 @@ export declare class WechatPublisher {
|
|
|
15
15
|
* 下载图片并以指定文件名上传到微信 CDN
|
|
16
16
|
*/
|
|
17
17
|
uploadImageFromUrl(imageUrl: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* 上传图片到微信 CDN(图文消息专用接口,返回可直接访问的 URL)
|
|
20
|
+
*/
|
|
21
|
+
uploadImageForArticle(filePath: string): Promise<string>;
|
|
18
22
|
/**
|
|
19
23
|
* 上传本地图片到微信 CDN(临时素材,用于正文嵌入)
|
|
20
24
|
*/
|
|
@@ -35,11 +39,12 @@ export declare class WechatPublisher {
|
|
|
35
39
|
uploadPermanentImageFromUrl(imageUrl: string): Promise<string>;
|
|
36
40
|
/**
|
|
37
41
|
* 处理 HTML 中的图片:本地图片上传到微信 CDN,外部图片保留原 URL
|
|
38
|
-
* 返回处理后的 HTML
|
|
42
|
+
* 返回处理后的 HTML、第一张 media_id(临时)和第一张本地路径(用于封面永久上传)
|
|
39
43
|
*/
|
|
40
44
|
processImages(html: string): Promise<{
|
|
41
45
|
html: string;
|
|
42
46
|
firstMediaId?: string;
|
|
47
|
+
firstLocalPath?: string;
|
|
43
48
|
}>;
|
|
44
49
|
/**
|
|
45
50
|
* 发布到微信公众号草稿箱
|
|
@@ -10,7 +10,14 @@ function extFromUrl(url) {
|
|
|
10
10
|
return ext || '.jpg';
|
|
11
11
|
}
|
|
12
12
|
function nameFromUrl(url) {
|
|
13
|
-
|
|
13
|
+
const ext = extFromUrl(url);
|
|
14
|
+
const pathname = new URL(url).pathname;
|
|
15
|
+
const basename = path.basename(pathname);
|
|
16
|
+
// If basename has no extension (or is just numbers like "720"), generate a proper name
|
|
17
|
+
if (!path.extname(basename) || /^\d+$/.test(basename)) {
|
|
18
|
+
return `cover${ext}`;
|
|
19
|
+
}
|
|
20
|
+
return basename;
|
|
14
21
|
}
|
|
15
22
|
export class WechatPublisher {
|
|
16
23
|
runtime;
|
|
@@ -65,6 +72,21 @@ export class WechatPublisher {
|
|
|
65
72
|
this.uploadCache[imageUrl] = mediaId;
|
|
66
73
|
return mediaId;
|
|
67
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* 上传图片到微信 CDN(图文消息专用接口,返回可直接访问的 URL)
|
|
77
|
+
*/
|
|
78
|
+
async uploadImageForArticle(filePath) {
|
|
79
|
+
const token = await this.getAccessToken();
|
|
80
|
+
const name = path.basename(filePath);
|
|
81
|
+
const form = new FormData();
|
|
82
|
+
form.append('media', readFileSync(filePath), { filename: name, contentType: 'image/png' });
|
|
83
|
+
const res = await axios.post(`https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${token}`, form, { headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity });
|
|
84
|
+
const data = res.data;
|
|
85
|
+
if (!data.url) {
|
|
86
|
+
throw new Error(`图片上传失败: ${data.errmsg || JSON.stringify(data)}`);
|
|
87
|
+
}
|
|
88
|
+
return data.url;
|
|
89
|
+
}
|
|
68
90
|
/**
|
|
69
91
|
* 上传本地图片到微信 CDN(临时素材,用于正文嵌入)
|
|
70
92
|
*/
|
|
@@ -142,11 +164,12 @@ export class WechatPublisher {
|
|
|
142
164
|
}
|
|
143
165
|
/**
|
|
144
166
|
* 处理 HTML 中的图片:本地图片上传到微信 CDN,外部图片保留原 URL
|
|
145
|
-
* 返回处理后的 HTML
|
|
167
|
+
* 返回处理后的 HTML、第一张 media_id(临时)和第一张本地路径(用于封面永久上传)
|
|
146
168
|
*/
|
|
147
169
|
async processImages(html) {
|
|
148
170
|
const imgPattern = /<img\s+[^>]*src=["']([^"']+)["'][^>]*>/gi;
|
|
149
171
|
let firstMediaId;
|
|
172
|
+
let firstLocalPath;
|
|
150
173
|
let replaced = html;
|
|
151
174
|
for (const match of [...html.matchAll(imgPattern)]) {
|
|
152
175
|
const original = match[0];
|
|
@@ -160,24 +183,35 @@ export class WechatPublisher {
|
|
|
160
183
|
continue;
|
|
161
184
|
}
|
|
162
185
|
try {
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
const isLocalPath = !src.startsWith('http://') && !src.startsWith('https://');
|
|
187
|
+
if (isLocalPath) {
|
|
188
|
+
// 本地文件用 uploadimg 接口,返回直接可访问的 URL
|
|
189
|
+
const url = await this.uploadImageForArticle(src);
|
|
190
|
+
if (!firstLocalPath)
|
|
191
|
+
firstLocalPath = src;
|
|
192
|
+
replaced = replaced.replace(original, original.replace(src, url));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// 外部 URL 下载后上传
|
|
196
|
+
const mediaId = await this.uploadImageFromUrl(src);
|
|
197
|
+
if (!firstMediaId)
|
|
198
|
+
firstMediaId = mediaId;
|
|
199
|
+
const cdnUrl = `https://mmbiz.qpic.cn/mmbiz_png/${mediaId}/0`;
|
|
200
|
+
replaced = replaced.replace(original, original.replace(src, cdnUrl));
|
|
201
|
+
}
|
|
168
202
|
}
|
|
169
203
|
catch (err) {
|
|
170
204
|
console.warn(`[WechatPublisher] 上传图片失败 ${src}: ${err.message},保留原 URL`);
|
|
171
205
|
}
|
|
172
206
|
}
|
|
173
|
-
return { html: replaced, firstMediaId };
|
|
207
|
+
return { html: replaced, firstMediaId, firstLocalPath };
|
|
174
208
|
}
|
|
175
209
|
/**
|
|
176
210
|
* 发布到微信公众号草稿箱
|
|
177
211
|
*/
|
|
178
212
|
async publishToDraft(options) {
|
|
179
213
|
const token = await this.getAccessToken();
|
|
180
|
-
const { html: processedHtml, firstMediaId } = await this.processImages(options.content);
|
|
214
|
+
const { html: processedHtml, firstMediaId, firstLocalPath } = await this.processImages(options.content);
|
|
181
215
|
// 上传封面图:优先用 front-matter 指定的封面图,其次用正文第一张图片的 media_id
|
|
182
216
|
let thumbMediaId;
|
|
183
217
|
try {
|
|
@@ -185,10 +219,15 @@ export class WechatPublisher {
|
|
|
185
219
|
}
|
|
186
220
|
catch (err) {
|
|
187
221
|
console.warn(`[WechatPublisher] 封面上传失败: ${err.message}`);
|
|
188
|
-
// 用 processImages
|
|
189
|
-
if (
|
|
222
|
+
// 用 processImages 返回的第一张图片(本地路径需要用永久素材接口)
|
|
223
|
+
if (firstLocalPath) {
|
|
224
|
+
thumbMediaId = await this.uploadPermanentImageFromPath(firstLocalPath);
|
|
225
|
+
console.log('[WechatPublisher] 使用正文第一张图片作为封面(永久素材)');
|
|
226
|
+
}
|
|
227
|
+
else if (firstMediaId) {
|
|
228
|
+
// 外部 URL 图片用临时素材
|
|
190
229
|
thumbMediaId = firstMediaId;
|
|
191
|
-
console.log('[WechatPublisher]
|
|
230
|
+
console.log('[WechatPublisher] 使用正文第一张图片作为封面(临时素材)');
|
|
192
231
|
}
|
|
193
232
|
else {
|
|
194
233
|
// fallback:直接从 processedHtml 中查找 img 标签
|
|
@@ -197,12 +236,14 @@ export class WechatPublisher {
|
|
|
197
236
|
const src = m[1];
|
|
198
237
|
if (src.includes('mmbiz.qpic.cn') || src.includes('mmbiz.qlogo.cn'))
|
|
199
238
|
continue;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
239
|
+
if (src.startsWith('http')) {
|
|
240
|
+
try {
|
|
241
|
+
thumbMediaId = await this.uploadPermanentImageFromUrl(src);
|
|
242
|
+
console.log(`[WechatPublisher] 用正文图片作封面: ${src}`);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
catch { /* continue */ }
|
|
204
246
|
}
|
|
205
|
-
catch { /* continue */ }
|
|
206
247
|
}
|
|
207
248
|
}
|
|
208
249
|
}
|
|
@@ -8,7 +8,12 @@ export declare class WeixinAdapter implements IPlatformAdapter {
|
|
|
8
8
|
readonly meta: PlatformMeta;
|
|
9
9
|
private runtime;
|
|
10
10
|
private publisher;
|
|
11
|
+
private getPublisher;
|
|
11
12
|
init(runtime: RuntimeInterface): Promise<void>;
|
|
12
13
|
checkAuth(): Promise<AuthResult>;
|
|
14
|
+
processMermaid(html: string): Promise<{
|
|
15
|
+
html: string;
|
|
16
|
+
tempFiles: string[];
|
|
17
|
+
}>;
|
|
13
18
|
publish(article: Article): Promise<SyncResult>;
|
|
14
19
|
}
|
package/dist/adapters/weixin.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { WechatPublisher } from './wechat-publisher.js';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
2
4
|
export class WeixinAdapter {
|
|
3
5
|
meta = {
|
|
4
6
|
id: 'weixin',
|
|
@@ -9,13 +11,18 @@ export class WeixinAdapter {
|
|
|
9
11
|
};
|
|
10
12
|
runtime;
|
|
11
13
|
publisher;
|
|
14
|
+
getPublisher() {
|
|
15
|
+
if (!this.publisher) {
|
|
16
|
+
this.publisher = new WechatPublisher(this.runtime);
|
|
17
|
+
}
|
|
18
|
+
return this.publisher;
|
|
19
|
+
}
|
|
12
20
|
async init(runtime) {
|
|
13
21
|
this.runtime = runtime;
|
|
14
|
-
this.publisher = new WechatPublisher(runtime);
|
|
15
22
|
}
|
|
16
23
|
async checkAuth() {
|
|
17
24
|
try {
|
|
18
|
-
const token =
|
|
25
|
+
const token = this.getPublisher().getAccessToken();
|
|
19
26
|
return {
|
|
20
27
|
isAuthenticated: !!token,
|
|
21
28
|
userId: 'authenticated',
|
|
@@ -26,20 +33,49 @@ export class WeixinAdapter {
|
|
|
26
33
|
return { isAuthenticated: false, error: msg };
|
|
27
34
|
}
|
|
28
35
|
}
|
|
36
|
+
async processMermaid(html) {
|
|
37
|
+
const { processMermaid: convert } = await import('../core/renderer.js');
|
|
38
|
+
const { html: processed, tempFiles } = await convert(html, os.tmpdir());
|
|
39
|
+
// 上传图片到微信 CDN 并替换 URL
|
|
40
|
+
const imgPattern = /<img\s+[^>]*src=["']([^"']+)["'][^>]*>/gi;
|
|
41
|
+
let result = processed;
|
|
42
|
+
for (const match of [...processed.matchAll(imgPattern)]) {
|
|
43
|
+
const original = match[0];
|
|
44
|
+
const src = match[1];
|
|
45
|
+
if (!src.startsWith('http://') && !src.startsWith('https://')) {
|
|
46
|
+
try {
|
|
47
|
+
const url = await this.getPublisher().uploadImageForArticle(src);
|
|
48
|
+
result = result.replace(original, original.replace(src, url));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.warn(`[weixin] mermaid 图片上传失败 ${src}: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { html: result, tempFiles };
|
|
56
|
+
}
|
|
29
57
|
async publish(article) {
|
|
30
58
|
const start = Date.now();
|
|
31
59
|
try {
|
|
32
60
|
if (!article.html) {
|
|
33
61
|
throw new Error('article.html is required for WeChat publishing');
|
|
34
62
|
}
|
|
35
|
-
|
|
63
|
+
// 处理 Mermaid 代码块(转换为图片并上传 CDN)
|
|
64
|
+
let processedHtml = article.html;
|
|
65
|
+
if (this.processMermaid) {
|
|
66
|
+
const { html: mermaidHtml, tempFiles } = await this.processMermaid(article.html);
|
|
67
|
+
processedHtml = mermaidHtml;
|
|
68
|
+
// 清理临时文件
|
|
69
|
+
await Promise.all(tempFiles.map(f => fs.unlink(f).catch(() => { })));
|
|
70
|
+
}
|
|
71
|
+
const result = await this.getPublisher().publishToDraft({
|
|
36
72
|
title: article.title,
|
|
37
|
-
content:
|
|
73
|
+
content: processedHtml,
|
|
38
74
|
cover: article.cover ?? '',
|
|
39
75
|
author: article.author ?? '',
|
|
40
76
|
source_url: article.source_url ?? '',
|
|
41
77
|
});
|
|
42
|
-
const token = await this.
|
|
78
|
+
const token = await this.getPublisher().getAccessToken();
|
|
43
79
|
const draftUrl = `https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit&action=edit&type=77&appmsgid=${result.media_id}&token=${token}&lang=zh_CN`;
|
|
44
80
|
return {
|
|
45
81
|
platform: this.meta.id,
|
package/dist/cli/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export function createProgram() {
|
|
|
24
24
|
.requiredOption('-f, --file <path>', 'Markdown 文件路径(支持本地文件和 URL)')
|
|
25
25
|
.option('-p, --platform <platform>', '目标平台 (weixin|zhihu|juejin|csdn)', 'weixin')
|
|
26
26
|
.option('-t, --theme <theme-id>', '主题 ID', 'default')
|
|
27
|
+
.option('-m, --cover-mode <mode>', '封面模式 (sharp|network|auto)', 'auto')
|
|
27
28
|
.option('--app-id <appId>', '微信公众号 AppID(可省略,从配置文件读取)')
|
|
28
29
|
.option('--no-mac-style', '禁用 Mac 风格代码块')
|
|
29
30
|
.option('--auto-cover', '当文章无封面图时,根据标题自动从网络获取封面')
|
package/dist/cli/login.js
CHANGED
|
@@ -95,6 +95,9 @@ async function saveCookies(platformId, cookies) {
|
|
|
95
95
|
case 'cto51':
|
|
96
96
|
await ConfigStore.setCto51Cookies(cookies);
|
|
97
97
|
break;
|
|
98
|
+
case 'qq':
|
|
99
|
+
await ConfigStore.setQQCookies(cookies);
|
|
100
|
+
break;
|
|
98
101
|
default:
|
|
99
102
|
console.warn(`⚠️ 尚未为 ${platformId} 实现 Cookie 保存,请手动保存`);
|
|
100
103
|
}
|
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>;
|