koishi-plugin-wordpress-notifier 2.8.5 → 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/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,7 +129,7 @@ function apply(ctx, config) {
129
129
  ctx.logger.info('WordPress 推送插件已加载');
130
130
  // 修复 MySQL 自增主键问题,使用正确的模型配置
131
131
  // 确保 id 字段被正确设置为自增主键,并且在插入时不会被设置为 NULL
132
- // 显式指定 NOT NULL 约束以适配 MySQL 特有要求
132
+ // 显式指定 nullable: false 约束以适配 MySQL 特有要求
133
133
  ctx.model.extend('wordpress_post_updates', {
134
134
  id: { type: 'integer', nullable: false },
135
135
  siteId: { type: 'string', nullable: false },
@@ -153,43 +153,108 @@ function apply(ctx, config) {
153
153
  });
154
154
  // 配置存储表
155
155
  ctx.model.extend('wordpress_config', {
156
- id: { type: 'integer', nullable: false },
156
+ id: { type: 'string', nullable: false },
157
157
  key: { type: 'string', nullable: false },
158
158
  value: { type: 'string', nullable: false },
159
159
  updatedAt: { type: 'timestamp', nullable: false }
160
160
  }, {
161
161
  primary: 'id',
162
- autoInc: true,
163
162
  unique: ['key']
164
163
  });
165
164
  // 版本记录表
166
165
  ctx.model.extend('wordpress_version', {
167
- id: { type: 'integer', nullable: false },
166
+ id: { type: 'string', nullable: false },
168
167
  version: { type: 'string', nullable: false },
169
168
  updatedAt: { type: 'timestamp', nullable: false }
170
169
  }, {
171
170
  primary: 'id',
172
- autoInc: true,
173
171
  unique: ['version']
174
172
  });
175
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
+ }
176
191
  // 为所有数据库操作添加详细日志,便于诊断自增主键问题
177
192
  ctx.on('ready', async () => {
178
193
  ctx.logger.info('WordPress 推送插件已就绪,开始初始化推送任务');
179
194
  ctx.logger.info('数据库表配置:');
180
- ctx.logger.info('wordpress_post_updates: id 字段设置为 autoInc: true');
181
- 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');
182
197
  ctx.logger.info('wordpress_config: 配置持久化存储表');
183
198
  ctx.logger.info('wordpress_version: 数据库版本记录表');
184
199
  ctx.logger.info('所有群聊共用一个文章标记,不再区分群聊');
185
200
  // 检查并修复数据库表结构问题
186
201
  await checkAndFixTableStructure();
202
+ // 检查和更新数据库版本
203
+ await checkAndUpdateDatabaseVersion();
187
204
  // 加载持久化配置
188
205
  await loadPersistentConfig();
189
206
  // 执行初始推送
190
207
  await pushNewPosts();
191
208
  });
192
- // 加载持久化配置
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
+ // 加载持久化配置,使用分页查询避免内存溢出
193
258
  async function loadPersistentConfig() {
194
259
  try {
195
260
  ctx.logger.info('开始加载持久化配置...');
@@ -199,7 +264,10 @@ function apply(ctx, config) {
199
264
  ctx.logger.warn('数据库连接异常,跳过加载持久化配置');
200
265
  return;
201
266
  }
202
- const configRecords = await ctx.database.get('wordpress_config', {});
267
+ // 使用分页查询,每次最多获取 100 条记录
268
+ const configRecords = await ctx.database.get('wordpress_config', {}, {
269
+ limit: 100
270
+ });
203
271
  ctx.logger.info(`找到 ${configRecords.length} 条持久化配置记录`);
204
272
  for (const record of configRecords) {
205
273
  try {
@@ -244,7 +312,7 @@ function apply(ctx, config) {
244
312
  // 更新现有配置
245
313
  await ctx.database.remove('wordpress_config', { key });
246
314
  }
247
- // 创建新配置记录
315
+ // 创建新配置记录,只保存指定的键值对
248
316
  await ctx.database.create('wordpress_config', {
249
317
  key,
250
318
  value: JSON.stringify(value),
@@ -256,6 +324,47 @@ function apply(ctx, config) {
256
324
  ctx.logger.error(`保存配置失败,键: ${key},错误: ${error}`);
257
325
  }
258
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
+ }
259
368
  // 缓存对象,用于存储高频格式化结果,有效期1小时
260
369
  const formatCache = {
261
370
  post: new Map(),
@@ -282,8 +391,8 @@ function apply(ctx, config) {
282
391
  }
283
392
  }
284
393
  }
285
- // 定期清理过期缓存
286
- setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
394
+ // 定期清理过期缓存,绑定到 ctx 生命周期
395
+ const cacheCleanupTimer = ctx.setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
287
396
  // 健壮获取 QQ Bot 实例,兼容多种适配器,优先选择 QQ 官方 bot
288
397
  function getValidBot() {
289
398
  // 支持的 QQ 相关适配器列表,'qq' 为 QQ 官方 bot
@@ -294,8 +403,9 @@ function apply(ctx, config) {
294
403
  const connectedBots = botList.filter(bot => {
295
404
  // 检查 Bot 是否已连接
296
405
  // 不同适配器可能有不同的状态属性
297
- // 由于TypeScript类型限制,我们使用更通用的检查方式
298
- return true; // 暂时返回所有Bot,实际项目中需要根据具体适配器实现状态检查
406
+ // 尝试检查常见的状态属性
407
+ const status = String(bot.status || '');
408
+ return status === 'online' || status === 'connected' || status === '';
299
409
  });
300
410
  ctx.logger.info(`找到 ${connectedBots.length} 个已连接的 Bot,总 Bot 数: ${botList.length}`);
301
411
  if (connectedBots.length === 0) {
@@ -320,18 +430,53 @@ function apply(ctx, config) {
320
430
  ctx.logger.info(`选择可用的已连接 Bot: ${connectedBots[0].platform} - ${connectedBots[0].selfId || 'unknown'}`);
321
431
  return connectedBots[0];
322
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
+ }
323
465
  const failedPushQueue = [];
324
466
  const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
325
467
  const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
326
468
  // 添加到失败队列
327
469
  function addToFailedQueue(type, data, targets, siteConfig) {
470
+ const now = new Date();
471
+ const expireAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24小时过期
328
472
  const item = {
329
473
  id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
330
474
  type,
331
475
  data,
332
476
  targets,
333
477
  retries: 0,
334
- createdAt: new Date(),
478
+ createdAt: now,
479
+ expireAt,
335
480
  siteConfig
336
481
  };
337
482
  failedPushQueue.push(item);
@@ -348,9 +493,16 @@ function apply(ctx, config) {
348
493
  ctx.logger.error('没有可用的 Bot 实例,无法处理失败队列');
349
494
  return;
350
495
  }
496
+ const now = new Date();
351
497
  const itemsToRemove = [];
352
498
  for (let i = 0; i < failedPushQueue.length; i++) {
353
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
+ }
354
506
  if (item.retries >= MAX_RETRIES) {
355
507
  ctx.logger.warn(`达到最大重试次数,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
356
508
  itemsToRemove.push(i);
@@ -391,8 +543,8 @@ function apply(ctx, config) {
391
543
  }
392
544
  ctx.logger.info(`失败队列处理完成,剩余队列长度: ${failedPushQueue.length}`);
393
545
  }
394
- // 定期处理失败队列
395
- setInterval(processFailedQueue, RETRY_INTERVAL);
546
+ // 定期处理失败队列,绑定到 ctx 生命周期
547
+ const failedQueueTimer = ctx.setInterval(processFailedQueue, RETRY_INTERVAL);
396
548
  // 通用权限检查函数,检查用户是否为超级管理员
397
549
  function checkSuperAdmin(session) {
398
550
  // 检查是否为超级管理员
@@ -596,13 +748,12 @@ function apply(ctx, config) {
596
748
  ctx.logger.info('旧表删除成功,重新初始化表结构...');
597
749
  // 重新扩展模型
598
750
  ctx.model.extend('wordpress_config', {
599
- id: { type: 'integer', nullable: false },
751
+ id: { type: 'string', nullable: false },
600
752
  key: { type: 'string', nullable: false },
601
753
  value: { type: 'string', nullable: false },
602
754
  updatedAt: { type: 'timestamp', nullable: false }
603
755
  }, {
604
756
  primary: 'id',
605
- autoInc: true,
606
757
  unique: ['key']
607
758
  });
608
759
  ctx.logger.info('wordpress_config 表重新初始化成功');
@@ -627,12 +778,11 @@ function apply(ctx, config) {
627
778
  ctx.logger.info('旧表删除成功,重新初始化表结构...');
628
779
  // 重新扩展模型
629
780
  ctx.model.extend('wordpress_version', {
630
- id: { type: 'integer', nullable: false },
781
+ id: { type: 'string', nullable: false },
631
782
  version: { type: 'string', nullable: false },
632
783
  updatedAt: { type: 'timestamp', nullable: false }
633
784
  }, {
634
785
  primary: 'id',
635
- autoInc: true,
636
786
  unique: ['version']
637
787
  });
638
788
  ctx.logger.info('wordpress_version 表重新初始化成功');
@@ -706,18 +856,22 @@ function apply(ctx, config) {
706
856
  }
707
857
  const response = await httpRequest(url, requestConfig);
708
858
  if (!response) {
709
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`);
710
- 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 };
711
864
  }
712
865
  ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
713
- return response;
866
+ return { success: true, data: response, error: '' };
714
867
  }
715
868
  catch (error) {
716
869
  const errorMessage = error instanceof Error ? error.message : String(error);
717
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`);
870
+ const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`;
871
+ ctx.logger.error(fullErrorMessage);
718
872
  // 记录失败
719
873
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
720
- return [];
874
+ return { success: false, data: [], error: fullErrorMessage };
721
875
  }
722
876
  }
723
877
  async function fetchLatestUsers(site) {
@@ -740,12 +894,13 @@ function apply(ctx, config) {
740
894
  }
741
895
  const response = await httpRequest(url, requestConfig);
742
896
  if (!response) {
743
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`);
897
+ const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`;
898
+ ctx.logger.error(errorMessage);
744
899
  ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
745
900
  // 记录失败
746
901
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
747
902
  // 返回空数组,确保插件继续运行
748
- return [];
903
+ return { success: false, data: [], error: errorMessage };
749
904
  }
750
905
  ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
751
906
  // 添加调试日志,查看API返回的实际数据结构
@@ -755,16 +910,17 @@ function apply(ctx, config) {
755
910
  const user = response[0];
756
911
  ctx.logger.info(`用户日期字段: date=${user.date}, date_registered=${user.date_registered}, registered_date=${user.registered_date}, created_at=${user.created_at}`);
757
912
  }
758
- return response;
913
+ return { success: true, data: response, error: '' };
759
914
  }
760
915
  catch (error) {
761
916
  const errorMessage = error instanceof Error ? error.message : String(error);
762
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`);
917
+ const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`;
918
+ ctx.logger.error(fullErrorMessage);
763
919
  ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
764
920
  // 记录失败
765
921
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
766
922
  // 返回空数组,确保插件继续运行
767
- return [];
923
+ return { success: false, data: [], error: fullErrorMessage };
768
924
  }
769
925
  }
770
926
  async function fetchUpdatedPosts(site) {
@@ -784,20 +940,22 @@ function apply(ctx, config) {
784
940
  }
785
941
  const response = await httpRequest(url, requestConfig);
786
942
  if (!response) {
787
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`);
943
+ const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`;
944
+ ctx.logger.error(errorMessage);
788
945
  // 记录失败
789
946
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
790
- return [];
947
+ return { success: false, data: [], error: errorMessage };
791
948
  }
792
949
  ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
793
- return response;
950
+ return { success: true, data: response, error: '' };
794
951
  }
795
952
  catch (error) {
796
953
  const errorMessage = error instanceof Error ? error.message : String(error);
797
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`);
954
+ const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`;
955
+ ctx.logger.error(fullErrorMessage);
798
956
  // 记录失败
799
957
  recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
800
- return [];
958
+ return { success: false, data: [], error: fullErrorMessage };
801
959
  }
802
960
  }
803
961
  async function isUserPushed(siteId, userId) {
@@ -809,7 +967,7 @@ function apply(ctx, config) {
809
967
  return false;
810
968
  }
811
969
  ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
812
- 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) });
813
971
  const result = record.length > 0;
814
972
  ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
815
973
  return result;
@@ -831,7 +989,7 @@ function apply(ctx, config) {
831
989
  return null;
832
990
  }
833
991
  ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
834
- 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) });
835
993
  const result = records.length > 0 ? records[0] : null;
