@tachybase/plugin-wechat-official-account 0.23.8

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.
@@ -0,0 +1,614 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var plugin_exports = {};
29
+ __export(plugin_exports, {
30
+ PluginReplacePageServer: () => PluginReplacePageServer,
31
+ default: () => plugin_default
32
+ });
33
+ module.exports = __toCommonJS(plugin_exports);
34
+ var import_crypto = __toESM(require("crypto"));
35
+ var import_module_auth = require("@tachybase/module-auth");
36
+ var import_server = require("@tachybase/server");
37
+ var import_axios = __toESM(require("axios"));
38
+ var import_sequelize = require("sequelize");
39
+ var import_xml2js = __toESM(require("xml2js"));
40
+ var import_constants = require("../constants");
41
+ class PluginReplacePageServer extends import_server.Plugin {
42
+ // 从数据库查询wechatConfig表
43
+ async getWeChatConfig() {
44
+ const repo = this.app.db.getRepository("wechatConfig");
45
+ const config = await repo.findById(1);
46
+ const { appid, appsecret, token, accesstoken, token_expires_at } = config.get();
47
+ return { appid, appsecret, token, accesstoken, token_expires_at };
48
+ }
49
+ // 从数据库查询wechatMessage表
50
+ async getWeChatMessage() {
51
+ const repo = this.app.db.getRepository("wechatMessage");
52
+ const messages = await repo.find();
53
+ return messages;
54
+ }
55
+ // 从数据库查询wechatMenu表
56
+ async getWeChatMenu() {
57
+ const repo = this.app.db.getRepository("wechatMenu");
58
+ const primaryMenus = await repo.find({
59
+ where: { button: null },
60
+ order: [["createdAt", "DESC"]],
61
+ limit: 3
62
+ });
63
+ const secondaryMenus = await repo.find({
64
+ where: { button: { [import_sequelize.Op.ne]: null } },
65
+ order: [["createdAt", "DESC"]],
66
+ limit: 5
67
+ });
68
+ const menuStructure = { button: [] };
69
+ const reversedPrimaryMenus = primaryMenus.reverse();
70
+ reversedPrimaryMenus.forEach((menu) => {
71
+ if (!menu.button) {
72
+ let name = menu.name;
73
+ if (Buffer.byteLength(name, "utf8") > 12) {
74
+ while (Buffer.byteLength(name, "utf8") > 12 - 2) {
75
+ name = name.slice(0, -1);
76
+ }
77
+ name += "..";
78
+ }
79
+ menuStructure.button.push({
80
+ type: menu.type,
81
+ name,
82
+ key: menu.key || void 0,
83
+ url: menu.url || void 0,
84
+ sub_button: []
85
+ });
86
+ }
87
+ });
88
+ const reversedSecondaryMenus = secondaryMenus.reverse();
89
+ reversedSecondaryMenus.forEach((menu) => {
90
+ if (menu.button) {
91
+ const parentMenu = menuStructure.button.find((btn) => btn.name === menu.button);
92
+ if (parentMenu) {
93
+ let name = menu.name;
94
+ if (Buffer.byteLength(name, "utf8") > 32) {
95
+ while (Buffer.byteLength(name, "utf8") > 32 - 2) {
96
+ name = name.slice(0, -1);
97
+ }
98
+ name += "..";
99
+ }
100
+ parentMenu.sub_button.push({
101
+ type: menu.type,
102
+ name,
103
+ key: menu.key || void 0,
104
+ url: menu.url || void 0
105
+ });
106
+ }
107
+ }
108
+ });
109
+ return menuStructure;
110
+ }
111
+ // 从数据库查询wechatArticle表
112
+ async getWeChatArticle() {
113
+ const repo = this.app.db.getRepository("wechatArticle");
114
+ const articles = await repo.find({ order: [["id", "desc"]], limit: 1 });
115
+ if (articles.length > 0) {
116
+ return articles[0];
117
+ } else {
118
+ this.app.logger.error("No articles found in the database");
119
+ return null;
120
+ }
121
+ }
122
+ // 验证Wechat平台的请求签名
123
+ async checkSignature(query) {
124
+ const { signature, timestamp, nonce } = query;
125
+ const hash = import_crypto.default.createHash("sha1");
126
+ const { token } = await this.getWeChatConfig();
127
+ const str = [token, timestamp, nonce].sort().join("");
128
+ hash.update(str);
129
+ const result = hash.digest("hex") === signature;
130
+ return result;
131
+ }
132
+ // 获取AccessToken
133
+ async getAccessToken(forceRefresh = false) {
134
+ const configRepo = this.app.db.getRepository("wechatConfig");
135
+ const config = await this.getWeChatConfig();
136
+ const { appid, appsecret, accesstoken, token_expires_at } = config;
137
+ const now = Date.now();
138
+ const bufferTime = 10 * 60 * 1e3;
139
+ if (!forceRefresh && accesstoken && token_expires_at && now < token_expires_at - bufferTime) {
140
+ return accesstoken;
141
+ }
142
+ const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`;
143
+ try {
144
+ const response = await import_axios.default.get(url);
145
+ const newAccessToken = response.data.access_token;
146
+ const expiresIn = response.data.expires_in * 1e3;
147
+ const newTokenExpiresAt = now + expiresIn;
148
+ await configRepo.update({
149
+ filter: { id: 1 },
150
+ values: { accesstoken: newAccessToken, token_expires_at: newTokenExpiresAt }
151
+ });
152
+ return newAccessToken;
153
+ } catch (error) {
154
+ this.app.logger.error(`Error getting access token: ${JSON.stringify(error)}`);
155
+ return null;
156
+ }
157
+ }
158
+ // 生成OAuth授权URL
159
+ async generateAuthUrl() {
160
+ const { appid } = await this.getWeChatConfig();
161
+ const redirectUri = encodeURIComponent("https://lu.dev.daoyoucloud.com/api/wechat@handleOAuthCallback");
162
+ const scope = "snsapi_userinfo";
163
+ const state = "STATE";
164
+ const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
165
+ return authUrl;
166
+ }
167
+ // 生成二维码URL
168
+ async generateQrCode() {
169
+ const authUrl = await this.generateAuthUrl();
170
+ if (!authUrl) {
171
+ this.app.logger.error("Failed to generate OAuth URL");
172
+ return null;
173
+ }
174
+ const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(authUrl)}&size=300x300`;
175
+ return qrCodeUrl;
176
+ }
177
+ // 创建菜单
178
+ async createCustomMenu() {
179
+ const accessToken = await this.getAccessToken();
180
+ const menu = await this.getWeChatMenu();
181
+ if (!accessToken) {
182
+ this.app.logger.error("Failed to get access token");
183
+ return;
184
+ }
185
+ const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${accessToken}`;
186
+ try {
187
+ const response = await import_axios.default.post(url, menu);
188
+ this.app.logger.info(`Create menu response: ${JSON.stringify(response.data)}`);
189
+ } catch (error) {
190
+ this.app.logger.error(`Error creating menu: ${JSON.stringify(error)}`);
191
+ }
192
+ }
193
+ // Wechat从用户这里获取签名,若通过则允许后续交互
194
+ async get(ctx) {
195
+ const { query } = ctx.request;
196
+ if (await this.checkSignature(query)) {
197
+ ctx.body = query.echostr;
198
+ } else {
199
+ ctx.status = 403;
200
+ }
201
+ }
202
+ // 微信向服务发送post请求
203
+ async post(ctx) {
204
+ const { query, body } = ctx.request;
205
+ if (!await this.checkSignature(query)) {
206
+ ctx.status = 403;
207
+ ctx.body = "Forbidden";
208
+ return;
209
+ }
210
+ const messages = await this.getWeChatMessage();
211
+ const messagesList = messages.map((message) => `${message.id}\u3001 ${message.key}`).join("\n");
212
+ let replyContent = `\u60A8\u597D\uFF0C\u6B22\u8FCE\u5173\u6CE8\u9053\u6709\u4E91\u7F51\u7EDC\u79D1\u6280\u6709\u9650\u516C\u53F8\uFF01\u60A8\u53EF\u4EE5\u8F93\u5165\u4E0B\u9762\u5173\u952E\u5B57\u6765\u83B7\u53D6\u60A8\u8981\u4E86\u89E3\u7684\u4FE1\u606F\uFF01
213
+ (\u8F93\u5165\u5E8F\u53F7\u6216\u5173\u952E\u5B57)\uFF1A
214
+ ${messagesList}`;
215
+ import_xml2js.default.parseString(body, { explicitArray: false }, (err, result) => {
216
+ if (err) {
217
+ this.app.logger.error(`Error parsing XML: ${JSON.stringify(err)}`);
218
+ ctx.status = 500;
219
+ ctx.body = "Server Error";
220
+ return;
221
+ }
222
+ const msg = result.xml;
223
+ let replyMessage;
224
+ if (msg.MsgType === "event" && msg.Event === "SCAN") {
225
+ replyContent = "\u626B\u7801\u6210\u529F\uFF0C\u6B22\u8FCE\u60A8\uFF01";
226
+ replyMessage = `
227
+ <xml>
228
+ <ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
229
+ <FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
230
+ <CreateTime>${Math.floor(Date.now() / 1e3)}</CreateTime>
231
+ <MsgType><![CDATA[text]]></MsgType>
232
+ <Content><![CDATA[${replyContent}]]></Content>
233
+ </xml>
234
+ `;
235
+ } else if (msg.MsgType === "text") {
236
+ const valueById = messages.find((message) => msg.Content.includes(message.id.toString()));
237
+ const valueByKey = messages.find((message) => msg.Content.includes(message.key));
238
+ if (valueById) {
239
+ replyContent = valueById.value;
240
+ } else if (valueByKey) {
241
+ replyContent = valueByKey.value;
242
+ }
243
+ replyMessage = `
244
+ <xml>
245
+ <ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
246
+ <FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
247
+ <CreateTime>${Math.floor(Date.now() / 1e3)}</CreateTime>
248
+ <MsgType><![CDATA[text]]></MsgType>
249
+ <Content><![CDATA[${replyContent}]]></Content>
250
+ </xml>
251
+ `;
252
+ } else if (msg.MsgType === "image") {
253
+ replyMessage = `
254
+ <xml>
255
+ <ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
256
+ <FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
257
+ <CreateTime>${Math.floor(Date.now() / 1e3)}</CreateTime>
258
+ <MsgType><![CDATA[image]]></MsgType>
259
+ <Image>
260
+ <MediaId><![CDATA[${msg.MediaId}]]></MediaId>
261
+ </Image>
262
+ </xml>
263
+ `;
264
+ } else {
265
+ replyMessage = `
266
+ <xml>
267
+ <ToUserName><![CDATA[${msg.FromUserName}]]></ToUserName>
268
+ <FromUserName><![CDATA[${msg.ToUserName}]]></FromUserName>
269
+ <CreateTime>${Math.floor(Date.now() / 1e3)}</CreateTime>
270
+ <MsgType><![CDATA[text]]></MsgType>
271
+ <Content><![CDATA[\u5BF9\u4E0D\u8D77\uFF0C\u6211\u53EA\u652F\u6301\u6587\u672C\u548C\u56FE\u7247\u6D88\u606F\u3002]]></Content>
272
+ </xml>
273
+ `;
274
+ }
275
+ ctx.type = "application/xml";
276
+ ctx.body = replyMessage;
277
+ });
278
+ }
279
+ // 处理OAuth回调
280
+ async handleOAuthCallback(ctx) {
281
+ const { code } = ctx.query;
282
+ if (!code) {
283
+ ctx.status = 400;
284
+ ctx.body = "Authorization code not found";
285
+ return;
286
+ }
287
+ try {
288
+ const { appid, appsecret } = await this.getWeChatConfig();
289
+ const tokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appsecret}&code=${code}&grant_type=authorization_code`;
290
+ const tokenResponse = await import_axios.default.get(tokenUrl);
291
+ const { access_token, openid } = tokenResponse.data;
292
+ if (!access_token || !openid) {
293
+ throw new Error("Failed to get access token or openid");
294
+ }
295
+ const userInfoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`;
296
+ const userInfoResponse = await import_axios.default.get(userInfoUrl);
297
+ const userInfo = userInfoResponse.data;
298
+ const userRepo = this.app.db.getRepository("users");
299
+ const existingUser = await userRepo.findOne({
300
+ filter: {
301
+ username: openid
302
+ }
303
+ });
304
+ if (!existingUser) {
305
+ await userRepo.create({
306
+ values: {
307
+ username: openid.slice(-6),
308
+ password: openid,
309
+ nickname: userInfo.nickname
310
+ }
311
+ });
312
+ this.app.logger.info(`\u65B0\u7528\u6237\u5DF2\u521B\u5EFA: ${JSON.stringify(userInfo.nickname)}`);
313
+ } else {
314
+ this.app.logger.info(`\u7528\u6237\u5DF2\u5B58\u5728:' ${JSON.stringify(existingUser.nickname)}`);
315
+ }
316
+ ctx.body = "\u5B8C\u6210\u6CE8\u518C\uFF0C\u767B\u5F55\u6210\u529F!";
317
+ } catch (error) {
318
+ this.app.logger.error(`Error handling OAuth callback: ${JSON.stringify(error)}`);
319
+ ctx.status = 500;
320
+ ctx.body = "Server Error";
321
+ }
322
+ }
323
+ // 从素材库获取素材列表
324
+ async getMaterialList() {
325
+ const accessToken = await this.getAccessToken();
326
+ if (!accessToken) {
327
+ this.app.logger.error("Failed to get access token");
328
+ return null;
329
+ }
330
+ const url = `https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${accessToken}`;
331
+ const data = {
332
+ type: "image",
333
+ // 需要检查的素材类型,如:image, video, voice, news
334
+ offset: 0,
335
+ count: 20
336
+ // 每次获取的素材数量,可以根据需要调整
337
+ };
338
+ try {
339
+ const response = await import_axios.default.post(url, data);
340
+ if (response.data.item) {
341
+ response.data.item.forEach((material) => {
342
+ this.app.logger.info(
343
+ `Media ID: ${JSON.stringify(material.media_id)}, Name: ${JSON.stringify(material.name)}, Update Time: ${JSON.stringify(material.update_time)}`
344
+ );
345
+ });
346
+ return response.data.item;
347
+ } else {
348
+ this.app.logger.error(`Failed to get material list: ${JSON.stringify(response.data)}`);
349
+ return null;
350
+ }
351
+ } catch (error) {
352
+ this.app.logger.error(`Error getting material list: ${JSON.stringify(error)}`);
353
+ return null;
354
+ }
355
+ }
356
+ // 检查素材是否为永久素材
357
+ async checkMaterialType(mediaId) {
358
+ const materials = await this.getMaterialList();
359
+ if (materials) {
360
+ const material = materials.find((item) => item.media_id === mediaId);
361
+ if (material) {
362
+ this.app.logger.info(`Material ID: ${JSON.stringify(mediaId)} is a permanent material.`);
363
+ return true;
364
+ } else {
365
+ this.app.logger.info(`Material ID: ${JSON.stringify(mediaId)} is not found in permanent materials.`);
366
+ return false;
367
+ }
368
+ } else {
369
+ this.app.logger.error("No materials found.");
370
+ return false;
371
+ }
372
+ }
373
+ // 更新articles表中的thumb_media_id字段
374
+ async updateLatestArticleWithLatestMediaId() {
375
+ const materialList = await this.getMaterialList();
376
+ if (materialList && materialList.length > 0) {
377
+ const latestMediaId = materialList[materialList.length - 1].media_id;
378
+ const repo = this.app.db.getRepository("wechatArticle");
379
+ const articles = await repo.find({ order: [["id", "desc"]], limit: 1 });
380
+ if (articles.length > 0) {
381
+ const latestArticle = articles[0];
382
+ await repo.update({
383
+ filter: { id: latestArticle.id },
384
+ values: { thumb_media_id: latestMediaId }
385
+ });
386
+ this.app.logger.info(
387
+ `Article ${JSON.stringify(latestArticle.id)} thumb_media_id updated to ${JSON.stringify(latestMediaId)}`
388
+ );
389
+ } else {
390
+ this.app.logger.error("No articles found in the database");
391
+ }
392
+ } else {
393
+ this.app.logger.error("No materials found in WeChat material list");
394
+ }
395
+ }
396
+ // 上传图文到草稿箱
397
+ async uploadNewsToDraft(article) {
398
+ const isPermanent = await this.checkMaterialType(article.thumb_media_id);
399
+ if (!isPermanent) {
400
+ this.app.logger.error(`Thumb media ID: ${JSON.stringify(article.thumb_media_id)} is not a permanent material.`);
401
+ return null;
402
+ }
403
+ const accessToken = await this.getAccessToken();
404
+ if (!accessToken) {
405
+ this.app.logger.error("Failed to get access token");
406
+ return null;
407
+ }
408
+ const url = `https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${accessToken}`;
409
+ const articleData = {
410
+ articles: [
411
+ {
412
+ title: article.title,
413
+ thumb_media_id: article.thumb_media_id,
414
+ // 确保这是一个永久素材的 thumb_media_id
415
+ author: article.author,
416
+ digest: article.digest || "",
417
+ show_cover_pic: 1,
418
+ content: article.content,
419
+ content_source_url: article.content_source_url || ""
420
+ }
421
+ ]
422
+ };
423
+ try {
424
+ const response = await import_axios.default.post(url, articleData);
425
+ this.app.logger.info(`Upload news to draft response: ${JSON.stringify(response.data)}`);
426
+ if (response.data.errcode) {
427
+ this.app.logger.error(
428
+ `Error code: ${JSON.stringify(response.data.errcode)}, message: ${JSON.stringify(response.data.errmsg)}`
429
+ );
430
+ return null;
431
+ }
432
+ return response.data.media_id;
433
+ } catch (error) {
434
+ this.app.logger.error(`Error uploading news to draft: ${JSON.stringify(error)}`);
435
+ return null;
436
+ }
437
+ }
438
+ // 发送推文接口
439
+ async sendNewsToAll(mediaId) {
440
+ const accessToken = await this.getAccessToken();
441
+ if (!accessToken) {
442
+ this.app.logger.error("Failed to get access token");
443
+ return null;
444
+ }
445
+ const url = `https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=${accessToken}`;
446
+ const messageData = {
447
+ filter: {
448
+ is_to_all: true
449
+ // 发送给所有用户
450
+ },
451
+ mpnews: {
452
+ media_id: mediaId
453
+ },
454
+ msgtype: "mpnews",
455
+ send_ignore_reprint: 0
456
+ };
457
+ try {
458
+ const response = await import_axios.default.post(url, messageData);
459
+ this.app.logger.info(`Send news response: ${JSON.stringify(response.data)}`);
460
+ if (response.data.errcode) {
461
+ this.app.logger.error(
462
+ `Error code: ${JSON.stringify(response.data.errcode)}, message: ${JSON.stringify(response.data.errmsg)}`
463
+ );
464
+ return null;
465
+ }
466
+ return response.data;
467
+ } catch (error) {
468
+ this.app.logger.error(`Error sending news: ${JSON.stringify(error)}`);
469
+ return null;
470
+ }
471
+ }
472
+ // 发布文章接口
473
+ async publishLastArticleToWeChat() {
474
+ const article = await this.getWeChatArticle();
475
+ if (article) {
476
+ const mediaId = await this.uploadNewsToDraft(article);
477
+ if (mediaId) {
478
+ const result = await this.sendNewsToAll(mediaId);
479
+ return result;
480
+ }
481
+ }
482
+ return null;
483
+ }
484
+ // load() 生命周期函数,在插件加载时执行
485
+ async load() {
486
+ this.app.db.on("wechatMessage.afterCreate", this.handleMessageCreate.bind(this));
487
+ this.app.db.on("wechatMenu.afterUpdate", this.handleMenuUpdate.bind(this));
488
+ this.app.authManager.registerTypes(import_constants.authType, {
489
+ auth: import_module_auth.BasicAuth
490
+ });
491
+ this.app.resourcer.define({
492
+ name: "wechat",
493
+ actions: {
494
+ // 创建菜单接口
495
+ createMenu: async (ctx) => {
496
+ await this.createCustomMenu();
497
+ this.app.logger.info("Custom menu created successfully.");
498
+ ctx.body = "success";
499
+ },
500
+ // Wechat 消息处理接口/服务器URL配置接口
501
+ handler: async (ctx) => {
502
+ ctx.withoutDataWrapping = true;
503
+ if (ctx.request.method.toLowerCase() === "get") {
504
+ await this.get(ctx);
505
+ } else {
506
+ await this.post(ctx);
507
+ }
508
+ },
509
+ // 生成二维码接口
510
+ generateQrCode: async (ctx) => {
511
+ const qrCodeUrl = await this.generateQrCode();
512
+ if (qrCodeUrl) {
513
+ ctx.body = { data: { url: qrCodeUrl } };
514
+ this.app.logger.info(`\u6211\u662F\u751F\u6210\u4E8C\u7EF4\u7801\u63A5\u53E3\u7684URL: ${JSON.stringify(qrCodeUrl)}`);
515
+ } else {
516
+ ctx.status = 500;
517
+ ctx.body = "Failed to generate QR code";
518
+ }
519
+ },
520
+ // 处理OAuth回调接口
521
+ handleOAuthCallback: async (ctx) => {
522
+ await this.handleOAuthCallback(ctx);
523
+ },
524
+ // 获取素材列表并更新最新文章的封面图片接口
525
+ updateLatestArticleWithLatestMediaId: async (ctx) => {
526
+ await this.updateLatestArticleWithLatestMediaId();
527
+ ctx.body = "Latest article updated with latest media ID";
528
+ },
529
+ // 发布最新文章到微信公众号接口
530
+ publishLastArticleToWeChat: async (ctx) => {
531
+ const result = await this.publishLastArticleToWeChat();
532
+ if (result) {
533
+ ctx.body = "Article published to WeChat successfully";
534
+ } else {
535
+ ctx.status = 500;
536
+ ctx.body = "Failed to publish article to WeChat";
537
+ }
538
+ }
539
+ }
540
+ });
541
+ this.app.acl.allow("wechat", "*", "public");
542
+ }
543
+ // 处理 message 表的 afterCreate 事件
544
+ async handleMessageCreate(model, options) {
545
+ const { id, key, value } = model.get();
546
+ this.app.logger.info(`New message created: ${JSON.stringify(key)} - ${JSON.stringify(value)}`);
547
+ const userIds = await this.getAllFollowers();
548
+ this.app.logger.info(`Follower IDs: ${JSON.stringify(userIds)}`);
549
+ await this.pushMessageToFollowers(userIds, `\u76EE\u524D\u7CFB\u7EDF\u63D0\u793A\u5DF2\u7ECF\u66F4\u65B0\uFF0C\u5927\u5BB6\u53EF\u4EE5\u4F7F\u7528\u65B0\u7684\u5173\u952E\u5B57 "${key}" \u6765\u83B7\u53D6\u4FE1\u606F\uFF01`);
550
+ }
551
+ // 处理 menu 表的 afterUpdate 事件
552
+ async handleMenuUpdate(model, options) {
553
+ const { id, name } = model.get();
554
+ this.app.logger.info(`Menu item updated: ${JSON.stringify(name)} (ID: ${JSON.stringify(id)})`);
555
+ const userIds = await this.getAllFollowers();
556
+ this.app.logger.info(`Follower IDs: ${JSON.stringify(userIds)}`);
557
+ await this.pushMessageToFollowers(userIds, "\u7528\u6237\u4F60\u597D\uFF0C\u83DC\u5355\u9879\u5DF2\u7ECF\u66F4\u65B0");
558
+ }
559
+ // 获取用户列表
560
+ async getAllFollowers() {
561
+ const accessToken = await this.getAccessToken();
562
+ if (!accessToken) {
563
+ this.app.logger.error("Failed to get access token");
564
+ return [];
565
+ }
566
+ const url = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${accessToken}`;
567
+ try {
568
+ const response = await import_axios.default.get(url);
569
+ if (response.data.errcode) {
570
+ this.app.logger.error(`Error getting followers: ${JSON.stringify(response.data.errmsg)}`);
571
+ return [];
572
+ }
573
+ return response.data.data.openid;
574
+ } catch (error) {
575
+ this.app.logger.error(`Error getting followers: ${JSON.stringify(error)}`);
576
+ return [];
577
+ }
578
+ }
579
+ // 给用户发送消息
580
+ async pushMessageToFollowers(userIds, message) {
581
+ const accessToken = await this.getAccessToken();
582
+ if (!accessToken) {
583
+ this.app.logger.error("Failed to get access token");
584
+ return;
585
+ }
586
+ const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`;
587
+ for (const userId of userIds) {
588
+ const data = {
589
+ touser: userId,
590
+ msgtype: "text",
591
+ text: {
592
+ content: message
593
+ }
594
+ };
595
+ try {
596
+ const response = await import_axios.default.post(url, data);
597
+ if (response.data.errcode) {
598
+ this.app.logger.error(
599
+ `Error pushing message to user ${JSON.stringify(userId)}: ${JSON.stringify(response.data.errmsg)}`
600
+ );
601
+ } else {
602
+ this.app.logger.info(`Message pushed to user ${JSON.stringify(userId)} successfully.`);
603
+ }
604
+ } catch (error) {
605
+ this.app.logger.error(`Error pushing message to user ${JSON.stringify(userId)}: ${JSON.stringify(error)}`);
606
+ }
607
+ }
608
+ }
609
+ }
610
+ var plugin_default = PluginReplacePageServer;
611
+ // Annotate the CommonJS export names for ESM import in node:
612
+ 0 && (module.exports = {
613
+ PluginReplacePageServer
614
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@tachybase/plugin-wechat-official-account",
3
+ "version": "0.23.8",
4
+ "keywords": [
5
+ "Authentication"
6
+ ],
7
+ "main": "dist/server/index.js",
8
+ "dependencies": {
9
+ "@ant-design/icons": "^5.5.2",
10
+ "@types/xml2js": "^0.4.14",
11
+ "antd": "5.22.5",
12
+ "axios": "^1.7.9",
13
+ "body-parser": "^1.20.3",
14
+ "jsonwebtoken": "^8.5.1",
15
+ "koa-bodyparser": "^4.4.1",
16
+ "qrcode.react": "^3.2.0",
17
+ "react-router-dom": "6.28.1",
18
+ "sequelize": "^6.37.5",
19
+ "xml2js": "^0.6.2"
20
+ },
21
+ "peerDependencies": {
22
+ "@tachybase/client": "0.23.8",
23
+ "@tachybase/actions": "0.23.8",
24
+ "@tachybase/module-auth": "0.23.8",
25
+ "@tachybase/test": "0.23.8",
26
+ "@tachybase/server": "0.23.8"
27
+ },
28
+ "scripts": {
29
+ "build": "tachybase-build --no-dts @tachybase/plugin-wechat-official-account"
30
+ }
31
+ }
package/server.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './dist/server';
2
+ export { default } from './dist/server';
package/server.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/server/index.js');