koishi-plugin-wordpress-notifier 2.8.1 → 2.8.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/index.js CHANGED
@@ -18,6 +18,7 @@ const CONSTANTS = {
18
18
  HTTP_TIMEOUT: 10000, // 10秒
19
19
  MAX_RETRIES: 2,
20
20
  RETRY_DELAY: 1000, // 1秒
21
+ API_RATE_LIMIT: 1000, // API请求频率限制(毫秒),每秒最多1次
21
22
  // 失败队列相关
22
23
  MAX_PUSH_RETRIES: 3,
23
24
  PUSH_RETRY_INTERVAL: 5 * 60 * 1000, // 5分钟
@@ -25,19 +26,104 @@ const CONSTANTS = {
25
26
  DEFAULT_CLEAN_DAYS: 30,
26
27
  // QQ适配器相关
27
28
  QQ_ADAPTERS: ['qq', 'onebot', 'milky', 'satori'],
29
+ // 安全过滤相关
30
+ SENSITIVE_WORDS: [
31
+ // 常见敏感词列表(示例)
32
+ '敏感词1', '敏感词2', '敏感词3',
33
+ // 可以根据实际需求扩展
34
+ ],
35
+ SENSITIVE_REPLACEMENT: '***', // 敏感词替换为***
28
36
  };
37
+ // API 请求速率控制变量
38
+ let lastRequestTime = 0;
39
+ // 数据库连接状态
40
+ let databaseConnected = true;
41
+ let lastDatabaseCheck = 0;
42
+ const DATABASE_CHECK_INTERVAL = 30000; // 30秒检查一次数据库连接
43
+ // 推送失败统计
44
+ let consecutiveFailureCount = 0;
45
+ let lastFailureTime = 0;
46
+ const FAILURE_RESET_INTERVAL = 60 * 60 * 1000; // 1小时内无失败则重置计数
47
+ // 数据库版本
48
+ const DATABASE_VERSION = '2.0.0'; // 当前数据库结构版本
49
+ const runtimeStats = {
50
+ pushSuccessCount: 0,
51
+ pushFailureCount: 0,
52
+ apiCallCount: 0,
53
+ apiSuccessCount: 0,
54
+ apiFailureCount: 0,
55
+ lastResetTime: Date.now()
56
+ };
57
+ // 更新 API 调用统计
58
+ function updateApiStats(success) {
59
+ runtimeStats.apiCallCount++;
60
+ if (success) {
61
+ runtimeStats.apiSuccessCount++;
62
+ }
63
+ else {
64
+ runtimeStats.apiFailureCount++;
65
+ }
66
+ }
67
+ // 更新推送统计
68
+ function updatePushStats(success) {
69
+ if (success) {
70
+ runtimeStats.pushSuccessCount++;
71
+ }
72
+ else {
73
+ runtimeStats.pushFailureCount++;
74
+ }
75
+ }
29
76
  exports.Config = koishi_1.Schema.object({
30
- wordpressUrl: koishi_1.Schema.string().description('WordPress 网站地址(例如:https://example.com)'),
31
- interval: koishi_1.Schema.number().default(3600000).description('检查间隔(毫秒,默认 1 小时)'),
32
- targets: koishi_1.Schema.array(koishi_1.Schema.string()).description('推送目标(群号或 QQ 号)'),
33
- enableAutoPush: koishi_1.Schema.boolean().default(true).description('是否启用自动推送'),
34
- enableUpdatePush: koishi_1.Schema.boolean().default(false).description('是否启用文章更新推送'),
35
- enableUserPush: koishi_1.Schema.boolean().default(false).description('是否启用新用户注册推送'),
36
- mentionAll: koishi_1.Schema.boolean().default(false).description('是否 @全体成员'),
37
- maxArticles: koishi_1.Schema.number().default(5).description('每次最多推送的文章数量'),
38
- username: koishi_1.Schema.string().default('').description('WordPress 用户名(用于 Basic 认证,与应用程序密码配合使用)'),
39
- applicationPassword: koishi_1.Schema.string().default('').description('WordPress 应用程序密码(用于 Basic 认证,例如:hGR2sPFuYnclxHc4AvJq cUtB)'),
40
- superAdmins: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description('超级管理员 QQ 号列表')
77
+ // 多站点配置
78
+ sites: koishi_1.Schema.array(koishi_1.Schema.object({
79
+ id: koishi_1.Schema.string().description('站点唯一标识(如 "blog"、"news" 等)'),
80
+ name: koishi_1.Schema.string().description('站点名称(用于显示)'),
81
+ url: koishi_1.Schema.string().description('WordPress 网站地址(例如:https://your-wordpress-site.com)'),
82
+ interval: koishi_1.Schema.number().default(3600000).description('检查间隔(毫秒,默认 1 小时)'),
83
+ targets: koishi_1.Schema.array(koishi_1.Schema.string()).description('推送目标(群号或 QQ 号)'),
84
+ enableAutoPush: koishi_1.Schema.boolean().default(true).description('是否启用自动推送'),
85
+ enableUpdatePush: koishi_1.Schema.boolean().default(false).description('是否启用文章更新推送'),
86
+ enableUserPush: koishi_1.Schema.boolean().default(false).description('是否启用新用户注册推送'),
87
+ mentionAll: koishi_1.Schema.boolean().default(false).description('是否 @全体成员'),
88
+ maxArticles: koishi_1.Schema.number().default(5).description('每次最多推送的文章数量'),
89
+ username: koishi_1.Schema.string().default('').description('WordPress 用户名(用于 Basic 认证,与应用程序密码配合使用)'),
90
+ applicationPassword: koishi_1.Schema.string().default('').description('WordPress 应用程序密码(用于 Basic 认证,例如:hGR2sPFuYnclxHc4AvJq cUtB)'),
91
+ // 站点特定的推送模板配置
92
+ pushTemplate: koishi_1.Schema.object({
93
+ showExcerpt: koishi_1.Schema.boolean().default(false).description('是否显示文章摘要'),
94
+ dateFormat: koishi_1.Schema.string().default('YYYY-MM-DD HH:mm').description('日期格式,支持 YYYY-MM-DD HH:mm 等格式'),
95
+ linkPosition: koishi_1.Schema.union(['top', 'bottom', 'none']).default('bottom').description('链接位置:顶部、底部或不显示'),
96
+ showAuthor: koishi_1.Schema.boolean().default(false).description('是否显示文章作者')
97
+ }).description('推送模板配置')
98
+ })).description('多站点配置列表').default([
99
+ {
100
+ id: 'default',
101
+ name: '默认站点',
102
+ url: 'https://your-wordpress-site.com',
103
+ interval: 3600000,
104
+ targets: [],
105
+ enableAutoPush: true,
106
+ enableUpdatePush: false,
107
+ enableUserPush: false,
108
+ mentionAll: false,
109
+ maxArticles: 5,
110
+ username: '',
111
+ applicationPassword: '',
112
+ pushTemplate: {
113
+ showExcerpt: false,
114
+ dateFormat: 'YYYY-MM-DD HH:mm',
115
+ linkPosition: 'bottom',
116
+ showAuthor: false
117
+ }
118
+ }
119
+ ]),
120
+ superAdmins: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description('超级管理员 QQ 号列表'),
121
+ // 全局推送失败通知配置
122
+ failureNotification: koishi_1.Schema.object({
123
+ enable: koishi_1.Schema.boolean().default(true).description('是否启用推送失败通知'),
124
+ threshold: koishi_1.Schema.number().default(3).description('连续失败阈值,达到此值时发送通知'),
125
+ notificationTargets: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description('通知目标(超级管理员 QQ 号),留空则使用 superAdmins')
126
+ }).description('推送失败通知配置')
41
127
  });
42
128
  function apply(ctx, config) {
43
129
  ctx.logger.info('WordPress 推送插件已加载');
@@ -45,22 +131,24 @@ function apply(ctx, config) {
45
131
  // 确保 id 字段被正确设置为自增主键,并且在插入时不会被设置为 NULL
46
132
  ctx.model.extend('wordpress_post_updates', {
47
133
  id: 'integer',
134
+ siteId: 'string',
48
135
  postId: 'integer',
49
136
  lastModified: 'timestamp',
50
137
  pushedAt: 'timestamp'
51
138
  }, {
52
139
  primary: 'id',
53
140
  autoInc: true,
54
- unique: ['postId']
141
+ unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
55
142
  });
56
143
  ctx.model.extend('wordpress_user_registrations', {
57
144
  id: 'integer',
145
+ siteId: 'string',
58
146
  userId: 'integer',
59
147
  pushedAt: 'timestamp'
60
148
  }, {
61
149
  primary: 'id',
62
150
  autoInc: true,
63
- unique: ['userId']
151
+ unique: ['siteId', 'userId'] // 每个站点的用户 ID 唯一
64
152
  });
65
153
  // 配置存储表
66
154
  ctx.model.extend('wordpress_config', {
@@ -73,6 +161,16 @@ function apply(ctx, config) {
73
161
  autoInc: true,
74
162
  unique: ['key']
75
163
  });
164
+ // 版本记录表
165
+ ctx.model.extend('wordpress_version', {
166
+ id: 'integer',
167
+ version: 'string',
168
+ updatedAt: 'timestamp'
169
+ }, {
170
+ primary: 'id',
171
+ autoInc: true,
172
+ unique: ['version']
173
+ });
76
174
  ctx.logger.info('数据库表配置完成,autoInc: true 已启用,确保插入操作不手动指定 id 字段');
77
175
  // 为所有数据库操作添加详细日志,便于诊断自增主键问题
78
176
  ctx.on('ready', async () => {
@@ -81,6 +179,7 @@ function apply(ctx, config) {
81
179
  ctx.logger.info('wordpress_post_updates: id 字段设置为 autoInc: true');
82
180
  ctx.logger.info('wordpress_user_registrations: id 字段设置为 autoInc: true');
83
181
  ctx.logger.info('wordpress_config: 配置持久化存储表');
182
+ ctx.logger.info('wordpress_version: 数据库版本记录表');
84
183
  ctx.logger.info('所有群聊共用一个文章标记,不再区分群聊');
85
184
  // 检查并修复数据库表结构问题
86
185
  await checkAndFixTableStructure();
@@ -93,6 +192,12 @@ function apply(ctx, config) {
93
192
  async function loadPersistentConfig() {
94
193
  try {
95
194
  ctx.logger.info('开始加载持久化配置...');
195
+ // 确保数据库连接正常
196
+ const connected = await ensureDatabaseConnection();
197
+ if (!connected) {
198
+ ctx.logger.warn('数据库连接异常,跳过加载持久化配置');
199
+ return;
200
+ }
96
201
  const configRecords = await ctx.database.get('wordpress_config', {});
97
202
  ctx.logger.info(`找到 ${configRecords.length} 条持久化配置记录`);
98
203
  for (const record of configRecords) {
@@ -126,6 +231,12 @@ function apply(ctx, config) {
126
231
  async function saveConfig(key, value) {
127
232
  try {
128
233
  ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
234
+ // 确保数据库连接正常
235
+ const connected = await ensureDatabaseConnection();
236
+ if (!connected) {
237
+ ctx.logger.warn('数据库连接异常,跳过保存配置');
238
+ return;
239
+ }
129
240
  // 检查配置是否已存在
130
241
  const existingRecords = await ctx.database.get('wordpress_config', { key });
131
242
  if (existingRecords.length > 0) {
@@ -212,14 +323,15 @@ function apply(ctx, config) {
212
323
  const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
213
324
  const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
214
325
  // 添加到失败队列
215
- function addToFailedQueue(type, data, targets) {
326
+ function addToFailedQueue(type, data, targets, siteConfig) {
216
327
  const item = {
217
328
  id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
218
329
  type,
219
330
  data,
220
331
  targets,
221
332
  retries: 0,
222
- createdAt: new Date()
333
+ createdAt: new Date(),
334
+ siteConfig
223
335
  };
224
336
  failedPushQueue.push(item);
225
337
  ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
@@ -248,10 +360,10 @@ function apply(ctx, config) {
248
360
  // 根据类型格式化消息
249
361
  let message;
250
362
  if (item.type === 'post' || item.type === 'update') {
251
- message = formatPostMessage(item.data, true, item.type === 'update');
363
+ message = formatPostMessage(item.data, true, item.type === 'update', item.siteConfig);
252
364
  }
253
365
  else {
254
- message = formatUserMessage(item.data, true);
366
+ message = formatUserMessage(item.data, true, item.siteConfig);
255
367
  }
256
368
  // 推送到所有目标
257
369
  for (const target of item.targets) {
@@ -297,6 +409,106 @@ function apply(ctx, config) {
297
409
  // 权限检查通过,继续执行命令
298
410
  return { valid: true, userId };
299
411
  }
412
+ // 检查数据库连接状态
413
+ async function checkDatabaseConnection() {
414
+ const now = Date.now();
415
+ // 避免频繁检查,30秒内只检查一次
416
+ if (now - lastDatabaseCheck < DATABASE_CHECK_INTERVAL && databaseConnected) {
417
+ return true;
418
+ }
419
+ try {
420
+ ctx.logger.info('开始检查数据库连接状态...');
421
+ // 执行一个简单的数据库查询来测试连接
422
+ await ctx.database.get('wordpress_config', {}, { limit: 1 });
423
+ ctx.logger.info('数据库连接正常');
424
+ databaseConnected = true;
425
+ lastDatabaseCheck = now;
426
+ return true;
427
+ }
428
+ catch (error) {
429
+ ctx.logger.error(`数据库连接异常: ${error}`);
430
+ databaseConnected = false;
431
+ lastDatabaseCheck = now;
432
+ return false;
433
+ }
434
+ }
435
+ // 确保数据库连接正常
436
+ async function ensureDatabaseConnection() {
437
+ const connected = await checkDatabaseConnection();
438
+ if (!connected) {
439
+ ctx.logger.warn('数据库连接异常,暂停推送任务');
440
+ // 记录失败
441
+ recordPushFailure('数据库连接异常');
442
+ // 等待3秒后再次尝试
443
+ await new Promise(resolve => setTimeout(resolve, 3000));
444
+ return await checkDatabaseConnection();
445
+ }
446
+ return true;
447
+ }
448
+ // 记录推送失败并发送通知
449
+ async function recordPushFailure(reason) {
450
+ const now = Date.now();
451
+ // 检查是否需要重置失败计数
452
+ if (now - lastFailureTime > FAILURE_RESET_INTERVAL) {
453
+ consecutiveFailureCount = 0;
454
+ }
455
+ // 增加失败计数
456
+ consecutiveFailureCount++;
457
+ lastFailureTime = now;
458
+ ctx.logger.error(`推送失败,原因: ${reason},连续失败次数: ${consecutiveFailureCount}`);
459
+ // 检查是否需要发送通知
460
+ if (config.failureNotification.enable &&
461
+ consecutiveFailureCount >= config.failureNotification.threshold) {
462
+ await sendFailureNotification(reason);
463
+ }
464
+ }
465
+ // 发送失败通知
466
+ async function sendFailureNotification(reason) {
467
+ try {
468
+ const bot = getValidBot();
469
+ if (!bot) {
470
+ ctx.logger.error('无法发送失败通知,没有可用的 Bot 实例');
471
+ return;
472
+ }
473
+ // 确定通知目标
474
+ const notificationTargets = config.failureNotification.notificationTargets.length > 0
475
+ ? config.failureNotification.notificationTargets
476
+ : config.superAdmins;
477
+ if (notificationTargets.length === 0) {
478
+ ctx.logger.warn('无法发送失败通知,未配置通知目标');
479
+ return;
480
+ }
481
+ // 构建通知消息
482
+ const message = `🚨 WordPress 推送插件告警\n\n` +
483
+ `📅 时间: ${new Date().toLocaleString()}\n` +
484
+ `❌ 连续失败次数: ${consecutiveFailureCount}\n` +
485
+ `🔍 失败原因: ${reason}\n` +
486
+ `🌐 站点数: ${config.sites.length}\n` +
487
+ `📡 总推送目标数: ${config.sites.reduce((total, site) => total + site.targets.length, 0)}\n\n` +
488
+ `请及时检查插件状态和相关配置!`;
489
+ // 发送通知
490
+ for (const target of notificationTargets) {
491
+ try {
492
+ await bot.sendMessage(target, message);
493
+ ctx.logger.info(`已向 ${target} 发送推送失败通知`);
494
+ }
495
+ catch (error) {
496
+ ctx.logger.error(`发送通知到 ${target} 失败: ${error}`);
497
+ }
498
+ }
499
+ // 重置失败计数
500
+ consecutiveFailureCount = 0;
501
+ }
502
+ catch (error) {
503
+ ctx.logger.error(`发送失败通知时发生错误: ${error}`);
504
+ }
505
+ }
506
+ // 重置失败计数
507
+ function resetFailureCount() {
508
+ consecutiveFailureCount = 0;
509
+ lastFailureTime = 0;
510
+ ctx.logger.info('推送失败计数已重置');
511
+ }
300
512
  // 检查数据库表结构的函数
301
513
  async function checkAndFixTableStructure() {
302
514
  try {
@@ -321,13 +533,14 @@ function apply(ctx, config) {
321
533
  // 重新扩展模型
322
534
  ctx.model.extend('wordpress_post_updates', {
323
535
  id: 'integer',
536
+ siteId: 'string',
324
537
  postId: 'integer',
325
538
  lastModified: 'timestamp',
326
539
  pushedAt: 'timestamp'
327
540
  }, {
328
541
  primary: 'id',
329
542
  autoInc: true,
330
- unique: ['postId']
543
+ unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
331
544
  });
332
545
  ctx.logger.info('wordpress_post_updates 表重新初始化成功');
333
546
  }
@@ -352,12 +565,13 @@ function apply(ctx, config) {
352
565
  // 重新扩展模型
353
566
  ctx.model.extend('wordpress_user_registrations', {
354
567
  id: 'integer',
568
+ siteId: 'string',
355
569
  userId: 'integer',
356
570
  pushedAt: 'timestamp'
357
571
  }, {
358
572
  primary: 'id',
359
573
  autoInc: true,
360
- unique: ['userId']
574
+ unique: ['siteId', 'userId'] // 每个站点的用户 ID 唯一
361
575
  });
362
576
  ctx.logger.info('wordpress_user_registrations 表重新初始化成功');
363
577
  }
@@ -413,6 +627,16 @@ function apply(ctx, config) {
413
627
  let retries = 0;
414
628
  while (retries <= maxRetries) {
415
629
  try {
630
+ // API 请求频率控制
631
+ const now = Date.now();
632
+ const timeSinceLastRequest = now - lastRequestTime;
633
+ if (timeSinceLastRequest < CONSTANTS.API_RATE_LIMIT) {
634
+ const waitTime = CONSTANTS.API_RATE_LIMIT - timeSinceLastRequest;
635
+ ctx.logger.info(`API 请求频率限制,等待 ${waitTime}ms 后继续请求`);
636
+ await new Promise(resolve => setTimeout(resolve, waitTime));
637
+ }
638
+ // 更新上次请求时间
639
+ lastRequestTime = Date.now();
416
640
  ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
417
641
  const response = await ctx.http.get(url, requestConfig);
418
642
  ctx.logger.info(`HTTP请求成功: ${url}`);
@@ -424,6 +648,8 @@ function apply(ctx, config) {
424
648
  retries++;
425
649
  if (retries > maxRetries) {
426
650
  ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
651
+ // 记录推送失败
652
+ recordPushFailure(`API请求失败: ${errorMessage}`);
427
653
  return null;
428
654
  }
429
655
  // 重试前等待
@@ -432,16 +658,16 @@ function apply(ctx, config) {
432
658
  }
433
659
  return null;
434
660
  }
435
- async function fetchLatestPosts() {
661
+ async function fetchLatestPosts(site) {
436
662
  try {
437
- const url = `${config.wordpressUrl}/wp-json/wp/v2/posts?per_page=${config.maxArticles}&orderby=date&order=desc`;
438
- ctx.logger.info(`正在获取文章: ${url}`);
663
+ const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=date&order=desc`;
664
+ ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的文章: ${url}`);
439
665
  // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
440
666
  const requestConfig = {};
441
- if (config.username && config.applicationPassword) {
667
+ if (site.username && site.applicationPassword) {
442
668
  // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
443
- const username = config.username;
444
- const password = config.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
669
+ const username = site.username;
670
+ const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
445
671
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
446
672
  requestConfig.headers = {
447
673
  Authorization: `Basic ${auth}`
@@ -449,30 +675,33 @@ function apply(ctx, config) {
449
675
  }
450
676
  const response = await httpRequest(url, requestConfig);
451
677
  if (!response) {
452
- ctx.logger.error(`获取 WordPress 文章失败,已达到最大重试次数`);
678
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`);
453
679
  return [];
454
680
  }
455
- ctx.logger.info(`成功获取 ${response.length} 篇文章`);
681
+ ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
456
682
  return response;
457
683
  }
458
684
  catch (error) {
459
- ctx.logger.error(`获取 WordPress 文章失败: ${error}`);
685
+ const errorMessage = error instanceof Error ? error.message : String(error);
686
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`);
687
+ // 记录失败
688
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
460
689
  return [];
461
690
  }
462
691
  }
463
- async function fetchLatestUsers() {
692
+ async function fetchLatestUsers(site) {
464
693
  try {
465
694
  // 修改API请求,添加_fields参数明确请求注册日期字段
466
695
  // WordPress REST API 默认可能不会返回注册日期,需要明确请求
467
696
  const fields = 'id,name,slug,date,date_registered,registered_date,created_at,registeredAt,email,roles,url,description,link,avatar_urls';
468
- const url = `${config.wordpressUrl}/wp-json/wp/v2/users?per_page=${config.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
469
- ctx.logger.info(`正在获取用户: ${url}`);
697
+ const url = `${site.url}/wp-json/wp/v2/users?per_page=${site.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
698
+ ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的用户: ${url}`);
470
699
  // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
471
700
  const requestConfig = {};
472
- if (config.username && config.applicationPassword) {
701
+ if (site.username && site.applicationPassword) {
473
702
  // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
474
- const username = config.username;
475
- const password = config.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
703
+ const username = site.username;
704
+ const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
476
705
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
477
706
  requestConfig.headers = {
478
707
  Authorization: `Basic ${auth}`
@@ -480,12 +709,14 @@ function apply(ctx, config) {
480
709
  }
481
710
  const response = await httpRequest(url, requestConfig);
482
711
  if (!response) {
483
- ctx.logger.error(`获取 WordPress 用户失败,已达到最大重试次数`);
484
- ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在插件配置中添加 WordPress 用户名和应用程序密码`);
712
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`);
713
+ ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
714
+ // 记录失败
715
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
485
716
  // 返回空数组,确保插件继续运行
486
717
  return [];
487
718
  }
488
- ctx.logger.info(`成功获取 ${response.length} 位用户`);
719
+ ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
489
720
  // 添加调试日志,查看API返回的实际数据结构
490
721
  if (response.length > 0) {
491
722
  ctx.logger.info(`用户数据示例: ${JSON.stringify(response[0], null, 2)}`);
@@ -496,22 +727,25 @@ function apply(ctx, config) {
496
727
  return response;
497
728
  }
498
729
  catch (error) {
499
- ctx.logger.error(`获取 WordPress 用户失败: ${error}`);
500
- ctx.logger.error(`WordPress REST APIusers 端点需要认证才能访问,请在插件配置中添加 WordPress 用户名和应用程序密码`);
730
+ const errorMessage = error instanceof Error ? error.message : String(error);
731
+ ctx.logger.error(`获取站点 ${site.id} (${site.name})WordPress 用户失败: ${errorMessage}`);
732
+ ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
733
+ // 记录失败
734
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
501
735
  // 返回空数组,确保插件继续运行
502
736
  return [];
503
737
  }
504
738
  }
505
- async function fetchUpdatedPosts() {
739
+ async function fetchUpdatedPosts(site) {
506
740
  try {
507
- const url = `${config.wordpressUrl}/wp-json/wp/v2/posts?per_page=${config.maxArticles}&orderby=modified&order=desc`;
508
- ctx.logger.info(`正在获取更新文章: ${url}`);
741
+ const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=modified&order=desc`;
742
+ ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的更新文章: ${url}`);
509
743
  // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
510
744
  const requestConfig = {};
511
- if (config.username && config.applicationPassword) {
745
+ if (site.username && site.applicationPassword) {
512
746
  // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
513
- const username = config.username;
514
- const password = config.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
747
+ const username = site.username;
748
+ const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
515
749
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
516
750
  requestConfig.headers = {
517
751
  Authorization: `Basic ${auth}`
@@ -519,23 +753,34 @@ function apply(ctx, config) {
519
753
  }
520
754
  const response = await httpRequest(url, requestConfig);
521
755
  if (!response) {
522
- ctx.logger.error(`获取 WordPress 更新文章失败,已达到最大重试次数`);
756
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`);
757
+ // 记录失败
758
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
523
759
  return [];
524
760
  }
525
- ctx.logger.info(`成功获取 ${response.length} 篇更新文章`);
761
+ ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
526
762
  return response;
527
763
  }
528
764
  catch (error) {
529
- ctx.logger.error(`获取 WordPress 更新文章失败: ${error}`);
765
+ const errorMessage = error instanceof Error ? error.message : String(error);
766
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`);
767
+ // 记录失败
768
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
530
769
  return [];
531
770
  }
532
771
  }
533
- async function isUserPushed(userId) {
772
+ async function isUserPushed(siteId, userId) {
534
773
  try {
535
- ctx.logger.info(`检查用户是否已推送,用户 ID: ${userId}`);
536
- const record = await ctx.database.get('wordpress_user_registrations', { userId });
774
+ // 确保数据库连接正常
775
+ const connected = await ensureDatabaseConnection();
776
+ if (!connected) {
777
+ ctx.logger.warn('数据库连接异常,跳过检查用户推送记录');
778
+ return false;
779
+ }
780
+ ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
781
+ const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId });
537
782
  const result = record.length > 0;
538
- ctx.logger.info(`检查结果:用户 ${userId} 已推送:${result ? '是' : '否'}`);
783
+ ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
539
784
  return result;
540
785
  }
541
786
  catch (error) {
@@ -546,12 +791,18 @@ function apply(ctx, config) {
546
791
  return false;
547
792
  }
548
793
  }
549
- async function getPostUpdateRecord(postId) {
794
+ async function getPostUpdateRecord(siteId, postId) {
550
795
  try {
551
- ctx.logger.info(`获取文章更新记录,文章 ID: ${postId}`);
552
- const records = await ctx.database.get('wordpress_post_updates', { postId });
796
+ // 确保数据库连接正常
797
+ const connected = await ensureDatabaseConnection();
798
+ if (!connected) {
799
+ ctx.logger.warn('数据库连接异常,跳过获取文章更新记录');
800
+ return null;
801
+ }
802
+ ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
803
+ const records = await ctx.database.get('wordpress_post_updates', { siteId, postId });
553
804
  const result = records.length > 0 ? records[0] : null;
554
- ctx.logger.info(`获取结果:文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
805
+ ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
555
806
  return result;
556
807
  }
557
808
  catch (error) {
@@ -562,74 +813,116 @@ function apply(ctx, config) {
562
813
  return null;
563
814
  }
564
815
  }
565
- async function markUserAsPushed(userId) {
816
+ async function markUserAsPushed(siteId, userId) {
566
817
  try {
567
- ctx.logger.info(`开始标记用户已推送,用户 ID: ${userId}`);
818
+ // 确保数据库连接正常
819
+ const connected = await ensureDatabaseConnection();
820
+ if (!connected) {
821
+ ctx.logger.warn('数据库连接异常,跳过标记用户推送记录');
822
+ return;
823
+ }
824
+ ctx.logger.info(`开始标记用户已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
568
825
  // 创建新记录,不手动指定id,让数据库自动生成
569
826
  const newRecord = {
827
+ siteId,
570
828
  userId,
571
829
  pushedAt: new Date()
572
830
  };
573
831
  ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
574
832
  await ctx.database.create('wordpress_user_registrations', newRecord);
575
- ctx.logger.info(`已成功标记用户 ${userId} 为已推送`);
833
+ ctx.logger.info(`已成功标记站点 ${siteId} 用户 ${userId} 为已推送`);
576
834
  }
577
835
  catch (error) {
578
836
  const errorMessage = error instanceof Error ? error.message : String(error);
579
837
  if (errorMessage.includes('UNIQUE constraint failed')) {
580
- ctx.logger.warn(`用户推送记录已存在,跳过重复插入:用户 ${userId}`);
838
+ ctx.logger.warn(`用户推送记录已存在,跳过重复插入:站点 ${siteId} 用户 ${userId}`);
581
839
  ctx.logger.warn(`完整错误信息:${errorMessage}`);
582
840
  }
583
841
  else {
584
842
  ctx.logger.error(`标记用户推送记录失败:${errorMessage}`);
585
843
  ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
586
- ctx.logger.error(`插入参数:userId=${userId}`);
844
+ ctx.logger.error(`插入参数:siteId=${siteId}, userId=${userId}`);
587
845
  // 非约束冲突错误,不抛出,确保插件继续运行
588
846
  }
589
847
  }
590
848
  }
591
- async function updatePostUpdateRecord(postId, modifiedDate) {
849
+ async function updatePostUpdateRecord(siteId, postId, modifiedDate) {
592
850
  try {
593
- ctx.logger.info(`开始更新文章更新记录,文章 ID: ${postId},修改时间: ${modifiedDate}`);
594
- const record = await getPostUpdateRecord(postId);
851
+ // 确保数据库连接正常
852
+ const connected = await ensureDatabaseConnection();
853
+ if (!connected) {
854
+ ctx.logger.warn('数据库连接异常,跳过更新文章更新记录');
855
+ return;
856
+ }
857
+ ctx.logger.info(`开始更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId},修改时间: ${modifiedDate}`);
858
+ const record = await getPostUpdateRecord(siteId, postId);
595
859
  if (record) {
596
- ctx.logger.info(`发现现有记录,文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
860
+ ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
597
861
  // Koishi database API 不支持 update 方法,使用 remove + create 代替
598
- await ctx.database.remove('wordpress_post_updates', { postId });
599
- ctx.logger.info(`已删除旧记录,文章 ID: ${postId}`);
862
+ await ctx.database.remove('wordpress_post_updates', { siteId, postId });
863
+ ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
600
864
  }
601
865
  // 创建新记录,不指定 id 字段,让数据库自动生成
602
866
  const newRecord = {
867
+ siteId,
603
868
  postId,
604
869
  lastModified: modifiedDate,
605
870
  pushedAt: new Date()
606
871
  };
607
- ctx.logger.info(`准备创建新记录,文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
872
+ ctx.logger.info(`准备创建新记录,站点 ID: ${siteId},文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
608
873
  await ctx.database.create('wordpress_post_updates', newRecord);
609
- ctx.logger.info(`已成功更新文章更新记录,文章 ID: ${postId}`);
874
+ ctx.logger.info(`已成功更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
610
875
  }
611
876
  catch (error) {
612
877
  const errorMessage = error instanceof Error ? error.message : String(error);
613
- ctx.logger.error(`更新文章更新记录失败,文章 ID: ${postId}`);
878
+ ctx.logger.error(`更新文章更新记录失败,站点 ID: ${siteId},文章 ID: ${postId}`);
614
879
  ctx.logger.error(`错误信息: ${errorMessage}`);
615
880
  ctx.logger.error(`错误栈: ${error instanceof Error ? error.stack : '无'}`);
616
881
  // 不再抛出错误,确保推送流程继续运行
617
882
  // 发生错误时,默认返回,避免阻塞推送流程
618
- ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,文章 ID: ${postId}`);
883
+ ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,站点 ID: ${siteId},文章 ID: ${postId}`);
619
884
  }
620
885
  }
621
886
  // 1. 新增强清洗函数:针对性解决敏感字符问题
622
887
  function sanitizeContent(content) {
623
- return content
888
+ let sanitized = content
624
889
  .replace(/<[^>]*>/g, '') // 移除所有 HTML 标签
625
890
  .replace(/[\x00-\x1F\x7F]/g, '') // 移除不可见控制符,QQ 接口明确禁止
626
891
  .replace(/\u3000/g, ' ') // 全角空格转半角空格,解决适配器编码缺陷
627
892
  .replace(/\s+/g, ' ') // 标准化所有空白符为单个半角空格
628
893
  .trim(); // 移除首尾空格
894
+ // 添加敏感词过滤
895
+ sanitized = filterSensitiveWords(sanitized);
896
+ return sanitized;
629
897
  }
630
- function formatPostMessage(post, mention = false, isUpdate = false) {
631
- // 生成缓存键
632
- const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${config.mentionAll}`;
898
+ // 敏感词过滤函数
899
+ function filterSensitiveWords(content) {
900
+ let filteredContent = content;
901
+ let hasSensitiveWords = false;
902
+ // 遍历敏感词列表进行替换
903
+ for (const word of CONSTANTS.SENSITIVE_WORDS) {
904
+ if (filteredContent.includes(word)) {
905
+ hasSensitiveWords = true;
906
+ const regex = new RegExp(word, 'gi'); // 不区分大小写
907
+ filteredContent = filteredContent.replace(regex, CONSTANTS.SENSITIVE_REPLACEMENT);
908
+ }
909
+ }
910
+ // 如果检测到敏感词,记录日志
911
+ if (hasSensitiveWords) {
912
+ ctx.logger.info('消息内容包含敏感词,已进行过滤处理');
913
+ // 仅记录处理信息,不记录具体内容,保护隐私
914
+ }
915
+ return filteredContent;
916
+ }
917
+ function formatPostMessage(post, mention = false, isUpdate = false, siteConfig) {
918
+ // 使用默认站点配置或传入的站点配置
919
+ const configToUse = siteConfig || config.sites[0];
920
+ if (!configToUse) {
921
+ ctx.logger.error('无可用的站点配置');
922
+ return '';
923
+ }
924
+ // 生成缓存键,包含推送模板配置信息
925
+ const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${configToUse.mentionAll}_${JSON.stringify(configToUse.pushTemplate)}`;
633
926
  // 检查缓存
634
927
  const cached = formatCache.post.get(cacheKey);
635
928
  if (cached && isCacheValid(cached.timestamp)) {
@@ -642,7 +935,7 @@ function apply(ctx, config) {
642
935
  if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
643
936
  title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
644
937
  }
645
- // 自定义时间格式:年-月-日 时:分
938
+ // 根据配置格式化日期
646
939
  const formatDate = (dateString) => {
647
940
  const date = new Date(dateString);
648
941
  const year = date.getFullYear();
@@ -650,19 +943,50 @@ function apply(ctx, config) {
650
943
  const day = String(date.getDate()).padStart(2, '0');
651
944
  const hours = String(date.getHours()).padStart(2, '0');
652
945
  const minutes = String(date.getMinutes()).padStart(2, '0');
653
- return `${year}-${month}-${day} ${hours}:${minutes}`;
946
+ // 根据配置的日期格式进行替换
947
+ let formattedDate = configToUse.pushTemplate.dateFormat
948
+ .replace('YYYY', year.toString())
949
+ .replace('MM', month)
950
+ .replace('DD', day)
951
+ .replace('HH', hours)
952
+ .replace('mm', minutes);
953
+ return formattedDate;
654
954
  };
655
955
  const date = formatDate(post.date);
656
956
  // 链接强制编码
657
957
  const encodedLink = encodeURI(post.link);
658
958
  // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
659
- const atAllText = mention && config.mentionAll ? '@全体成员 ' : '';
959
+ const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
660
960
  // 只使用一个极简表情
661
961
  const messageType = isUpdate ? '📝' : '📰';
662
- // 构建核心消息内容,严格控制格式
663
- // 格式:[表情] [@全体] [时间] - [标题]
664
- // [链接]
665
- let message = `${messageType} ${atAllText}${date} - ${title}\n${encodedLink}`;
962
+ // 构建消息内容
963
+ let messageParts = [];
964
+ // 添加头部
965
+ messageParts.push(`${messageType} ${atAllText}${date} - ${title}`);
966
+ // 根据配置添加作者
967
+ if (configToUse.pushTemplate.showAuthor && post.author) {
968
+ messageParts.push(`👤 作者: ${post.author}`);
969
+ }
970
+ // 根据配置添加摘要
971
+ if (configToUse.pushTemplate.showExcerpt && post.excerpt) {
972
+ let excerpt = sanitizeContent(post.excerpt.rendered);
973
+ // 截断摘要长度
974
+ if (excerpt.length > 100) {
975
+ excerpt = excerpt.substring(0, 97) + '...';
976
+ }
977
+ if (excerpt) {
978
+ messageParts.push(`📄 摘要: ${excerpt}`);
979
+ }
980
+ }
981
+ // 根据配置添加链接
982
+ if (configToUse.pushTemplate.linkPosition === 'top') {
983
+ messageParts.unshift(`🔗 ${encodedLink}`);
984
+ }
985
+ else if (configToUse.pushTemplate.linkPosition === 'bottom') {
986
+ messageParts.push(`🔗 ${encodedLink}`);
987
+ }
988
+ // 合并消息部分
989
+ let message = messageParts.join('\n');
666
990
  // 双级长度控制:整体消息兜底最大长度
667
991
  if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
668
992
  message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
@@ -676,9 +1000,15 @@ function apply(ctx, config) {
676
1000
  // 直接返回纯字符串,跳过适配器复杂编码
677
1001
  return message;
678
1002
  }
679
- function formatUserMessage(user, mention = false) {
1003
+ function formatUserMessage(user, mention = false, siteConfig) {
1004
+ // 使用默认站点配置或传入的站点配置
1005
+ const configToUse = siteConfig || config.sites[0];
1006
+ if (!configToUse) {
1007
+ ctx.logger.error('无可用的站点配置');
1008
+ return '';
1009
+ }
680
1010
  // 生成缓存键
681
- const cacheKey = `user_${user.id}_${mention}_${config.mentionAll}`;
1011
+ const cacheKey = `user_${user.id}_${mention}_${configToUse.mentionAll}`;
682
1012
  // 检查缓存
683
1013
  const cached = formatCache.user.get(cacheKey);
684
1014
  if (cached && isCacheValid(cached.timestamp)) {
@@ -738,7 +1068,7 @@ function apply(ctx, config) {
738
1068
  ctx.logger.error(`处理用户 ${username} 日期时出错: ${error}`);
739
1069
  }
740
1070
  // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
741
- const atAllText = mention && config.mentionAll ? '@全体成员 ' : '';
1071
+ const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
742
1072
  // 只使用一个极简表情
743
1073
  const messageType = '👤';
744
1074
  // 构建核心消息内容,严格控制格式和换行
@@ -762,138 +1092,159 @@ function apply(ctx, config) {
762
1092
  const bot = getValidBot();
763
1093
  if (!bot) {
764
1094
  ctx.logger.error('没有可用的 Bot 实例');
1095
+ recordPushFailure('没有可用的 Bot 实例');
765
1096
  return;
766
1097
  }
767
1098
  // 修复 Bot 标识 undefined 问题
768
1099
  const botId = bot.selfId || 'unknown';
769
1100
  ctx.logger.info(`使用 bot ${bot.platform}:${botId} 进行推送`);
770
- // 推送新文章
771
- if (config.enableAutoPush) {
772
- const posts = await fetchLatestPosts();
773
- ctx.logger.info(`开始检查 ${posts.length} 篇文章是否需要推送`);
774
- if (posts.length > 0) {
775
- for (const post of posts) {
776
- ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
777
- ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
778
- // 检查文章是否已推送过(所有群聊共用一个标记)
779
- const postRecord = await getPostUpdateRecord(post.id);
780
- const hasPushed = !!postRecord;
781
- ctx.logger.info(`检查结果: 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
782
- if (!hasPushed) {
783
- // 推送到所有目标群聊
784
- const failedTargets = [];
785
- for (const target of config.targets) {
786
- try {
787
- ctx.logger.info(`正在处理目标: ${target}`);
788
- // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
789
- const stringTarget = target;
790
- const message = formatPostMessage(post, true, false);
791
- ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
792
- await bot.sendMessage(stringTarget, message);
793
- ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
1101
+ // 遍历所有站点
1102
+ for (const site of config.sites) {
1103
+ ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
1104
+ // 推送新文章
1105
+ if (site.enableAutoPush) {
1106
+ const posts = await fetchLatestPosts(site);
1107
+ ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
1108
+ if (posts.length > 0) {
1109
+ for (const post of posts) {
1110
+ ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
1111
+ ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
1112
+ // 检查文章是否已推送过(所有群聊共用一个标记)
1113
+ const postRecord = await getPostUpdateRecord(site.id, post.id);
1114
+ const hasPushed = !!postRecord;
1115
+ ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
1116
+ if (!hasPushed) {
1117
+ // 推送到该站点的所有目标群聊
1118
+ const failedTargets = [];
1119
+ for (const target of site.targets) {
1120
+ try {
1121
+ ctx.logger.info(`正在处理目标: ${target}`);
1122
+ // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
1123
+ const stringTarget = target;
1124
+ const message = formatPostMessage(post, site.mentionAll, false, site);
1125
+ ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
1126
+ await bot.sendMessage(stringTarget, message);
1127
+ ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
1128
+ }
1129
+ catch (error) {
1130
+ const errorMessage = error instanceof Error ? error.message : String(error);
1131
+ ctx.logger.error(`推送新文章到 ${target} 失败: ${errorMessage}`);
1132
+ ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1133
+ failedTargets.push(target);
1134
+ // 记录推送失败
1135
+ recordPushFailure(`推送消息失败到 ${target}: ${errorMessage}`);
1136
+ }
794
1137
  }
795
- catch (error) {
796
- ctx.logger.error(`推送新文章到 ${target} 失败: ${error}`);
797
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
798
- failedTargets.push(target);
1138
+ // 如果有失败的目标,添加到失败队列
1139
+ if (failedTargets.length > 0) {
1140
+ addToFailedQueue('post', post, failedTargets, site);
799
1141
  }
1142
+ // 标记文章已推送(所有群聊共用一个标记)
1143
+ await updatePostUpdateRecord(site.id, post.id, new Date(post.modified));
1144
+ ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
800
1145
  }
801
- // 如果有失败的目标,添加到失败队列
802
- if (failedTargets.length > 0) {
803
- addToFailedQueue('post', post, failedTargets);
1146
+ else {
1147
+ ctx.logger.info(`跳过推送: 站点 ${site.id} 文章 ${post.id} 已推送过,所有群聊将不再推送`);
804
1148
  }
805
- // 标记文章已推送(所有群聊共用一个标记)
806
- await updatePostUpdateRecord(post.id, new Date(post.modified));
807
- ctx.logger.info(`已标记文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
808
- }
809
- else {
810
- ctx.logger.info(`跳过推送: 文章 ${post.id} 已推送过,所有群聊将不再推送`);
811
1149
  }
812
1150
  }
813
1151
  }
814
- }
815
- // 推送文章更新
816
- if (config.enableUpdatePush) {
817
- const posts = await fetchUpdatedPosts();
818
- if (posts.length > 0) {
819
- for (const post of posts) {
820
- const updateRecord = await getPostUpdateRecord(post.id);
821
- const postModifiedDate = new Date(post.modified);
822
- // 检查文章是否有更新
823
- if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
824
- ctx.logger.info(`文章 ${post.id} 有更新,准备推送更新通知`);
825
- // 推送到所有目标群聊
826
- const failedTargets = [];
827
- for (const target of config.targets) {
828
- try {
829
- ctx.logger.info(`正在处理目标: ${target}`);
830
- const stringTarget = target;
831
- const message = formatPostMessage(post, true, true);
832
- ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
833
- await bot.sendMessage(stringTarget, message);
834
- ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
1152
+ // 推送文章更新
1153
+ if (site.enableUpdatePush) {
1154
+ const posts = await fetchUpdatedPosts(site);
1155
+ if (posts.length > 0) {
1156
+ for (const post of posts) {
1157
+ const updateRecord = await getPostUpdateRecord(site.id, post.id);
1158
+ const postModifiedDate = new Date(post.modified);
1159
+ // 检查文章是否有更新
1160
+ if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
1161
+ ctx.logger.info(`站点 ${site.id} 文章 ${post.id} 有更新,准备推送更新通知`);
1162
+ // 推送到该站点的所有目标群聊
1163
+ const failedTargets = [];
1164
+ for (const target of site.targets) {
1165
+ try {
1166
+ ctx.logger.info(`正在处理目标: ${target}`);
1167
+ const stringTarget = target;
1168
+ const message = formatPostMessage(post, site.mentionAll, true, site);
1169
+ ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
1170
+ await bot.sendMessage(stringTarget, message);
1171
+ ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
1172
+ }
1173
+ catch (error) {
1174
+ const errorMessage = error instanceof Error ? error.message : String(error);
1175
+ ctx.logger.error(`推送文章更新到 ${target} 失败: ${errorMessage}`);
1176
+ ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1177
+ failedTargets.push(target);
1178
+ // 记录推送失败
1179
+ recordPushFailure(`推送文章更新失败到 ${target}: ${errorMessage}`);
1180
+ }
835
1181
  }
836
- catch (error) {
837
- ctx.logger.error(`推送文章更新到 ${target} 失败: ${error}`);
838
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
839
- failedTargets.push(target);
1182
+ // 如果有失败的目标,添加到失败队列
1183
+ if (failedTargets.length > 0) {
1184
+ addToFailedQueue('update', post, failedTargets, site);
840
1185
  }
1186
+ // 更新文章更新记录(所有群聊共用一个标记)
1187
+ await updatePostUpdateRecord(site.id, post.id, postModifiedDate);
1188
+ ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
841
1189
  }
842
- // 如果有失败的目标,添加到失败队列
843
- if (failedTargets.length > 0) {
844
- addToFailedQueue('update', post, failedTargets);
845
- }
846
- // 更新文章更新记录(所有群聊共用一个标记)
847
- await updatePostUpdateRecord(post.id, postModifiedDate);
848
- ctx.logger.info(`已更新文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
849
1190
  }
850
1191
  }
851
1192
  }
852
- }
853
- // 推送新用户注册
854
- if (config.enableUserPush) {
855
- const users = await fetchLatestUsers();
856
- if (users.length > 0) {
857
- for (const user of users) {
858
- if (!(await isUserPushed(user.id))) {
859
- const failedTargets = [];
860
- for (const target of config.targets) {
861
- try {
862
- ctx.logger.info(`正在处理目标: ${target}`);
863
- // 直接使用原始目标字符串,与新文章推送逻辑保持一致
864
- const stringTarget = target;
865
- const message = formatUserMessage(user, true);
866
- ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
867
- await bot.sendMessage(stringTarget, message);
868
- ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
1193
+ // 推送新用户注册
1194
+ if (site.enableUserPush) {
1195
+ const users = await fetchLatestUsers(site);
1196
+ if (users.length > 0) {
1197
+ for (const user of users) {
1198
+ if (!(await isUserPushed(site.id, user.id))) {
1199
+ const failedTargets = [];
1200
+ for (const target of site.targets) {
1201
+ try {
1202
+ ctx.logger.info(`正在处理目标: ${target}`);
1203
+ // 直接使用原始目标字符串,与新文章推送逻辑保持一致
1204
+ const stringTarget = target;
1205
+ const message = formatUserMessage(user, site.mentionAll, site);
1206
+ ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
1207
+ await bot.sendMessage(stringTarget, message);
1208
+ ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
1209
+ }
1210
+ catch (error) {
1211
+ const errorMessage = error instanceof Error ? error.message : String(error);
1212
+ ctx.logger.error(`推送新用户到 ${target} 失败: ${errorMessage}`);
1213
+ ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1214
+ failedTargets.push(target);
1215
+ // 记录推送失败
1216
+ recordPushFailure(`推送新用户失败到 ${target}: ${errorMessage}`);
1217
+ }
869
1218
  }
870
- catch (error) {
871
- ctx.logger.error(`推送新用户到 ${target} 失败: ${error}`);
872
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
873
- failedTargets.push(target);
1219
+ // 如果有失败的目标,添加到失败队列
1220
+ if (failedTargets.length > 0) {
1221
+ addToFailedQueue('user', user, failedTargets, site);
874
1222
  }
1223
+ // 标记用户已推送
1224
+ await markUserAsPushed(site.id, user.id);
875
1225
  }
876
- // 如果有失败的目标,添加到失败队列
877
- if (failedTargets.length > 0) {
878
- addToFailedQueue('user', user, failedTargets);
879
- }
880
- // 标记用户已推送
881
- await markUserAsPushed(user.id);
882
1226
  }
883
1227
  }
884
1228
  }
885
1229
  }
886
1230
  }
887
- ctx.command('wordpress.latest', '查看最新文章')
888
- .action(async ({ session }) => {
889
- ctx.logger.info('命令 wordpress.latest 被调用');
890
- const posts = await fetchLatestPosts();
1231
+ ctx.command('wordpress.latest [siteId]', '查看最新文章,可选站点 ID')
1232
+ .action(async ({ session }, siteId) => {
1233
+ ctx.logger.info(`命令 wordpress.latest 被调用,站点 ID: ${siteId || '默认'}`);
1234
+ // 选择站点
1235
+ const targetSite = siteId
1236
+ ? config.sites.find(site => site.id === siteId)
1237
+ : config.sites[0];
1238
+ if (!targetSite) {
1239
+ return `未找到站点 ID: ${siteId}`;
1240
+ }
1241
+ const posts = await fetchLatestPosts(targetSite);
891
1242
  if (posts.length === 0) {
892
- ctx.logger.info('没有找到文章');
893
- return '暂无文章';
1243
+ ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
1244
+ return `站点 ${targetSite.name} 暂无文章`;
894
1245
  }
895
1246
  // 动态添加文章,确保消息长度不超过500字符
896
- let message = '📰 最新文章:\n';
1247
+ let message = `📰 ${targetSite.name} 最新文章:\n`;
897
1248
  let addedCount = 0;
898
1249
  for (const post of posts) {
899
1250
  const title = sanitizeContent(post.title.rendered);
@@ -919,15 +1270,22 @@ function apply(ctx, config) {
919
1270
  ctx.logger.info(`准备返回消息,长度: ${message.length},显示 ${addedCount}/${posts.length} 篇文章`);
920
1271
  return message;
921
1272
  });
922
- ctx.command('wordpress.list', '查看文章列表')
923
- .action(async () => {
924
- ctx.logger.info('命令 wordpress.list 被调用');
925
- const posts = await fetchLatestPosts();
1273
+ ctx.command('wordpress.list [siteId]', '查看文章列表,可选站点 ID')
1274
+ .action(async (_, siteId) => {
1275
+ ctx.logger.info(`命令 wordpress.list 被调用,站点 ID: ${siteId || '默认'}`);
1276
+ // 选择站点
1277
+ const targetSite = siteId
1278
+ ? config.sites.find(site => site.id === siteId)
1279
+ : config.sites[0];
1280
+ if (!targetSite) {
1281
+ return `未找到站点 ID: ${siteId}`;
1282
+ }
1283
+ const posts = await fetchLatestPosts(targetSite);
926
1284
  if (posts.length === 0) {
927
- return '暂无文章';
1285
+ return `站点 ${targetSite.name} 暂无文章`;
928
1286
  }
929
1287
  // 使用数组拼接消息,便于控制格式和长度
930
- const messageParts = ['📚 文章列表:'];
1288
+ const messageParts = [`📚 ${targetSite.name} 文章列表:`];
931
1289
  for (const post of posts) {
932
1290
  const title = sanitizeContent(post.title.rendered);
933
1291
  // 截断标题,避免单条过长
@@ -952,97 +1310,152 @@ function apply(ctx, config) {
952
1310
  await pushNewPosts();
953
1311
  return '已检查并推送最新文章';
954
1312
  });
955
- ctx.command('wordpress.status', '查看插件状态')
956
- .action(({ session }) => {
957
- ctx.logger.info('命令 wordpress.status 被调用');
1313
+ ctx.command('wordpress.status [siteId]', '查看插件状态,可选站点 ID')
1314
+ .action(({ session }, siteId) => {
1315
+ ctx.logger.info(`命令 wordpress.status 被调用,站点 ID: ${siteId || '所有'}`);
958
1316
  // 获取当前群号,如果有的话
959
1317
  const currentGroup = session?.channelId || '未知群聊';
960
- // 推送目标仅显示本群
961
- const targetText = `🎯 推送目标: ${currentGroup}`;
962
- // 使用数组拼接消息,便于控制格式和长度
963
- const messageParts = [
964
- '📊 WordPress 插件状态',
965
- `🌐 站点: ${config.wordpressUrl}`,
966
- `⏰ 间隔: ${config.interval / 1000} 秒`,
967
- targetText,
968
- `🔔 自动推送: ${config.enableAutoPush ? '开启' : '关闭'}`,
969
- `🔄 更新推送: ${config.enableUpdatePush ? '开启' : '关闭'}`,
970
- `👤 用户推送: ${config.enableUserPush ? '开启' : '关闭'}`,
971
- `📢 @全体: ${config.mentionAll ? '开启' : '关闭'}`,
972
- `📝 最多推送: ${config.maxArticles} 篇`
973
- ];
974
- // 合并为单行文本,统一换行符
975
- let message = messageParts.join('\n');
976
- // 长度验证,超过 500 字符则精简
977
- if (message.length > 500) {
978
- ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
979
- message = messageParts.slice(0, 5).join('\n') + '\n... 更多配置请查看完整状态';
1318
+ if (siteId) {
1319
+ // 显示单个站点状态
1320
+ const targetSite = config.sites.find(site => site.id === siteId);
1321
+ if (!targetSite) {
1322
+ return `未找到站点 ID: ${siteId}`;
1323
+ }
1324
+ // 使用数组拼接消息,便于控制格式和长度
1325
+ const messageParts = [
1326
+ `📊 WordPress 插件状态 - ${targetSite.name}`,
1327
+ `🌐 站点: ${targetSite.url}`,
1328
+ `⏰ 间隔: ${targetSite.interval / 1000} 秒`,
1329
+ `🎯 推送目标: ${targetSite.targets.join(', ') || ''}`,
1330
+ `🔔 自动推送: ${targetSite.enableAutoPush ? '开启' : '关闭'}`,
1331
+ `🔄 更新推送: ${targetSite.enableUpdatePush ? '开启' : '关闭'}`,
1332
+ `👤 用户推送: ${targetSite.enableUserPush ? '开启' : '关闭'}`,
1333
+ `📢 @全体: ${targetSite.mentionAll ? '开启' : '关闭'}`,
1334
+ `📝 最多推送: ${targetSite.maxArticles} 篇`
1335
+ ];
1336
+ // 合并为单行文本,统一换行符
1337
+ let message = messageParts.join('\n');
1338
+ // 长度验证,超过 500 字符则精简
1339
+ if (message.length > 500) {
1340
+ ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1341
+ message = messageParts.slice(0, 5).join('\n') + '\n... 更多配置请查看完整状态';
1342
+ }
1343
+ ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1344
+ // 直接返回纯字符串,跳过适配器复杂编码
1345
+ return message;
1346
+ }
1347
+ else {
1348
+ // 显示所有站点状态
1349
+ let message = '📊 WordPress 插件状态\n\n';
1350
+ config.sites.forEach((site, index) => {
1351
+ const siteMessage = `站点 ${index + 1}: ${site.name} (${site.id})\n` +
1352
+ `🌐 URL: ${site.url}\n` +
1353
+ `⏰ 间隔: ${site.interval / 1000} 秒\n` +
1354
+ `🎯 目标: ${site.targets.join(', ') || '无'}\n` +
1355
+ `🔔 自动: ${site.enableAutoPush ? '开启' : '关闭'}\n` +
1356
+ `🔄 更新: ${site.enableUpdatePush ? '开启' : '关闭'}\n` +
1357
+ `👤 用户: ${site.enableUserPush ? '开启' : '关闭'}\n\n`;
1358
+ // 检查添加后是否超过500字符
1359
+ if (message.length + siteMessage.length > 500) {
1360
+ message += '... 更多站点请使用站点 ID 查看详细状态';
1361
+ return false;
1362
+ }
1363
+ message += siteMessage;
1364
+ });
1365
+ ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1366
+ return message;
980
1367
  }
981
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
982
- // 直接返回纯字符串,跳过适配器复杂编码
983
- return message;
984
1368
  });
985
- ctx.command('wordpress.toggle-update', '切换文章更新推送开关')
986
- .action(async ({ session }) => {
1369
+ ctx.command('wordpress.site.toggle-update <siteId>', '切换指定站点的文章更新推送开关')
1370
+ .action(async ({ session }, siteId) => {
987
1371
  // 检查权限
988
1372
  const authResult = checkSuperAdmin(session);
989
1373
  if (!authResult.valid) {
990
1374
  return authResult.message;
991
1375
  }
992
- ctx.logger.info('命令 wordpress.toggle-update 被调用');
993
- config.enableUpdatePush = !config.enableUpdatePush;
994
- await saveConfig('enableUpdatePush', config.enableUpdatePush);
995
- return `文章更新推送已${config.enableUpdatePush ? '开启' : '关闭'}`;
1376
+ ctx.logger.info(`命令 wordpress.site.toggle-update 被调用,站点 ID: ${siteId}`);
1377
+ // 查找站点
1378
+ const site = config.sites.find(s => s.id === siteId);
1379
+ if (!site) {
1380
+ return `未找到站点 ID: ${siteId}`;
1381
+ }
1382
+ // 切换开关
1383
+ site.enableUpdatePush = !site.enableUpdatePush;
1384
+ await saveConfig('sites', config.sites);
1385
+ return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
996
1386
  });
997
- ctx.command('wordpress.toggle-user', '切换新用户注册推送开关')
998
- .action(async ({ session }) => {
1387
+ ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
1388
+ .action(async ({ session }, siteId) => {
999
1389
  // 检查权限
1000
1390
  const authResult = checkSuperAdmin(session);
1001
1391
  if (!authResult.valid) {
1002
1392
  return authResult.message;
1003
1393
  }
1004
- ctx.logger.info('命令 wordpress.toggle-user 被调用');
1005
- config.enableUserPush = !config.enableUserPush;
1006
- await saveConfig('enableUserPush', config.enableUserPush);
1007
- return `新用户注册推送已${config.enableUserPush ? '开启' : '关闭'}`;
1394
+ ctx.logger.info(`命令 wordpress.site.toggle-user 被调用,站点 ID: ${siteId}`);
1395
+ // 查找站点
1396
+ const site = config.sites.find(s => s.id === siteId);
1397
+ if (!site) {
1398
+ return `未找到站点 ID: ${siteId}`;
1399
+ }
1400
+ // 切换开关
1401
+ site.enableUserPush = !site.enableUserPush;
1402
+ await saveConfig('sites', config.sites);
1403
+ return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
1008
1404
  });
1009
- ctx.command('wordpress.toggle', '切换自动推送开关')
1010
- .action(async ({ session }) => {
1405
+ ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
1406
+ .action(async ({ session }, siteId) => {
1011
1407
  // 检查权限
1012
1408
  const authResult = checkSuperAdmin(session);
1013
1409
  if (!authResult.valid) {
1014
1410
  return authResult.message;
1015
1411
  }
1016
- ctx.logger.info('命令 wordpress.toggle 被调用');
1017
- config.enableAutoPush = !config.enableAutoPush;
1018
- await saveConfig('enableAutoPush', config.enableAutoPush);
1019
- return `自动推送已${config.enableAutoPush ? '开启' : '关闭'}`;
1412
+ ctx.logger.info(`命令 wordpress.site.toggle 被调用,站点 ID: ${siteId}`);
1413
+ // 查找站点
1414
+ const site = config.sites.find(s => s.id === siteId);
1415
+ if (!site) {
1416
+ return `未找到站点 ID: ${siteId}`;
1417
+ }
1418
+ // 切换开关
1419
+ site.enableAutoPush = !site.enableAutoPush;
1420
+ await saveConfig('sites', config.sites);
1421
+ return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
1020
1422
  });
1021
- ctx.command('wordpress.mention', '切换 @全体成员 开关')
1022
- .action(async ({ session }) => {
1423
+ ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
1424
+ .action(async ({ session }, siteId) => {
1023
1425
  // 检查权限
1024
1426
  const authResult = checkSuperAdmin(session);
1025
1427
  if (!authResult.valid) {
1026
1428
  return authResult.message;
1027
1429
  }
1028
- ctx.logger.info('命令 wordpress.mention 被调用');
1029
- config.mentionAll = !config.mentionAll;
1030
- await saveConfig('mentionAll', config.mentionAll);
1031
- return `@全体 已${config.mentionAll ? '开启' : '关闭'}`;
1430
+ ctx.logger.info(`命令 wordpress.site.mention 被调用,站点 ID: ${siteId}`);
1431
+ // 查找站点
1432
+ const site = config.sites.find(s => s.id === siteId);
1433
+ if (!site) {
1434
+ return `未找到站点 ID: ${siteId}`;
1435
+ }
1436
+ // 切换开关
1437
+ site.mentionAll = !site.mentionAll;
1438
+ await saveConfig('sites', config.sites);
1439
+ return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
1032
1440
  });
1033
- ctx.command('wordpress.set-url <url>', '修改 WordPress 站点地址')
1034
- .action(async ({ session }, url) => {
1441
+ ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
1442
+ .action(async ({ session }, siteId, url) => {
1035
1443
  // 检查权限
1036
1444
  const authResult = checkSuperAdmin(session);
1037
1445
  if (!authResult.valid) {
1038
1446
  return authResult.message;
1039
1447
  }
1040
- ctx.logger.info(`命令 wordpress.set-url 被调用,调用者:${authResult.userId},新地址:${url}`);
1448
+ ctx.logger.info(`命令 wordpress.site.set-url 被调用,站点 ID: ${siteId},新地址:${url}`);
1449
+ // 查找站点
1450
+ const site = config.sites.find(s => s.id === siteId);
1451
+ if (!site) {
1452
+ return `未找到站点 ID: ${siteId}`;
1453
+ }
1041
1454
  // 修改站点地址
1042
- config.wordpressUrl = url;
1043
- await saveConfig('wordpressUrl', config.wordpressUrl);
1044
- ctx.logger.info(`站点地址已修改为:${url}`);
1045
- return `WordPress 站点地址已修改为:${url}`;
1455
+ site.url = url;
1456
+ await saveConfig('sites', config.sites);
1457
+ ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
1458
+ return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
1046
1459
  });
1047
1460
  ctx.command('wordpress.pushed', '查看已推送的文章列表')
1048
1461
  .action(async () => {
@@ -1138,8 +1551,90 @@ function apply(ctx, config) {
1138
1551
  catch (error) {
1139
1552
  ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
1140
1553
  }
1141
- ctx.logger.info(`已清理 ${result} 条 ${daysToKeep} 天前的推送记录`);
1142
- return `已清理 ${result} 条 ${daysToKeep} 天前的推送记录`;
1554
+ });
1555
+ ctx.command('wordpress.stats', '查看插件运行指标统计')
1556
+ .action(() => {
1557
+ ctx.logger.info('命令 wordpress.stats 被调用');
1558
+ // 计算 API 调用成功率
1559
+ const apiSuccessRate = runtimeStats.apiCallCount > 0
1560
+ ? (runtimeStats.apiSuccessCount / runtimeStats.apiCallCount * 100).toFixed(2)
1561
+ : '0.00';
1562
+ // 计算推送成功率
1563
+ const pushSuccessRate = (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) > 0
1564
+ ? (runtimeStats.pushSuccessCount / (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) * 100).toFixed(2)
1565
+ : '0.00';
1566
+ // 计算统计时间范围
1567
+ const startTime = new Date(runtimeStats.lastResetTime);
1568
+ const formattedStartTime = `${startTime.getFullYear()}-${String(startTime.getMonth() + 1).padStart(2, '0')}-${String(startTime.getDate()).padStart(2, '0')} ${String(startTime.getHours()).padStart(2, '0')}:${String(startTime.getMinutes()).padStart(2, '0')}`;
1569
+ // 构建统计消息
1570
+ const messageParts = [
1571
+ '📊 WordPress 插件运行统计',
1572
+ `📅 统计开始时间: ${formattedStartTime}`,
1573
+ `📈 API 调用: ${runtimeStats.apiCallCount} 次`,
1574
+ `✅ API 成功: ${runtimeStats.apiSuccessCount} 次`,
1575
+ `❌ API 失败: ${runtimeStats.apiFailureCount} 次`,
1576
+ `📊 API 成功率: ${apiSuccessRate}%`,
1577
+ `📤 推送成功: ${runtimeStats.pushSuccessCount} 次`,
1578
+ `📥 推送失败: ${runtimeStats.pushFailureCount} 次`,
1579
+ `📊 推送成功率: ${pushSuccessRate}%`
1580
+ ];
1581
+ let message = messageParts.join('\n');
1582
+ // 长度验证,超过 500 字符则精简
1583
+ if (message.length > 500) {
1584
+ ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1585
+ message = messageParts.slice(0, 6).join('\n') + '\n... 更多统计信息请查看完整数据';
1586
+ }
1587
+ return message;
1588
+ });
1589
+ ctx.command('wordpress.health', '查看插件健康状态')
1590
+ .action(async () => {
1591
+ ctx.logger.info('命令 wordpress.health 被调用');
1592
+ // 检查数据库连接
1593
+ const dbConnected = await checkDatabaseConnection();
1594
+ // 检查 Bot 在线状态
1595
+ const bot = getValidBot();
1596
+ const botOnline = !!bot;
1597
+ // 检查 API 可达性(尝试访问第一个站点的 API)
1598
+ let apiReachable = false;
1599
+ if (config.sites.length > 0) {
1600
+ const firstSite = config.sites[0];
1601
+ try {
1602
+ const url = `${firstSite.url}/wp-json/wp/v2/posts?per_page=1`;
1603
+ const response = await ctx.http.get(url, {
1604
+ timeout: 5000
1605
+ });
1606
+ apiReachable = true;
1607
+ }
1608
+ catch (error) {
1609
+ ctx.logger.warn(`API 可达性检查失败: ${error}`);
1610
+ }
1611
+ }
1612
+ // 构建健康状态消息
1613
+ const messageParts = [
1614
+ '🏥 WordPress 插件健康状态',
1615
+ `🗄️ 数据库连接: ${dbConnected ? '✅ 正常' : '❌ 异常'}`,
1616
+ `🤖 Bot 在线状态: ${botOnline ? '✅ 在线' : '❌ 离线'}`,
1617
+ `🌐 API 可达性: ${apiReachable ? '✅ 可达' : '❌ 不可达'}`
1618
+ ];
1619
+ // 添加站点状态
1620
+ if (config.sites.length > 0) {
1621
+ messageParts.push('\n📋 站点状态:');
1622
+ config.sites.forEach((site, index) => {
1623
+ if (index < 3) { // 只显示前3个站点
1624
+ messageParts.push(`${index + 1}. ${site.name}: ${site.enableAutoPush ? '✅ 启用' : '❌ 禁用'}`);
1625
+ }
1626
+ });
1627
+ if (config.sites.length > 3) {
1628
+ messageParts.push(`... 共 ${config.sites.length} 个站点`);
1629
+ }
1630
+ }
1631
+ let message = messageParts.join('\n');
1632
+ // 长度验证,超过 500 字符则精简
1633
+ if (message.length > 500) {
1634
+ ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1635
+ message = messageParts.slice(0, 5).join('\n') + '\n... 更多健康状态信息请查看完整数据';
1636
+ }
1637
+ return message;
1143
1638
  });
1144
1639
  ctx.command('wordpress', 'WordPress 推送插件菜单')
1145
1640
  .action(() => {
@@ -1164,15 +1659,18 @@ function apply(ctx, config) {
1164
1659
  ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1165
1660
  return message;
1166
1661
  });
1167
- ctx.setInterval(() => {
1168
- try {
1169
- pushNewPosts();
1170
- }
1171
- catch (error) {
1172
- const errorMessage = error instanceof Error ? error.message : String(error);
1173
- ctx.logger.error(`定时任务执行失败:${errorMessage}`);
1174
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1175
- ctx.logger.warn('定时任务将在下一个周期继续执行');
1176
- }
1177
- }, config.interval);
1662
+ // 为每个站点设置独立的定时任务
1663
+ config.sites.forEach(site => {
1664
+ ctx.setInterval(() => {
1665
+ try {
1666
+ pushNewPosts();
1667
+ }
1668
+ catch (error) {
1669
+ const errorMessage = error instanceof Error ? error.message : String(error);
1670
+ ctx.logger.error(`站点 ${site.id} 定时任务执行失败:${errorMessage}`);
1671
+ ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1672
+ ctx.logger.warn('定时任务将在下一个周期继续执行');
1673
+ }
1674
+ }, site.interval);
1675
+ });
1178
1676
  }