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
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **一行命令,Markdown 发布到全网。**
4
4
 
5
- 将排版好的 Markdown 文章一键发布到微信公众号、知乎、掘金、CSDN 等 20+ 平台,无需手动复制粘贴。支持浏览器自动登录获取 Cookie,14 套渲染主题,是内容创作者的效率神器。
5
+ 将排版好的 Markdown 文章一键发布到微信公众号、知乎、掘金、CSDN 等 20+ 平台,无需手动复制粘贴。支持浏览器自动登录获取 Cookie,4 套精排渲染主题(按平台自动匹配),是内容创作者的效率神器。
6
6
 
7
7
  ---
8
8
 
@@ -12,7 +12,7 @@
12
12
  |------|------|
13
13
  | **多平台支持** | 微信公众号、知乎、掘金、CSDN、微博、小红书、B站等 20+ 平台 |
14
14
  | **浏览器自动登录** | Playwright 驱动扫码/账号登录,Cookie 自动获取保存 |
15
- | **14 套渲染主题** | defaultwechatmodernminimal、cyberpunk、nord、paper、darkelite、sunset、zen、retro、midnight、brutalism、neumorphism |
15
+ | **4 套精排主题** | default(通用)、wechat(公众号)、modern(技术/知乎)、minimal(轻阅读/头条),均按平台自动匹配 |
16
16
  | **自动封面图生成** | AI 根据标题生成封面图,自动上传到微信 CDN |
17
17
  | **Markdown 直发** | front-matter 元数据、代码高亮、LaTeX 公式 |
18
18
  | **CI/CD 友好** | 纯命令行无需浏览器,配置文件统一管理 |
@@ -39,7 +39,7 @@ mpub --version
39
39
  ```bash
40
40
  # 在任意 Claude Code 对话中,直接描述你的需求:
41
41
  # "帮我把这篇 article.md 发布到微信公众号"
42
- # "用 cyberpunk 主题渲染预览一下"
42
+ # "用 wechat 主题渲染预览一下"
43
43
  # AI 会调用 mpub 完成操作
44
44
  ```
45
45
 
@@ -95,7 +95,7 @@ mpub publish-all -f article.md
95
95
  mpub render -f article.md
96
96
 
97
97
  # 指定主题渲染
98
- mpub render -f article.md -t cyberpunk
98
+ mpub render -f article.md -t wechat
99
99
 
100
100
  # 自动封面图(文章无封面时 AI 生成)
101
101
  mpub publish -f article.md -p weixin --auto-cover
@@ -152,37 +152,43 @@ mpub platforms
152
152
 
153
153
  ## 主题系统
154
154
 
155
- ### 🎨 14 套内置主题
155
+ ### 🎨 4 套内置主题(按平台自动匹配)
156
156
 
157
- 完整预览:[themes/all-themes-preview.html](themes/all-themes-preview.html)
157
+ mpub 内置 4 套浅色阅读主题,均针对公众号 / 知乎 / 头条等白底平台的真实排版规范做了优化:
158
+
159
+ | 主题 | 风格 | 适配平台 |
160
+ |------|------|----------|
161
+ | `default` | 通用简洁,绿调标题/引用 | 兜底,所有平台可用 |
162
+ | `wechat` | 微信官方排版(微信蓝链接、浅灰代码块、圆角图) | 微信公众号 |
163
+ | `modern` | 深色代码块、清晰层次(知乎/网页友好) | 知乎、技术博客 |
164
+ | `minimal` | 极简留白、轻装饰 | 头条号等轻阅读平台 |
158
165
 
159
- **主题截图示例:**
166
+ **平台自动匹配**:未用 `-t` 指定主题时,发布按平台自动选默认主题——
167
+ 微信 → `wechat`,知乎 → `modern`,头条 → `minimal`,其余回退 `default`。
168
+ (手动 `-t` 仍可覆盖。)
160
169
 
161
- | 风格 | 代表主题 | 预览 |
162
- |------|----------|------|
163
- | 技术博客 | Cyberpunk | ![Cyberpunk](themes/previews/cyberpunk.png) |
164
- | 程序员 | Nord | ![Nord](themes/previews/nord.png) |
165
- | 微信风格 | Wechat | ![Wechat](themes/previews/wechat.png) |
166
- | 简约 | Minimal | ![Minimal](themes/previews/minimal.png) |
170
+ 完整预览:[themes/all-themes-preview.html](themes/all-themes-preview.html)
167
171
 
