koishi-plugin-wordpress-notifier 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/push.js CHANGED
@@ -7,6 +7,7 @@ const crypto_1 = require("crypto");
7
7
  class PushService {
8
8
  constructor(ctx, config, storageService) {
9
9
  this.ctx = ctx;
10
+ this.atAllFrequencyMap = new Map(); // 记录每个群组的@全体成员时间
10
11
  this.config = config;
11
12
  this.storageService = storageService;
12
13
  }
@@ -19,7 +20,6 @@ class PushService {
19
20
  let retries = 0;
20
21
  while (retries <= maxRetries) {
21
22
  try {
22
- // 发送前添加基础延迟,避免触发频率限制
23
23
  if (retries > 0) {
24
24
  await new Promise(resolve => setTimeout(resolve, 1000));
25
25
  }
@@ -33,8 +33,7 @@ class PushService {
33
33
  this.ctx.logger.error('Failed to send message after all retries:', error);
34
34
  return false;
35
35
  }
36
- // 指数退避策略,增加基础延迟
37
- const delay = Math.pow(2, retries) * 1500; // 增加到 1.5 秒基础延迟
36
+ const delay = Math.pow(2, retries) * 1500;
38
37
  this.ctx.logger.info(`Waiting ${delay}ms before next retry`);
39
38
  await new Promise(resolve => setTimeout(resolve, delay));
40
39
  }
@@ -44,47 +43,29 @@ class PushService {
44
43
  // 分段发送消息
45
44
  async sendMessageInParts(bot, targetId, message) {
46
45
  try {
47
- // 消息分段阈值(根据实际适配器调整)
48
46
  const MAX_MESSAGE_LENGTH = 2000;
49
47
  if (message.length <= MAX_MESSAGE_LENGTH) {
50
- // 消息长度在限制内,直接发送(带重试)
51
48
  return await this.sendMessageWithRetry(bot, targetId, message);
52
49
  }
53
- // 消息过长,需要分段
54
50
  const parts = [];
55
- let currentPart = '';
56
- let currentLength = 0;
57
- // 按字符数分割消息,避免单段过长
58
- for (let i = 0; i < message.length; i++) {
59
- const char = message[i];
60
- currentPart += char;
61
- currentLength++;
62
- // 当达到长度限制且遇到空格或标点时分割
63
- if (currentLength >= MAX_MESSAGE_LENGTH - 50) { // 预留一些空间
64
- // 寻找最近的空格或标点符号
65
- let splitIndex = currentPart.length - 1;
66
- while (splitIndex > 0 && !/[\s,。!?;:,.!?;:]/.test(currentPart[splitIndex])) {
67
- splitIndex--;
68
- }
69
- if (splitIndex > 0) {
70
- // 从分割点断开
71
- parts.push(currentPart.substring(0, splitIndex + 1));
72
- currentPart = currentPart.substring(splitIndex + 1);
73
- currentLength = currentPart.length;
74
- }
75
- else {
76
- // 没有找到合适的分割点,强制分割
77
- parts.push(currentPart);
78
- currentPart = '';
79
- currentLength = 0;
51
+ let currentPosition = 0;
52
+ while (currentPosition < message.length) {
53
+ let endPosition = currentPosition + MAX_MESSAGE_LENGTH;
54
+ if (endPosition < message.length) {
55
+ const searchStart = Math.max(currentPosition, endPosition - 200);
56
+ let i = endPosition;
57
+ while (i >= searchStart) {
58
+ if (/[\s,。!?;:,.!?;:]/.test(message[i])) {
59
+ endPosition = i + 1;
60
+ break;
61
+ }
62
+ i--;
80
63
  }
81
64
  }
65
+ const part = message.substring(currentPosition, endPosition);
66
+ parts.push(part);
67
+ currentPosition = endPosition;
82
68
  }
83
- // 添加最后一部分
84
- if (currentPart) {
85
- parts.push(currentPart);
86
- }
87
- // 发送所有部分
88
69
  let allSuccess = true;
89
70
  for (let i = 0; i < parts.length; i++) {
90
71
  const partMessage = parts.length > 1
@@ -94,7 +75,6 @@ class PushService {
94
75
  if (!success) {
95
76
  allSuccess = false;
96
77
  }
97
- // 避免发送过快导致被限制
98
78
  if (i < parts.length - 1) {
99
79
  await new Promise(resolve => setTimeout(resolve, 1000));
100
80
  }
@@ -107,67 +87,52 @@ class PushService {
107
87
  }
108
88
  }
109
89
  // 获取可用的机器人实例
110
- getAvailableBot() {
90
+ getAvailableBot(targetId) {
111
91
  return this.ctx.bots[0] || null;
112
92
  }
113
- // 验证群聊 ID 有效性
114
- async validateGroupId(groupId) {
115
- if (!groupId || typeof groupId !== 'string') {
116
- this.ctx.logger.error('Invalid group ID format');
93
+ // 检查是否允许@全体成员
94
+ async isAtAllAllowed(groupId) {
95
+ if (!this.config.allowAtAll) {
117
96
  return false;
118
97
  }
119
- // 支持多平台 ID 格式
120
- // QQ: 纯数字字符串
121
- // Telegram: 带-前缀的数字字符串
122
- // Discord: 数字字符串
123
- // 其他平台: 允许字母、数字、-、_ 组合
124
- if (!/^[a-zA-Z0-9_-]+$/.test(groupId)) {
125
- this.ctx.logger.error('Invalid group ID format, only letters, numbers, hyphens and underscores are allowed');
98
+ const now = Date.now();
99
+ const lastAtTime = this.atAllFrequencyMap.get(groupId);
100
+ if (lastAtTime && (now - lastAtTime) < 60 * 60 * 1000) {
101
+ this.ctx.logger.warn(`At-all mention frequency limit reached for group ${groupId}`);
126
102
  return false;
127
103
  }
128
- // 可以添加更多验证逻辑,比如检查机器人是否在该群聊中
129
- // ...
104
+ this.atAllFrequencyMap.set(groupId, now);
130
105
  return true;
131
106
  }
132
- // 验证用户 ID 有效性
133
- async validateUserId(userId) {
134
- if (!userId || typeof userId !== 'string') {
135
- this.ctx.logger.error('Invalid user ID format');
136
- return false;
137
- }
138
- // 支持多平台 ID 格式
139
- // QQ: 纯数字字符串
140
- // Telegram: 带-前缀的数字字符串
141
- // Discord: 数字字符串
142
- // 其他平台: 允许字母、数字、-、_ 组合
143
- if (!/^[a-zA-Z0-9_-]+$/.test(userId)) {
144
- this.ctx.logger.error('Invalid user ID format, only letters, numbers, hyphens and underscores are allowed');
145
- return false;
146
- }
147
- return true;
107
+ // 获取@全体成员消息段
108
+ getAtAllMessage(bot) {
109
+ return (0, koishi_1.h)('at', { type: 'all' });
148
110
  }
149
111
  // 推送新文章
150
112
  async pushNewArticle(article, groupId, enableAtAll = false) {
151
113
  try {
152
- // 验证群聊 ID 有效性
153
- if (!await this.validateGroupId(groupId)) {
114
+ if (!this.isPushTimeAllowed()) {
115
+ this.ctx.logger.info('Current time is not in allowed push time range');
154
116
  return false;
155
117
  }
156
- // 检查群聊推送配置
157
118
  const pushConfig = await this.storageService.getPushConfig(groupId);
158
119
  if (!pushConfig?.enabled) {
159
120
  this.ctx.logger.info(`Push is disabled for group ${groupId}, skipping`);
160
121
  return false;
161
122
  }
162
- const bot = this.getAvailableBot();
123
+ const bot = this.getAvailableBot(groupId);
163
124
  if (!bot) {
164
125
  this.ctx.logger.error('No available bot for pushing messages');
165
126
  return false;
166
127
  }
167
- // 发送文本消息
168
- const textMessage = this.getNewArticleTemplate(article);
169
- const finalMessage = enableAtAll && this.config.allowAtAll ? `@全体成员\n${textMessage}` : textMessage;
170
- return await this.sendMessageInParts(bot, groupId, finalMessage);
128
+ const canAtAll = enableAtAll && await this.isAtAllAllowed(groupId);
129
+ const message = this.getPlatformCompatibleMessage(article, bot, false, canAtAll);
130
+ if (typeof message === 'string') {
131
+ return await this.sendMessageInParts(bot, groupId, message);
132
+ }
133
+ else {
134
+ return await this.sendMessageWithRetry(bot, groupId, message);
135
+ }
171
136
  }
172
137
  catch (error) {
173
138
  this.ctx.logger.error(`Failed to push new article to group ${groupId}:`, error);
@@ -177,24 +142,27 @@ class PushService {
177
142
  // 推送文章更新
178
143
  async pushArticleUpdate(article, groupId) {
179
144
  try {
180
- // 验证群聊 ID 有效性
181
- if (!await this.validateGroupId(groupId)) {
145
+ if (!this.isPushTimeAllowed()) {
146
+ this.ctx.logger.info('Current time is not in allowed push time range');
182
147
  return false;
183
148
  }
184
- // 检查群聊推送配置
185
149
  const pushConfig = await this.storageService.getPushConfig(groupId);
186
150
  if (!pushConfig?.enabled || !pushConfig?.enableUpdatePush) {
187
151
  this.ctx.logger.info(`Update push is disabled for group ${groupId}, skipping`);
188
152
  return false;
189
153
  }
190
- const bot = this.getAvailableBot();
154
+ const bot = this.getAvailableBot(groupId);
191
155
  if (!bot) {
192
156
  this.ctx.logger.error('No available bot for pushing messages');
193
157
  return false;
194
158
  }
195
- // 发送文本消息
196
- const textMessage = this.getArticleUpdateTemplate(article);
197
- return await this.sendMessageInParts(bot, groupId, textMessage);
159
+ const message = this.getPlatformCompatibleMessage(article, bot, true, false);
160
+ if (typeof message === 'string') {
161
+ return await this.sendMessageInParts(bot, groupId, message);
162
+ }
163
+ else {
164
+ return await this.sendMessageWithRetry(bot, groupId, message);
165
+ }
198
166
  }
199
167
  catch (error) {
200
168
  this.ctx.logger.error(`Failed to push article update to group ${groupId}:`, error);
@@ -204,24 +172,27 @@ class PushService {
204
172
  // 推送新用户注册
205
173
  async pushUserRegister(user, groupId) {
206
174
  try {
207
- // 验证群聊 ID 有效性
208
- if (!await this.validateGroupId(groupId)) {
175
+ if (!this.isPushTimeAllowed()) {
176
+ this.ctx.logger.info('Current time is not in allowed push time range');
209
177
  return false;
210
178
  }
211
- // 检查群聊推送配置
212
179
  const pushConfig = await this.storageService.getPushConfig(groupId);
213
180
  if (!pushConfig?.enabled) {
214
181
  this.ctx.logger.info(`Push is disabled for group ${groupId}, skipping`);
215
182
  return false;
216
183
  }
217
- const bot = this.getAvailableBot();
184
+ const bot = this.getAvailableBot(groupId);
218
185
  if (!bot) {
219
186
  this.ctx.logger.error('No available bot for pushing messages');
220
187
  return false;
221
188
  }
222
- // 发送文本消息
223
- const textMessage = this.getUserRegisterTemplate(user);
224
- return await this.sendMessageInParts(bot, groupId, textMessage);
189
+ const message = this.getPlatformCompatibleUserRegisterMessage(user, bot);
190
+ if (typeof message === 'string') {
191
+ return await this.sendMessageInParts(bot, groupId, message);
192
+ }
193
+ else {
194
+ return await this.sendMessageWithRetry(bot, groupId, message);
195
+ }
225
196
  }
226
197
  catch (error) {
227
198
  this.ctx.logger.error(`Failed to push user register to group ${groupId}:`, error);
@@ -231,22 +202,20 @@ class PushService {
231
202
  // 私聊推送
232
203
  async pushToUser(article, userId) {
233
204
  try {
234
- // 验证用户 ID 有效性
235
- if (!await this.validateUserId(userId)) {
205
+ if (!this.isPushTimeAllowed()) {
206
+ this.ctx.logger.info('Current time is not in allowed push time range');
236
207
  return false;
237
208
  }
238
- // 检查用户订阅状态
239
209
  const isSubscribed = await this.storageService.isUserSubscribed(userId, 'private');
240
210
  if (!isSubscribed) {
241
211
  this.ctx.logger.info(`User ${userId} is not subscribed, skipping`);
242
212
  return false;
243
213
  }
244
- const bot = this.getAvailableBot();
214
+ const bot = this.getAvailableBot(userId);
245
215
  if (!bot) {
246
216
  this.ctx.logger.error('No available bot for pushing messages');
247
217
  return false;
248
218
  }
249
- // 发送文本消息
250
219
  const textMessage = this.getNewArticleTemplate(article);
251
220
  return await this.sendMessageInParts(bot, userId, textMessage);
252
221
  }
@@ -255,145 +224,29 @@ class PushService {
255
224
  return false;
256
225
  }
257
226
  }
258
- // 创建文章卡片
259
- createArticleCard(article, isUpdate = false) {
260
- const title = article.title?.rendered || '无标题';
261
- const author = article.author_name || '未知作者';
262
- const date = isUpdate ? new Date(article.modified).toLocaleString('zh-CN') : new Date(article.date).toLocaleString('zh-CN');
263
- const excerpt = this.getExcerpt(article);
264
- const link = article.link || this.config.wordpressUrl;
265
- const type = isUpdate ? '更新' : '发布';
266
- // 使用腾讯官方 ARK 卡片格式
267
- try {
268
- // 官方 ARK 卡片格式(使用模板 23:链接+文本列表模板)
269
- return (0, koishi_1.h)('ark', {
270
- template_id: 23,
271
- kv: [
272
- {
273
- key: '#DESC#',
274
- value: isUpdate ? '文章更新通知' : '新文章推送'
275
- },
276
- {
277
- key: '#PROMPT#',
278
- value: 'WordPress 推送'
279
- },
280
- {
281
- key: '#TITLE#',
282
- value: title
283
- },
284
- {
285
- key: '#META_URL#',
286
- value: link
287
- },
288
- {
289
- key: '#META_LIST#',
290
- obj: [
291
- {
292
- obj_kv: [
293
- {
294
- key: '作者',
295
- value: author
296
- },
297
- {
298
- key: '时间',
299
- value: date
300
- }
301
- ]
302
- },
303
- {
304
- obj_kv: [
305
- {
306
- key: '摘要',
307
- value: excerpt
308
- }
309
- ]
310
- }
311
- ]
312
- }
313
- ]
314
- });
315
- }
316
- catch (arkError) {
317
- this.ctx.logger.warn('ARK card failed, falling back to text format:', arkError);
318
- // 降级到文本消息
319
- return isUpdate ? this.getArticleUpdateTemplate(article) : this.getNewArticleTemplate(article);
320
- }
321
- }
322
- // 检查平台是否支持卡片消息
323
- isCardMessageSupported(bot) {
324
- // 检查机器人平台是否为 QQ(支持 ARK 卡片)
325
- return bot.platform === 'qq';
326
- }
327
- // 获取平台兼容的消息
227
+ // 获取平台兼容的消息(默认使用文本消息)
328
228
  getPlatformCompatibleMessage(article, bot, isUpdate = false, enableAtAll = false) {
329
- // 检查平台是否支持卡片消息
330
- if (this.isCardMessageSupported(bot)) {
331
- // QQ 平台使用卡片消息
332
- return this.createArticleCard(article, isUpdate);
229
+ try {
230
+ const textMessage = isUpdate ? this.getArticleUpdateTemplate(article) : this.getNewArticleTemplate(article);
231
+ if (enableAtAll) {
232
+ const atAllMessage = this.getAtAllMessage(bot);
233
+ return [atAllMessage, textMessage];
234
+ }
235
+ return textMessage;
333
236
  }
334
- else {
335
- // 其他平台使用文本消息
336
- const template = isUpdate ? this.getArticleUpdateTemplate(article) : this.getNewArticleTemplate(article);
337
- return enableAtAll && this.config.allowAtAll ? `@全体成员\n${template}` : template;
237
+ catch (error) {
238
+ this.ctx.logger.warn('Message creation failed:', error);
239
+ const textMessage = isUpdate ? this.getArticleUpdateTemplate(article) : this.getNewArticleTemplate(article);
240
+ return textMessage;
338
241
  }
339
242
  }
340
- // 创建用户注册卡片
341
- createUserRegisterCard(user) {
342
- const username = user.name || '未知用户';
343
- const registerDate = new Date(user.registered_date || new Date()).toLocaleString('zh-CN');
344
- // 使用腾讯官方 ARK 卡片格式
243
+ // 获取平台兼容的用户注册消息(默认使用文本消息)
244
+ getPlatformCompatibleUserRegisterMessage(user, bot) {
345
245
  try {
346
- // 官方 ARK 卡片格式(使用模板 23:链接+文本列表模板)
347
- return (0, koishi_1.h)('ark', {
348
- template_id: 23,
349
- kv: [
350
- {
351
- key: '#DESC#',
352
- value: '新用户注册通知'
353
- },
354
- {
355
- key: '#PROMPT#',
356
- value: 'WordPress 推送'
357
- },
358
- {
359
- key: '#TITLE#',
360
- value: `新用户注册:${username}`
361
- },
362
- {
363
- key: '#META_URL#',
364
- value: this.config.wordpressUrl
365
- },
366
- {
367
- key: '#META_LIST#',
368
- obj: [
369
- {
370
- obj_kv: [
371
- {
372
- key: '用户名',
373
- value: username
374
- },
375
- {
376
- key: '注册时间',
377
- value: registerDate
378
- }
379
- ]
380
- },
381
- {
382
- obj_kv: [
383
- {
384
- key: '通知',
385
- value: '欢迎加入我们的社区!'
386
- }
387
- ]
388
- }
389
- ]
390
- }
391
- ]
392
- });
246
+ return this.getUserRegisterTemplate(user);
393
247
  }
394
- catch (arkError) {
395
- this.ctx.logger.warn('ARK card failed, falling back to text format:', arkError);
396
- // 降级到文本消息
248
+ catch (error) {
249
+ this.ctx.logger.warn('User register message creation failed:', error);
397
250
  return this.getUserRegisterTemplate(user);
398
251
  }
399
252
  }
@@ -401,7 +254,7 @@ class PushService {
401
254
  getNewArticleTemplate(article) {
402
255
  const title = article.title?.rendered || '无标题';
403
256
  const author = article.author_name || '未知作者';
404
- const publishDate = new Date(article.date).toLocaleString('zh-CN');
257
+ const publishDate = new Date(article.date || new Date()).toLocaleString('zh-CN');
405
258
  const excerpt = this.getExcerpt(article);
406
259
  const link = article.link || this.config.wordpressUrl;
407
260
  return `📰 新文章推送
@@ -420,7 +273,7 @@ class PushService {
420
273
  // 文章更新推送模板
421
274
  getArticleUpdateTemplate(article) {
422
275
  const title = article.title?.rendered || '无标题';
423
- const modifiedDate = new Date(article.modified).toLocaleString('zh-CN');
276
+ const modifiedDate = new Date(article.modified || article.date || new Date()).toLocaleString('zh-CN');
424
277
  const excerpt = this.getExcerpt(article);
425
278
  const link = article.link || this.config.wordpressUrl;
426
279
  return `🔄 文章更新通知
@@ -446,11 +299,9 @@ class PushService {
446
299
  // 获取文章摘要
447
300
  getExcerpt(article) {
448
301
  if (article.excerpt?.rendered) {
449
- // 移除 HTML 标签
450
302
  return article.excerpt.rendered.replace(/<[^>]+>/g, '').trim();
451
303
  }
452
304
  else if (article.content?.rendered) {
453
- // 从内容中提取摘要
454
305
  const content = article.content.rendered.replace(/<[^>]+>/g, '').trim();
455
306
  return content.length > 200 ? content.substring(0, 200) + '...' : content;
456
307
  }
@@ -460,23 +311,17 @@ class PushService {
460
311
  async batchPush(articles, groupIds, enableAtAll = false, messageType = 'text') {
461
312
  let success = 0;
462
313
  let failed = 0;
463
- // 并发推送,添加并发限制
464
- const pushPromises = [];
465
- const MAX_CONCURRENT = 5; // 最大并发数,避免 API 限流
466
- // 生成所有推送任务
314
+ const MAX_CONCURRENT = 3;
467
315
  const allTasks = [];
468
316
  for (const article of articles) {
469
317
  for (const groupId of groupIds) {
470
- allTasks.push({ article, groupId, enableAtAll, messageType });
318
+ allTasks.push({ article, groupId, enableAtAll });
471
319
  }
472
320
  }
473
- // 分批执行推送任务
474
321
  for (let i = 0; i < allTasks.length; i += MAX_CONCURRENT) {
475
322
  const batchTasks = allTasks.slice(i, i + MAX_CONCURRENT);
476
323
  const batchPromises = batchTasks.map(task => this.pushNewArticle(task.article, task.groupId, task.enableAtAll));
477
- // 等待当前批次完成
478
324
  const batchResults = await Promise.allSettled(batchPromises);
479
- // 统计当前批次结果
480
325
  batchResults.forEach(result => {
481
326
  if (result.status === 'fulfilled' && result.value) {
482
327
  success++;
@@ -485,9 +330,9 @@ class PushService {
485
330
  failed++;
486
331
  }
487
332
  });
488
- // 批次之间添加延迟,避免 API 限流
489
333
  if (i + MAX_CONCURRENT < allTasks.length) {
490
- await new Promise(resolve => setTimeout(resolve, 1000));
334
+ const delay = Math.max(1500, batchTasks.length * 500);
335
+ await new Promise(resolve => setTimeout(resolve, delay));
491
336
  }
492
337
  }
493
338
  return { success, failed };
@@ -495,7 +340,6 @@ class PushService {
495
340
  // 检查推送时间是否在允许范围内
496
341
  isPushTimeAllowed() {
497
342
  const hour = new Date().getHours();
498
- // 默认允许推送时间:9:00-22:00
499
343
  return hour >= 9 && hour <= 22;
500
344
  }
501
345
  }
package/lib/schedule.d.ts CHANGED
@@ -13,11 +13,17 @@ export declare class ScheduleService {
13
13
  private failureCount;
14
14
  private readonly FAILURE_THRESHOLD;
15
15
  private adminUserId;
16
+ private isRunning;
17
+ private isDisposed;
18
+ private readonly MAX_CONCURRENT_PUSHES;
16
19
  constructor(ctx: Context, config: Config, wordpressService: WordPressService, storageService: StorageService, pushService: PushService);
20
+ private validateAdminUserId;
17
21
  private initSchedule;
18
22
  startSchedule(): void;
19
23
  stopSchedule(): void;
20
24
  private sendFailureAlert;
25
+ private isPushTimeAllowed;
26
+ private executeWithConcurrencyLimit;
21
27
  checkAndPushArticles(): Promise<void>;
22
28
  checkNewUsers(): Promise<void>;
23
29
  triggerCheck(): Promise<void>;