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.
Files changed (42) hide show
  1. package/README.md +28 -22
  2. package/dist/adapters/csdn.d.ts +15 -0
  3. package/dist/adapters/csdn.js +136 -1
  4. package/dist/adapters/interface.d.ts +10 -0
  5. package/dist/adapters/qq.d.ts +19 -0
  6. package/dist/adapters/qq.js +185 -0
  7. package/dist/adapters/registry.js +2 -0
  8. package/dist/adapters/toutiao.d.ts +4 -0
  9. package/dist/adapters/toutiao.js +205 -40
  10. package/dist/adapters/wechat-publisher.d.ts +6 -1
  11. package/dist/adapters/wechat-publisher.js +58 -17
  12. package/dist/adapters/weixin.d.ts +5 -0
  13. package/dist/adapters/weixin.js +41 -5
  14. package/dist/cli/capture.d.ts +6 -0
  15. package/dist/cli/capture.js +49 -0
  16. package/dist/cli/index.js +8 -0
  17. package/dist/cli/login.js +3 -0
  18. package/dist/cli/platforms.js +2 -1
  19. package/dist/cli/publish-all.js +1 -1
  20. package/dist/cli/publish.d.ts +1 -0
  21. package/dist/cli/publish.js +29 -5
  22. package/dist/config.d.ts +8 -0
  23. package/dist/config.js +11 -0
  24. package/dist/core/parser.js +9 -1
  25. package/dist/core/renderer.d.ts +9 -1
  26. package/dist/core/renderer.js +86 -11
  27. package/dist/core/theme.d.ts +1 -1
  28. package/dist/core/theme.js +91 -336
  29. package/dist/runtime/browser-runtime.js +31 -2
  30. package/dist/tools/browser-upload.d.ts +13 -0
  31. package/dist/tools/browser-upload.js +349 -0
  32. package/dist/tools/capture.d.ts +26 -0
  33. package/dist/tools/capture.js +348 -0
  34. package/dist/tools/cover-fetcher.d.ts +10 -0
  35. package/dist/tools/cover-fetcher.js +100 -0
  36. package/dist/tools/imgbb-uploader.d.ts +18 -0
  37. package/dist/tools/imgbb-uploader.js +89 -0
  38. package/dist/tools/toutiao-upload.d.ts +10 -0
  39. package/dist/tools/toutiao-upload.js +166 -0
  40. package/package.json +1 -1
  41. package/themes/previews/cyberpunk.png +0 -0
  42. package/themes/previews/nord.png +0 -0
@@ -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🖥️ 支持的平台(共 20 个):\n');
25
+ console.log('\n🖥️ 支持的平台(共 21 个):\n');
25
26
  console.log('状态说明:');
26
27
  console.log(' ✅ 已验证 - 登录和发布功能已测试通过');
27
28
  console.log(' 🔄 待测试 - Cookie 登录支持,但发布功能尚未测试\n');
@@ -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,
@@ -5,4 +5,5 @@ export declare function runPublish(options: {
5
5
  appId?: string;
6
6
  macStyle?: boolean;
7
7
  autoCover?: boolean;
8
+ coverMode?: 'sharp' | 'network' | 'auto';
8
9
  }, input?: string): Promise<void>;
@@ -27,14 +27,30 @@ export async function runPublish(options, input) {
27
27
  else {
28
28
  throw new Error('请提供 -f 选项指定 Markdown 文件');
29
29
  }
30
- // 2. 渲染
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: options.theme || 'default',
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
- const platformId = options.platform || 'weixin';
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
- const coverImage = result.cover || result.autoCoverPath;
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();
@@ -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: parsed.attributes.title || '无标题',
40
+ title: title || '无标题',
33
41
  author: parsed.attributes.author,
34
42
  cover: parsed.attributes.cover,
35
43
  source_url: parsed.attributes.source_url,
@@ -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
- * 渲染 Markdown 文件为平台 HTML
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>;
@@ -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
- * 渲染 Markdown 文件为平台 HTML
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(/&lt;/g, '<')
30
+ .replace(/&gt;/g, '>')
31
+ .replace(/&amp;/g, '&')
32
+ .replace(/&quot;/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
- if (autoCover && !parsed.meta.cover && parsed.meta.title) {
27
- console.log(`[renderer] 未指定封面图,正在根据标题生成封面...`);
28
- const result = await generateCover(parsed.meta.title, parsed.meta.description || '');
29
- if (result.success && result.localPath) {
30
- autoCoverPath = result.localPath;
31
- console.log(`[renderer] 自动封面图生成成功: ${result.localPath}`);
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
- console.warn(`[renderer] 自动封面图生成失败: ${result.error}`);
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 {
@@ -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: 1em 0 0.5em 0;\n}\nh1 { font-size: 1.25em; line-height: 1.4em; }\nh2 { font-size: 1.125em; }\nh3 { font-size: 1.05em; }\nh4, h5, h6 { font-size: 1em; }\nli p { margin: 0; }\nul, ol { margin: 0; padding-left: 2em; }\nli { margin: 0; padding: 0; line-height: normal; }\nli + li { margin-top: 0.3em; }\npre {\n background-color: #f6f8fa;\n border-radius: 6px;\n padding: 16px;\n overflow-x: auto;\n font-size: 14px;\n line-height: 1.6;\n margin: 1em 0;\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; font-size: 14px; }\nblockquote { border-left: 4px solid #ddd; padding-left: 1em; margin: 1em 0; color: #666; }\nhr { border: none; border-top: 1px solid #ddd; margin: 1.5em 0; }\ni, cite, em, var, address { font-style: italic; }\nb, strong { font-weight: bolder; }\nimg { max-width: 100%; height: auto; display: block; margin: 1em auto; }\ntable { border-collapse: collapse; width: 100%; margin: 1em 0; }\ntable th, table td { border: 1px solid #ddd; padding: 8px 12px; }\ntable th { background-color: #f6f8fa; font-weight: bold; }\na { color: #0579b7; text-decoration: none; }\n";
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>;