koishi-plugin-wordpress-notifier 2.8.4 → 2.9.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/README.md CHANGED
@@ -389,6 +389,19 @@ npm install
389
389
 
390
390
  ## 版本历史
391
391
 
392
+ ### 2.8.5 (2026-02-19)
393
+
394
+ - 🐛 修复 MySQL 主键约束错误 (ER_PRIMARY_CANT_HAVE_NULL)
395
+
396
+ **核心修复**
397
+ - ✅ 增强 `checkAndFixTableStructure` 函数,添加对 `wordpress_version` 表的验证和修复逻辑
398
+ - ✅ 确保所有数据库表的主键约束正确设置,避免 NULL 值问题
399
+ - ✅ 优化表结构初始化过程,确保所有表都能正确创建和初始化
400
+
401
+ **稳定性提升**
402
+ - ✅ 提高插件在 MySQL 数据库环境下的稳定性
403
+ - ✅ 增强数据库表结构异常情况下的恢复能力
404
+
392
405
  ### 2.8.4 (2026-02-19)
393
406
 
394
407
  - 🔧 版本更新和文档完善
package/lib/index.d.ts CHANGED
@@ -10,12 +10,12 @@ declare module 'koishi' {
10
10
  }
11
11
  }
12
12
  export interface WordPressVersionRecord {
13
- id: number;
13
+ id: string;
14
14
  version: string;
15
15
  updatedAt: Date;
16
16
  }
17
17
  export interface WordPressConfigRecord {
18
- id: number;
18
+ id: string;
19
19
  key: string;
20
20
  value: string;
21
21
  updatedAt: Date;
@@ -50,7 +50,7 @@ export interface Config {
50
50
  };
51
51
  }
