@stylewx/publisher 0.1.0
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/dist/client.d.ts +59 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +120 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/cover.d.ts +8 -0
- package/dist/cover.d.ts.map +1 -0
- package/dist/cover.js +67 -0
- package/dist/cover.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/publish.d.ts +51 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +79 -0
- package/dist/publish.js.map +1 -0
- package/dist/publisher.test.d.ts +2 -0
- package/dist/publisher.test.d.ts.map +1 -0
- package/dist/publisher.test.js +196 -0
- package/dist/publisher.test.js.map +1 -0
- package/dist/relocate.d.ts +25 -0
- package/dist/relocate.d.ts.map +1 -0
- package/dist/relocate.js +86 -0
- package/dist/relocate.js.map +1 -0
- package/package.json +33 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { PublisherConfig } from './config.js';
|
|
2
|
+
export interface UploadResult {
|
|
3
|
+
media_id: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DraftArticle {
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
author?: string;
|
|
10
|
+
digest?: string;
|
|
11
|
+
thumb_media_id: string;
|
|
12
|
+
content_source_url?: string;
|
|
13
|
+
need_open_comment?: boolean;
|
|
14
|
+
only_fans_can_comment?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface DraftResult {
|
|
17
|
+
media_id: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 微信公众号 API 客户端。
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ 安全边界(有意为之):本客户端只实现「发布到草稿箱」draft/add,
|
|
23
|
+
* 绝不实现群发接口(freepublish/submit)。所有内容在进入草稿箱后仍需人工在
|
|
24
|
+
* 公众号后台确认,不提供任何绕过人工确认的自动化群发能力。
|
|
25
|
+
*/
|
|
26
|
+
export declare class WeChatClient {
|
|
27
|
+
private readonly baseUrl;
|
|
28
|
+
private readonly fetchImpl;
|
|
29
|
+
private readonly appId;
|
|
30
|
+
private readonly appSecret;
|
|
31
|
+
private tokenCache;
|
|
32
|
+
constructor(config: PublisherConfig);
|
|
33
|
+
/**
|
|
34
|
+
* 获取 access_token。
|
|
35
|
+
* 使用内存缓存;当剩余有效期不足 5 分钟时提前刷新,避免 token 在请求中途失效。
|
|
36
|
+
*/
|
|
37
|
+
getAccessToken(): Promise<string>;
|
|
38
|
+
/** 强制清空 token 缓存(测试用 / 手动刷新)。 */
|
|
39
|
+
clearTokenCache(): void;
|
|
40
|
+
/**
|
|
41
|
+
* 上传图片到微信公众号「永久素材库」。返回 { media_id, url }。
|
|
42
|
+
* url 即 mmbiz.qpic.cn 图床地址,可安全内联到文章 content 中。
|
|
43
|
+
*/
|
|
44
|
+
uploadImage(buffer: ArrayBuffer | Uint8Array, filename: string, mimeType?: string): Promise<UploadResult>;
|
|
45
|
+
/** 上传封面缩略图(type=thumb),返回 media_id。 */
|
|
46
|
+
uploadThumb(buffer: ArrayBuffer | Uint8Array, filename: string, mimeType?: string): Promise<UploadResult>;
|
|
47
|
+
private uploadMaterial;
|
|
48
|
+
/** 下载任意图片为字节(用于搬运外链图到素材库)。 */
|
|
49
|
+
downloadImage(url: string): Promise<{
|
|
50
|
+
bytes: Uint8Array;
|
|
51
|
+
mimeType: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* 新增文章到「草稿箱」。
|
|
55
|
+
* ⚠️ 仅 draft/add;不含任何群发(freepublish/submit)逻辑。
|
|
56
|
+
*/
|
|
57
|
+
addDraft(article: DraftArticle): Promise<DraftResult>;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAOlD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAID;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,UAAU,CAA0B;gBAEhC,MAAM,EAAE,eAAe;IAQnC;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAqBvC,iCAAiC;IACjC,eAAe,IAAI,IAAI;IAIvB;;;OAGG;IACG,WAAW,CACf,MAAM,EAAE,WAAW,GAAG,UAAU,EAChC,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC;IAIxB,uCAAuC;IACjC,WAAW,CACf,MAAM,EAAE,WAAW,GAAG,UAAU,EAChC,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC;YAIV,cAAc;IAwB5B,8BAA8B;IACxB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAUlF;;;OAGG;IACG,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;CAc5D"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { toHttpConfig } from './config.js';
|
|
2
|
+
const TOKEN_REFRESH_EARLY_MS = 5 * 60 * 1000; // 提前 5 分钟刷新
|
|
3
|
+
/**
|
|
4
|
+
* 微信公众号 API 客户端。
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ 安全边界(有意为之):本客户端只实现「发布到草稿箱」draft/add,
|
|
7
|
+
* 绝不实现群发接口(freepublish/submit)。所有内容在进入草稿箱后仍需人工在
|
|
8
|
+
* 公众号后台确认,不提供任何绕过人工确认的自动化群发能力。
|
|
9
|
+
*/
|
|
10
|
+
export class WeChatClient {
|
|
11
|
+
baseUrl;
|
|
12
|
+
fetchImpl;
|
|
13
|
+
appId;
|
|
14
|
+
appSecret;
|
|
15
|
+
tokenCache = null;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
const http = toHttpConfig(config);
|
|
18
|
+
this.baseUrl = http.baseUrl;
|
|
19
|
+
this.fetchImpl = http.fetchImpl;
|
|
20
|
+
this.appId = config.appId;
|
|
21
|
+
this.appSecret = config.appSecret;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 获取 access_token。
|
|
25
|
+
* 使用内存缓存;当剩余有效期不足 5 分钟时提前刷新,避免 token 在请求中途失效。
|
|
26
|
+
*/
|
|
27
|
+
async getAccessToken() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (this.tokenCache && this.tokenCache.expiresAt - TOKEN_REFRESH_EARLY_MS > now) {
|
|
30
|
+
return this.tokenCache.token;
|
|
31
|
+
}
|
|
32
|
+
const url = `${this.baseUrl}/cgi-bin/token?grant_type=client_credential` +
|
|
33
|
+
`&appid=${encodeURIComponent(this.appId)}&secret=${encodeURIComponent(this.appSecret)}`;
|
|
34
|
+
const response = await this.fetchImpl(url, { method: 'GET' });
|
|
35
|
+
const data = await parseJson(await response.text());
|
|
36
|
+
if (!data.access_token || !data.expires_in) {
|
|
37
|
+
throw apiError('获取 access_token 失败', data);
|
|
38
|
+
}
|
|
39
|
+
const expiresIn = Number(data.expires_in);
|
|
40
|
+
this.tokenCache = {
|
|
41
|
+
token: String(data.access_token),
|
|
42
|
+
expiresAt: now + expiresIn * 1000,
|
|
43
|
+
};
|
|
44
|
+
return this.tokenCache.token;
|
|
45
|
+
}
|
|
46
|
+
/** 强制清空 token 缓存(测试用 / 手动刷新)。 */
|
|
47
|
+
clearTokenCache() {
|
|
48
|
+
this.tokenCache = null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 上传图片到微信公众号「永久素材库」。返回 { media_id, url }。
|
|
52
|
+
* url 即 mmbiz.qpic.cn 图床地址,可安全内联到文章 content 中。
|
|
53
|
+
*/
|
|
54
|
+
async uploadImage(buffer, filename, mimeType) {
|
|
55
|
+
return this.uploadMaterial('image', buffer, filename, mimeType);
|
|
56
|
+
}
|
|
57
|
+
/** 上传封面缩略图(type=thumb),返回 media_id。 */
|
|
58
|
+
async uploadThumb(buffer, filename, mimeType) {
|
|
59
|
+
return this.uploadMaterial('thumb', buffer, filename, mimeType);
|
|
60
|
+
}
|
|
61
|
+
async uploadMaterial(type, buffer, filename, mimeType) {
|
|
62
|
+
const token = await this.getAccessToken();
|
|
63
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
64
|
+
const blob = new Blob([bytes], { type: mimeType ?? 'application/octet-stream' });
|
|
65
|
+
const form = new FormData();
|
|
66
|
+
form.append('media', blob, filename);
|
|
67
|
+
const url = `${this.baseUrl}/cgi-bin/material/add_material?access_token=${encodeURIComponent(token)}&type=${type}`;
|
|
68
|
+
const response = await this.fetchImpl(url, { method: 'POST', body: form });
|
|
69
|
+
const data = await parseJson(await response.text());
|
|
70
|
+
if (!data.media_id) {
|
|
71
|
+
throw apiError(`上传素材失败(type=${type})`, data);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
media_id: String(data.media_id),
|
|
75
|
+
url: typeof data.url === 'string' ? data.url : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 下载任意图片为字节(用于搬运外链图到素材库)。 */
|
|
79
|
+
async downloadImage(url) {
|
|
80
|
+
const response = await this.fetchImpl(url);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`下载图片失败(HTTP ${response.status}):${url}`);
|
|
83
|
+
}
|
|
84
|
+
const mimeType = response.headers.get('content-type')?.split(';')[0]?.trim() ?? 'image/jpeg';
|
|
85
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
86
|
+
return { bytes: new Uint8Array(arrayBuffer), mimeType };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 新增文章到「草稿箱」。
|
|
90
|
+
* ⚠️ 仅 draft/add;不含任何群发(freepublish/submit)逻辑。
|
|
91
|
+
*/
|
|
92
|
+
async addDraft(article) {
|
|
93
|
+
const token = await this.getAccessToken();
|
|
94
|
+
const url = `${this.baseUrl}/cgi-bin/draft/add?access_token=${encodeURIComponent(token)}`;
|
|
95
|
+
const response = await this.fetchImpl(url, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'content-type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ articles: [article] }),
|
|
99
|
+
});
|
|
100
|
+
const data = await parseJson(await response.text());
|
|
101
|
+
if (!data.media_id) {
|
|
102
|
+
throw apiError('发布草稿失败(draft/add)', data);
|
|
103
|
+
}
|
|
104
|
+
return { media_id: String(data.media_id) };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function parseJson(text) {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(text);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { raw: text };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function apiError(prefix, data) {
|
|
116
|
+
const code = data.errcode;
|
|
117
|
+
const message = data.errmsg ? String(data.errmsg) : JSON.stringify(data);
|
|
118
|
+
return new Error(`${prefix}:errcode=${String(code)},errmsg=${message}`);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信公众号发布配置。
|
|
3
|
+
* 仅通过环境变量注入凭据(绝不硬编码/提交):
|
|
4
|
+
* - WECHAT_APP_ID 公众号 AppID
|
|
5
|
+
* - WECHAT_APP_SECRET 公众号 AppSecret
|
|
6
|
+
* - WECHAT_API_BASE 可选,默认 https://api.weixin.qq.com
|
|
7
|
+
* 缺凭据时给出明确错误,避免静默失败。
|
|
8
|
+
*/
|
|
9
|
+
export interface PublisherConfig {
|
|
10
|
+
appId: string;
|
|
11
|
+
appSecret: string;
|
|
12
|
+
/** 微信 API 基地址,仅在测试/私有化时覆盖。 */
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
/** 注入的 fetch 实现(默认 global fetch),便于测试 mock。 */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
export interface HttpConfig {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
fetchImpl: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
/** 从环境变量加载配置;缺少凭据时抛出明确中文错误。 */
|
|
22
|
+
export declare function loadConfigFromEnv(env?: NodeJS.ProcessEnv): PublisherConfig;
|
|
23
|
+
export declare function toHttpConfig(config: PublisherConfig): HttpConfig;
|
|
24
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,KAAK,CAAA;CACxB;AAED,+BAA+B;AAC/B,wBAAgB,iBAAiB,CAC/B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAkBjB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU,CAKhE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 微信公众号发布配置。
|
|
3
|
+
* 仅通过环境变量注入凭据(绝不硬编码/提交):
|
|
4
|
+
* - WECHAT_APP_ID 公众号 AppID
|
|
5
|
+
* - WECHAT_APP_SECRET 公众号 AppSecret
|
|
6
|
+
* - WECHAT_API_BASE 可选,默认 https://api.weixin.qq.com
|
|
7
|
+
* 缺凭据时给出明确错误,避免静默失败。
|
|
8
|
+
*/
|
|
9
|
+
/** 从环境变量加载配置;缺少凭据时抛出明确中文错误。 */
|
|
10
|
+
export function loadConfigFromEnv(env = process.env) {
|
|
11
|
+
const appId = env.WECHAT_APP_ID?.trim();
|
|
12
|
+
const appSecret = env.WECHAT_APP_SECRET?.trim();
|
|
13
|
+
if (!appId || !appSecret) {
|
|
14
|
+
const missing = [];
|
|
15
|
+
if (!appId)
|
|
16
|
+
missing.push('WECHAT_APP_ID');
|
|
17
|
+
if (!appSecret)
|
|
18
|
+
missing.push('WECHAT_APP_SECRET');
|
|
19
|
+
throw new Error(`缺少微信公众号凭据环境变量:${missing.join('、')}。请在 .env 或启动命令中设置后重试。示例:MCP_APP 启动前 export WECHAT_APP_ID=... WECHAT_APP_SECRET=...`);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
appId,
|
|
23
|
+
appSecret,
|
|
24
|
+
baseUrl: env.WECHAT_API_BASE?.trim() || 'https://api.weixin.qq.com',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function toHttpConfig(config) {
|
|
28
|
+
return {
|
|
29
|
+
baseUrl: config.baseUrl,
|
|
30
|
+
fetchImpl: config.fetchImpl ?? globalThis.fetch,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,+BAA+B;AAC/B,MAAM,UAAU,iBAAiB,CAC/B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;IACvC,MAAM,SAAS,GAAG,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAA;IAE/C,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;QACzB,MAAM,OAAO,GAAa,EAAE,CAAA;QAC5B,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;QACzC,IAAI,CAAC,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACjD,MAAM,IAAI,KAAK,CACb,iBAAiB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,oFAAoF,CACvH,CAAA;IACH,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS;QACT,OAAO,EAAE,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,IAAI,2BAA2B;KACpE,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,UAAU,CAAC,KAAK;KAChD,CAAA;AACH,CAAC"}
|
package/dist/cover.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cover.d.ts","sourceRoot":"","sources":["../src/cover.ts"],"names":[],"mappings":"AAgEA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,uBAAuB;AACvB,wBAAgB,oBAAoB,IAAI,UAAU,CAGjD"}
|
package/dist/cover.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认封面生成器:为「无配图的纯文字文章」生成一张主题色渐变封面(900×383,≈2.35:1)。
|
|
3
|
+
* 纯 JS 生成 PNG(gradient + zlib deflate),不依赖任何图片库 / DOM,仅可在 Node 侧使用。
|
|
4
|
+
* publishDraft 在「无 coverImage 且正文无图」时自动调用它,避免发布缺封面而失败。
|
|
5
|
+
*/
|
|
6
|
+
import zlib from 'node:zlib';
|
|
7
|
+
const CRC_TABLE = (() => {
|
|
8
|
+
const t = new Uint32Array(256);
|
|
9
|
+
for (let n = 0; n < 256; n++) {
|
|
10
|
+
let c = n;
|
|
11
|
+
for (let k = 0; k < 8; k++)
|
|
12
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
13
|
+
t[n] = c >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return t;
|
|
16
|
+
})();
|
|
17
|
+
function crc32(buf) {
|
|
18
|
+
let c = 0xffffffff;
|
|
19
|
+
for (const x of buf)
|
|
20
|
+
c = (CRC_TABLE[(c ^ x) & 0xff] ?? 0) ^ (c >>> 8);
|
|
21
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
22
|
+
}
|
|
23
|
+
function chunk(type, data) {
|
|
24
|
+
const len = Buffer.alloc(4);
|
|
25
|
+
len.writeUInt32BE(data.length, 0);
|
|
26
|
+
const t = Buffer.from(type, 'ascii');
|
|
27
|
+
const crc = Buffer.alloc(4);
|
|
28
|
+
crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0);
|
|
29
|
+
return Buffer.concat([len, t, data, crc]);
|
|
30
|
+
}
|
|
31
|
+
/** 垂直渐变 PNG(RGB,top→bottom)。 */
|
|
32
|
+
function gradientPng(width, height, top, bottom) {
|
|
33
|
+
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
34
|
+
const ihdr = Buffer.alloc(13);
|
|
35
|
+
ihdr.writeUInt32BE(width, 0);
|
|
36
|
+
ihdr.writeUInt32BE(height, 4);
|
|
37
|
+
ihdr[8] = 8; // bit depth
|
|
38
|
+
ihdr[9] = 6; // color type RGBA
|
|
39
|
+
const raw = Buffer.alloc(height * (1 + width * 4));
|
|
40
|
+
for (let y = 0; y < height; y++) {
|
|
41
|
+
const t = y / (height - 1);
|
|
42
|
+
const r = Math.round(top[0] + (bottom[0] - top[0]) * t);
|
|
43
|
+
const g = Math.round(top[1] + (bottom[1] - top[1]) * t);
|
|
44
|
+
const b = Math.round(top[2] + (bottom[2] - top[2]) * t);
|
|
45
|
+
const row = y * (1 + width * 4);
|
|
46
|
+
raw[row] = 0; // filter: none
|
|
47
|
+
for (let x = 0; x < width; x++) {
|
|
48
|
+
const o = row + 1 + x * 4;
|
|
49
|
+
raw[o] = r;
|
|
50
|
+
raw[o + 1] = g;
|
|
51
|
+
raw[o + 2] = b;
|
|
52
|
+
raw[o + 3] = 255;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Buffer.concat([
|
|
56
|
+
sig,
|
|
57
|
+
chunk('IHDR', ihdr),
|
|
58
|
+
chunk('IDAT', zlib.deflateSync(raw)),
|
|
59
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
/** 生成默认主题封面(暖棕灰渐变)。 */
|
|
63
|
+
export function generateDefaultCover() {
|
|
64
|
+
const bytes = gradientPng(900, 383, [0x6b, 0x55, 0x45], [0xc9, 0xb4, 0x9e]);
|
|
65
|
+
return { bytes, filename: 'cover.png', mimeType: 'image/png' };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=cover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cover.js","sourceRoot":"","sources":["../src/cover.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布编排:搬运图片 → 确定封面 media_id → 调 draft/add 写入草稿箱。
|
|
3
|
+
* ⚠️ 只发布到「草稿箱」,不含任何群发(freepublish/submit)能力。
|
|
4
|
+
*/
|
|
5
|
+
import { WeChatClient } from './client.js';
|
|
6
|
+
export interface PublishParams {
|
|
7
|
+
/** 已渲染(内联样式)的 HTML 正文。 */
|
|
8
|
+
content: string;
|
|
9
|
+
/** 文章标题。 */
|
|
10
|
+
title: string;
|
|
11
|
+
/** 作者名(可选)。 */
|
|
12
|
+
author?: string;
|
|
13
|
+
/** 摘要(可选,缺省会自动从正文截取)。 */
|
|
14
|
+
digest?: string;
|
|
15
|
+
/** 封面图 URL(可选;若缺少会用文章第一张图作为封面)。 */
|
|
16
|
+
coverImage?: string;
|
|
17
|
+
/** 封面原始字节(本地上传用,优先生效;不需要 URL)。 */
|
|
18
|
+
coverData?: {
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
mimeType: string;
|
|
21
|
+
};
|
|
22
|
+
/** 原文链接(可选)。 */
|
|
23
|
+
contentSourceUrl?: string;
|
|
24
|
+
needOpenComment?: boolean;
|
|
25
|
+
onlyFansCanComment?: boolean;
|
|
26
|
+
/** 发布时是否把外链图自动搬运到微信素材库。默认 true;false 则保留外链 URL。 */
|
|
27
|
+
relocate?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface PublishResult {
|
|
30
|
+
/** draft/add 返回的草稿 media_id。 */
|
|
31
|
+
media_id: string;
|
|
32
|
+
/** 被搬运到素材库的图片。 */
|
|
33
|
+
uploadedImages: {
|
|
34
|
+
original: string;
|
|
35
|
+
url: string;
|
|
36
|
+
media_id: string;
|
|
37
|
+
}[];
|
|
38
|
+
/** 用作封面的素材 media_id。 */
|
|
39
|
+
coverMediaId?: string;
|
|
40
|
+
/** 搬运失败的图片。 */
|
|
41
|
+
failedImages: {
|
|
42
|
+
src: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 把一篇文章发布到微信公众号「草稿箱」。
|
|
48
|
+
* @returns 草稿 media_id 与确认信息。
|
|
49
|
+
*/
|
|
50
|
+
export declare function publishDraft(client: WeChatClient, params: PublishParams): Promise<PublishResult>;
|
|
51
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../src/publish.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAK1C,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,eAAe;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kCAAkC;IAClC,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,gBAAgB;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,kBAAkB;IAClB,cAAc,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrE,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe;IACf,YAAY,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAChD;AA4CD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,aAAa,CAAC,CAiCxB"}
|
package/dist/publish.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布编排:搬运图片 → 确定封面 media_id → 调 draft/add 写入草稿箱。
|
|
3
|
+
* ⚠️ 只发布到「草稿箱」,不含任何群发(freepublish/submit)能力。
|
|
4
|
+
*/
|
|
5
|
+
import { WeChatClient } from './client.js';
|
|
6
|
+
import { relocateExternalImages } from './relocate.js';
|
|
7
|
+
import { generateDefaultCover } from './cover.js';
|
|
8
|
+
/** 由封面图/正文首图解析出 thumb_media_id。 */
|
|
9
|
+
async function resolveThumbMediaId(client, params, uploaded) {
|
|
10
|
+
// 0. 本地上传的封面原始字节(最高优先,直接上传素材库,无需 URL)
|
|
11
|
+
if (params.coverData && params.coverData.bytes.length > 0) {
|
|
12
|
+
const ext = params.coverData.mimeType.includes('png') ? 'png' : 'jpg';
|
|
13
|
+
const thumb = await client.uploadThumb(params.coverData.bytes, `cover.${ext}`, params.coverData.mimeType);
|
|
14
|
+
return thumb.media_id;
|
|
15
|
+
}
|
|
16
|
+
// 1. 优先使用显式封面图
|
|
17
|
+
if (params.coverImage) {
|
|
18
|
+
const { bytes, mimeType } = await client.downloadImage(params.coverImage);
|
|
19
|
+
const thumb = await client.uploadThumb(bytes, guessThumbFilename(params.coverImage), mimeType);
|
|
20
|
+
return thumb.media_id;
|
|
21
|
+
}
|
|
22
|
+
// 2. 回退到正文第一张已上传的图(素材库 media_id 通用)
|
|
23
|
+
if (uploaded[0])
|
|
24
|
+
return uploaded[0].media_id;
|
|
25
|
+
// 3. 都没有 → 自动生成一张主题色渐变封面(纯文字文章也能发布成功)
|
|
26
|
+
const cover = generateDefaultCover();
|
|
27
|
+
const thumb = await client.uploadThumb(cover.bytes, cover.filename, cover.mimeType);
|
|
28
|
+
return thumb.media_id;
|
|
29
|
+
}
|
|
30
|
+
function guessThumbFilename(url) {
|
|
31
|
+
const last = url.split('?')[0]?.split('/').filter(Boolean).pop();
|
|
32
|
+
return last && /\.[a-z0-9]{2,5}$/i.test(last) ? last : 'cover.jpg';
|
|
33
|
+
}
|
|
34
|
+
function computeDigest(content, title, explicit) {
|
|
35
|
+
if (explicit)
|
|
36
|
+
return explicit;
|
|
37
|
+
const text = content
|
|
38
|
+
.replace(/<[^>]+>/g, ' ')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.trim();
|
|
41
|
+
if (!text)
|
|
42
|
+
return title || undefined;
|
|
43
|
+
const digest = text.slice(0, 120);
|
|
44
|
+
return digest.length > 0 ? digest : title || undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 把一篇文章发布到微信公众号「草稿箱」。
|
|
48
|
+
* @returns 草稿 media_id 与确认信息。
|
|
49
|
+
*/
|
|
50
|
+
export async function publishDraft(client, params) {
|
|
51
|
+
if (!params.title || !params.title.trim()) {
|
|
52
|
+
throw new Error('缺少文章标题 title。草稿发布必须提供标题。');
|
|
53
|
+
}
|
|
54
|
+
if (!params.content || !params.content.trim()) {
|
|
55
|
+
throw new Error('缺少文章正文 content(渲染后的 HTML)。请先调用 render_preview 或渲染步骤。');
|
|
56
|
+
}
|
|
57
|
+
const relocated = params.relocate === false
|
|
58
|
+
? { html: params.content, uploaded: [], failed: [] }
|
|
59
|
+
: await relocateExternalImages(params.content, client);
|
|
60
|
+
const coverMediaId = await resolveThumbMediaId(client, params, relocated.uploaded);
|
|
61
|
+
const article = {
|
|
62
|
+
title: params.title.trim(),
|
|
63
|
+
content: relocated.html,
|
|
64
|
+
thumb_media_id: coverMediaId,
|
|
65
|
+
author: params.author,
|
|
66
|
+
digest: computeDigest(relocated.html, params.title, params.digest),
|
|
67
|
+
content_source_url: params.contentSourceUrl,
|
|
68
|
+
need_open_comment: params.needOpenComment,
|
|
69
|
+
only_fans_can_comment: params.onlyFansCanComment,
|
|
70
|
+
};
|
|
71
|
+
const { media_id } = await client.addDraft(article);
|
|
72
|
+
return {
|
|
73
|
+
media_id,
|
|
74
|
+
uploadedImages: relocated.uploaded,
|
|
75
|
+
coverMediaId,
|
|
76
|
+
failedImages: relocated.failed,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=publish.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.js","sourceRoot":"","sources":["../src/publish.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publisher.test.d.ts","sourceRoot":"","sources":["../src/publisher.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { WeChatClient, loadConfigFromEnv, relocateExternalImages, publishDraft, } from './index.js';
|
|
3
|
+
/** 构造一个按 URL 分发的 mock fetch(返回微信 API 形状的 JSON / 图片字节)。 */
|
|
4
|
+
function makeMockFetch(opts) {
|
|
5
|
+
const calls = [];
|
|
6
|
+
const fetchMock = async (input, init) => {
|
|
7
|
+
const url = String(input);
|
|
8
|
+
calls.push({ url, method: init?.method, body: init?.body });
|
|
9
|
+
if (url.includes('/cgi-bin/token')) {
|
|
10
|
+
if (opts?.failToken) {
|
|
11
|
+
return Response.json({ errcode: 40013, errmsg: 'invalid appid' }, { status: 200 });
|
|
12
|
+
}
|
|
13
|
+
return Response.json({ access_token: 'test_token_123', expires_in: opts?.tokenExpiresIn ?? 7200 }, { status: 200 });
|
|
14
|
+
}
|
|
15
|
+
if (url.includes('/cgi-bin/material/add_material')) {
|
|
16
|
+
return Response.json({ media_id: 'media_image_1', url: 'https://mmbiz.qpic.cn/mmbiz_png/uploaded.png' }, { status: 200 });
|
|
17
|
+
}
|
|
18
|
+
if (url.includes('/cgi-bin/draft/add')) {
|
|
19
|
+
if (opts?.failDraft) {
|
|
20
|
+
return Response.json({ errcode: 48001, errmsg: 'api forbidden' }, { status: 200 });
|
|
21
|
+
}
|
|
22
|
+
return Response.json({ media_id: 'draft_abcdef' }, { status: 200 });
|
|
23
|
+
}
|
|
24
|
+
if (url.includes('example.com')) {
|
|
25
|
+
return new Response(new Uint8Array([1, 2, 3]), {
|
|
26
|
+
status: 200,
|
|
27
|
+
headers: { 'content-type': 'image/png' },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return Response.json({ ok: true }, { status: 200 });
|
|
31
|
+
};
|
|
32
|
+
return { fetchMock: fetchMock, calls };
|
|
33
|
+
}
|
|
34
|
+
function makeClient(overrides) {
|
|
35
|
+
const { fetchMock, calls } = makeMockFetch(overrides);
|
|
36
|
+
const client = new WeChatClient({
|
|
37
|
+
appId: 'appid',
|
|
38
|
+
appSecret: 'secret',
|
|
39
|
+
baseUrl: 'https://api.weixin.qq.com',
|
|
40
|
+
fetchImpl: fetchMock,
|
|
41
|
+
});
|
|
42
|
+
return { client, calls };
|
|
43
|
+
}
|
|
44
|
+
describe('loadConfigFromEnv', () => {
|
|
45
|
+
it('缺少凭据时抛明确错误', () => {
|
|
46
|
+
expect(() => loadConfigFromEnv({})).toThrow(/WECHAT_APP_ID|WECHAT_APP_SECRET/);
|
|
47
|
+
});
|
|
48
|
+
it('读取凭据并生成配置', () => {
|
|
49
|
+
const config = loadConfigFromEnv({
|
|
50
|
+
WECHAT_APP_ID: 'appid',
|
|
51
|
+
WECHAT_APP_SECRET: 'secret',
|
|
52
|
+
});
|
|
53
|
+
expect(config.appId).toBe('appid');
|
|
54
|
+
expect(config.baseUrl).toBe('https://api.weixin.qq.com');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('getAccessToken', () => {
|
|
58
|
+
it('首次获取并缓存,二次调用不重复请求', async () => {
|
|
59
|
+
const { client, calls } = makeClient();
|
|
60
|
+
const token1 = await client.getAccessToken();
|
|
61
|
+
const token2 = await client.getAccessToken();
|
|
62
|
+
expect(token1).toBe('test_token_123');
|
|
63
|
+
expect(token2).toBe('test_token_123');
|
|
64
|
+
expect(calls.filter((c) => c.url.includes('/cgi-bin/token'))).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
it('临近过期时提前刷新', async () => {
|
|
67
|
+
const { client, calls } = makeClient({ tokenExpiresIn: 60 }); // 60 秒
|
|
68
|
+
await client.getAccessToken();
|
|
69
|
+
// 强行把时间拨到接近过期
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
void now;
|
|
72
|
+
// 60 秒过期 → 剩余 60s < 提前 5 分钟(300s) → 下次调用必然刷新
|
|
73
|
+
await client.getAccessToken();
|
|
74
|
+
expect(calls.filter((c) => c.url.includes('/cgi-bin/token'))).toHaveLength(2);
|
|
75
|
+
});
|
|
76
|
+
it('获取 token 失败时抛错误', async () => {
|
|
77
|
+
const { client } = makeClient({ failToken: true });
|
|
78
|
+
await expect(client.getAccessToken()).rejects.toThrow(/errcode/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('uploadImage', () => {
|
|
82
|
+
it('上传图片到素材库,返回 url', async () => {
|
|
83
|
+
const { client } = makeClient();
|
|
84
|
+
const result = await client.uploadImage(new Uint8Array([1, 2, 3]), 'a.png', 'image/png');
|
|
85
|
+
expect(result.media_id).toBe('media_image_1');
|
|
86
|
+
expect(result.url).toContain('mmbiz.qpic.cn');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('addDraft', () => {
|
|
90
|
+
it('调用 draft/add,返回草稿 media_id', async () => {
|
|
91
|
+
const { client, calls } = makeClient();
|
|
92
|
+
const result = await client.addDraft({
|
|
93
|
+
title: '标题',
|
|
94
|
+
content: '<section><p>x</p></section>',
|
|
95
|
+
thumb_media_id: 'thumb_1',
|
|
96
|
+
});
|
|
97
|
+
expect(result.media_id).toBe('draft_abcdef');
|
|
98
|
+
const draftCall = calls.find((c) => c.url.includes('/cgi-bin/draft/add'));
|
|
99
|
+
expect(draftCall).toBeDefined();
|
|
100
|
+
const body = JSON.parse(String(draftCall?.body ?? ''));
|
|
101
|
+
expect(body.articles[0].title).toBe('标题');
|
|
102
|
+
expect(body.articles[0].thumb_media_id).toBe('thumb_1');
|
|
103
|
+
});
|
|
104
|
+
it('draft/add 失败时抛错误', async () => {
|
|
105
|
+
const { client } = makeClient({ failDraft: true });
|
|
106
|
+
await expect(client.addDraft({ title: 'x', content: '<p>x</p>', thumb_media_id: 't' })).rejects.toThrow(/errcode/);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('relocateExternalImages', () => {
|
|
110
|
+
it('把外链图搬运到素材库并替换 src', async () => {
|
|
111
|
+
const { client, calls } = makeClient();
|
|
112
|
+
const html = '<section><p><img src="https://example.com/a.png"></p><img src="https://mmbiz.qpic.cn/ok.png"></section>';
|
|
113
|
+
const result = await relocateExternalImages(html, client);
|
|
114
|
+
expect(result.uploaded).toHaveLength(1);
|
|
115
|
+
expect(result.uploaded[0]?.original).toBe('https://example.com/a.png');
|
|
116
|
+
expect(result.html).toContain('mmbiz.qpic.cn/mmbiz_png/uploaded.png');
|
|
117
|
+
// 微信图床未被重复搬运
|
|
118
|
+
expect(result.skipped).toContain('https://mmbiz.qpic.cn/ok.png');
|
|
119
|
+
// 至少一次下载 + 一次上传
|
|
120
|
+
expect(calls.some((c) => c.url.includes('example.com'))).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
it('下载失败时记录失败而不抛出', async () => {
|
|
123
|
+
const customFetch = async (input) => {
|
|
124
|
+
const url = String(input);
|
|
125
|
+
if (url.includes('example.com'))
|
|
126
|
+
return new Response('bad', { status: 404 });
|
|
127
|
+
if (url.includes('/cgi-bin/token'))
|
|
128
|
+
return Response.json({ access_token: 't', expires_in: 7200 });
|
|
129
|
+
if (url.includes('/cgi-bin/material/add_material'))
|
|
130
|
+
return Response.json({ media_id: 'm', url: 'https://mmbiz.qpic.cn/u' });
|
|
131
|
+
return Response.json({});
|
|
132
|
+
};
|
|
133
|
+
const client = new WeChatClient({
|
|
134
|
+
appId: 'a', appSecret: 's', baseUrl: 'https://api.weixin.qq.com',
|
|
135
|
+
fetchImpl: customFetch,
|
|
136
|
+
});
|
|
137
|
+
const result = await relocateExternalImages('<img src="https://example.com/broken.png">', client);
|
|
138
|
+
expect(result.failed).toHaveLength(1);
|
|
139
|
+
expect(result.failed[0]?.reason).toContain('404');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('publishDraft', () => {
|
|
143
|
+
it('发布草稿,优先使用 coverImage 作为封面', async () => {
|
|
144
|
+
const { client, calls } = makeClient();
|
|
145
|
+
const result = await publishDraft(client, {
|
|
146
|
+
title: '我的文章',
|
|
147
|
+
content: '<section><p>hello</p></section>',
|
|
148
|
+
coverImage: 'https://example.com/cover.png',
|
|
149
|
+
author: '作者',
|
|
150
|
+
});
|
|
151
|
+
expect(result.media_id).toBe('draft_abcdef');
|
|
152
|
+
expect(result.coverMediaId).toBe('media_image_1');
|
|
153
|
+
const draftCall = calls.find((c) => c.url.includes('/cgi-bin/draft/add'));
|
|
154
|
+
const body = JSON.parse(String(draftCall?.body ?? ''));
|
|
155
|
+
expect(body.articles[0].thumb_media_id).toBe('media_image_1');
|
|
156
|
+
expect(body.articles[0].digest).toContain('hello');
|
|
157
|
+
});
|
|
158
|
+
it('未提供封面时,用正文第一张图作为封面(回退)', async () => {
|
|
159
|
+
const { client, calls } = makeClient();
|
|
160
|
+
const result = await publishDraft(client, {
|
|
161
|
+
title: '带图文章',
|
|
162
|
+
content: '<section><p><img src="https://example.com/a.png"></p></section>',
|
|
163
|
+
});
|
|
164
|
+
expect(result.coverMediaId).toBe('media_image_1');
|
|
165
|
+
expect(result.uploadedImages).toHaveLength(1);
|
|
166
|
+
const draftCall = calls.find((c) => c.url.includes('/cgi-bin/draft/add'));
|
|
167
|
+
const body = JSON.parse(String(draftCall?.body ?? ''));
|
|
168
|
+
expect(body.articles[0].content).toContain('mmbiz.qpic.cn');
|
|
169
|
+
});
|
|
170
|
+
it('缺少标题时明确报错', async () => {
|
|
171
|
+
const { client } = makeClient();
|
|
172
|
+
await expect(publishDraft(client, { title: '', content: '<p>x</p>' })).rejects.toThrow(/标题/);
|
|
173
|
+
});
|
|
174
|
+
it('本地上传的 coverData 优先作为封面', async () => {
|
|
175
|
+
const { client, calls } = makeClient();
|
|
176
|
+
const r = await publishDraft(client, { title: 'x', content: '<p>a</p>', coverData: { bytes: new Uint8Array([1, 2, 3]), mimeType: 'image/png' } });
|
|
177
|
+
expect(r.media_id).toBe('draft_abcdef');
|
|
178
|
+
expect(calls.filter((c) => c.url.includes('add_material')).length).toBeGreaterThanOrEqual(1);
|
|
179
|
+
});
|
|
180
|
+
it('无封面且无任何图片时自动生成默认封面(不失败)', async () => {
|
|
181
|
+
const { client, calls } = makeClient();
|
|
182
|
+
const r = await publishDraft(client, { title: 'x', content: '<p>纯文字</p>' });
|
|
183
|
+
expect(r.media_id).toBe('draft_abcdef');
|
|
184
|
+
// 应发生了一次封面上传(material/add_material 的 thumb 上传)
|
|
185
|
+
expect(calls.some((c) => c.url.includes('/cgi-bin/material/add_material'))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
it('安全边界:不暴露任何 freepublish/submit 能力', () => {
|
|
188
|
+
const client = new WeChatClient({
|
|
189
|
+
appId: 'a', appSecret: 's', baseUrl: 'https://api.weixin.qq.com', fetchImpl: makeMockFetch().fetchMock,
|
|
190
|
+
});
|
|
191
|
+
// 客户端对象不存在群发方法
|
|
192
|
+
expect(client.submit).toBeUndefined();
|
|
193
|
+
expect(client.freepublish).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=publisher.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publisher.test.js","sourceRoot":"","sources":["../src/publisher.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WeChatClient } from './client.js';
|
|
2
|
+
export interface UploadedImage {
|
|
3
|
+
original: string;
|
|
4
|
+
url: string;
|
|
5
|
+
media_id: string;
|
|
6
|
+
}
|
|
7
|
+
export interface RelocateResult {
|
|
8
|
+
html: string;
|
|
9
|
+
/** 成功搬运到素材库的图片。 */
|
|
10
|
+
uploaded: UploadedImage[];
|
|
11
|
+
/** 被忽略(本就是微信/内联 data uri)的图片。 */
|
|
12
|
+
skipped: string[];
|
|
13
|
+
/** 搬运失败的图片(含原因)。 */
|
|
14
|
+
failed: {
|
|
15
|
+
src: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 搬运 HTML 中所有外链图片到微信素材库。
|
|
21
|
+
* @param html 已渲染的内联样式 HTML
|
|
22
|
+
* @param client 已配置的微信客户端
|
|
23
|
+
*/
|
|
24
|
+
export declare function relocateExternalImages(html: string, client: WeChatClient): Promise<RelocateResult>;
|
|
25
|
+
//# sourceMappingURL=relocate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relocate.d.ts","sourceRoot":"","sources":["../src/relocate.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI1C,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,mBAAmB;IACnB,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,iCAAiC;IACjC,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,oBAAoB;IACpB,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC1C;AAoED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,cAAc,CAAC,CAYzB"}
|
package/dist/relocate.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片搬运管线:把 HTML 中的外链图片下载后上传到微信素材库,
|
|
3
|
+
* 并把 img src 替换为微信图床(mmbiz.qpic.cn)URL。
|
|
4
|
+
* 这是「发布到草稿」前让内容真正可显示的关键一步。
|
|
5
|
+
*/
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import rehypeParse from 'rehype-parse';
|
|
8
|
+
import rehypeStringify from 'rehype-stringify';
|
|
9
|
+
import { WeChatClient } from './client.js';
|
|
10
|
+
const ALLOWED_IMAGE_HOSTS = ['mmbiz.qpic.cn', 'wx.qlogo.cn', 'mp.weixin.qq.com'];
|
|
11
|
+
function isWechatImage(src) {
|
|
12
|
+
const s = src.trim();
|
|
13
|
+
if (s.startsWith('data:image/'))
|
|
14
|
+
return true;
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(s);
|
|
17
|
+
const host = url.hostname.toLowerCase();
|
|
18
|
+
return host === 'mmbiz.qpic.cn' || host.endsWith('.qpic.cn') || ALLOWED_IMAGE_HOSTS.includes(host);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isElement(node) {
|
|
25
|
+
return node.type === 'element' && typeof node.tagName === 'string';
|
|
26
|
+
}
|
|
27
|
+
/** 递归处理所有 img,返回搬运结果。 */
|
|
28
|
+
async function processImages(node, client, result) {
|
|
29
|
+
if (!isElement(node))
|
|
30
|
+
return;
|
|
31
|
+
if (node.tagName.toLowerCase() === 'img') {
|
|
32
|
+
const src = node.properties.src;
|
|
33
|
+
if (typeof src !== 'string' || !src.trim()) {
|
|
34
|
+
result.skipped.push(String(src ?? '(no src)'));
|
|
35
|
+
}
|
|
36
|
+
else if (isWechatImage(src)) {
|
|
37
|
+
result.skipped.push(src);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
try {
|
|
41
|
+
const { bytes, mimeType } = await client.downloadImage(src);
|
|
42
|
+
const filename = guessFilename(src);
|
|
43
|
+
const uploaded = await client.uploadImage(bytes, filename, mimeType);
|
|
44
|
+
const finalUrl = uploaded.url ?? uploaded.media_id;
|
|
45
|
+
node.properties.src = finalUrl;
|
|
46
|
+
result.uploaded.push({ original: src, url: finalUrl, media_id: uploaded.media_id });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
result.failed.push({
|
|
50
|
+
src,
|
|
51
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (node.children) {
|
|
57
|
+
for (const child of node.children)
|
|
58
|
+
await processImages(child, client, result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function guessFilename(src) {
|
|
62
|
+
const clean = src.split('?')[0] ?? '';
|
|
63
|
+
const last = clean.split('/').filter(Boolean).pop() ?? 'image';
|
|
64
|
+
if (/\.[a-z0-9]{2,5}$/i.test(last))
|
|
65
|
+
return last;
|
|
66
|
+
return `image_${Math.random().toString(36).slice(2, 8)}.jpg`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 搬运 HTML 中所有外链图片到微信素材库。
|
|
70
|
+
* @param html 已渲染的内联样式 HTML
|
|
71
|
+
* @param client 已配置的微信客户端
|
|
72
|
+
*/
|
|
73
|
+
export async function relocateExternalImages(html, client) {
|
|
74
|
+
const processor = unified()
|
|
75
|
+
.use(rehypeParse, { fragment: true })
|
|
76
|
+
.use(rehypeStringify);
|
|
77
|
+
const tree = processor.parse(html);
|
|
78
|
+
const result = { html: '', uploaded: [], skipped: [], failed: [] };
|
|
79
|
+
if (tree.children) {
|
|
80
|
+
for (const child of tree.children)
|
|
81
|
+
await processImages(child, client, result);
|
|
82
|
+
}
|
|
83
|
+
result.html = String(processor.stringify(tree));
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=relocate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relocate.js","sourceRoot":"","sources":["../src/relocate.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stylewx/publisher",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "stylewx publisher: WeChat Official Account API wrapper (access_token, image upload, draft/add) + external-image relocation. Node-only by design.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"prepublishOnly": "pnpm build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"rehype-parse": "^9.0.1",
|
|
27
|
+
"rehype-stringify": "^10.0.1",
|
|
28
|
+
"unified": "^11.0.5"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|