168
172
  **命令行预览:**
169
173
  ```bash
170
- mpub render -f article.md -t cyberpunk
171
- mpub render -f article.md -t nord
172
- mpub render -f article.md -t paper
174
+ mpub render -f article.md -t wechat
175
+ mpub render -f article.md -t modern
176
+ mpub render -f article.md -t minimal
173
177
  ```
174
178
 
175
- **全部 14 主题:** default、wechat、modern、minimal、cyberpunk、nord、paper、darkelite、sunset、zen、retro、midnight、brutalism、neumorphism
179
+ **全部 4 主题:** default、wechat、modern、minimal
176
180
 
177
181
  ### 使用主题
178
182
 
179
183
  ```bash
180
- # 渲染时指定主题
181
- mpub render -f article.md -t cyberpunk
182
- mpub render -f article.md -t nord
183
- mpub render -f article.md -t paper
184
+ # 不指定主题:发布到微信自动用 wechat,知乎自动用 modern
185
+ mpub publish -f article.md -p weixin
186
+ mpub publish -f article.md -p zhihu
187
+
188
+ # 渲染时手动指定主题
189
+ mpub render -f article.md -t wechat
184
190
 
185
- # 发布时指定主题
191
+ # 发布时手动覆盖主题
186
192
  mpub publish -f article.md -t modern -p zhihu
187
193
  ```
188
194
 
@@ -24,6 +24,21 @@ export declare class CSDNAdapter implements IPlatformAdapter {
24
24
  * 生成 CSDN API 签名
25
25
  */
26
26
  private signRequest;
27
+ /**
28
+ * 上传图片到 CSDN
29
+ * 流程:1. 调用签名 API 获取 policy 2. 直接上传到华为云 OBS
30
+ * @param imagePath 本地文件路径或网络 URL
31
+ * @returns CSDN 图片 URL
32
+ */
33
+ uploadImage(imagePath: string): Promise<string>;
34
+ /**
35
+ * 获取华为云 OBS 上传签名
36
+ */
37
+ private getUploadSignature;
38
+ /**
39
+ * 处理封面图:上传本地文件或使用网络 URL
40
+ */
41
+ private processCover;
27
42
  checkAuth(): Promise<AuthResult>;
28
43
  publish(article: Article): Promise<SyncResult>;
29
44
  }
@@ -1,8 +1,16 @@
1
1
  import { ConfigStore } from '../config.js';
2
+ import { uploadCoverViaBrowser } from '../tools/browser-upload.js';
3
+ import { downloadCoverUrl } from '../tools/cover-fetcher.js';
2
4
  import crypto from 'node:crypto';
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { existsSync } from 'node:fs';
3
8
  // CSDN API 签名密钥
4
9
  const API_KEY = '203803574';
5
10
  const API_SECRET = '9znpamsyl2c7cdrr9sas0le9vbc3r6ba';