52
52
  export interface WordPressPost {
53
- id: number;
53
+ id: string;
54
54
  title: {
55
55
  rendered: string;
56
56
  };
@@ -60,12 +60,12 @@ export interface WordPressPost {
60
60
  excerpt: {
61
61
  rendered: string;
62
62
  };
63
- author: number;
64
- categories: number[];
65
- tags: number[];
63
+ author: string;
64
+ categories: string[];
65
+ tags: string[];
66
66
  }
67
67
  export interface WordPressUser {
68
- id: number;
68
+ id: string;
69
69
  name: string;
70
70
  slug: string;
71
71
  date?: string;
package/lib/index.js CHANGED
@@ -129,22 +129,23 @@ function apply(ctx, config) {
129
129
  ctx.logger.info('WordPress 推送插件已加载');
130
130
  // 修复 MySQL 自增主键问题,使用正确的模型配置
131
131
  // 确保 id 字段被正确设置为自增主键,并且在插入时不会被设置为 NULL
132
+ // 显式指定 nullable: false 约束以适配 MySQL 特有要求
132
133
  ctx.model.extend('wordpress_post_updates', {
133
- id: 'integer',
134
- siteId: 'string',
135
- postId: 'integer',
136
- lastModified: 'timestamp',
137
- pushedAt: 'timestamp'
134
+ id: { type: 'integer', nullable: false },
135
+ siteId: { type: 'string', nullable: false },
136
+ postId: { type: 'integer', nullable: false },
137
+ lastModified: { type: 'timestamp', nullable: false },
138
+ pushedAt: { type: 'timestamp', nullable: false }
138
139
  }, {
139
140
  primary: 'id',
140
141
  autoInc: true,
141
142
  unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
142
143
  });
143
144
  ctx.model.extend('wordpress_user_registrations', {
144
- id: 'integer',
145
- siteId: 'string',
146
- userId: 'integer',
147
- pushedAt: 'timestamp'
145
+ id: { type: 'integer', nullable: false },
146
+ siteId: { type: 'string', nullable: false },
147
+ userId: { type: 'integer', nullable: false },
148
+ pushedAt: { type: 'timestamp', nullable: false }
148
149
  }, {
149
150
  primary: 'id',
150
151
  autoInc: true,
@@ -152,43 +153,108 @@ function apply(ctx, config) {
152
153
  });
153
154
  // 配置存储表
154
155
  ctx.model.extend('wordpress_config', {
155
- id: 'integer',
156
- key: 'string',
157
- value: 'string',
158
- updatedAt: 'timestamp'
156
+ id: { type: 'string', nullable: false },
157
+ key: { type: 'string', nullable: false },
158
+ value: { type: 'string', nullable: false },
159
+ updatedAt: { type: 'timestamp', nullable: false }
159
160
  }, {
160
161
  primary: 'id',
161
- autoInc: true,
162
162
  unique: ['key']
163
163
  });
164
164
  // 版本记录表
165
165
  ctx.model.extend('wordpress_version', {
166
- id: 'integer',
167
- version: 'string',
168
- updatedAt: 'timestamp'
166
+ id: { type: 'string', nullable: false },
167
+ version: { type: 'string', nullable: false },
168
+ updatedAt: { type: 'timestamp', nullable: false }
169
169
  }, {
170
170
  primary: 'id',
171
- autoInc: true,
172
171
  unique: ['version']
173
172
  });
174
173
  ctx.logger.info('数据库表配置完成,autoInc: true 已启用,确保插入操作不手动指定 id 字段');
174
+ // 时间处理工具函数
175
+ function parseWPDate(dateStr) {
176
+ if (!dateStr) {
177
+ return null;
178
+ }
179
+ try {
180
+ const date = new Date(dateStr);
181
+ if (isNaN(date.getTime())) {
182
+ return null;
183
+ }
184
+ return date;
185
+ }
186
+ catch (error) {
187
+ ctx.logger.warn(`解析日期失败: ${dateStr}, 错误: ${error}`);
188
+ return null;
189
+ }
190
+ }
175
191
  // 为所有数据库操作添加详细日志,便于诊断自增主键问题
176
192
  ctx.on('ready', async () => {
177
193
  ctx.logger.info('WordPress 推送插件已就绪,开始初始化推送任务');
178
194
  ctx.logger.info('数据库表配置:');
179
- ctx.logger.info('wordpress_post_updates: id 字段设置为 autoInc: true');
180
- ctx.logger.info('wordpress_user_registrations: id 字段设置为 autoInc: true');
195
+ ctx.logger.info('wordpress_post_updates: id 字段设置为 autoIncrement: true');
196
+ ctx.logger.info('wordpress_user_registrations: id 字段设置为 autoIncrement: true');
181
197
  ctx.logger.info('wordpress_config: 配置持久化存储表');
182
198
  ctx.logger.info('wordpress_version: 数据库版本记录表');
183
199
  ctx.logger.info('所有群聊共用一个文章标记,不再区分群聊');
184
200
  // 检查并修复数据库表结构问题
185
201
  await checkAndFixTableStructure();
202
+ // 检查和更新数据库版本
203
+ await checkAndUpdateDatabaseVersion();
186
204
  // 加载持久化配置
187
205
  await loadPersistentConfig();
188
206
  // 执行初始推送
189
207
  await pushNewPosts();
190
208
  });
191
- // 加载持久化配置
209
+ // 检查和更新数据库版本
210
+ async function checkAndUpdateDatabaseVersion() {
211
+ try {
212
+ ctx.logger.info('开始检查数据库版本...');
213
+ // 确保数据库连接正常
214
+ const connected = await ensureDatabaseConnection();
215
+ if (!connected) {
216
+ ctx.logger.warn('数据库连接异常,跳过版本检查');
217
+ return;
218
+ }
219
+ // 检查现有版本记录
220
+ const versionRecords = await ctx.database.get('wordpress_version', {}, { limit: 1 });
221
+ if (versionRecords.length === 0) {
222
+ // 首次安装,创建版本记录
223
+ await ctx.database.create('wordpress_version', {
224
+ id: '1',
225
+ version: DATABASE_VERSION,
226
+ updatedAt: new Date()
227
+ });
228
+ ctx.logger.info(`数据库版本初始化完成,当前版本: ${DATABASE_VERSION}`);
229
+ }
230
+ else {
231
+ const currentVersion = versionRecords[0].version;
232
+ if (currentVersion !== DATABASE_VERSION) {
233
+ // 版本不一致,执行升级逻辑
234
+ ctx.logger.info(`数据库版本更新,从 ${currentVersion} 升级到 ${DATABASE_VERSION}`);
235
+ // 这里可以添加具体的升级逻辑,例如:
236
+ // 1. 表结构变更
237
+ // 2. 数据迁移
238
+ // 3. 配置更新
239
+ // 更新版本记录
240
+ await ctx.database.remove('wordpress_version', { id: versionRecords[0].id });
241
+ await ctx.database.create('wordpress_version', {
242
+ id: versionRecords[0].id,
243
+ version: DATABASE_VERSION,
244
+ updatedAt: new Date()
245
+ });
246
+ ctx.logger.info(`数据库版本升级完成,当前版本: ${DATABASE_VERSION}`);
247
+ }
248
+ else {
249
+ ctx.logger.info(`数据库版本检查完成,当前版本: ${DATABASE_VERSION} (最新)`);
250
+ }
251
+ }
252
+ }
253
+ catch (error) {
254
+ ctx.logger.error(`检查数据库版本失败: ${error}`);
255
+ }
256
+ }
257
+ // 加载持久化配置,使用分页查询避免内存溢出
192
258
  async function loadPersistentConfig() {
193
259
  try {
194
260
  ctx.logger.info('开始加载持久化配置...');
@@ -198,7 +264,10 @@ function apply(ctx, config) {
198
264
  ctx.logger.warn('数据库连接异常,跳过加载持久化配置');
199
265
  return;
200
266
  }
201
- const configRecords = await ctx.database.get('wordpress_config', {});
267
+ // 使用分页查询,每次最多获取 100 条记录
268
+ const configRecords = await ctx.database.get('wordpress_config', {}, {
269
+ limit: 100
270
+ });
202
271
  ctx.logger.info(`找到 ${configRecords.length} 条持久化配置记录`);
203
272
  for (const record of configRecords) {
204
273
  try {
@@ -243,7 +312,7 @@ function apply(ctx, config) {
243
312
  // 更新现有配置
244
313
  await ctx.database.remove('wordpress_config', { key });
245
314
  }
246
- // 创建新配置记录
315
+ // 创建新配置记录,只保存指定的键值对
247
316
  await ctx.database.create('wordpress_config', {
248
317
  key,
249
318
  value: JSON.stringify(value),
@@ -255,6 +324,47 @@ function apply(ctx, config) {
255
324
  ctx.logger.error(`保存配置失败,键: ${key},错误: ${error}`);
256
325
  }
257
326
  }
327
+ // 保存单个站点配置
328
+ async function saveSiteConfig(siteId, siteConfig) {
329
+ try {
330
+ ctx.logger.info(`保存单个站点配置: ${siteId} = ${JSON.stringify(siteConfig)}`);
331
+ // 确保数据库连接正常
332
+ const connected = await ensureDatabaseConnection();
333
+ if (!connected) {
334
+ ctx.logger.warn('数据库连接异常,跳过保存站点配置');
335
+ return;
336
+ }
337
+ // 获取当前所有站点配置
338
+ const sitesConfigKey = 'sites';
339
+ const existingSitesConfig = await ctx.database.get('wordpress_config', { key: sitesConfigKey });
340
+ let currentSites = [];
341
+ if (existingSitesConfig.length > 0) {
342
+ try {
343
+ currentSites = JSON.parse(existingSitesConfig[0].value);
344
+ }
345
+ catch (error) {
346
+ ctx.logger.error(`解析现有站点配置失败: ${error}`);
347
+ currentSites = [];
348
+ }
349
+ }
350
+ // 查找站点是否已存在
351
+ const siteIndex = currentSites.findIndex(site => site.id === siteId);
352
+ if (siteIndex >= 0) {
353
+ // 更新现有站点
354
+ currentSites[siteIndex] = siteConfig;
355
+ }
356
+ else {
357
+ // 添加新站点
358
+ currentSites.push(siteConfig);
359
+ }
360
+ // 保存更新后的站点配置
361
+ await saveConfig(sitesConfigKey, currentSites);
362
+ ctx.logger.info(`站点配置保存成功: ${siteId}`);
363
+ }
364
+ catch (error) {
365
+ ctx.logger.error(`保存站点配置失败,站点 ID: ${siteId},错误: ${error}`);
366
+ }
367
+ }
258
368
  // 缓存对象,用于存储高频格式化结果,有效期1小时
259
369
  const formatCache = {
260
370
  post: new Map(),
@@ -281,8 +391,8 @@ function apply(ctx, config) {
281
391
  }
282
392
  }
283
393
  }
284
- // 定期清理过期缓存
285
- setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
394
+ // 定期清理过期缓存,绑定到 ctx 生命周期
395
+ const cacheCleanupTimer = ctx.setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
286
396
  // 健壮获取 QQ Bot 实例,兼容多种适配器,优先选择 QQ 官方 bot
287
397
  function getValidBot() {
288
398
  // 支持的 QQ 相关适配器列表,'qq' 为 QQ 官方 bot
@@ -293,8 +403,9 @@ function apply(ctx, config) {
293
403
  const connectedBots = botList.filter(bot => {
294
404
  // 检查 Bot 是否已连接
295
405
  // 不同适配器可能有不同的状态属性
296
- // 由于TypeScript类型限制,我们使用更通用的检查方式
297
- return true; // 暂时返回所有Bot,实际项目中需要根据具体适配器实现状态检查
406
+ // 尝试检查常见的状态属性
407
+ const status = String(bot.status || '');
408
+ return status === 'online' || status === 'connected' || status === '';
298
409
  });
299
410
  ctx.logger.info(`找到 ${connectedBots.length} 个已连接的 Bot,总 Bot 数: ${botList.length}`);
300
411
  if (connectedBots.length === 0) {
@@ -319,18 +430,53 @@ function apply(ctx, config) {
319
430
  ctx.logger.info(`选择可用的已连接 Bot: ${connectedBots[0].platform} - ${connectedBots[0].selfId || 'unknown'}`);
320
431
  return connectedBots[0];
321
432
  }
433
+ // 检查 Bot 是否有发送消息的权限
434
+ async function checkBotPermission(bot, target) {
435
+ try {
436
+ // 不同适配器可能有不同的权限检查方法
437
+ // 这里使用通用的检查方法,尝试发送一条空消息或检查权限
438
+ // 注意:这种方法可能会在某些适配器上失败,所以需要捕获异常
439
+ // 检查 Bot 是否有 sendMessage 方法
440
+ if (!bot.sendMessage) {
441
+ ctx.logger.warn(`Bot 实例没有 sendMessage 方法: ${bot.platform}:${bot.selfId || 'unknown'}`);
442
+ return false;
443
+ }
444
+ // 检查 Bot 是否在线
445
+ if (bot.status && bot.status !== 'online' && bot.status !== 'connected') {
446
+ ctx.logger.warn(`Bot 不在线: ${bot.platform}:${bot.selfId || 'unknown'},状态: ${bot.status}`);
447
+ return false;
448
+ }
449
+ // 对于 QQ 官方 bot,检查是否有权限发送消息
450
+ if (bot.platform === 'qq') {
451
+ // QQ 官方 bot 可能有专门的权限检查方法
452
+ // 这里暂时返回 true,实际项目中需要根据具体适配器实现权限检查
453
+ return true;
454
+ }
455
+ // 对于其他适配器,尝试发送一条测试消息或检查权限
456
+ // 这里暂时返回 true,实际项目中需要根据具体适配器实现权限检查
457
+ return true;
458
+ }
459
+ catch (error) {
460
+ const errorMessage = error instanceof Error ? error.message : String(error);
461
+ ctx.logger.error(`检查 Bot 权限失败: ${errorMessage}`);
462
+ return false;
463
+ }
464
+ }
322
465
  const failedPushQueue = [];
323
466
  const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
324
467
  const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
325
468
  // 添加到失败队列
326
469
  function addToFailedQueue(type, data, targets, siteConfig) {
470
+ const now = new Date();
471
+ const expireAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24小时过期
327
472
  const item = {
328
473
  id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
329
474
  type,
330
475
  data,
331
476
  targets,
332
477
  retries: 0,
333
- createdAt: new Date(),
478
+ createdAt: now,
479
+ expireAt,
334
480
  siteConfig
335
481
  };
336
482
  failedPushQueue.push(item);
@@ -347,9 +493,16 @@ function apply(ctx, config) {
347
493
  ctx.logger.error('没有可用的 Bot 实例,无法处理失败队列');
348
494
  return;
349
495
  }
496
+ const now = new Date();
350
497
  const itemsToRemove = [];
351
498
  for (let i = 0; i < failedPushQueue.length; i++) {
352
499
  const item = failedPushQueue[i];
500
+ // 检查是否过期
501
+ if (now > item.expireAt) {
502
+ ctx.logger.warn(`任务已过期,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
503
+ itemsToRemove.push(i);
504
+ continue;
505
+ }
353
506
  if (item.retries >= MAX_RETRIES) {
354
507
  ctx.logger.warn(`达到最大重试次数,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
355
508
  itemsToRemove.push(i);
@@ -390,8 +543,8 @@ function apply(ctx, config) {
390
543
  }
391
544
  ctx.logger.info(`失败队列处理完成,剩余队列长度: ${failedPushQueue.length}`);
392
545
  }
393
- // 定期处理失败队列
394
- setInterval(processFailedQueue, RETRY_INTERVAL);
546
+ // 定期处理失败队列,绑定到 ctx 生命周期
547
+ const failedQueueTimer = ctx.setInterval(processFailedQueue, RETRY_INTERVAL);
395
548
  // 通用权限检查函数,检查用户是否为超级管理员
396
549
  function checkSuperAdmin(session) {
397
550
  // 检查是否为超级管理员
@@ -532,11 +685,11 @@ function apply(ctx, config) {
532
685
  ctx.logger.info('旧表删除成功,重新初始化表结构...');
533
686
  // 重新扩展模型
534
687
  ctx.model.extend('wordpress_post_updates', {
535
- id: 'integer',
536
- siteId: 'string',
537
- postId: 'integer',
538
- lastModified: 'timestamp',
539
- pushedAt: 'timestamp'
688
+ id: { type: 'integer', nullable: false },
689
+ siteId: { type: 'string', nullable: false },
690
+ postId: { type: 'integer', nullable: false },
691
+ lastModified: { type: 'timestamp', nullable: false },
692
+ pushedAt: { type: 'timestamp', nullable: false }
540
693
  }, {
541
694
  primary: 'id',
542
695
  autoInc: true,
@@ -564,10 +717,10 @@ function apply(ctx, config) {
564
717
  ctx.logger.info('旧表删除成功,重新初始化表结构...');
565
718
  // 重新扩展模型
566
719
  ctx.model.extend('wordpress_user_registrations', {
567
- id: 'integer',
568
- siteId: 'string',
569
- userId: 'integer',
570
- pushedAt: 'timestamp'
720
+ id: { type: 'integer', nullable: false },
721
+ siteId: { type: 'string', nullable: false },
722
+ userId: { type: 'integer', nullable: false },
723
+ pushedAt: { type: 'timestamp', nullable: false }
571
724
  }, {
572
725
  primary: 'id',
573
726
  autoInc: true,
@@ -595,13 +748,12 @@ function apply(ctx, config) {
595
748
  ctx.logger.info('旧表删除成功,重新初始化表结构...');
596
749
  // 重新扩展模型
597
750
  ctx.model.extend('wordpress_config', {
598
- id: 'integer',
599
- key: 'string',
600
- value: 'string',
601
- updatedAt: 'timestamp'
751
+ id: { type: 'string', nullable: false },
752
+ key: { type: 'string', nullable: false },
753
+ value: { type: 'string', nullable: false },
754
+ updatedAt: { type: 'timestamp', nullable: false }
602
755
  }, {
603
756
  primary: 'id',
604
- autoInc: true,
605
757
  unique: ['key']
606
758
  });
607
759
  ctx.logger.info('wordpress_config 表重新初始化成功');
@@ -610,6 +762,35 @@ function apply(ctx, config) {
610
762
  ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
611
763
  }
612
764
  }
765
+ try {
766
+ ctx.logger.info('验证 wordpress_version 表结构...');
767
+ const testVersion = await ctx.database.get('wordpress_version', {}, {
768
+ limit: 1
769
+ });
770
+ ctx.logger.info(`wordpress_version 表验证成功,现有记录数:${testVersion.length}`);
771
+ }
772
+ catch (error) {
773
+ ctx.logger.warn(`wordpress_version 表可能结构不正确,尝试重新初始化...`);
774
+ // 尝试删除旧表并重新初始化
775
+ try {
776
+ ctx.logger.info('尝试删除旧的 wordpress_version 表...');
777
+ await ctx.database.drop('wordpress_version');
778
+ ctx.logger.info('旧表删除成功,重新初始化表结构...');
779
+ // 重新扩展模型
780
+ ctx.model.extend('wordpress_version', {
781
+ id: { type: 'string', nullable: false },
782
+ version: { type: 'string', nullable: false },
783
+ updatedAt: { type: 'timestamp', nullable: false }
784
+ }, {
785
+ primary: 'id',
786
+ unique: ['version']
787
+ });
788
+ ctx.logger.info('wordpress_version 表重新初始化成功');
789
+ }
790
+ catch (dropError) {
791
+ ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
792
+ }
793
+ }
613
794
  ctx.logger.info('表结构检查和修复完成');
614
795
  }
615
796
  catch (error) {
@@ -675,18 +856,22 @@ function apply(ctx, config) {
675
856
  }
676
857
  const response = await httpRequest(url, requestConfig);
677
858
  if (!response) {
678
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`);
679
- return [];
859
+ const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`;
860
+ ctx.logger.error(errorMessage);
861
+ // 记录失败
862
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
863
+ return { success: false, data: [], error: errorMessage };
680
864
  }
681
865
  ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
682
- return response;
866
+ return { success: true, data: response, error: '' };
683
867
  }
684
868
  catch (error) {
685
869
  const errorMessage = error instanceof Error ? error.message : String(error);
686
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`);
870
+ const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`;
871
+ ctx.logger.error(fullErrorMessage);
687
872
  // 记录失败
688
873
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
689
- return [];
874
+ return { success: false, data: [], error: fullErrorMessage };
690
875
  }
691
876
  }
692
877
  async function fetchLatestUsers(site) {
@@ -709,12 +894,13 @@ function apply(ctx, config) {
709
894
  }
710
895
  const response = await httpRequest(url, requestConfig);
711
896
  if (!response) {
712
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`);
897
+ const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`;
898
+ ctx.logger.error(errorMessage);
713
899
  ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
714
900
  // 记录失败
715
901
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
716
902
  // 返回空数组,确保插件继续运行
717
- return [];
903
+ return { success: false, data: [], error: errorMessage };
718
904
  }
719
905
  ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
720
906
  // 添加调试日志,查看API返回的实际数据结构
@@ -724,16 +910,17 @@ function apply(ctx, config) {
724
910
  const user = response[0];
725
911
  ctx.logger.info(`用户日期字段: date=${user.date}, date_registered=${user.date_registered}, registered_date=${user.registered_date}, created_at=${user.created_at}`);
726
912
  }
727
- return response;
913
+ return { success: true, data: response, error: '' };
728
914
  }
729
915
  catch (error) {
730
916
  const errorMessage = error instanceof Error ? error.message : String(error);
731
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`);
917
+ const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`;
918
+ ctx.logger.error(fullErrorMessage);
732
919
  ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
733
920
  // 记录失败
734
921
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
735
922
  // 返回空数组,确保插件继续运行
736
- return [];
923
+ return { success: false, data: [], error: fullErrorMessage };
737
924
  }
738
925
  }
739
926
  async function fetchUpdatedPosts(site) {
@@ -753,20 +940,22 @@ function apply(ctx, config) {
753
940
  }
754
941
  const response = await httpRequest(url, requestConfig);
755
942
  if (!response) {
756
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`);
943
+ const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`;
944
+ ctx.logger.error(errorMessage);
757
945
  // 记录失败
758
946
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
759
- return [];
947
+ return { success: false, data: [], error: errorMessage };
760
948
  }
761
949
  ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
762
- return response;
950
+ return { success: true, data: response, error: '' };
763
951
  }
764
952
  catch (error) {
765
953
  const errorMessage = error instanceof Error ? error.message : String(error);
766
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`);
954
+ const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`;
955
+ ctx.logger.error(fullErrorMessage);
767
956
  // 记录失败
768
957
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
769
- return [];
958
+ return { success: false, data: [], error: fullErrorMessage };
770
959
  }
771
960
  }
772
961
  async function isUserPushed(siteId, userId) {
@@ -778,7 +967,7 @@ function apply(ctx, config) {
778
967
  return false;
779
968
  }
780
969
  ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
781
- const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId });
970
+ const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId: parseInt(userId) });
782
971
  const result = record.length > 0;
783
972
  ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
784
973
  return result;
@@ -800,7 +989,7 @@ function apply(ctx, config) {
800
989
  return null;
801
990
  }
802
991
  ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
803
- const records = await ctx.database.get('wordpress_post_updates', { siteId, postId });
992
+ const records = await ctx.database.get('wordpress_post_updates', { siteId, postId: parseInt(postId) });
804
993
  const result = records.length > 0 ? records[0] : null;
805
994
  ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
806
995
  return result;
@@ -825,7 +1014,7 @@ function apply(ctx, config) {
825
1014
  // 创建新记录,不手动指定id,让数据库自动生成
826
1015
  const newRecord = {
827
1016
  siteId,
828
- userId,
1017
+ userId: parseInt(userId),
829
1018
  pushedAt: new Date()
830
1019
  };
831
1020
  ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
@@ -859,13 +1048,13 @@ function apply(ctx, config) {
859
1048
  if (record) {
860
1049
  ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
861
1050
  // Koishi database API 不支持 update 方法,使用 remove + create 代替
862
- await ctx.database.remove('wordpress_post_updates', { siteId, postId });
1051
+ await ctx.database.remove('wordpress_post_updates', { siteId, postId: parseInt(postId) });
863
1052
  ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
864
1053
  }
865
1054
  // 创建新记录,不指定 id 字段,让数据库自动生成
866
1055
  const newRecord = {
867
1056
  siteId,
868
- postId,
1057
+ postId: parseInt(postId),
869
1058
  lastModified: modifiedDate,
870
1059
  pushedAt: new Date()
871
1060
  };
@@ -937,7 +1126,11 @@ function apply(ctx, config) {
937
1126
  }
938
1127
  // 根据配置格式化日期
939
1128
  const formatDate = (dateString) => {
940
- const date = new Date(dateString);
1129
+ // 使用统一的时间处理工具函数
1130
+ const date = parseWPDate(dateString);
1131
+ if (!date) {
1132
+ return '未知时间';
1133
+ }
941
1134
  const year = date.getFullYear();
942
1135
  const month = String(date.getMonth() + 1).padStart(2, '0');
943
1136
  const day = String(date.getDate()).padStart(2, '0');
@@ -1049,10 +1242,9 @@ function apply(ctx, config) {
1049
1242
  ctx.logger.info(`用户 ${username} 的原始数据: ${JSON.stringify(user)}`);
1050
1243
  }
1051
1244
  if (dateStr) {
1052
- // 尝试解析日期,使用自定义格式:年-月-日 时:分
1053
- const date = new Date(dateStr);
1054
- ctx.logger.info(`解析日期 ${dateStr} 结果: ${date.toString()}`);
1055
- if (!isNaN(date.getTime())) {
1245
+ // 使用统一的时间处理工具函数
1246
+ const date = parseWPDate(dateStr);
1247
+ if (date) {
1056
1248
  const year = date.getFullYear();
1057
1249
  const month = String(date.getMonth() + 1).padStart(2, '0');
1058
1250
  const day = String(date.getDate()).padStart(2, '0');
@@ -1103,14 +1295,21 @@ function apply(ctx, config) {
1103
1295
  ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
1104
1296
  // 推送新文章
1105
1297
  if (site.enableAutoPush) {
1106
- const posts = await fetchLatestPosts(site);
1298
+ const postsResult = await fetchLatestPosts(site);
1299
+ if (!postsResult.success) {
1300
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的文章失败: ${postsResult.error}`);
1301
+ // 记录失败
1302
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${postsResult.error}`);
1303
+ continue;
1304
+ }
1305
+ const posts = postsResult.data;
1107
1306
  ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
1108
1307
  if (posts.length > 0) {
1109
1308
  for (const post of posts) {
1110
1309
  ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
1111
1310
  ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
1112
1311
  // 检查文章是否已推送过(所有群聊共用一个标记)
1113
- const postRecord = await getPostUpdateRecord(site.id, post.id);
1312
+ const postRecord = await getPostUpdateRecord(site.id, String(post.id));
1114
1313
  const hasPushed = !!postRecord;
1115
1314
  ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
1116
1315
  if (!hasPushed) {
@@ -1121,6 +1320,13 @@ function apply(ctx, config) {
1121
1320
  ctx.logger.info(`正在处理目标: ${target}`);
1122
1321
  // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
1123
1322
  const stringTarget = target;
1323
+ // 检查 Bot 是否有发送消息的权限
1324
+ const hasPermission = await checkBotPermission(bot, stringTarget);
1325
+ if (!hasPermission) {
1326
+ ctx.logger.warn(`Bot 没有权限向 ${stringTarget} 发送消息,跳过推送`);
1327
+ failedTargets.push(target);
1328
+ continue;
1329
+ }
1124
1330
  const message = formatPostMessage(post, site.mentionAll, false, site);
1125
1331
  ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
1126
1332
  await bot.sendMessage(stringTarget, message);
@@ -1140,7 +1346,7 @@ function apply(ctx, config) {
1140
1346
  addToFailedQueue('post', post, failedTargets, site);
1141
1347
  }
1142
1348
  // 标记文章已推送(所有群聊共用一个标记)
1143
- await updatePostUpdateRecord(site.id, post.id, new Date(post.modified));
1349
+ await updatePostUpdateRecord(site.id, String(post.id), new Date(post.modified));
1144
1350
  ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
1145
1351
  }
1146
1352
  else {
@@ -1151,10 +1357,17 @@ function apply(ctx, config) {
1151
1357
  }
1152
1358
  // 推送文章更新
1153
1359
  if (site.enableUpdatePush) {
1154
- const posts = await fetchUpdatedPosts(site);
1360
+ const postsResult = await fetchUpdatedPosts(site);
1361
+ if (!postsResult.success) {
1362
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${postsResult.error}`);
1363
+ // 记录失败
1364
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${postsResult.error}`);
1365
+ continue;
1366
+ }
1367
+ const posts = postsResult.data;
1155
1368
  if (posts.length > 0) {
1156
1369
  for (const post of posts) {
1157
- const updateRecord = await getPostUpdateRecord(site.id, post.id);
1370
+ const updateRecord = await getPostUpdateRecord(site.id, String(post.id));
1158
1371
  const postModifiedDate = new Date(post.modified);
1159
1372
  // 检查文章是否有更新
1160
1373
  if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
@@ -1165,6 +1378,13 @@ function apply(ctx, config) {
1165
1378
  try {
1166
1379
  ctx.logger.info(`正在处理目标: ${target}`);
1167
1380
  const stringTarget = target;
1381
+ // 检查 Bot 是否有发送消息的权限
1382
+ const hasPermission = await checkBotPermission(bot, stringTarget);
1383
+ if (!hasPermission) {
1384
+ ctx.logger.warn(`Bot 没有权限向 ${stringTarget} 发送消息,跳过推送`);
1385
+ failedTargets.push(target);
1386
+ continue;
1387
+ }
1168
1388
  const message = formatPostMessage(post, site.mentionAll, true, site);
1169
1389
  ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
1170
1390
  await bot.sendMessage(stringTarget, message);
@@ -1184,7 +1404,7 @@ function apply(ctx, config) {
1184
1404
  addToFailedQueue('update', post, failedTargets, site);
1185
1405
  }
1186
1406
  // 更新文章更新记录(所有群聊共用一个标记)
1187
- await updatePostUpdateRecord(site.id, post.id, postModifiedDate);
1407
+ await updatePostUpdateRecord(site.id, String(post.id), postModifiedDate);
1188
1408
  ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
1189
1409
  }
1190
1410
  }
@@ -1192,16 +1412,30 @@ function apply(ctx, config) {
1192
1412
  }
1193
1413
  // 推送新用户注册
1194
1414
  if (site.enableUserPush) {
1195
- const users = await fetchLatestUsers(site);
1415
+ const usersResult = await fetchLatestUsers(site);
1416
+ if (!usersResult.success) {
1417
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的用户失败: ${usersResult.error}`);
1418
+ // 记录失败
1419
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${usersResult.error}`);
1420
+ continue;
1421
+ }
1422
+ const users = usersResult.data;
1196
1423
  if (users.length > 0) {
1197
1424
  for (const user of users) {
1198
- if (!(await isUserPushed(site.id, user.id))) {
1425
+ if (!(await isUserPushed(site.id, String(user.id)))) {
1199
1426
  const failedTargets = [];
1200
1427
  for (const target of site.targets) {
1201
1428
  try {
1202
1429
  ctx.logger.info(`正在处理目标: ${target}`);
1203
1430
  // 直接使用原始目标字符串,与新文章推送逻辑保持一致
1204
1431
  const stringTarget = target;
1432
+ // 检查 Bot 是否有发送消息的权限
1433
+ const hasPermission = await checkBotPermission(bot, stringTarget);
1434
+ if (!hasPermission) {
1435
+ ctx.logger.warn(`Bot 没有权限向 ${stringTarget} 发送消息,跳过推送`);
1436
+ failedTargets.push(target);
1437
+ continue;
1438
+ }
1205
1439
  const message = formatUserMessage(user, site.mentionAll, site);
1206
1440
  ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
1207
1441
  await bot.sendMessage(stringTarget, message);
@@ -1221,7 +1455,7 @@ function apply(ctx, config) {
1221
1455
  addToFailedQueue('user', user, failedTargets, site);
1222
1456
  }
1223
1457
  // 标记用户已推送
1224
- await markUserAsPushed(site.id, user.id);
1458
+ await markUserAsPushed(site.id, String(user.id));
1225
1459
  }
1226
1460
  }
1227
1461
  }
@@ -1238,7 +1472,12 @@ function apply(ctx, config) {
1238
1472
  if (!targetSite) {
1239
1473
  return `未找到站点 ID: ${siteId}`;
1240
1474
  }
1241
- const posts = await fetchLatestPosts(targetSite);
1475
+ const postsResult = await fetchLatestPosts(targetSite);
1476
+ if (!postsResult.success) {
1477
+ ctx.logger.error(`获取最新文章失败: ${postsResult.error}`);
1478
+ return `获取文章失败: ${postsResult.error}`;
1479
+ }
1480
+ const posts = postsResult.data;
1242
1481
  if (posts.length === 0) {
1243
1482
  ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
1244
1483
  return `站点 ${targetSite.name} 暂无文章`;
@@ -1280,7 +1519,12 @@ function apply(ctx, config) {
1280
1519
  if (!targetSite) {
1281
1520
  return `未找到站点 ID: ${siteId}`;
1282
1521
  }
1283
- const posts = await fetchLatestPosts(targetSite);
1522
+ const postsResult = await fetchLatestPosts(targetSite);
1523
+ if (!postsResult.success) {
1524
+ ctx.logger.error(`获取文章列表失败: ${postsResult.error}`);
1525
+ return `获取文章失败: ${postsResult.error}`;
1526
+ }
1527
+ const posts = postsResult.data;
1284
1528
  if (posts.length === 0) {
1285
1529
  return `站点 ${targetSite.name} 暂无文章`;
1286
1530
  }
@@ -1381,7 +1625,7 @@ function apply(ctx, config) {
1381
1625
  }
1382
1626
  // 切换开关
1383
1627
  site.enableUpdatePush = !site.enableUpdatePush;
1384
- await saveConfig('sites', config.sites);
1628
+ await saveSiteConfig(siteId, site);
1385
1629
  return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
1386
1630
  });
1387
1631
  ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
@@ -1399,7 +1643,7 @@ function apply(ctx, config) {
1399
1643
  }
1400
1644
  // 切换开关
1401
1645
  site.enableUserPush = !site.enableUserPush;
1402
- await saveConfig('sites', config.sites);
1646
+ await saveSiteConfig(siteId, site);
1403
1647
  return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
1404
1648
  });
1405
1649
  ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
@@ -1417,7 +1661,7 @@ function apply(ctx, config) {
1417
1661
  }
1418
1662
  // 切换开关
1419
1663
  site.enableAutoPush = !site.enableAutoPush;
1420
- await saveConfig('sites', config.sites);
1664
+ await saveSiteConfig(siteId, site);
1421
1665
  return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
1422
1666
  });
1423
1667
  ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
@@ -1435,7 +1679,7 @@ function apply(ctx, config) {
1435
1679
  }
1436
1680
  // 切换开关
1437
1681
  site.mentionAll = !site.mentionAll;
1438
- await saveConfig('sites', config.sites);
1682
+ await saveSiteConfig(siteId, site);
1439
1683
  return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
1440
1684
  });
1441
1685
  ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
@@ -1453,7 +1697,7 @@ function apply(ctx, config) {
1453
1697
  }
1454
1698
  // 修改站点地址
1455
1699
  site.url = url;
1456
- await saveConfig('sites', config.sites);
1700
+ await saveSiteConfig(siteId, site);
1457
1701
  ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
1458
1702
  return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
1459
1703
  });
@@ -1497,8 +1741,8 @@ function apply(ctx, config) {
1497
1741
  ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'},调用者:${authResult.userId}`);
1498
1742
  // 设置默认天数
1499
1743
  const daysToKeep = days ? parseInt(days) : CONSTANTS.DEFAULT_CLEAN_DAYS;
1500
- if (isNaN(daysToKeep) || daysToKeep <= 0) {
1501
- return '请输入有效的天数';
1744
+ if (isNaN(daysToKeep) || daysToKeep <= 0 || daysToKeep > 365) {
1745
+ return '请输入有效的天数(1-365天)';
1502
1746
  }
1503
1747
  // 计算清理时间点
1504
1748
  const cutoffDate = new Date();
@@ -1507,46 +1751,84 @@ function apply(ctx, config) {
1507
1751
  let result = 0;
1508
1752
  // 批量删除 wordpress_post_updates 中的旧记录
1509
1753
  try {
1510
- // 先获取需要删除的记录数量
1511
- const updateRecords = await ctx.database.get('wordpress_post_updates', {});
1512
- const updateRecordsToRemove = updateRecords.filter(record => {
1513
- return new Date(record.pushedAt) < cutoffDate;
1514
- });
1515
- if (updateRecordsToRemove.length > 0) {
1516
- // 使用批量删除方式(假设数据库API支持)
1517
- // 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
1518
- const idsToRemove = updateRecordsToRemove.map(record => record.id);
1519
- // 尝试使用更高效的批量删除方式
1520
- // 注意:具体实现取决于Koishi数据库API的支持情况
1521
- for (const id of idsToRemove) {
1522
- await ctx.database.remove('wordpress_post_updates', { id });
1754
+ // 使用分页查询,每次处理 100 条记录,避免内存溢出
1755
+ let processed = 0;
1756
+ let hasMore = true;
1757
+ const queryBatchSize = 100;
1758
+ while (hasMore) {
1759
+ // 分页查询记录
1760
+ const updateRecords = await ctx.database.get('wordpress_post_updates', {}, {
1761
+ limit: queryBatchSize,
1762
+ offset: processed
1763
+ });
1764
+ if (updateRecords.length === 0) {
1765
+ hasMore = false;
1766
+ break;
1767
+ }
1768
+ // 过滤需要删除的记录
1769
+ const recordsToRemove = updateRecords.filter(record => {
1770
+ return new Date(record.pushedAt) < cutoffDate;
1771
+ });
1772
+ if (recordsToRemove.length === 0) {
1773
+ processed += updateRecords.length;
1774
+ continue;
1775
+ }
1776
+ // 批量删除,每批最多删除 10 条记录
1777
+ const deleteBatchSize = 10;
1778
+ for (let i = 0; i < recordsToRemove.length; i += deleteBatchSize) {
1779
+ const batch = recordsToRemove.slice(i, i + deleteBatchSize);
1780
+ // 并行删除,提高效率
1781
+ await Promise.all(batch.map(record => ctx.database.remove('wordpress_post_updates', { id: record.id })));
1523
1782
  }
1524
- result += updateRecordsToRemove.length;
1525
- ctx.logger.info(`批量删除了 ${updateRecordsToRemove.length} 条 wordpress_post_updates 旧记录`);
1783
+ result += recordsToRemove.length;
1784
+ processed += updateRecords.length;
1785
+ ctx.logger.info(`已处理 ${processed} 条记录,删除了 ${recordsToRemove.length} 条旧记录`);
1786
+ // 避免数据库压力过大,每批处理后稍作延迟
1787
+ await new Promise(resolve => setTimeout(resolve, 100));
1526
1788
  }
1789
+ ctx.logger.info(`批量删除 wordpress_post_updates 旧记录完成,共删除 ${result} 条记录`);
1527
1790
  }
1528
1791
  catch (error) {
1529
1792
  ctx.logger.error(`批量删除 wordpress_post_updates 旧记录失败: ${error}`);
1530
1793
  }
1531
1794
  // 批量删除 wordpress_user_registrations 中的旧记录
1532
1795
  try {
1533
- // 先获取需要删除的记录数量
1534
- const userRecords = await ctx.database.get('wordpress_user_registrations', {});
1535
- const userRecordsToRemove = userRecords.filter(record => {
1536
- return new Date(record.pushedAt) < cutoffDate;
1537
- });
1538
- if (userRecordsToRemove.length > 0) {
1539
- // 使用批量删除方式(假设数据库API支持)
1540
- // 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
1541
- const idsToRemove = userRecordsToRemove.map(record => record.id);
1542
- // 尝试使用更高效的批量删除方式
1543
- // 注意:具体实现取决于Koishi数据库API的支持情况
1544
- for (const id of idsToRemove) {
1545
- await ctx.database.remove('wordpress_user_registrations', { id });
1796
+ // 使用分页查询,每次处理 100 条记录,避免内存溢出
1797
+ let processed = 0;
1798
+ let hasMore = true;
1799
+ const queryBatchSize = 100;
1800
+ while (hasMore) {
1801
+ // 分页查询记录
1802
+ const userRecords = await ctx.database.get('wordpress_user_registrations', {}, {
1803
+ limit: queryBatchSize,
1804
+ offset: processed
1805
+ });
1806
+ if (userRecords.length === 0) {
1807
+ hasMore = false;
1808
+ break;
1546
1809
  }
1547
- result += userRecordsToRemove.length;
1548
- ctx.logger.info(`批量删除了 ${userRecordsToRemove.length} 条 wordpress_user_registrations 旧记录`);
1810
+ // 过滤需要删除的记录
1811
+ const recordsToRemove = userRecords.filter(record => {
1812
+ return new Date(record.pushedAt) < cutoffDate;
1813
+ });
1814
+ if (recordsToRemove.length === 0) {
1815
+ processed += userRecords.length;
1816
+ continue;
1817
+ }
1818
+ // 批量删除,每批最多删除 10 条记录
1819
+ const deleteBatchSize = 10;
1820
+ for (let i = 0; i < recordsToRemove.length; i += deleteBatchSize) {
1821
+ const batch = recordsToRemove.slice(i, i + deleteBatchSize);
1822
+ // 并行删除,提高效率
1823
+ await Promise.all(batch.map(record => ctx.database.remove('wordpress_user_registrations', { id: record.id })));
1824
+ }
1825
+ result += recordsToRemove.length;
1826
+ processed += userRecords.length;
1827
+ ctx.logger.info(`已处理 ${processed} 条记录,删除了 ${recordsToRemove.length} 条旧记录`);
1828
+ // 避免数据库压力过大,每批处理后稍作延迟
1829
+ await new Promise(resolve => setTimeout(resolve, 100));
1549
1830
  }
1831
+ ctx.logger.info(`批量删除 wordpress_user_registrations 旧记录完成,共删除 ${result} 条记录`);
1550
1832
  }
1551
1833
  catch (error) {
1552
1834
  ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
@@ -1659,9 +1941,10 @@ function apply(ctx, config) {
1659
1941
  ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1660
1942
  return message;
1661
1943
  });
1662
- // 为每个站点设置独立的定时任务
1944
+ // 为每个站点设置独立的定时任务,绑定到 ctx 生命周期
1945
+ const siteTimers = [];
1663
1946
  config.sites.forEach(site => {
1664
- ctx.setInterval(() => {
1947
+ const timer = ctx.setInterval(() => {
1665
1948
  try {
1666
1949
  pushNewPosts();
1667
1950
  }
@@ -1672,5 +1955,28 @@ function apply(ctx, config) {
1672
1955
  ctx.logger.warn('定时任务将在下一个周期继续执行');
1673
1956
  }
1674
1957
  }, site.interval);
1958
+ siteTimers.push(timer);
1959
+ });
1960
+ // 绑定到 ctx 生命周期,插件卸载时清理所有定时器
1961
+ ctx.on('dispose', () => {
1962
+ ctx.logger.info('WordPress 插件开始清理资源...');
1963
+ // 清理缓存清理定时器
1964
+ if (typeof cacheCleanupTimer === 'function') {
1965
+ cacheCleanupTimer();
1966
+ ctx.logger.info('缓存清理定时器已清理');
1967
+ }
1968
+ // 清理失败队列定时器
1969
+ if (typeof failedQueueTimer === 'function') {
1970
+ failedQueueTimer();
1971
+ ctx.logger.info('失败队列定时器已清理');
1972
+ }
1973
+ // 清理站点定时任务
1974
+ siteTimers.forEach((timer, index) => {
1975
+ if (typeof timer === 'function') {
1976
+ timer();
1977
+ ctx.logger.info(`站点定时任务 ${index + 1} 已清理`);
1978
+ }
1979
+ });
1980
+ ctx.logger.info('WordPress 插件资源清理完成');
1675
1981
  });
1676
1982
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-wordpress-notifier",
3
- "version": "2.8.4",
3
+ "version": "2.9.0",
4
4
  "description": "WordPress 文章自动推送到 QQ",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",