koishi-plugin-wordpress-notifier 2.8.0 → 2.8.2

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) {
@@ -114,13 +219,24 @@ function apply(ctx, config) {
114
219
  ctx.logger.info('持久化配置加载完成');
115
220
  }
116
221
  catch (error) {
117
- ctx.logger.error(`加载持久化配置失败: ${error}`);
222
+ const errorMessage = error instanceof Error ? error.message : String(error);
223
+ ctx.logger.error(`加载持久化配置失败: ${errorMessage}`);
224
+ ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
225
+ // 发生错误时,不抛出异常,确保插件继续运行
226
+ // 使用默认配置
227
+ ctx.logger.warn('使用默认配置继续运行,持久化配置将在下次保存时重新创建');
118
228
  }
119
229
  }
120
230
  // 保存配置到数据库
121
231
  async function saveConfig(key, value) {
122
232
  try {
123
233
  ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
234
+ // 确保数据库连接正常
235
+ const connected = await ensureDatabaseConnection();
236
+ if (!connected) {
237
+ ctx.logger.warn('数据库连接异常,跳过保存配置');
238
+ return;
239
+ }
124
240
  // 检查配置是否已存在
125
241
  const existingRecords = await ctx.database.get('wordpress_config', { key });
126
242
  if (existingRecords.length > 0) {
@@ -207,14 +323,15 @@ function apply(ctx, config) {
207
323
  const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
208
324
  const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
209
325
  // 添加到失败队列
210
- function addToFailedQueue(type, data, targets) {
326
+ function addToFailedQueue(type, data, targets, siteConfig) {
211
327
  const item = {
212
328
  id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
213
329
  type,
214
330
  data,
215
331
  targets,
216
332
  retries: 0,
217
- createdAt: new Date()
333
+ createdAt: new Date(),
334
+ siteConfig
218
335
  };
219
336
  failedPushQueue.push(item);
220
337
  ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
@@ -243,10 +360,10 @@ function apply(ctx, config) {
243
360
  // 根据类型格式化消息
244
361
  let message;
245
362
  if (item.type === 'post' || item.type === 'update') {
246
- message = formatPostMessage(item.data, true, item.type === 'update');
363
+ message = formatPostMessage(item.data, true, item.type === 'update', item.siteConfig);
247
364
  }
248
365
  else {
249
- message = formatUserMessage(item.data, true);
366
+ message = formatUserMessage(item.data, true, item.siteConfig);
250
367
  }
251
368
  // 推送到所有目标
252
369
  for (const target of item.targets) {
@@ -292,6 +409,106 @@ function apply(ctx, config) {
292
409
  // 权限检查通过,继续执行命令
293
410
  return { valid: true, userId };
294
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
+ }
295
512
  // 检查数据库表结构的函数
296
513
  async function checkAndFixTableStructure() {
297
514
  try {
@@ -308,7 +525,27 @@ function apply(ctx, config) {
308
525
  }
309
526
  catch (error) {
310
527
  ctx.logger.warn(`wordpress_post_updates 表可能结构不正确,尝试重新初始化...`);
311
- // 这里可以添加删除旧表的逻辑,但为了安全起见,我们只记录错误
528
+ // 尝试删除旧表并重新初始化
529
+ try {
530
+ ctx.logger.info('尝试删除旧的 wordpress_post_updates 表...');
531
+ await ctx.database.drop('wordpress_post_updates');
532
+ ctx.logger.info('旧表删除成功,重新初始化表结构...');
533
+ // 重新扩展模型
534
+ ctx.model.extend('wordpress_post_updates', {
535
+ id: 'integer',
536
+ postId: 'integer',
537
+ lastModified: 'timestamp',
538
+ pushedAt: 'timestamp'
539
+ }, {
540
+ primary: 'id',
541
+ autoInc: true,
542
+ unique: ['postId']
543
+ });
544
+ ctx.logger.info('wordpress_post_updates 表重新初始化成功');
545
+ }
546
+ catch (dropError) {
547
+ ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
548
+ }
312
549
  }
313
550
  try {
314
551
  ctx.logger.info('验证 wordpress_user_registrations 表结构...');
@@ -319,7 +556,57 @@ function apply(ctx, config) {
319
556
  }
320
557
  catch (error) {
321
558
  ctx.logger.warn(`wordpress_user_registrations 表可能结构不正确,尝试重新初始化...`);
322
- // 这里可以添加删除旧表的逻辑,但为了安全起见,我们只记录错误
559
+ // 尝试删除旧表并重新初始化
560
+ try {
561
+ ctx.logger.info('尝试删除旧的 wordpress_user_registrations 表...');
562
+ await ctx.database.drop('wordpress_user_registrations');
563
+ ctx.logger.info('旧表删除成功,重新初始化表结构...');
564
+ // 重新扩展模型
565
+ ctx.model.extend('wordpress_user_registrations', {
566
+ id: 'integer',
567
+ userId: 'integer',
568
+ pushedAt: 'timestamp'
569
+ }, {
570
+ primary: 'id',
571
+ autoInc: true,
572
+ unique: ['userId']
573
+ });
574
+ ctx.logger.info('wordpress_user_registrations 表重新初始化成功');
575
+ }
576
+ catch (dropError) {
577
+ ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
578
+ }
579
+ }
580
+ try {
581
+ ctx.logger.info('验证 wordpress_config 表结构...');
582
+ const testConfig = await ctx.database.get('wordpress_config', {}, {
583
+ limit: 1
584
+ });
585
+ ctx.logger.info(`wordpress_config 表验证成功,现有记录数:${testConfig.length}`);
586
+ }
587
+ catch (error) {
588
+ ctx.logger.warn(`wordpress_config 表可能结构不正确,尝试重新初始化...`);
589
+ // 尝试删除旧表并重新初始化
590
+ try {
591
+ ctx.logger.info('尝试删除旧的 wordpress_config 表...');
592
+ await ctx.database.drop('wordpress_config');
593
+ ctx.logger.info('旧表删除成功,重新初始化表结构...');
594
+ // 重新扩展模型
595
+ ctx.model.extend('wordpress_config', {
596
+ id: 'integer',
597
+ key: 'string',
598
+ value: 'string',
599
+ updatedAt: 'timestamp'
600
+ }, {
601
+ primary: 'id',
602
+ autoInc: true,
603
+ unique: ['key']
604
+ });
605
+ ctx.logger.info('wordpress_config 表重新初始化成功');
606
+ }
607
+ catch (dropError) {
608
+ ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
609
+ }
323
610
  }
324
611
  ctx.logger.info('表结构检查和修复完成');
325
612
  }
@@ -338,6 +625,16 @@ function apply(ctx, config) {
338
625
  let retries = 0;
339
626
  while (retries <= maxRetries) {
340
627
  try {
628
+ // API 请求频率控制
629
+ const now = Date.now();
630
+ const timeSinceLastRequest = now - lastRequestTime;
631
+ if (timeSinceLastRequest < CONSTANTS.API_RATE_LIMIT) {
632
+ const waitTime = CONSTANTS.API_RATE_LIMIT - timeSinceLastRequest;
633
+ ctx.logger.info(`API 请求频率限制,等待 ${waitTime}ms 后继续请求`);
634
+ await new Promise(resolve => setTimeout(resolve, waitTime));
635
+ }
636
+ // 更新上次请求时间
637
+ lastRequestTime = Date.now();
341
638
  ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
342
639
  const response = await ctx.http.get(url, requestConfig);
343
640
  ctx.logger.info(`HTTP请求成功: ${url}`);
@@ -349,6 +646,8 @@ function apply(ctx, config) {
349
646
  retries++;
350
647
  if (retries > maxRetries) {
351
648
  ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
649
+ // 记录推送失败
650
+ recordPushFailure(`API请求失败: ${errorMessage}`);
352
651
  return null;
353
652
  }
354
653
  // 重试前等待
@@ -357,16 +656,16 @@ function apply(ctx, config) {
357
656
  }
358
657
  return null;
359
658
  }
360
- async function fetchLatestPosts() {
659
+ async function fetchLatestPosts(site) {
361
660
  try {
362
- const url = `${config.wordpressUrl}/wp-json/wp/v2/posts?per_page=${config.maxArticles}&orderby=date&order=desc`;
363
- ctx.logger.info(`正在获取文章: ${url}`);
661
+ const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=date&order=desc`;
662
+ ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的文章: ${url}`);
364
663
  // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
365
664
  const requestConfig = {};
366
- if (config.username && config.applicationPassword) {
665
+ if (site.username && site.applicationPassword) {
367
666
  // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
368
- const username = config.username;
369
- const password = config.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
667
+ const username = site.username;
668
+ const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
370
669
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
371
670
  requestConfig.headers = {
372
671
  Authorization: `Basic ${auth}`
@@ -374,30 +673,33 @@ function apply(ctx, config) {
374
673
  }
375
674
  const response = await httpRequest(url, requestConfig);
376
675
  if (!response) {
377
- ctx.logger.error(`获取 WordPress 文章失败,已达到最大重试次数`);
676
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`);
378
677
  return [];
379
678
  }
380
- ctx.logger.info(`成功获取 ${response.length} 篇文章`);
679
+ ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
381
680
  return response;
382
681
  }
383
682
  catch (error) {
384
- ctx.logger.error(`获取 WordPress 文章失败: ${error}`);
683
+ const errorMessage = error instanceof Error ? error.message : String(error);
684
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`);
685
+ // 记录失败
686
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
385
687
  return [];
386
688
  }
387
689
  }
388
- async function fetchLatestUsers() {
690
+ async function fetchLatestUsers(site) {
389
691
  try {
390
692
  // 修改API请求,添加_fields参数明确请求注册日期字段
391
693
  // WordPress REST API 默认可能不会返回注册日期,需要明确请求
392
694
  const fields = 'id,name,slug,date,date_registered,registered_date,created_at,registeredAt,email,roles,url,description,link,avatar_urls';
393
- const url = `${config.wordpressUrl}/wp-json/wp/v2/users?per_page=${config.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
394
- ctx.logger.info(`正在获取用户: ${url}`);
695
+ const url = `${site.url}/wp-json/wp/v2/users?per_page=${site.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
696
+ ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的用户: ${url}`);
395
697
  // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
396
698
  const requestConfig = {};
397
- if (config.username && config.applicationPassword) {
699
+ if (site.username && site.applicationPassword) {
398
700
  // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
399
- const username = config.username;
400
- const password = config.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
701
+ const username = site.username;
702
+ const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
401
703
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
402
704
  requestConfig.headers = {
403
705
  Authorization: `Basic ${auth}`
@@ -405,12 +707,14 @@ function apply(ctx, config) {
405
707
  }
406
708
  const response = await httpRequest(url, requestConfig);
407
709
  if (!response) {
408
- ctx.logger.error(`获取 WordPress 用户失败,已达到最大重试次数`);
409
- ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在插件配置中添加 WordPress 用户名和应用程序密码`);
710
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`);
711
+ ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
712
+ // 记录失败
713
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
410
714
  // 返回空数组,确保插件继续运行
411
715
  return [];
412
716
  }
413
- ctx.logger.info(`成功获取 ${response.length} 位用户`);
717
+ ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
414
718
  // 添加调试日志,查看API返回的实际数据结构
415
719
  if (response.length > 0) {
416
720
  ctx.logger.info(`用户数据示例: ${JSON.stringify(response[0], null, 2)}`);
@@ -421,22 +725,25 @@ function apply(ctx, config) {
421
725
  return response;
422
726
  }
423
727
  catch (error) {
424
- ctx.logger.error(`获取 WordPress 用户失败: ${error}`);
425
- ctx.logger.error(`WordPress REST APIusers 端点需要认证才能访问,请在插件配置中添加 WordPress 用户名和应用程序密码`);
728
+ const errorMessage = error instanceof Error ? error.message : String(error);
729
+ ctx.logger.error(`获取站点 ${site.id} (${site.name})WordPress 用户失败: ${errorMessage}`);
730
+ ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
731
+ // 记录失败
732
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
426
733
  // 返回空数组,确保插件继续运行
427
734
  return [];
428
735
  }
429
736
  }
430
- async function fetchUpdatedPosts() {
737
+ async function fetchUpdatedPosts(site) {
431
738
  try {
432
- const url = `${config.wordpressUrl}/wp-json/wp/v2/posts?per_page=${config.maxArticles}&orderby=modified&order=desc`;
433
- ctx.logger.info(`正在获取更新文章: ${url}`);
739
+ const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=modified&order=desc`;
740
+ ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的更新文章: ${url}`);
434
741
  // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
435
742
  const requestConfig = {};
436
- if (config.username && config.applicationPassword) {
743
+ if (site.username && site.applicationPassword) {
437
744
  // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
438
- const username = config.username;
439
- const password = config.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
745
+ const username = site.username;
746
+ const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
440
747
  const auth = Buffer.from(`${username}:${password}`).toString('base64');
441
748
  requestConfig.headers = {
442
749
  Authorization: `Basic ${auth}`
@@ -444,23 +751,34 @@ function apply(ctx, config) {
444
751
  }
445
752
  const response = await httpRequest(url, requestConfig);
446
753
  if (!response) {
447
- ctx.logger.error(`获取 WordPress 更新文章失败,已达到最大重试次数`);
754
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`);
755
+ // 记录失败
756
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
448
757
  return [];
449
758
  }
450
- ctx.logger.info(`成功获取 ${response.length} 篇更新文章`);
759
+ ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
451
760
  return response;
452
761
  }
453
762
  catch (error) {
454
- ctx.logger.error(`获取 WordPress 更新文章失败: ${error}`);
763
+ const errorMessage = error instanceof Error ? error.message : String(error);
764
+ ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`);
765
+ // 记录失败
766
+ recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
455
767
  return [];
456
768
  }
457
769
  }
458
- async function isUserPushed(userId) {
770
+ async function isUserPushed(siteId, userId) {
459
771
  try {
460
- ctx.logger.info(`检查用户是否已推送,用户 ID: ${userId}`);
461
- const record = await ctx.database.get('wordpress_user_registrations', { userId });
772
+ // 确保数据库连接正常
773
+ const connected = await ensureDatabaseConnection();
774
+ if (!connected) {
775
+ ctx.logger.warn('数据库连接异常,跳过检查用户推送记录');
776
+ return false;
777
+ }
778
+ ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
779
+ const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId });
462
780
  const result = record.length > 0;
463
- ctx.logger.info(`检查结果:用户 ${userId} 已推送:${result ? '是' : '否'}`);
781
+ ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
464
782
  return result;
465
783
  }
466
784
  catch (error) {
@@ -471,12 +789,18 @@ function apply(ctx, config) {
471
789
  return false;
472
790
  }
473
791
  }
474
- async function getPostUpdateRecord(postId) {
792
+ async function getPostUpdateRecord(siteId, postId) {
475
793
  try {
476
- ctx.logger.info(`获取文章更新记录,文章 ID: ${postId}`);
477
- const records = await ctx.database.get('wordpress_post_updates', { postId });
794
+ // 确保数据库连接正常
795
+ const connected = await ensureDatabaseConnection();
796
+ if (!connected) {
797
+ ctx.logger.warn('数据库连接异常,跳过获取文章更新记录');
798
+ return null;
799
+ }
800
+ ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
801
+ const records = await ctx.database.get('wordpress_post_updates', { siteId, postId });
478
802
  const result = records.length > 0 ? records[0] : null;
479
- ctx.logger.info(`获取结果:文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
803
+ ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
480
804
  return result;
481
805
  }
482
806
  catch (error) {
@@ -487,72 +811,116 @@ function apply(ctx, config) {
487
811
  return null;
488
812
  }
489
813
  }
490
- async function markUserAsPushed(userId) {
814
+ async function markUserAsPushed(siteId, userId) {
491
815
  try {
492
- ctx.logger.info(`开始标记用户已推送,用户 ID: ${userId}`);
816
+ // 确保数据库连接正常
817
+ const connected = await ensureDatabaseConnection();
818
+ if (!connected) {
819
+ ctx.logger.warn('数据库连接异常,跳过标记用户推送记录');
820
+ return;
821
+ }
822
+ ctx.logger.info(`开始标记用户已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
493
823
  // 创建新记录,不手动指定id,让数据库自动生成
494
824
  const newRecord = {
825
+ siteId,
495
826
  userId,
496
827
  pushedAt: new Date()
497
828
  };
498
829
  ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
499
830
  await ctx.database.create('wordpress_user_registrations', newRecord);
500
- ctx.logger.info(`已成功标记用户 ${userId} 为已推送`);
831
+ ctx.logger.info(`已成功标记站点 ${siteId} 用户 ${userId} 为已推送`);
501
832
  }
502
833
  catch (error) {
503
834
  const errorMessage = error instanceof Error ? error.message : String(error);
504
835
  if (errorMessage.includes('UNIQUE constraint failed')) {
505
- ctx.logger.warn(`用户推送记录已存在,跳过重复插入:用户 ${userId}`);
836
+ ctx.logger.warn(`用户推送记录已存在,跳过重复插入:站点 ${siteId} 用户 ${userId}`);
506
837
  ctx.logger.warn(`完整错误信息:${errorMessage}`);
507
838
  }
508
839
  else {
509
840
  ctx.logger.error(`标记用户推送记录失败:${errorMessage}`);
510
841
  ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
511
- ctx.logger.error(`插入参数:userId=${userId}`);
842
+ ctx.logger.error(`插入参数:siteId=${siteId}, userId=${userId}`);
512
843
  // 非约束冲突错误,不抛出,确保插件继续运行
513
844
  }
514
845
  }
515
846
  }
516
- async function updatePostUpdateRecord(postId, modifiedDate) {
847
+ async function updatePostUpdateRecord(siteId, postId, modifiedDate) {
517
848
  try {
518
- ctx.logger.info(`开始更新文章更新记录,文章 ID: ${postId},修改时间: ${modifiedDate}`);
519
- const record = await getPostUpdateRecord(postId);
849
+ // 确保数据库连接正常
850
+ const connected = await ensureDatabaseConnection();
851
+ if (!connected) {
852
+ ctx.logger.warn('数据库连接异常,跳过更新文章更新记录');
853
+ return;
854
+ }
855
+ ctx.logger.info(`开始更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId},修改时间: ${modifiedDate}`);
856
+ const record = await getPostUpdateRecord(siteId, postId);
520
857
  if (record) {
521
- ctx.logger.info(`发现现有记录,文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
858
+ ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
522
859
  // Koishi database API 不支持 update 方法,使用 remove + create 代替
523
- await ctx.database.remove('wordpress_post_updates', { postId });
524
- ctx.logger.info(`已删除旧记录,文章 ID: ${postId}`);
860
+ await ctx.database.remove('wordpress_post_updates', { siteId, postId });
861
+ ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
525
862
  }
526
- // 创建新记录
863
+ // 创建新记录,不指定 id 字段,让数据库自动生成
527
864
  const newRecord = {
865
+ siteId,
528
866
  postId,
529
867
  lastModified: modifiedDate,
530
868
  pushedAt: new Date()
531
869
  };
532
- ctx.logger.info(`准备创建新记录,文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
870
+ ctx.logger.info(`准备创建新记录,站点 ID: ${siteId},文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
533
871
  await ctx.database.create('wordpress_post_updates', newRecord);
534
- ctx.logger.info(`已成功更新文章更新记录,文章 ID: ${postId}`);
872
+ ctx.logger.info(`已成功更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
535
873
  }
536
874
  catch (error) {
537
875
  const errorMessage = error instanceof Error ? error.message : String(error);
538
- ctx.logger.error(`更新文章更新记录失败,文章 ID: ${postId}`);
876
+ ctx.logger.error(`更新文章更新记录失败,站点 ID: ${siteId},文章 ID: ${postId}`);
539
877
  ctx.logger.error(`错误信息: ${errorMessage}`);
540
878
  ctx.logger.error(`错误栈: ${error instanceof Error ? error.stack : '无'}`);
541
- throw error;
879
+ // 不再抛出错误,确保推送流程继续运行
880
+ // 发生错误时,默认返回,避免阻塞推送流程
881
+ ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,站点 ID: ${siteId},文章 ID: ${postId}`);
542
882
  }
543
883
  }
544
884
  // 1. 新增强清洗函数:针对性解决敏感字符问题
545
885
  function sanitizeContent(content) {
546
- return content
886
+ let sanitized = content
547
887
  .replace(/<[^>]*>/g, '') // 移除所有 HTML 标签
548
888
  .replace(/[\x00-\x1F\x7F]/g, '') // 移除不可见控制符,QQ 接口明确禁止
549
889
  .replace(/\u3000/g, ' ') // 全角空格转半角空格,解决适配器编码缺陷
550
890
  .replace(/\s+/g, ' ') // 标准化所有空白符为单个半角空格
551
891
  .trim(); // 移除首尾空格
892
+ // 添加敏感词过滤
893
+ sanitized = filterSensitiveWords(sanitized);
894
+ return sanitized;
552
895
  }
553
- function formatPostMessage(post, mention = false, isUpdate = false) {
554
- // 生成缓存键
555
- const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${config.mentionAll}`;
896
+ // 敏感词过滤函数
897
+ function filterSensitiveWords(content) {
898
+ let filteredContent = content;
899
+ let hasSensitiveWords = false;
900
+ // 遍历敏感词列表进行替换
901
+ for (const word of CONSTANTS.SENSITIVE_WORDS) {
902
+ if (filteredContent.includes(word)) {
903
+ hasSensitiveWords = true;
904
+ const regex = new RegExp(word, 'gi'); // 不区分大小写
905
+ filteredContent = filteredContent.replace(regex, CONSTANTS.SENSITIVE_REPLACEMENT);
906
+ }
907
+ }
908
+ // 如果检测到敏感词,记录日志
909
+ if (hasSensitiveWords) {
910
+ ctx.logger.info('消息内容包含敏感词,已进行过滤处理');
911
+ // 仅记录处理信息,不记录具体内容,保护隐私
912
+ }
913
+ return filteredContent;
914
+ }
915
+ function formatPostMessage(post, mention = false, isUpdate = false, siteConfig) {
916
+ // 使用默认站点配置或传入的站点配置
917
+ const configToUse = siteConfig || config.sites[0];
918
+ if (!configToUse) {
919
+ ctx.logger.error('无可用的站点配置');
920
+ return '';
921
+ }
922
+ // 生成缓存键,包含推送模板配置信息
923
+ const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${configToUse.mentionAll}_${JSON.stringify(configToUse.pushTemplate)}`;
556
924
  // 检查缓存
557
925
  const cached = formatCache.post.get(cacheKey);
558
926
  if (cached && isCacheValid(cached.timestamp)) {
@@ -565,7 +933,7 @@ function apply(ctx, config) {
565
933
  if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
566
934
  title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
567
935
  }
568
- // 自定义时间格式:年-月-日 时:分
936
+ // 根据配置格式化日期
569
937
  const formatDate = (dateString) => {
570
938
  const date = new Date(dateString);
571
939
  const year = date.getFullYear();
@@ -573,19 +941,50 @@ function apply(ctx, config) {
573
941
  const day = String(date.getDate()).padStart(2, '0');
574
942
  const hours = String(date.getHours()).padStart(2, '0');
575
943
  const minutes = String(date.getMinutes()).padStart(2, '0');
576
- return `${year}-${month}-${day} ${hours}:${minutes}`;
944
+ // 根据配置的日期格式进行替换
945
+ let formattedDate = configToUse.pushTemplate.dateFormat
946
+ .replace('YYYY', year.toString())
947
+ .replace('MM', month)
948
+ .replace('DD', day)
949
+ .replace('HH', hours)
950
+ .replace('mm', minutes);
951
+ return formattedDate;
577
952
  };
578
953
  const date = formatDate(post.date);
579
954
  // 链接强制编码
580
955
  const encodedLink = encodeURI(post.link);
581
956
  // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
582
- const atAllText = mention && config.mentionAll ? '@全体成员 ' : '';
957
+ const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
583
958
  // 只使用一个极简表情
584
959
  const messageType = isUpdate ? '📝' : '📰';
585
- // 构建核心消息内容,严格控制格式
586
- // 格式:[表情] [@全体] [时间] - [标题]
587
- // [链接]
588
- let message = `${messageType} ${atAllText}${date} - ${title}\n${encodedLink}`;
960
+ // 构建消息内容
961
+ let messageParts = [];
962
+ // 添加头部
963
+ messageParts.push(`${messageType} ${atAllText}${date} - ${title}`);
964
+ // 根据配置添加作者
965
+ if (configToUse.pushTemplate.showAuthor && post.author) {
966
+ messageParts.push(`👤 作者: ${post.author}`);
967
+ }
968
+ // 根据配置添加摘要
969
+ if (configToUse.pushTemplate.showExcerpt && post.excerpt) {
970
+ let excerpt = sanitizeContent(post.excerpt.rendered);
971
+ // 截断摘要长度
972
+ if (excerpt.length > 100) {
973
+ excerpt = excerpt.substring(0, 97) + '...';
974
+ }
975
+ if (excerpt) {
976
+ messageParts.push(`📄 摘要: ${excerpt}`);
977
+ }
978
+ }
979
+ // 根据配置添加链接
980
+ if (configToUse.pushTemplate.linkPosition === 'top') {
981
+ messageParts.unshift(`🔗 ${encodedLink}`);
982
+ }
983
+ else if (configToUse.pushTemplate.linkPosition === 'bottom') {
984
+ messageParts.push(`🔗 ${encodedLink}`);
985
+ }
986
+ // 合并消息部分
987
+ let message = messageParts.join('\n');
589
988
  // 双级长度控制:整体消息兜底最大长度
590
989
  if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
591
990
  message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
@@ -599,9 +998,15 @@ function apply(ctx, config) {
599
998
  // 直接返回纯字符串,跳过适配器复杂编码
600
999
  return message;
601
1000
  }
602
- function formatUserMessage(user, mention = false) {
1001
+ function formatUserMessage(user, mention = false, siteConfig) {
1002
+ // 使用默认站点配置或传入的站点配置
1003
+ const configToUse = siteConfig || config.sites[0];
1004
+ if (!configToUse) {
1005
+ ctx.logger.error('无可用的站点配置');
1006
+ return '';
1007
+ }
603
1008
  // 生成缓存键
604
- const cacheKey = `user_${user.id}_${mention}_${config.mentionAll}`;
1009
+ const cacheKey = `user_${user.id}_${mention}_${configToUse.mentionAll}`;
605
1010
  // 检查缓存
606
1011
  const cached = formatCache.user.get(cacheKey);
607
1012
  if (cached && isCacheValid(cached.timestamp)) {
@@ -661,7 +1066,7 @@ function apply(ctx, config) {
661
1066
  ctx.logger.error(`处理用户 ${username} 日期时出错: ${error}`);
662
1067
  }
663
1068
  // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
664
- const atAllText = mention && config.mentionAll ? '@全体成员 ' : '';
1069
+ const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
665
1070
  // 只使用一个极简表情
666
1071
  const messageType = '👤';
667
1072
  // 构建核心消息内容,严格控制格式和换行
@@ -685,138 +1090,159 @@ function apply(ctx, config) {
685
1090
  const bot = getValidBot();
686
1091
  if (!bot) {
687
1092
  ctx.logger.error('没有可用的 Bot 实例');
1093
+ recordPushFailure('没有可用的 Bot 实例');
688
1094
  return;
689
1095
  }
690
1096
  // 修复 Bot 标识 undefined 问题
691
1097
  const botId = bot.selfId || 'unknown';
692
1098
  ctx.logger.info(`使用 bot ${bot.platform}:${botId} 进行推送`);
693
- // 推送新文章
694
- if (config.enableAutoPush) {
695
- const posts = await fetchLatestPosts();
696
- ctx.logger.info(`开始检查 ${posts.length} 篇文章是否需要推送`);
697
- if (posts.length > 0) {
698
- for (const post of posts) {
699
- ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
700
- ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
701
- // 检查文章是否已推送过(所有群聊共用一个标记)
702
- const postRecord = await getPostUpdateRecord(post.id);
703
- const hasPushed = !!postRecord;
704
- ctx.logger.info(`检查结果: 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
705
- if (!hasPushed) {
706
- // 推送到所有目标群聊
707
- const failedTargets = [];
708
- for (const target of config.targets) {
709
- try {
710
- ctx.logger.info(`正在处理目标: ${target}`);
711
- // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
712
- const stringTarget = target;
713
- const message = formatPostMessage(post, true, false);
714
- ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
715
- await bot.sendMessage(stringTarget, message);
716
- ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
1099
+ // 遍历所有站点
1100
+ for (const site of config.sites) {
1101
+ ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
1102
+ // 推送新文章
1103
+ if (site.enableAutoPush) {
1104
+ const posts = await fetchLatestPosts(site);
1105
+ ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
1106
+ if (posts.length > 0) {
1107
+ for (const post of posts) {
1108
+ ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
1109
+ ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
1110
+ // 检查文章是否已推送过(所有群聊共用一个标记)
1111
+ const postRecord = await getPostUpdateRecord(site.id, post.id);
1112
+ const hasPushed = !!postRecord;
1113
+ ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
1114
+ if (!hasPushed) {
1115
+ // 推送到该站点的所有目标群聊
1116
+ const failedTargets = [];
1117
+ for (const target of site.targets) {
1118
+ try {
1119
+ ctx.logger.info(`正在处理目标: ${target}`);
1120
+ // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
1121
+ const stringTarget = target;
1122
+ const message = formatPostMessage(post, site.mentionAll, false, site);
1123
+ ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
1124
+ await bot.sendMessage(stringTarget, message);
1125
+ ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
1126
+ }
1127
+ catch (error) {
1128
+ const errorMessage = error instanceof Error ? error.message : String(error);
1129
+ ctx.logger.error(`推送新文章到 ${target} 失败: ${errorMessage}`);
1130
+ ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1131
+ failedTargets.push(target);
1132
+ // 记录推送失败
1133
+ recordPushFailure(`推送消息失败到 ${target}: ${errorMessage}`);
1134
+ }
717
1135
  }
718
- catch (error) {
719
- ctx.logger.error(`推送新文章到 ${target} 失败: ${error}`);
720
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
721
- failedTargets.push(target);
1136
+ // 如果有失败的目标,添加到失败队列
1137
+ if (failedTargets.length > 0) {
1138
+ addToFailedQueue('post', post, failedTargets, site);
722
1139
  }
1140
+ // 标记文章已推送(所有群聊共用一个标记)
1141
+ await updatePostUpdateRecord(site.id, post.id, new Date(post.modified));
1142
+ ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
723
1143
  }
724
- // 如果有失败的目标,添加到失败队列
725
- if (failedTargets.length > 0) {
726
- addToFailedQueue('post', post, failedTargets);
1144
+ else {
1145
+ ctx.logger.info(`跳过推送: 站点 ${site.id} 文章 ${post.id} 已推送过,所有群聊将不再推送`);
727
1146
  }
728
- // 标记文章已推送(所有群聊共用一个标记)
729
- await updatePostUpdateRecord(post.id, new Date(post.modified));
730
- ctx.logger.info(`已标记文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
731
- }
732
- else {
733
- ctx.logger.info(`跳过推送: 文章 ${post.id} 已推送过,所有群聊将不再推送`);
734
1147
  }
735
1148
  }
736
1149
  }
737
- }
738
- // 推送文章更新
739
- if (config.enableUpdatePush) {
740
- const posts = await fetchUpdatedPosts();
741
- if (posts.length > 0) {
742
- for (const post of posts) {
743
- const updateRecord = await getPostUpdateRecord(post.id);
744
- const postModifiedDate = new Date(post.modified);
745
- // 检查文章是否有更新
746
- if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
747
- ctx.logger.info(`文章 ${post.id} 有更新,准备推送更新通知`);
748
- // 推送到所有目标群聊
749
- const failedTargets = [];
750
- for (const target of config.targets) {
751
- try {
752
- ctx.logger.info(`正在处理目标: ${target}`);
753
- const stringTarget = target;
754
- const message = formatPostMessage(post, true, true);
755
- ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
756
- await bot.sendMessage(stringTarget, message);
757
- ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
1150
+ // 推送文章更新
1151
+ if (site.enableUpdatePush) {
1152
+ const posts = await fetchUpdatedPosts(site);
1153
+ if (posts.length > 0) {
1154
+ for (const post of posts) {
1155
+ const updateRecord = await getPostUpdateRecord(site.id, post.id);
1156
+ const postModifiedDate = new Date(post.modified);
1157
+ // 检查文章是否有更新
1158
+ if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
1159
+ ctx.logger.info(`站点 ${site.id} 文章 ${post.id} 有更新,准备推送更新通知`);
1160
+ // 推送到该站点的所有目标群聊
1161
+ const failedTargets = [];
1162
+ for (const target of site.targets) {
1163
+ try {
1164
+ ctx.logger.info(`正在处理目标: ${target}`);
1165
+ const stringTarget = target;
1166
+ const message = formatPostMessage(post, site.mentionAll, true, site);
1167
+ ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
1168
+ await bot.sendMessage(stringTarget, message);
1169
+ ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
1170
+ }
1171
+ catch (error) {
1172
+ const errorMessage = error instanceof Error ? error.message : String(error);
1173
+ ctx.logger.error(`推送文章更新到 ${target} 失败: ${errorMessage}`);
1174
+ ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1175
+ failedTargets.push(target);
1176
+ // 记录推送失败
1177
+ recordPushFailure(`推送文章更新失败到 ${target}: ${errorMessage}`);
1178
+ }
758
1179
  }
759
- catch (error) {
760
- ctx.logger.error(`推送文章更新到 ${target} 失败: ${error}`);
761
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
762
- failedTargets.push(target);
1180
+ // 如果有失败的目标,添加到失败队列
1181
+ if (failedTargets.length > 0) {
1182
+ addToFailedQueue('update', post, failedTargets, site);
763
1183
  }
1184
+ // 更新文章更新记录(所有群聊共用一个标记)
1185
+ await updatePostUpdateRecord(site.id, post.id, postModifiedDate);
1186
+ ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
764
1187
  }
765
- // 如果有失败的目标,添加到失败队列
766
- if (failedTargets.length > 0) {
767
- addToFailedQueue('update', post, failedTargets);
768
- }
769
- // 更新文章更新记录(所有群聊共用一个标记)
770
- await updatePostUpdateRecord(post.id, postModifiedDate);
771
- ctx.logger.info(`已更新文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
772
1188
  }
773
1189
  }
774
1190
  }
775
- }
776
- // 推送新用户注册
777
- if (config.enableUserPush) {
778
- const users = await fetchLatestUsers();
779
- if (users.length > 0) {
780
- for (const user of users) {
781
- if (!(await isUserPushed(user.id))) {
782
- const failedTargets = [];
783
- for (const target of config.targets) {
784
- try {
785
- ctx.logger.info(`正在处理目标: ${target}`);
786
- // 直接使用原始目标字符串,与新文章推送逻辑保持一致
787
- const stringTarget = target;
788
- const message = formatUserMessage(user, true);
789
- ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
790
- await bot.sendMessage(stringTarget, message);
791
- ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
1191
+ // 推送新用户注册
1192
+ if (site.enableUserPush) {
1193
+ const users = await fetchLatestUsers(site);
1194
+ if (users.length > 0) {
1195
+ for (const user of users) {
1196
+ if (!(await isUserPushed(site.id, user.id))) {
1197
+ const failedTargets = [];
1198
+ for (const target of site.targets) {
1199
+ try {
1200
+ ctx.logger.info(`正在处理目标: ${target}`);
1201
+ // 直接使用原始目标字符串,与新文章推送逻辑保持一致
1202
+ const stringTarget = target;
1203
+ const message = formatUserMessage(user, site.mentionAll, site);
1204
+ ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
1205
+ await bot.sendMessage(stringTarget, message);
1206
+ ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
1207
+ }
1208
+ catch (error) {
1209
+ const errorMessage = error instanceof Error ? error.message : String(error);
1210
+ ctx.logger.error(`推送新用户到 ${target} 失败: ${errorMessage}`);
1211
+ ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1212
+ failedTargets.push(target);
1213
+ // 记录推送失败
1214
+ recordPushFailure(`推送新用户失败到 ${target}: ${errorMessage}`);
1215
+ }
792
1216
  }
793
- catch (error) {
794
- ctx.logger.error(`推送新用户到 ${target} 失败: ${error}`);
795
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
796
- failedTargets.push(target);
1217
+ // 如果有失败的目标,添加到失败队列
1218
+ if (failedTargets.length > 0) {
1219
+ addToFailedQueue('user', user, failedTargets, site);
797
1220
  }
1221
+ // 标记用户已推送
1222
+ await markUserAsPushed(site.id, user.id);
798
1223
  }
799
- // 如果有失败的目标,添加到失败队列
800
- if (failedTargets.length > 0) {
801
- addToFailedQueue('user', user, failedTargets);
802
- }
803
- // 标记用户已推送
804
- await markUserAsPushed(user.id);
805
1224
  }
806
1225
  }
807
1226
  }
808
1227
  }
809
1228
  }
810
- ctx.command('wordpress.latest', '查看最新文章')
811
- .action(async ({ session }) => {
812
- ctx.logger.info('命令 wordpress.latest 被调用');
813
- const posts = await fetchLatestPosts();
1229
+ ctx.command('wordpress.latest [siteId]', '查看最新文章,可选站点 ID')
1230
+ .action(async ({ session }, siteId) => {
1231
+ ctx.logger.info(`命令 wordpress.latest 被调用,站点 ID: ${siteId || '默认'}`);
1232
+ // 选择站点
1233
+ const targetSite = siteId
1234
+ ? config.sites.find(site => site.id === siteId)
1235
+ : config.sites[0];
1236
+ if (!targetSite) {
1237
+ return `未找到站点 ID: ${siteId}`;
1238
+ }
1239
+ const posts = await fetchLatestPosts(targetSite);
814
1240
  if (posts.length === 0) {
815
- ctx.logger.info('没有找到文章');
816
- return '暂无文章';
1241
+ ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
1242
+ return `站点 ${targetSite.name} 暂无文章`;
817
1243
  }
818
1244
  // 动态添加文章,确保消息长度不超过500字符
819
- let message = '📰 最新文章:\n';
1245
+ let message = `📰 ${targetSite.name} 最新文章:\n`;
820
1246
  let addedCount = 0;
821
1247
  for (const post of posts) {
822
1248
  const title = sanitizeContent(post.title.rendered);
@@ -842,15 +1268,22 @@ function apply(ctx, config) {
842
1268
  ctx.logger.info(`准备返回消息,长度: ${message.length},显示 ${addedCount}/${posts.length} 篇文章`);
843
1269
  return message;
844
1270
  });
845
- ctx.command('wordpress.list', '查看文章列表')
846
- .action(async () => {
847
- ctx.logger.info('命令 wordpress.list 被调用');
848
- const posts = await fetchLatestPosts();
1271
+ ctx.command('wordpress.list [siteId]', '查看文章列表,可选站点 ID')
1272
+ .action(async (_, siteId) => {
1273
+ ctx.logger.info(`命令 wordpress.list 被调用,站点 ID: ${siteId || '默认'}`);
1274
+ // 选择站点
1275
+ const targetSite = siteId
1276
+ ? config.sites.find(site => site.id === siteId)
1277
+ : config.sites[0];
1278
+ if (!targetSite) {
1279
+ return `未找到站点 ID: ${siteId}`;
1280
+ }
1281
+ const posts = await fetchLatestPosts(targetSite);
849
1282
  if (posts.length === 0) {
850
- return '暂无文章';
1283
+ return `站点 ${targetSite.name} 暂无文章`;
851
1284
  }
852
1285
  // 使用数组拼接消息,便于控制格式和长度
853
- const messageParts = ['📚 文章列表:'];
1286
+ const messageParts = [`📚 ${targetSite.name} 文章列表:`];
854
1287
  for (const post of posts) {
855
1288
  const title = sanitizeContent(post.title.rendered);
856
1289
  // 截断标题,避免单条过长
@@ -875,97 +1308,152 @@ function apply(ctx, config) {
875
1308
  await pushNewPosts();
876
1309
  return '已检查并推送最新文章';
877
1310
  });
878
- ctx.command('wordpress.status', '查看插件状态')
879
- .action(({ session }) => {
880
- ctx.logger.info('命令 wordpress.status 被调用');
1311
+ ctx.command('wordpress.status [siteId]', '查看插件状态,可选站点 ID')
1312
+ .action(({ session }, siteId) => {
1313
+ ctx.logger.info(`命令 wordpress.status 被调用,站点 ID: ${siteId || '所有'}`);
881
1314
  // 获取当前群号,如果有的话
882
1315
  const currentGroup = session?.channelId || '未知群聊';
883
- // 推送目标仅显示本群
884
- const targetText = `🎯 推送目标: ${currentGroup}`;
885
- // 使用数组拼接消息,便于控制格式和长度
886
- const messageParts = [
887
- '📊 WordPress 插件状态',
888
- `🌐 站点: ${config.wordpressUrl}`,
889
- `⏰ 间隔: ${config.interval / 1000} 秒`,
890
- targetText,
891
- `🔔 自动推送: ${config.enableAutoPush ? '开启' : '关闭'}`,
892
- `🔄 更新推送: ${config.enableUpdatePush ? '开启' : '关闭'}`,
893
- `👤 用户推送: ${config.enableUserPush ? '开启' : '关闭'}`,
894
- `📢 @全体: ${config.mentionAll ? '开启' : '关闭'}`,
895
- `📝 最多推送: ${config.maxArticles} 篇`
896
- ];
897
- // 合并为单行文本,统一换行符
898
- let message = messageParts.join('\n');
899
- // 长度验证,超过 500 字符则精简
900
- if (message.length > 500) {
901
- ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
902
- message = messageParts.slice(0, 5).join('\n') + '\n... 更多配置请查看完整状态';
1316
+ if (siteId) {
1317
+ // 显示单个站点状态
1318
+ const targetSite = config.sites.find(site => site.id === siteId);
1319
+ if (!targetSite) {
1320
+ return `未找到站点 ID: ${siteId}`;
1321
+ }
1322
+ // 使用数组拼接消息,便于控制格式和长度
1323
+ const messageParts = [
1324
+ `📊 WordPress 插件状态 - ${targetSite.name}`,
1325
+ `🌐 站点: ${targetSite.url}`,
1326
+ `⏰ 间隔: ${targetSite.interval / 1000} 秒`,
1327
+ `🎯 推送目标: ${targetSite.targets.join(', ') || ''}`,
1328
+ `🔔 自动推送: ${targetSite.enableAutoPush ? '开启' : '关闭'}`,
1329
+ `🔄 更新推送: ${targetSite.enableUpdatePush ? '开启' : '关闭'}`,
1330
+ `👤 用户推送: ${targetSite.enableUserPush ? '开启' : '关闭'}`,
1331
+ `📢 @全体: ${targetSite.mentionAll ? '开启' : '关闭'}`,
1332
+ `📝 最多推送: ${targetSite.maxArticles} 篇`
1333
+ ];
1334
+ // 合并为单行文本,统一换行符
1335
+ let message = messageParts.join('\n');
1336
+ // 长度验证,超过 500 字符则精简
1337
+ if (message.length > 500) {
1338
+ ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1339
+ message = messageParts.slice(0, 5).join('\n') + '\n... 更多配置请查看完整状态';
1340
+ }
1341
+ ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1342
+ // 直接返回纯字符串,跳过适配器复杂编码
1343
+ return message;
1344
+ }
1345
+ else {
1346
+ // 显示所有站点状态
1347
+ let message = '📊 WordPress 插件状态\n\n';
1348
+ config.sites.forEach((site, index) => {
1349
+ const siteMessage = `站点 ${index + 1}: ${site.name} (${site.id})\n` +
1350
+ `🌐 URL: ${site.url}\n` +
1351
+ `⏰ 间隔: ${site.interval / 1000} 秒\n` +
1352
+ `🎯 目标: ${site.targets.join(', ') || '无'}\n` +
1353
+ `🔔 自动: ${site.enableAutoPush ? '开启' : '关闭'}\n` +
1354
+ `🔄 更新: ${site.enableUpdatePush ? '开启' : '关闭'}\n` +
1355
+ `👤 用户: ${site.enableUserPush ? '开启' : '关闭'}\n\n`;
1356
+ // 检查添加后是否超过500字符
1357
+ if (message.length + siteMessage.length > 500) {
1358
+ message += '... 更多站点请使用站点 ID 查看详细状态';
1359
+ return false;
1360
+ }
1361
+ message += siteMessage;
1362
+ });
1363
+ ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1364
+ return message;
903
1365
  }
904
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
905
- // 直接返回纯字符串,跳过适配器复杂编码
906
- return message;
907
1366
  });
908
- ctx.command('wordpress.toggle-update', '切换文章更新推送开关')
909
- .action(async ({ session }) => {
1367
+ ctx.command('wordpress.site.toggle-update <siteId>', '切换指定站点的文章更新推送开关')
1368
+ .action(async ({ session }, siteId) => {
910
1369
  // 检查权限
911
1370
  const authResult = checkSuperAdmin(session);
912
1371
  if (!authResult.valid) {
913
1372
  return authResult.message;
914
1373
  }
915
- ctx.logger.info('命令 wordpress.toggle-update 被调用');
916
- config.enableUpdatePush = !config.enableUpdatePush;
917
- await saveConfig('enableUpdatePush', config.enableUpdatePush);
918
- return `文章更新推送已${config.enableUpdatePush ? '开启' : '关闭'}`;
1374
+ ctx.logger.info(`命令 wordpress.site.toggle-update 被调用,站点 ID: ${siteId}`);
1375
+ // 查找站点
1376
+ const site = config.sites.find(s => s.id === siteId);
1377
+ if (!site) {
1378
+ return `未找到站点 ID: ${siteId}`;
1379
+ }
1380
+ // 切换开关
1381
+ site.enableUpdatePush = !site.enableUpdatePush;
1382
+ await saveConfig('sites', config.sites);
1383
+ return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
919
1384
  });
920
- ctx.command('wordpress.toggle-user', '切换新用户注册推送开关')
921
- .action(async ({ session }) => {
1385
+ ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
1386
+ .action(async ({ session }, siteId) => {
922
1387
  // 检查权限
923
1388
  const authResult = checkSuperAdmin(session);
924
1389
  if (!authResult.valid) {
925
1390
  return authResult.message;
926
1391
  }
927
- ctx.logger.info('命令 wordpress.toggle-user 被调用');
928
- config.enableUserPush = !config.enableUserPush;
929
- await saveConfig('enableUserPush', config.enableUserPush);
930
- return `新用户注册推送已${config.enableUserPush ? '开启' : '关闭'}`;
1392
+ ctx.logger.info(`命令 wordpress.site.toggle-user 被调用,站点 ID: ${siteId}`);
1393
+ // 查找站点
1394
+ const site = config.sites.find(s => s.id === siteId);
1395
+ if (!site) {
1396
+ return `未找到站点 ID: ${siteId}`;
1397
+ }
1398
+ // 切换开关
1399
+ site.enableUserPush = !site.enableUserPush;
1400
+ await saveConfig('sites', config.sites);
1401
+ return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
931
1402
  });
932
- ctx.command('wordpress.toggle', '切换自动推送开关')
933
- .action(async ({ session }) => {
1403
+ ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
1404
+ .action(async ({ session }, siteId) => {
934
1405
  // 检查权限
935
1406
  const authResult = checkSuperAdmin(session);
936
1407
  if (!authResult.valid) {
937
1408
  return authResult.message;
938
1409
  }
939
- ctx.logger.info('命令 wordpress.toggle 被调用');
940
- config.enableAutoPush = !config.enableAutoPush;
941
- await saveConfig('enableAutoPush', config.enableAutoPush);
942
- return `自动推送已${config.enableAutoPush ? '开启' : '关闭'}`;
1410
+ ctx.logger.info(`命令 wordpress.site.toggle 被调用,站点 ID: ${siteId}`);
1411
+ // 查找站点
1412
+ const site = config.sites.find(s => s.id === siteId);
1413
+ if (!site) {
1414
+ return `未找到站点 ID: ${siteId}`;
1415
+ }
1416
+ // 切换开关
1417
+ site.enableAutoPush = !site.enableAutoPush;
1418
+ await saveConfig('sites', config.sites);
1419
+ return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
943
1420
  });
944
- ctx.command('wordpress.mention', '切换 @全体成员 开关')
945
- .action(async ({ session }) => {
1421
+ ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
1422
+ .action(async ({ session }, siteId) => {
946
1423
  // 检查权限
947
1424
  const authResult = checkSuperAdmin(session);
948
1425
  if (!authResult.valid) {
949
1426
  return authResult.message;
950
1427
  }
951
- ctx.logger.info('命令 wordpress.mention 被调用');
952
- config.mentionAll = !config.mentionAll;
953
- await saveConfig('mentionAll', config.mentionAll);
954
- return `@全体 已${config.mentionAll ? '开启' : '关闭'}`;
1428
+ ctx.logger.info(`命令 wordpress.site.mention 被调用,站点 ID: ${siteId}`);
1429
+ // 查找站点
1430
+ const site = config.sites.find(s => s.id === siteId);
1431
+ if (!site) {
1432
+ return `未找到站点 ID: ${siteId}`;
1433
+ }
1434
+ // 切换开关
1435
+ site.mentionAll = !site.mentionAll;
1436
+ await saveConfig('sites', config.sites);
1437
+ return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
955
1438
  });
956
- ctx.command('wordpress.set-url <url>', '修改 WordPress 站点地址')
957
- .action(async ({ session }, url) => {
1439
+ ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
1440
+ .action(async ({ session }, siteId, url) => {
958
1441
  // 检查权限
959
1442
  const authResult = checkSuperAdmin(session);
960
1443
  if (!authResult.valid) {
961
1444
  return authResult.message;
962
1445
  }
963
- ctx.logger.info(`命令 wordpress.set-url 被调用,调用者:${authResult.userId},新地址:${url}`);
1446
+ ctx.logger.info(`命令 wordpress.site.set-url 被调用,站点 ID: ${siteId},新地址:${url}`);
1447
+ // 查找站点
1448
+ const site = config.sites.find(s => s.id === siteId);
1449
+ if (!site) {
1450
+ return `未找到站点 ID: ${siteId}`;
1451
+ }
964
1452
  // 修改站点地址
965
- config.wordpressUrl = url;
966
- await saveConfig('wordpressUrl', config.wordpressUrl);
967
- ctx.logger.info(`站点地址已修改为:${url}`);
968
- return `WordPress 站点地址已修改为:${url}`;
1453
+ site.url = url;
1454
+ await saveConfig('sites', config.sites);
1455
+ ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
1456
+ return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
969
1457
  });
970
1458
  ctx.command('wordpress.pushed', '查看已推送的文章列表')
971
1459
  .action(async () => {
@@ -1061,8 +1549,90 @@ function apply(ctx, config) {
1061
1549
  catch (error) {
1062
1550
  ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
1063
1551
  }
1064
- ctx.logger.info(`已清理 ${result} 条 ${daysToKeep} 天前的推送记录`);
1065
- return `已清理 ${result} 条 ${daysToKeep} 天前的推送记录`;
1552
+ });
1553
+ ctx.command('wordpress.stats', '查看插件运行指标统计')
1554
+ .action(() => {
1555
+ ctx.logger.info('命令 wordpress.stats 被调用');
1556
+ // 计算 API 调用成功率
1557
+ const apiSuccessRate = runtimeStats.apiCallCount > 0
1558
+ ? (runtimeStats.apiSuccessCount / runtimeStats.apiCallCount * 100).toFixed(2)
1559
+ : '0.00';
1560
+ // 计算推送成功率
1561
+ const pushSuccessRate = (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) > 0
1562
+ ? (runtimeStats.pushSuccessCount / (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) * 100).toFixed(2)
1563
+ : '0.00';
1564
+ // 计算统计时间范围
1565
+ const startTime = new Date(runtimeStats.lastResetTime);
1566
+ 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')}`;
1567
+ // 构建统计消息
1568
+ const messageParts = [
1569
+ '📊 WordPress 插件运行统计',
1570
+ `📅 统计开始时间: ${formattedStartTime}`,
1571
+ `📈 API 调用: ${runtimeStats.apiCallCount} 次`,
1572
+ `✅ API 成功: ${runtimeStats.apiSuccessCount} 次`,
1573
+ `❌ API 失败: ${runtimeStats.apiFailureCount} 次`,
1574
+ `📊 API 成功率: ${apiSuccessRate}%`,
1575
+ `📤 推送成功: ${runtimeStats.pushSuccessCount} 次`,
1576
+ `📥 推送失败: ${runtimeStats.pushFailureCount} 次`,
1577
+ `📊 推送成功率: ${pushSuccessRate}%`
1578
+ ];
1579
+ let message = messageParts.join('\n');
1580
+ // 长度验证,超过 500 字符则精简
1581
+ if (message.length > 500) {
1582
+ ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1583
+ message = messageParts.slice(0, 6).join('\n') + '\n... 更多统计信息请查看完整数据';
1584
+ }
1585
+ return message;
1586
+ });
1587
+ ctx.command('wordpress.health', '查看插件健康状态')
1588
+ .action(async () => {
1589
+ ctx.logger.info('命令 wordpress.health 被调用');
1590
+ // 检查数据库连接
1591
+ const dbConnected = await checkDatabaseConnection();
1592
+ // 检查 Bot 在线状态
1593
+ const bot = getValidBot();
1594
+ const botOnline = !!bot;
1595
+ // 检查 API 可达性(尝试访问第一个站点的 API)
1596
+ let apiReachable = false;
1597
+ if (config.sites.length > 0) {
1598
+ const firstSite = config.sites[0];
1599
+ try {
1600
+ const url = `${firstSite.url}/wp-json/wp/v2/posts?per_page=1`;
1601
+ const response = await ctx.http.get(url, {
1602
+ timeout: 5000
1603
+ });
1604
+ apiReachable = true;
1605
+ }
1606
+ catch (error) {
1607
+ ctx.logger.warn(`API 可达性检查失败: ${error}`);
1608
+ }
1609
+ }
1610
+ // 构建健康状态消息
1611
+ const messageParts = [
1612
+ '🏥 WordPress 插件健康状态',
1613
+ `🗄️ 数据库连接: ${dbConnected ? '✅ 正常' : '❌ 异常'}`,
1614
+ `🤖 Bot 在线状态: ${botOnline ? '✅ 在线' : '❌ 离线'}`,
1615
+ `🌐 API 可达性: ${apiReachable ? '✅ 可达' : '❌ 不可达'}`
1616
+ ];
1617
+ // 添加站点状态
1618
+ if (config.sites.length > 0) {
1619
+ messageParts.push('\n📋 站点状态:');
1620
+ config.sites.forEach((site, index) => {
1621
+ if (index < 3) { // 只显示前3个站点
1622
+ messageParts.push(`${index + 1}. ${site.name}: ${site.enableAutoPush ? '✅ 启用' : '❌ 禁用'}`);
1623
+ }
1624
+ });
1625
+ if (config.sites.length > 3) {
1626
+ messageParts.push(`... 共 ${config.sites.length} 个站点`);
1627
+ }
1628
+ }
1629
+ let message = messageParts.join('\n');
1630
+ // 长度验证,超过 500 字符则精简
1631
+ if (message.length > 500) {
1632
+ ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1633
+ message = messageParts.slice(0, 5).join('\n') + '\n... 更多健康状态信息请查看完整数据';
1634
+ }
1635
+ return message;
1066
1636
  });
1067
1637
  ctx.command('wordpress', 'WordPress 推送插件菜单')
1068
1638
  .action(() => {
@@ -1087,15 +1657,18 @@ function apply(ctx, config) {
1087
1657
  ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1088
1658
  return message;
1089
1659
  });
1090
- ctx.setInterval(() => {
1091
- try {
1092
- pushNewPosts();
1093
- }
1094
- catch (error) {
1095
- const errorMessage = error instanceof Error ? error.message : String(error);
1096
- ctx.logger.error(`定时任务执行失败:${errorMessage}`);
1097
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1098
- ctx.logger.warn('定时任务将在下一个周期继续执行');
1099
- }
1100
- }, config.interval);
1660
+ // 为每个站点设置独立的定时任务
1661
+ config.sites.forEach(site => {
1662
+ ctx.setInterval(() => {
1663
+ try {
1664
+ pushNewPosts();
1665
+ }
1666
+ catch (error) {
1667
+ const errorMessage = error instanceof Error ? error.message : String(error);
1668
+ ctx.logger.error(`站点 ${site.id} 定时任务执行失败:${errorMessage}`);
1669
+ ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1670
+ ctx.logger.warn('定时任务将在下一个周期继续执行');
1671
+ }
1672
+ }, site.interval);
1673
+ });
1101
1674
  }