836
994
  ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
837
995
  return result;
@@ -856,7 +1014,7 @@ function apply(ctx, config) {
856
1014
  // 创建新记录,不手动指定id,让数据库自动生成
857
1015
  const newRecord = {
858
1016
  siteId,
859
- userId,
1017
+ userId: parseInt(userId),
860
1018
  pushedAt: new Date()
861
1019
  };
862
1020
  ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
@@ -890,13 +1048,13 @@ function apply(ctx, config) {
890
1048
  if (record) {
891
1049
  ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
892
1050
  // Koishi database API 不支持 update 方法,使用 remove + create 代替
893
- await ctx.database.remove('wordpress_post_updates', { siteId, postId });
1051
+ await ctx.database.remove('wordpress_post_updates', { siteId, postId: parseInt(postId) });
894
1052
  ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
895
1053
  }
896
1054
  // 创建新记录,不指定 id 字段,让数据库自动生成
897
1055
  const newRecord = {
898
1056
  siteId,
899
- postId,
1057
+ postId: parseInt(postId),
900
1058
  lastModified: modifiedDate,
901
1059
  pushedAt: new Date()
902
1060
  };
@@ -968,7 +1126,11 @@ function apply(ctx, config) {
968
1126
  }
969
1127
  // 根据配置格式化日期
970
1128
  const formatDate = (dateString) => {
971
- const date = new Date(dateString);
1129
+ // 使用统一的时间处理工具函数
1130
+ const date = parseWPDate(dateString);
1131
+ if (!date) {
1132
+ return '未知时间';
1133
+ }
972
1134
  const year = date.getFullYear();
973
1135
  const month = String(date.getMonth() + 1).padStart(2, '0');
974
1136
  const day = String(date.getDate()).padStart(2, '0');
@@ -1080,10 +1242,9 @@ function apply(ctx, config) {
1080
1242
  ctx.logger.info(`用户 ${username} 的原始数据: ${JSON.stringify(user)}`);
1081
1243
  }
1082
1244
  if (dateStr) {
1083
- // 尝试解析日期,使用自定义格式:年-月-日 时:分
1084
- const date = new Date(dateStr);
1085
- ctx.logger.info(`解析日期 ${dateStr} 结果: ${date.toString()}`);
1086
- if (!isNaN(date.getTime())) {
1245
+ // 使用统一的时间处理工具函数
1246
+ const date = parseWPDate(dateStr);
1247
+ if (date) {
1087
1248
  const year = date.getFullYear();
1088
1249
  const month = String(date.getMonth() + 1).padStart(2, '0');
1089
1250
  const day = String(date.getDate()).padStart(2, '0');
@@ -1134,14 +1295,21 @@ function apply(ctx, config) {
1134
1295
  ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
1135
1296
  // 推送新文章
1136
1297
  if (site.enableAutoPush) {
1137
- 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;
1138
1306
  ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
1139
1307
  if (posts.length > 0) {
1140
1308
  for (const post of posts) {
1141
1309
  ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
1142
1310
  ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
1143
1311
  // 检查文章是否已推送过(所有群聊共用一个标记)
1144
- const postRecord = await getPostUpdateRecord(site.id, post.id);
1312
+ const postRecord = await getPostUpdateRecord(site.id, String(post.id));
1145
1313
  const hasPushed = !!postRecord;
1146
1314
  ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
1147
1315
  if (!hasPushed) {
@@ -1152,6 +1320,13 @@ function apply(ctx, config) {
1152
1320
  ctx.logger.info(`正在处理目标: ${target}`);
1153
1321
  // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
1154
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
+ }
1155
1330
  const message = formatPostMessage(post, site.mentionAll, false, site);
1156
1331
  ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
1157
1332
  await bot.sendMessage(stringTarget, message);
@@ -1171,7 +1346,7 @@ function apply(ctx, config) {
1171
1346
  addToFailedQueue('post', post, failedTargets, site);
1172
1347
  }
1173
1348
  // 标记文章已推送(所有群聊共用一个标记)
1174
- await updatePostUpdateRecord(site.id, post.id, new Date(post.modified));
1349
+ await updatePostUpdateRecord(site.id, String(post.id), new Date(post.modified));
1175
1350
  ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
1176
1351
  }
1177
1352
  else {
@@ -1182,10 +1357,17 @@ function apply(ctx, config) {
1182
1357
  }
1183
1358
  // 推送文章更新
1184
1359
  if (site.enableUpdatePush) {
1185
- 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;
1186
1368
  if (posts.length > 0) {
1187
1369
  for (const post of posts) {
1188
- const updateRecord = await getPostUpdateRecord(site.id, post.id);
1370
+ const updateRecord = await getPostUpdateRecord(site.id, String(post.id));
1189
1371
  const postModifiedDate = new Date(post.modified);
1190
1372
  // 检查文章是否有更新
1191
1373
  if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
@@ -1196,6 +1378,13 @@ function apply(ctx, config) {
1196
1378
  try {
1197
1379
  ctx.logger.info(`正在处理目标: ${target}`);
1198
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
+ }
1199
1388
  const message = formatPostMessage(post, site.mentionAll, true, site);
1200
1389
  ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
1201
1390
  await bot.sendMessage(stringTarget, message);
@@ -1215,7 +1404,7 @@ function apply(ctx, config) {
1215
1404
  addToFailedQueue('update', post, failedTargets, site);
1216
1405
  }
1217
1406
  // 更新文章更新记录(所有群聊共用一个标记)
1218
- await updatePostUpdateRecord(site.id, post.id, postModifiedDate);
1407
+ await updatePostUpdateRecord(site.id, String(post.id), postModifiedDate);
1219
1408
  ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
1220
1409
  }
1221
1410
  }
@@ -1223,16 +1412,30 @@ function apply(ctx, config) {
1223
1412
  }
1224
1413
  // 推送新用户注册
1225
1414
  if (site.enableUserPush) {
1226
- 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;
1227
1423
  if (users.length > 0) {
1228
1424
  for (const user of users) {
1229
- if (!(await isUserPushed(site.id, user.id))) {
1425
+ if (!(await isUserPushed(site.id, String(user.id)))) {
1230
1426
  const failedTargets = [];
1231
1427
  for (const target of site.targets) {
1232
1428
  try {
1233
1429
  ctx.logger.info(`正在处理目标: ${target}`);
1234
1430
  // 直接使用原始目标字符串,与新文章推送逻辑保持一致
1235
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
+ }
1236
1439
  const message = formatUserMessage(user, site.mentionAll, site);
1237
1440
  ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
1238
1441
  await bot.sendMessage(stringTarget, message);
@@ -1252,7 +1455,7 @@ function apply(ctx, config) {
1252
1455
  addToFailedQueue('user', user, failedTargets, site);
1253
1456
  }
1254
1457
  // 标记用户已推送
1255
- await markUserAsPushed(site.id, user.id);
1458
+ await markUserAsPushed(site.id, String(user.id));
1256
1459
  }
1257
1460
  }
1258
1461
  }
@@ -1269,7 +1472,12 @@ function apply(ctx, config) {
1269
1472
  if (!targetSite) {
1270
1473
  return `未找到站点 ID: ${siteId}`;
1271
1474
  }
1272
- 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;
1273
1481
  if (posts.length === 0) {
1274
1482
  ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
1275
1483
  return `站点 ${targetSite.name} 暂无文章`;
@@ -1311,7 +1519,12 @@ function apply(ctx, config) {
1311
1519
  if (!targetSite) {
1312
1520
  return `未找到站点 ID: ${siteId}`;
1313
1521
  }
1314
- 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;
1315
1528
  if (posts.length === 0) {
1316
1529
  return `站点 ${targetSite.name} 暂无文章`;
1317
1530
  }
@@ -1412,7 +1625,7 @@ function apply(ctx, config) {
1412
1625
  }
1413
1626
  // 切换开关
1414
1627
  site.enableUpdatePush = !site.enableUpdatePush;
1415
- await saveConfig('sites', config.sites);
1628
+ await saveSiteConfig(siteId, site);
1416
1629
  return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
1417
1630
  });
1418
1631
  ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
@@ -1430,7 +1643,7 @@ function apply(ctx, config) {
1430
1643
  }
1431
1644
  // 切换开关
1432
1645
  site.enableUserPush = !site.enableUserPush;
1433
- await saveConfig('sites', config.sites);
1646
+ await saveSiteConfig(siteId, site);
1434
1647
  return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
1435
1648
  });
1436
1649
  ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
@@ -1448,7 +1661,7 @@ function apply(ctx, config) {
1448
1661
  }
1449
1662
  // 切换开关
1450
1663
  site.enableAutoPush = !site.enableAutoPush;
1451
- await saveConfig('sites', config.sites);
1664
+ await saveSiteConfig(siteId, site);
1452
1665
  return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
1453
1666
  });
1454
1667
  ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
@@ -1466,7 +1679,7 @@ function apply(ctx, config) {
1466
1679
  }
1467
1680
  // 切换开关
1468
1681
  site.mentionAll = !site.mentionAll;
1469
- await saveConfig('sites', config.sites);
1682
+ await saveSiteConfig(siteId, site);
1470
1683
  return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
1471
1684
  });
1472
1685
  ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
@@ -1484,7 +1697,7 @@ function apply(ctx, config) {
1484
1697
  }
1485
1698
  // 修改站点地址
1486
1699
  site.url = url;
1487
- await saveConfig('sites', config.sites);
1700
+ await saveSiteConfig(siteId, site);
1488
1701
  ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
1489
1702
  return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
1490
1703
  });
@@ -1528,8 +1741,8 @@ function apply(ctx, config) {
1528
1741
  ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'},调用者:${authResult.userId}`);
1529
1742
  // 设置默认天数
1530
1743
  const daysToKeep = days ? parseInt(days) : CONSTANTS.DEFAULT_CLEAN_DAYS;
1531
- if (isNaN(daysToKeep) || daysToKeep <= 0) {
1532
- return '请输入有效的天数';
1744
+ if (isNaN(daysToKeep) || daysToKeep <= 0 || daysToKeep > 365) {
1745
+ return '请输入有效的天数(1-365天)';
1533
1746
  }
1534
1747
  // 计算清理时间点
1535
1748
  const cutoffDate = new Date();
@@ -1538,46 +1751,84 @@ function apply(ctx, config) {
1538
1751
  let result = 0;
1539
1752
  // 批量删除 wordpress_post_updates 中的旧记录
1540
1753
  try {
1541
- // 先获取需要删除的记录数量
1542
- const updateRecords = await ctx.database.get('wordpress_post_updates', {});
1543
- const updateRecordsToRemove = updateRecords.filter(record => {
1544
- return new Date(record.pushedAt) < cutoffDate;
1545
- });
1546
- if (updateRecordsToRemove.length > 0) {
1547
- // 使用批量删除方式(假设数据库API支持)
1548
- // 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
1549
- const idsToRemove = updateRecordsToRemove.map(record => record.id);
1550
- // 尝试使用更高效的批量删除方式
1551
- // 注意:具体实现取决于Koishi数据库API的支持情况
1552
- for (const id of idsToRemove) {
1553
- 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;
1554
1767
  }
1555
- result += updateRecordsToRemove.length;
1556
- ctx.logger.info(`批量删除了 ${updateRecordsToRemove.length} 条 wordpress_post_updates 旧记录`);
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 })));
1782
+ }
1783
+ result += recordsToRemove.length;
1784
+ processed += updateRecords.length;
1785
+ ctx.logger.info(`已处理 ${processed} 条记录,删除了 ${recordsToRemove.length} 条旧记录`);
1786
+ // 避免数据库压力过大,每批处理后稍作延迟
1787
+ await new Promise(resolve => setTimeout(resolve, 100));
1557
1788
  }
1789
+ ctx.logger.info(`批量删除 wordpress_post_updates 旧记录完成,共删除 ${result} 条记录`);
1558
1790
  }
1559
1791
  catch (error) {
1560
1792
  ctx.logger.error(`批量删除 wordpress_post_updates 旧记录失败: ${error}`);
1561
1793
  }
1562
1794
  // 批量删除 wordpress_user_registrations 中的旧记录
1563
1795
  try {
1564
- // 先获取需要删除的记录数量
1565
- const userRecords = await ctx.database.get('wordpress_user_registrations', {});
1566
- const userRecordsToRemove = userRecords.filter(record => {
1567
- return new Date(record.pushedAt) < cutoffDate;
1568
- });
1569
- if (userRecordsToRemove.length > 0) {
1570
- // 使用批量删除方式(假设数据库API支持)
1571
- // 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
1572
- const idsToRemove = userRecordsToRemove.map(record => record.id);
1573
- // 尝试使用更高效的批量删除方式
1574
- // 注意:具体实现取决于Koishi数据库API的支持情况
1575
- for (const id of idsToRemove) {
1576
- 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;
1577
1809
  }
1578
- result += userRecordsToRemove.length;
1579
- 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));
1580
1830
  }
1831
+ ctx.logger.info(`批量删除 wordpress_user_registrations 旧记录完成,共删除 ${result} 条记录`);
1581
1832
  }
1582
1833
  catch (error) {
1583
1834
  ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
@@ -1690,9 +1941,10 @@ function apply(ctx, config) {
1690
1941
  ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1691
1942
  return message;
1692
1943
  });
1693
- // 为每个站点设置独立的定时任务
1944
+ // 为每个站点设置独立的定时任务,绑定到 ctx 生命周期
1945
+ const siteTimers = [];
1694
1946
  config.sites.forEach(site => {
1695
- ctx.setInterval(() => {
1947
+ const timer = ctx.setInterval(() => {
1696
1948
  try {
1697
1949
  pushNewPosts();
1698
1950
  }
@@ -1703,5 +1955,28 @@ function apply(ctx, config) {
1703
1955
  ctx.logger.warn('定时任务将在下一个周期继续执行');
1704
1956
  }
1705
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 插件资源清理完成');
1706
1981
  });
1707
1982
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-wordpress-notifier",
3
- "version": "2.8.5",
3
+ "version": "2.9.0",
4
4
  "description": "WordPress 文章自动推送到 QQ",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",