11
+ // 封面图上传 API 的 key 和 secret
12
+ const COVER_API_KEY = '260196572';
13
+ const COVER_API_SECRET = 't5PaqxVQpWoHgLGt7XPIvd5ipJcwJTU7';
6
14
  export class CSDNAdapter {
7
15
  meta = {
8
16
  id: 'csdn',
@@ -75,6 +83,106 @@ export class CSDNAdapter {
75
83
  }
76
84
  return headers;
77
85
  }
86
+ /**
87
+ * 上传图片到 CSDN
88
+ * 流程:1. 调用签名 API 获取 policy 2. 直接上传到华为云 OBS
89
+ * @param imagePath 本地文件路径或网络 URL
90
+ * @returns CSDN 图片 URL
91
+ */
92
+ async uploadImage(imagePath) {
93
+ if (!this.cookieData?.cookies) {
94
+ throw new Error('未配置 CSDN Cookie');
95
+ }
96
+ // 如果是网络 URL,直接返回
97
+ if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
98
+ return imagePath;
99
+ }
100
+ // 读取本地文件
101
+ const fileBuffer = await fs.readFile(imagePath);
102
+ const filename = path.basename(imagePath);
103
+ const ext = path.extname(filename).toLowerCase();
104
+ const imageSuffix = ext.replace('.', '');
105
+ try {
106
+ // 步骤 1: 获取上传签名 (policy)
107
+ const signatureData = await this.getUploadSignature(imageSuffix);
108
+ const { accessId, policy, signature, host, dir } = signatureData;
109
+ // 步骤 2: 直接上传到华为云 OBS
110
+ const objectKey = `${dir}${filename}`;
111
+ const obsUrl = `${host}/${objectKey}`;
112
+ const formData = new FormData();
113
+ formData.append('key', objectKey);
114
+ formData.append('OSSAccessKeyId', accessId);
115
+ formData.append('policy', policy);
116
+ formData.append('signature', signature);
117
+ formData.append('expire', signatureData.expire);
118
+ formData.append('file', new Blob([fileBuffer]), filename);
119
+ const response = await this.runtime.fetch(obsUrl, {
120
+ method: 'POST',
121
+ body: formData,
122
+ });
123
+ if (!response.ok) {
124
+ throw new Error(`OBS 上传失败: ${response.status}`);
125
+ }
126
+ // 返回 CSDN 图片 URL
127
+ return `https://csdn-img-blog.obs.cn-north-4.myhuaweicloud.com/${objectKey}`;
128
+ }
129
+ catch (apiError) {
130
+ throw new Error(`封面 API 上传失败: ${apiError.message}`);
131
+ }
132
+ }
133
+ /**
134
+ * 获取华为云 OBS 上传签名
135
+ */
136
+ async getUploadSignature(imageSuffix) {
137
+ const cookieStr = this.buildCookieString(this.cookieData);
138
+ // CSDN 封面图签名 API 使用的 key 和之前的不同
139
+ const coverApiKey = '260196572';
140
+ const nonce = this.createUuid();
141
+ const timestamp = Date.now().toString();
142
+ const apiPath = '/resource-api/v1/image/direct/upload/signature';
143
+ // 按照 Lt 函数格式: method\naccept\n\ncontentType\ntimestamp\n(sorted headers)\nurl
144
+ const signStr = `POST\n*/*\n\napplication/json\n${timestamp}\nx-ca-key:${coverApiKey}\nx-ca-nonce:${nonce}\nx-ca-timestamp:${timestamp}\n${apiPath}`;
145
+ // 封面签名使用专用的 secret
146
+ const signature = await this.hmacSha256(signStr, COVER_API_SECRET);
147
+ const response = await this.runtime.fetch(`https://bizapi.csdn.net${apiPath}`, {
148
+ method: 'POST',
149
+ headers: {
150
+ 'accept': 'application/json',
151
+ 'x-ca-key': coverApiKey,
152
+ 'x-ca-nonce': nonce,
153
+ 'x-ca-timestamp': timestamp,
154
+ 'x-ca-signature': signature,
155
+ 'x-ca-signature-headers': 'x-ca-key,x-ca-nonce,x-ca-timestamp',
156
+ 'content-type': 'application/json',
157
+ 'Cookie': cookieStr,
158
+ },
159
+ body: JSON.stringify({
160
+ imageTemplate: '',
161
+ appName: 'direct_blog_coverimage',
162
+ imageSuffix,
163
+ }),
164
+ });
165
+ const res = await response.json();
166
+ if (res.code !== 200 || !res.data) {
167
+ throw new Error(res.message || '获取上传签名失败');
168
+ }
169
+ return res.data;
170
+ }
171
+ /**
172
+ * 处理封面图:上传本地文件或使用网络 URL
173
+ */
174
+ async processCover(cover) {
175
+ if (!cover)
176
+ return [];
177
+ try {
178
+ const coverUrl = await this.uploadImage(cover);
179
+ return [coverUrl];
180
+ }
181
+ catch (err) {
182
+ console.warn(`[CSDN] 封面上传失败: ${err.message}`);
183
+ return [];
184
+ }
185
+ }
78
186
  async checkAuth() {
79
187
  if (!this.cookieData?.cookies) {
80
188
  return { isAuthenticated: false, error: '未配置 CSDN Cookie,请运行: mpub cookie --platform csdn --set' };
@@ -118,6 +226,7 @@ export class CSDNAdapter {
118
226
  // 生成签名
119
227
  const apiPath = '/blog-console-api/v3/mdeditor/saveArticle';
120
228
  const headers = await this.signRequest(apiPath);
229
+ // 先发布文章(不带封面,因为 API 签名有问题)
121
230
  const response = await this.runtime.fetch(`https://bizapi.csdn.net${apiPath}`, {
122
231
  method: 'POST',
123
232
  headers: {
@@ -139,7 +248,7 @@ export class CSDNAdapter {
139
248
  not_auto_saved: '1',
140
249
  source: 'pc_mdeditor',
141
250
  cover_images: [],
142
- cover_type: 1,
251
+ cover_type: 0,
143
252
  is_new: 1,
144
253
  vote_id: 0,
145
254
  resource_id: '',
@@ -153,6 +262,32 @@ export class CSDNAdapter {
153
262
  }
154
263
  const postId = res.data.id;
155
264
  const draftUrl = `https://editor.csdn.net/md?articleId=${postId}`;
265
+ // 如果有封面图片,先下载 URL 封面到本地,再通过浏览器上传
266
+ if (article.cover) {
267
+ let localCover = article.cover;
268
+ // 如果是 URL 封面,先下载到本地
269
+ if (!existsSync(article.cover)) {
270
+ console.log(`[CSDN] 封面是 URL,正在下载到本地...`);
271
+ const downloadResult = await downloadCoverUrl(article.cover);
272
+ if (!downloadResult.success || !downloadResult.localPath) {
273
+ console.warn(`[CSDN] 封面下载失败: ${downloadResult.error},跳过封面上传`);
274
+ }
275
+ else {
276
+ localCover = downloadResult.localPath;
277
+ console.log(`[CSDN] 封面下载成功: ${localCover}`);
278
+ }
279
+ }
280
+ if (existsSync(localCover)) {
281
+ console.log(`[CSDN] 封面上传中...`);
282
+ const coverResult = await uploadCoverViaBrowser(postId, localCover);
283
+ if (coverResult.success && coverResult.coverUrl) {
284
+ console.log(`[CSDN] 封面 URL: ${coverResult.coverUrl}`);
285
+ }
286
+ else {
287
+ console.warn(`[CSDN] 封面上传失败: ${coverResult.error}`);
288
+ }
289
+ }
290
+ }
156
291
  return {
157
292
  platform: this.meta.id,
158
293
  success: true,
@@ -36,9 +36,19 @@ export interface AuthResult {
36
36
  avatar?: string;
37
37
  error?: string;
38
38
  }
39
+ export interface MermaidProcessResult {
40
+ html: string;
41
+ tempFiles: string[];
42
+ }
39
43
  export interface IPlatformAdapter {
40
44
  readonly meta: PlatformMeta;
41
45
  init(runtime: RuntimeInterface): Promise<void>;
42
46
  checkAuth(): Promise<AuthResult>;
47
+ /**
48
+ * 可选钩子:自定义处理 Mermaid 代码块
49
+ * 如果平台不支持原生 Mermaid 渲染,实现此方法进行转换
50
+ * 返回处理后的 HTML 和需要清理的临时文件路径
51
+ */
52
+ processMermaid?(html: string): Promise<MermaidProcessResult>;
43
53
  publish(article: Article): Promise<SyncResult>;
44
54
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 企鹅号适配器
3
+ * 认证方式: Cookie
4
+ * 发布方式: API (editorCache/update)
5
+ */
6
+ import type { IPlatformAdapter, Article, SyncResult, AuthResult, PlatformMeta } from './interface.js';
7
+ import type { RuntimeInterface } from '../runtime/index.js';
8
+ export declare class QQAdapter implements IPlatformAdapter {
9
+ readonly meta: PlatformMeta;
10
+ private runtime;
11
+ init(runtime: RuntimeInterface): Promise<void>;
12
+ private getCookies;
13
+ checkAuth(): Promise<AuthResult>;
14
+ /**
15
+ * 上传封面图片到企鹅号,返回图片 URL
16
+ */
17
+ private uploadCover;
18
+ publish(article: Article): Promise<SyncResult>;
19
+ }
@@ -0,0 +1,185 @@
1
+ import { ConfigStore } from '../config.js';
2
+ import { existsSync } from 'node:fs';
3
+ export class QQAdapter {
4
+ meta = {
5
+ id: 'qq',
6
+ name: '企鹅号',
7
+ icon: 'https://inews.gtimg.com/news_lite/20210806/favicon.ico',
8
+ homepage: 'https://om.qq.com/',
9
+ capabilities: ['article', 'draft', 'image_upload'],
10
+ };
11
+ runtime;
12
+ async init(runtime) {
13
+ this.runtime = runtime;
14
+ }
15
+ async getCookies() {
16
+ return await ConfigStore.getQQCookies();
17
+ }
18
+ async checkAuth() {
19
+ const cookieData = await this.getCookies();
20
+ if (!cookieData || Object.keys(cookieData).length === 0) {
21
+ return { isAuthenticated: false, error: '未配置企鹅号 Cookie,请运行: mpub login --platform qq' };
22
+ }
23
+ if (!cookieData['userid']) {
24
+ return { isAuthenticated: false, error: 'Cookie 中缺少 userid,可能登录已失效' };
25
+ }
26
+ try {
27
+ return {
28
+ isAuthenticated: true,
29
+ userId: cookieData['userid'],
30
+ username: cookieData['pt2gguin'] ? `o${cookieData['pt2gguin']}` : undefined,
31
+ };
32
+ }
33
+ catch (err) {
34
+ return { isAuthenticated: false, error: err.message };
35
+ }
36
+ }
37
+ /**
38
+ * 上传封面图片到企鹅号,返回图片 URL
39
+ */
40
+ async uploadCover(imagePath, cookieStr) {
41
+ const FormData = require('form-data');
42
+ const fs = require('fs');
43
+ const form = new FormData();
44
+ const fileBuffer = fs.readFileSync(imagePath);
45
+ const fileName = imagePath.split(/[\\/]/).pop() || 'image.jpg';
46
+ form.append('file', fileBuffer, {
47
+ filename: fileName,
48
+ contentType: 'image/jpeg',
49
+ });
50
+ const response = await this.runtime.fetch('https://image.om.qq.com/cpom_pimage/ArchacaleUploadViaFile', {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Cookie': cookieStr,
54
+ 'Referer': 'https://om.qq.com/',
55
+ 'X-Requested-With': 'XMLHttpRequest',
56
+ ...form.getHeaders(),
57
+ },
58
+ body: form,
59
+ });
60
+ if (!response.ok) {
61
+ return null;
62
+ }
63
+ try {
64
+ const data = await response.json();
65
+ if (data.code === 0 && data.data?.url) {
66
+ const urlData = data.data.url;
67
+ return urlData['640']?.imageUrl || urlData['0']?.imageUrl || Object.values(urlData)[0]?.imageUrl || null;
68
+ }
69
+ }
70
+ catch { }
71
+ return null;
72
+ }
73
+ async publish(article) {
74
+ const start = Date.now();
75
+ try {
76
+ const cookieData = await this.getCookies();
77
+ if (!cookieData || Object.keys(cookieData).length === 0) {
78
+ throw new Error('未配置企鹅号 Cookie,请运行: mpub login --platform qq');
79
+ }
80
+ const cookieStr = Object.entries(cookieData)
81
+ .map(([k, v]) => `${k}=${v}`)
82
+ .join('; ');
83
+ const mediaId = cookieData['userid'];
84
+ // 上传封面(如果有)
85
+ let coverUrl = '';
86
+ if (article.cover && existsSync(article.cover)) {
87
+ try {
88
+ coverUrl = await this.uploadCover(article.cover, cookieStr) || '';
89
+ console.log(`[QQ] 封面上传结果: ${coverUrl || '失败'}`);
90
+ }
91
+ catch (err) {
92
+ console.warn(`封面图上传失败: ${err.message}`);
93
+ }
94
+ }
95
+ // 构建文章数据
96
+ const articleData = {
97
+ title: article.title,
98
+ title2: '',
99
+ tag: '',
100
+ video: '',
101
+ cover_type: coverUrl ? '1' : '1',
102
+ imgurl_ext: '[]',
103
+ category_id: '',
104
+ content: `<p>${(article.markdown || article.html || '').replace(/<[^>]+>/g, '')}</p>`,
105
+ orignal: 0,
106
+ user_original: 0,
107
+ music: '',
108
+ activity: '',
109
+ apply_olympic_flag: 0,
110
+ apply_push_flag: 0,
111
+ apply_reward_flag: 0,
112
+ reward_flag: 0,
113
+ survey_id: '',
114
+ survey_name: '',
115
+ imgurlsrc: coverUrl || null,
116
+ om_activity_id: '',
117
+ om_activity_name: '',
118
+ activityInfo: '',
119
+ commercialization_source: '',
120
+ caiMaiInfo: '',
121
+ isHowto: '0',
122
+ howtoInfo: '',
123
+ daihuoInfo: '',
124
+ novel: '',
125
+ needpub: 1,
126
+ event_id: '',
127
+ event_name: '',
128
+ activity_scene_id: 0,
129
+ hotBreak: '',
130
+ self_declare: '',
131
+ resource_aigc_mark_info: '{}',
132
+ parent_article_id: '',
133
+ conclusion: '',
134
+ summary: article.summary || '',
135
+ failedImage: [],
136
+ adContentImgs: [],
137
+ mediaId: mediaId,
138
+ type: 0,
139
+ unmount: false,
140
+ };
141
+ // 发送保存请求
142
+ const postData = new URLSearchParams();
143
+ postData.append('cache', JSON.stringify(articleData));
144
+ postData.append('mediaid', mediaId);
145
+ const response = await this.runtime.fetch('https://om.qq.com/editorCache/update', {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/x-www-form-urlencoded',
149
+ 'Cookie': cookieStr,
150
+ 'Referer': 'https://om.qq.com/main/creation/article',
151
+ },
152
+ body: postData.toString(),
153
+ });
154
+ const responseText = await response.text();
155
+ if (!response.ok) {
156
+ throw new Error(`保存文章失败: ${response.status} - ${responseText.substring(0, 100)}`);
157
+ }
158
+ let resultData;
159
+ try {
160
+ resultData = JSON.parse(responseText);
161
+ }
162
+ catch {
163
+ throw new Error(`保存响应无效: ${responseText.substring(0, 100)}`);
164
+ }
165
+ if (resultData.response?.code !== '0') {
166
+ throw new Error(resultData.response?.msg || '保存文章失败');
167
+ }
168
+ return {
169
+ platform: this.meta.id,
170
+ success: true,
171
+ draftOnly: true, // 企鹅号需要审核
172
+ timestamp: Date.now() - start,
173
+ postUrl: 'https://om.qq.com/main/creation/article',
174
+ };
175
+ }
176
+ catch (err) {
177
+ return {
178
+ platform: this.meta.id,
179
+ success: false,
180
+ error: err.message,
181
+ timestamp: Date.now() - start,
182
+ };
183
+ }
184
+ }
185
+ }
@@ -18,6 +18,7 @@ import { YuqueAdapter } from './yuque.js';
18
18
  import { Cto51Adapter } from './cto51.js';
19
19
  import { ToutiaoAdapter } from './toutiao.js';
20
20
  import { XiaohongshuAdapter } from './xiaohongshu.js';
21
+ import { QQAdapter } from './qq.js';
21
22
  // 所有适配器类
22
23
  const ADAPTER_CLASSES = {
23
24
  weixin: WeixinAdapter,
@@ -40,6 +41,7 @@ const ADAPTER_CLASSES = {
40
41
  cto51: Cto51Adapter,
41
42
  toutiao: ToutiaoAdapter,
42
43
  xiaohongshu: XiaohongshuAdapter,
44
+ qq: QQAdapter,
43
45
  };
44
46
  /**
45
47
  * 适配器注册表
@@ -4,6 +4,10 @@ export declare class ToutiaoAdapter implements IPlatformAdapter {
4
4
  private cookieData;
5
5
  init(): Promise<void>;
6
6
  checkAuth(): Promise<AuthResult>;
7
+ processMermaid(html: string): Promise<{
8
+ html: string;
9
+ tempFiles: string[];
10
+ }>;
7
11
  publish(article: Article): Promise<SyncResult>;
8
12
  }
9
13
  import type { IPlatformAdapter } from './interface.js';