koishi-plugin-wordpress-notifier 2.9.0 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,1982 +1,163 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Config = exports.name = exports.inject = void 0;
3
+ exports.schema = exports.inject = exports.name = void 0;
4
4
  exports.apply = apply;
5
5
  const koishi_1 = require("koishi");
6
- exports.inject = ['database', 'http'];
7
- exports.name = 'wordpress-notifier';
8
- // 全局常量
9
- const CONSTANTS = {
10
- // 消息相关
11
- MAX_MESSAGE_LENGTH: 500,
12
- MAX_TITLE_LENGTH: 60,
13
- MAX_USERNAME_LENGTH: 50,
14
- MAX_POST_TITLE_DISPLAY_LENGTH: 40,
15
- // 缓存相关
16
- CACHE_EXPIRY: 60 * 60 * 1000, // 1小时
17
- // HTTP请求相关
18
- HTTP_TIMEOUT: 10000, // 10秒
19
- MAX_RETRIES: 2,
20
- RETRY_DELAY: 1000, // 1秒
21
- API_RATE_LIMIT: 1000, // API请求频率限制(毫秒),每秒最多1次
22
- // 失败队列相关
23
- MAX_PUSH_RETRIES: 3,
24
- PUSH_RETRY_INTERVAL: 5 * 60 * 1000, // 5分钟
25
- // 清理相关
26
- DEFAULT_CLEAN_DAYS: 30,
27
- // QQ适配器相关
28
- QQ_ADAPTERS: ['qq', 'onebot', 'milky', 'satori'],
29
- // 安全过滤相关
30
- SENSITIVE_WORDS: [
31
- // 常见敏感词列表(示例)
32
- '敏感词1', '敏感词2', '敏感词3',
33
- // 可以根据实际需求扩展
34
- ],
35
- SENSITIVE_REPLACEMENT: '***', // 敏感词替换为***
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
- }
76
- exports.Config = koishi_1.Schema.object({
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('推送失败通知配置')
6
+ const wordpress_1 = require("./wordpress");
7
+ const push_1 = require("./push");
8
+ const storage_1 = require("./storage");
9
+ const commands_1 = require("./commands");
10
+ const schedule_1 = require("./schedule");
11
+ exports.name = 'wordpress-push';
12
+ exports.inject = ['database'];
13
+ exports.schema = koishi_1.Schema.object({
14
+ wordpressUrl: koishi_1.Schema.string().required().description('WordPress 网站地址'),
15
+ apiEndpoint: koishi_1.Schema.string().default('/wp-json/wp/v2').description('REST API 端点'),
16
+ authType: koishi_1.Schema.union(['none', 'basic', 'application-password']).default('none').description('认证方式'),
17
+ username: koishi_1.Schema.string().description('认证用户名'),
18
+ password: koishi_1.Schema.string().description('认证密码'),
19
+ checkInterval: koishi_1.Schema.number().default(30).min(5).max(1440).description('检查间隔 (分钟)'),
20
+ enableAutoPush: koishi_1.Schema.boolean().default(true).description('自动推送开关'),
21
+ enableUpdatePush: koishi_1.Schema.boolean().default(true).description('文章更新推送开关'),
22
+ enableUserRegisterPush: koishi_1.Schema.boolean().default(false).description('用户注册推送开关'),
23
+ defaultGroups: koishi_1.Schema.array(koishi_1.Schema.string()).description('默认推送群聊列表'),
24
+ allowAtAll: koishi_1.Schema.boolean().default(false).description('是否允许@全体成员'),
25
+ messageType: koishi_1.Schema.union(['text', 'card']).default('text').description('消息类型:text(文本)或 card(卡片)'),
26
+ signatureKey: koishi_1.Schema.string().description('卡片消息签名密钥'),
27
+ adminUserId: koishi_1.Schema.string().description('管理员用户 ID,用于接收推送失败告警'),
28
+ superAdminUserIds: koishi_1.Schema.array(koishi_1.Schema.string()).description('超级管理员 QQ 号码列表,拥有最高权限'),
29
+ pushStartTime: koishi_1.Schema.number().default(9).min(0).max(23).description('允许推送的开始时间 (小时)'),
30
+ pushEndTime: koishi_1.Schema.number().default(22).min(0).max(23).description('允许推送的结束时间 (小时)')
127
31
  });
128
32
  function apply(ctx, config) {
129
- ctx.logger.info('WordPress 推送插件已加载');
130
- // 修复 MySQL 自增主键问题,使用正确的模型配置
131
- // 确保 id 字段被正确设置为自增主键,并且在插入时不会被设置为 NULL
132
- // 显式指定 nullable: false 约束以适配 MySQL 特有要求
133
- ctx.model.extend('wordpress_post_updates', {
134
- id: { type: 'integer', nullable: false },
135
- siteId: { type: 'string', nullable: false },
136
- postId: { type: 'integer', nullable: false },
137
- lastModified: { type: 'timestamp', nullable: false },
138
- pushedAt: { type: 'timestamp', nullable: false }
139
- }, {
140
- primary: 'id',
141
- autoInc: true,
142
- unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
143
- });
144
- ctx.model.extend('wordpress_user_registrations', {
145
- id: { type: 'integer', nullable: false },
146
- siteId: { type: 'string', nullable: false },
147
- userId: { type: 'integer', nullable: false },
148
- pushedAt: { type: 'timestamp', nullable: false }
149
- }, {
150
- primary: 'id',
151
- autoInc: true,
152
- unique: ['siteId', 'userId'] // 每个站点的用户 ID 唯一
153
- });
154
- // 配置存储表
155
- ctx.model.extend('wordpress_config', {
156
- id: { type: 'string', nullable: false },
157
- key: { type: 'string', nullable: false },
158
- value: { type: 'string', nullable: false },
159
- updatedAt: { type: 'timestamp', nullable: false }
160
- }, {
161
- primary: 'id',
162
- unique: ['key']
163
- });
164
- // 版本记录表
165
- ctx.model.extend('wordpress_version', {
166
- id: { type: 'string', nullable: false },
167
- version: { type: 'string', nullable: false },
168
- updatedAt: { type: 'timestamp', nullable: false }
169
- }, {
170
- primary: 'id',
171
- unique: ['version']
172
- });
173
- ctx.logger.info('数据库表配置完成,autoInc: true 已启用,确保插入操作不手动指定 id 字段');
174
- // 时间处理工具函数
175
- function parseWPDate(dateStr) {
176
- if (!dateStr) {
177
- return null;
178
- }
179
- try {
180
- const date = new Date(dateStr);
181
- if (isNaN(date.getTime())) {
182
- return null;
183
- }
184
- return date;
185
- }
186
- catch (error) {
187
- ctx.logger.warn(`解析日期失败: ${dateStr}, 错误: ${error}`);
188
- return null;
189
- }
190
- }
191
- // 为所有数据库操作添加详细日志,便于诊断自增主键问题
192
- ctx.on('ready', async () => {
193
- ctx.logger.info('WordPress 推送插件已就绪,开始初始化推送任务');
194
- ctx.logger.info('数据库表配置:');
195
- ctx.logger.info('wordpress_post_updates: id 字段设置为 autoIncrement: true');
196
- ctx.logger.info('wordpress_user_registrations: id 字段设置为 autoIncrement: true');
197
- ctx.logger.info('wordpress_config: 配置持久化存储表');
198
- ctx.logger.info('wordpress_version: 数据库版本记录表');
199
- ctx.logger.info('所有群聊共用一个文章标记,不再区分群聊');
200
- // 检查并修复数据库表结构问题
201
- await checkAndFixTableStructure();
202
- // 检查和更新数据库版本
203
- await checkAndUpdateDatabaseVersion();
204
- // 加载持久化配置
205
- await loadPersistentConfig();
206
- // 执行初始推送
207
- await pushNewPosts();
208
- });
209
- // 检查和更新数据库版本
210
- async function checkAndUpdateDatabaseVersion() {
211
- try {
212
- ctx.logger.info('开始检查数据库版本...');
213
- // 确保数据库连接正常
214
- const connected = await ensureDatabaseConnection();
215
- if (!connected) {
216
- ctx.logger.warn('数据库连接异常,跳过版本检查');
217
- return;
218
- }
219
- // 检查现有版本记录
220
- const versionRecords = await ctx.database.get('wordpress_version', {}, { limit: 1 });
221
- if (versionRecords.length === 0) {
222
- // 首次安装,创建版本记录
223
- await ctx.database.create('wordpress_version', {
224
- id: '1',
225
- version: DATABASE_VERSION,
226
- updatedAt: new Date()
227
- });
228
- ctx.logger.info(`数据库版本初始化完成,当前版本: ${DATABASE_VERSION}`);
229
- }
230
- else {
231
- const currentVersion = versionRecords[0].version;
232
- if (currentVersion !== DATABASE_VERSION) {
233
- // 版本不一致,执行升级逻辑
234
- ctx.logger.info(`数据库版本更新,从 ${currentVersion} 升级到 ${DATABASE_VERSION}`);
235
- // 这里可以添加具体的升级逻辑,例如:
236
- // 1. 表结构变更
237
- // 2. 数据迁移
238
- // 3. 配置更新
239
- // 更新版本记录
240
- await ctx.database.remove('wordpress_version', { id: versionRecords[0].id });
241
- await ctx.database.create('wordpress_version', {
242
- id: versionRecords[0].id,
243
- version: DATABASE_VERSION,
244
- updatedAt: new Date()
245
- });
246
- ctx.logger.info(`数据库版本升级完成,当前版本: ${DATABASE_VERSION}`);
247
- }
248
- else {
249
- ctx.logger.info(`数据库版本检查完成,当前版本: ${DATABASE_VERSION} (最新)`);
250
- }
251
- }
252
- }
253
- catch (error) {
254
- ctx.logger.error(`检查数据库版本失败: ${error}`);
255
- }
256
- }
257
- // 加载持久化配置,使用分页查询避免内存溢出
258
- async function loadPersistentConfig() {
259
- try {
260
- ctx.logger.info('开始加载持久化配置...');
261
- // 确保数据库连接正常
262
- const connected = await ensureDatabaseConnection();
263
- if (!connected) {
264
- ctx.logger.warn('数据库连接异常,跳过加载持久化配置');
265
- return;
266
- }
267
- // 使用分页查询,每次最多获取 100 条记录
268
- const configRecords = await ctx.database.get('wordpress_config', {}, {
269
- limit: 100
270
- });
271
- ctx.logger.info(`找到 ${configRecords.length} 条持久化配置记录`);
272
- for (const record of configRecords) {
273
- try {
274
- const value = JSON.parse(record.value);
275
- if (record.key in config) {
276
- // 添加类型断言,确保类型安全
277
- config[record.key] = value;
278
- ctx.logger.info(`已加载持久化配置: ${record.key} = ${JSON.stringify(value)}`);
279
- }
280
- else {
281
- ctx.logger.warn(`未知的配置键: ${record.key}`);
282
- }
283
- }
284
- catch (error) {
285
- ctx.logger.error(`解析配置值失败,键: ${record.key},错误: ${error}`);
286
- }
287
- }
288
- ctx.logger.info('持久化配置加载完成');
289
- }
290
- catch (error) {
291
- const errorMessage = error instanceof Error ? error.message : String(error);
292
- ctx.logger.error(`加载持久化配置失败: ${errorMessage}`);
293
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
294
- // 发生错误时,不抛出异常,确保插件继续运行
295
- // 使用默认配置
296
- ctx.logger.warn('使用默认配置继续运行,持久化配置将在下次保存时重新创建');
297
- }
298
- }
299
- // 保存配置到数据库
300
- async function saveConfig(key, value) {
301
- try {
302
- ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
303
- // 确保数据库连接正常
304
- const connected = await ensureDatabaseConnection();
305
- if (!connected) {
306
- ctx.logger.warn('数据库连接异常,跳过保存配置');
307
- return;
308
- }
309
- // 检查配置是否已存在
310
- const existingRecords = await ctx.database.get('wordpress_config', { key });
311
- if (existingRecords.length > 0) {
312
- // 更新现有配置
313
- await ctx.database.remove('wordpress_config', { key });
314
- }
315
- // 创建新配置记录,只保存指定的键值对
316
- await ctx.database.create('wordpress_config', {
317
- key,
318
- value: JSON.stringify(value),
319
- updatedAt: new Date()
320
- });
321
- ctx.logger.info(`配置保存成功: ${key}`);
322
- }
323
- catch (error) {
324
- ctx.logger.error(`保存配置失败,键: ${key},错误: ${error}`);
325
- }
326
- }
327
- // 保存单个站点配置
328
- async function saveSiteConfig(siteId, siteConfig) {
329
- try {
330
- ctx.logger.info(`保存单个站点配置: ${siteId} = ${JSON.stringify(siteConfig)}`);
331
- // 确保数据库连接正常
332
- const connected = await ensureDatabaseConnection();
333
- if (!connected) {
334
- ctx.logger.warn('数据库连接异常,跳过保存站点配置');
335
- return;
336
- }
337
- // 获取当前所有站点配置
338
- const sitesConfigKey = 'sites';
339
- const existingSitesConfig = await ctx.database.get('wordpress_config', { key: sitesConfigKey });
340
- let currentSites = [];
341
- if (existingSitesConfig.length > 0) {
33
+ // 验证配置
34
+ try {
35
+ validateConfig(config);
36
+ }
37
+ catch (error) {
38
+ ctx.logger.error('Configuration validation failed:', error);
39
+ return;
40
+ }
41
+ // 确保配置默认值
42
+ config.apiEndpoint = config.apiEndpoint || '/wp-json/wp/v2';
43
+ config.checkInterval = config.checkInterval || 30;
44
+ config.enableAutoPush = config.enableAutoPush ?? true;
45
+ config.enableUpdatePush = config.enableUpdatePush ?? true;
46
+ config.enableUserRegisterPush = config.enableUserRegisterPush ?? false;
47
+ config.allowAtAll = config.allowAtAll ?? false;
48
+ config.messageType = config.messageType || 'text';
49
+ // 服务实例
50
+ let storageService;
51
+ let wordpressService;
52
+ let pushService;
53
+ let commandService;
54
+ let scheduleService;
55
+ // 初始化核心服务(包括命令注册)
56
+ function initializeCoreServices() {
57
+ try {
58
+ ctx.logger.info('Initializing WordPress push plugin core services...');
59
+ // 1. 初始化 WordPress 服务(仅依赖配置)
60
+ wordpressService = new wordpress_1.WordPressService(ctx, config);
61
+ ctx.logger.info('WordPressService initialized successfully');
62
+ // 2. 初始化存储服务(依赖数据库)
63
+ storageService = new storage_1.StorageService(ctx, config);
64
+ ctx.logger.info('StorageService initialized successfully');
65
+ // 3. 初始化推送服务(依赖存储服务)
66
+ pushService = new push_1.PushService(ctx, config, storageService);
67
+ ctx.logger.info('PushService initialized successfully');
68
+ // 4. 初始化定时任务服务(依赖所有其他服务)
69
+ scheduleService = new schedule_1.ScheduleService(ctx, config, wordpressService, storageService, pushService);
70
+ ctx.logger.info('ScheduleService initialized successfully');
71
+ // 5. 初始化命令服务(依赖 WordPress、存储服务和定时任务服务)
72
+ // 命令注册需要在插件加载时完成,不能等到 ready 事件
73
+ commandService = new commands_1.CommandService(ctx, config, wordpressService, storageService, scheduleService);
74
+ ctx.logger.info('CommandService initialized successfully - commands registered');
75
+ ctx.logger.info('WordPress 推送插件核心服务初始化完成');
76
+ }
77
+ catch (error) {
78
+ ctx.logger.error('Failed to initialize WordPress push plugin core services:', error);
79
+ // 清理已初始化的服务
80
+ cleanupServices();
81
+ }
82
+ }
83
+ // 初始化默认群组配置
84
+ async function initializeDefaultGroups() {
85
+ if (config.defaultGroups && config.defaultGroups.length > 0) {
86
+ ctx.logger.info(`Initializing default push config for ${config.defaultGroups.length} groups...`);
87
+ for (const groupId of config.defaultGroups) {
342
88
  try {
343
- currentSites = JSON.parse(existingSitesConfig[0].value);
344
- }
345
- catch (error) {
346
- ctx.logger.error(`解析现有站点配置失败: ${error}`);
347
- currentSites = [];
348
- }
349
- }
350
- // 查找站点是否已存在
351
- const siteIndex = currentSites.findIndex(site => site.id === siteId);
352
- if (siteIndex >= 0) {
353
- // 更新现有站点
354
- currentSites[siteIndex] = siteConfig;
355
- }
356
- else {
357
- // 添加新站点
358
- currentSites.push(siteConfig);
359
- }
360
- // 保存更新后的站点配置
361
- await saveConfig(sitesConfigKey, currentSites);
362
- ctx.logger.info(`站点配置保存成功: ${siteId}`);
363
- }
364
- catch (error) {
365
- ctx.logger.error(`保存站点配置失败,站点 ID: ${siteId},错误: ${error}`);
366
- }
367
- }
368
- // 缓存对象,用于存储高频格式化结果,有效期1小时
369
- const formatCache = {
370
- post: new Map(),
371
- user: new Map()
372
- };
373
- // 检查缓存是否有效
374
- function isCacheValid(timestamp) {
375
- const now = Date.now();
376
- return now - timestamp < CONSTANTS.CACHE_EXPIRY;
377
- }
378
- // 清理过期缓存
379
- function cleanExpiredCache() {
380
- const now = Date.now();
381
- // 清理文章格式化缓存
382
- for (const [key, value] of formatCache.post.entries()) {
383
- if (now - value.timestamp >= CONSTANTS.CACHE_EXPIRY) {
384
- formatCache.post.delete(key);
385
- }
386
- }
387
- // 清理用户格式化缓存
388
- for (const [key, value] of formatCache.user.entries()) {
389
- if (now - value.timestamp >= CONSTANTS.CACHE_EXPIRY) {
390
- formatCache.user.delete(key);
391
- }
392
- }
393
- }
394
- // 定期清理过期缓存,绑定到 ctx 生命周期
395
- const cacheCleanupTimer = ctx.setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
396
- // 健壮获取 QQ Bot 实例,兼容多种适配器,优先选择 QQ 官方 bot
397
- function getValidBot() {
398
- // 支持的 QQ 相关适配器列表,'qq' 为 QQ 官方 bot
399
- const qqAdapters = CONSTANTS.QQ_ADAPTERS;
400
- // ctx.bots 是对象,需转换为数组后遍历
401
- const botList = Object.values(ctx.bots);
402
- // 筛选已连接的 Bot
403
- const connectedBots = botList.filter(bot => {
404
- // 检查 Bot 是否已连接
405
- // 不同适配器可能有不同的状态属性
406
- // 尝试检查常见的状态属性
407
- const status = String(bot.status || '');
408
- return status === 'online' || status === 'connected' || status === '';
409
- });
410
- ctx.logger.info(`找到 ${connectedBots.length} 个已连接的 Bot,总 Bot 数: ${botList.length}`);
411
- if (connectedBots.length === 0) {
412
- ctx.logger.warn('没有已连接的 Bot 实例');
413
- return null;
414
- }
415
- // 1. 优先选择 QQ 官方 bot(platform === 'qq')
416
- for (const bot of connectedBots) {
417
- if (bot.platform === 'qq') {
418
- ctx.logger.info(`选择 QQ 官方 bot: ${bot.selfId || 'unknown'}`);
419
- return bot;
420
- }
421
- }
422
- // 2. 其次选择其他 QQ 适配器 Bot
423
- for (const bot of connectedBots) {
424
- if (bot.platform && qqAdapters.includes(bot.platform)) {
425
- ctx.logger.info(`选择其他 QQ 适配器 Bot: ${bot.platform} - ${bot.selfId || 'unknown'}`);
426
- return bot;
427
- }
428
- }
429
- // 3. 最后选择任何可用的已连接 Bot
430
- ctx.logger.info(`选择可用的已连接 Bot: ${connectedBots[0].platform} - ${connectedBots[0].selfId || 'unknown'}`);
431
- return connectedBots[0];
432
- }
433
- // 检查 Bot 是否有发送消息的权限
434
- async function checkBotPermission(bot, target) {
435
- try {
436
- // 不同适配器可能有不同的权限检查方法
437
- // 这里使用通用的检查方法,尝试发送一条空消息或检查权限
438
- // 注意:这种方法可能会在某些适配器上失败,所以需要捕获异常
439
- // 检查 Bot 是否有 sendMessage 方法
440
- if (!bot.sendMessage) {
441
- ctx.logger.warn(`Bot 实例没有 sendMessage 方法: ${bot.platform}:${bot.selfId || 'unknown'}`);
442
- return false;
443
- }
444
- // 检查 Bot 是否在线
445
- if (bot.status && bot.status !== 'online' && bot.status !== 'connected') {
446
- ctx.logger.warn(`Bot 不在线: ${bot.platform}:${bot.selfId || 'unknown'},状态: ${bot.status}`);
447
- return false;
448
- }
449
- // 对于 QQ 官方 bot,检查是否有权限发送消息
450
- if (bot.platform === 'qq') {
451
- // QQ 官方 bot 可能有专门的权限检查方法
452
- // 这里暂时返回 true,实际项目中需要根据具体适配器实现权限检查
453
- return true;
454
- }
455
- // 对于其他适配器,尝试发送一条测试消息或检查权限
456
- // 这里暂时返回 true,实际项目中需要根据具体适配器实现权限检查
457
- return true;
458
- }
459
- catch (error) {
460
- const errorMessage = error instanceof Error ? error.message : String(error);
461
- ctx.logger.error(`检查 Bot 权限失败: ${errorMessage}`);
462
- return false;
463
- }
464
- }
465
- const failedPushQueue = [];
466
- const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
467
- const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
468
- // 添加到失败队列
469
- function addToFailedQueue(type, data, targets, siteConfig) {
470
- const now = new Date();
471
- const expireAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24小时过期
472
- const item = {
473
- id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
474
- type,
475
- data,
476
- targets,
477
- retries: 0,
478
- createdAt: now,
479
- expireAt,
480
- siteConfig
481
- };
482
- failedPushQueue.push(item);
483
- ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
484
- }
485
- // 处理失败队列
486
- async function processFailedQueue() {
487
- if (failedPushQueue.length === 0) {
488
- return;
489
- }
490
- ctx.logger.info(`开始处理失败队列,队列长度: ${failedPushQueue.length}`);
491
- const bot = getValidBot();
492
- if (!bot) {
493
- ctx.logger.error('没有可用的 Bot 实例,无法处理失败队列');
494
- return;
495
- }
496
- const now = new Date();
497
- const itemsToRemove = [];
498
- for (let i = 0; i < failedPushQueue.length; i++) {
499
- const item = failedPushQueue[i];
500
- // 检查是否过期
501
- if (now > item.expireAt) {
502
- ctx.logger.warn(`任务已过期,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
503
- itemsToRemove.push(i);
504
- continue;
505
- }
506
- if (item.retries >= MAX_RETRIES) {
507
- ctx.logger.warn(`达到最大重试次数,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
508
- itemsToRemove.push(i);
509
- continue;
510
- }
511
- try {
512
- ctx.logger.info(`重试推送,类型: ${item.type},重试次数: ${item.retries + 1}/${MAX_RETRIES}`);
513
- // 根据类型格式化消息
514
- let message;
515
- if (item.type === 'post' || item.type === 'update') {
516
- message = formatPostMessage(item.data, true, item.type === 'update', item.siteConfig);
517
- }
518
- else {
519
- message = formatUserMessage(item.data, true, item.siteConfig);
520
- }
521
- // 推送到所有目标
522
- for (const target of item.targets) {
523
- try {
524
- await bot.sendMessage(target, message);
525
- ctx.logger.info(`重试推送成功,类型: ${item.type},目标: ${target}`);
526
- }
527
- catch (error) {
528
- ctx.logger.error(`重试推送失败,类型: ${item.type},目标: ${target},错误: ${error}`);
529
- }
530
- }
531
- // 推送成功,从队列中移除
532
- itemsToRemove.push(i);
533
- ctx.logger.info(`推送成功,从失败队列中移除,类型: ${item.type}`);
534
- }
535
- catch (error) {
536
- ctx.logger.error(`处理失败队列项失败,类型: ${item.type},错误: ${error}`);
537
- item.retries++;
538
- }
539
- }
540
- // 移除处理完成的项(从后往前移除,避免索引错乱)
541
- for (let i = itemsToRemove.length - 1; i >= 0; i--) {
542
- failedPushQueue.splice(itemsToRemove[i], 1);
543
- }
544
- ctx.logger.info(`失败队列处理完成,剩余队列长度: ${failedPushQueue.length}`);
545
- }
546
- // 定期处理失败队列,绑定到 ctx 生命周期
547
- const failedQueueTimer = ctx.setInterval(processFailedQueue, RETRY_INTERVAL);
548
- // 通用权限检查函数,检查用户是否为超级管理员
549
- function checkSuperAdmin(session) {
550
- // 检查是否为超级管理员
551
- if (!session || !session.userId) {
552
- ctx.logger.warn('匿名用户尝试调用需要权限的命令');
553
- return { valid: false, message: '您不是超级管理员,无法执行此命令' };
554
- }
555
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
556
- const userId = session.userId.replace(/^\w+:/, '');
557
- // 检查当前用户是否在插件配置的超级管理员列表中
558
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
559
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用需要权限的命令`);
560
- return { valid: false, message: '您不是超级管理员,无法执行此命令' };
561
- }
562
- // 权限检查通过,继续执行命令
563
- return { valid: true, userId };
564
- }
565
- // 检查数据库连接状态
566
- async function checkDatabaseConnection() {
567
- const now = Date.now();
568
- // 避免频繁检查,30秒内只检查一次
569
- if (now - lastDatabaseCheck < DATABASE_CHECK_INTERVAL && databaseConnected) {
570
- return true;
571
- }
572
- try {
573
- ctx.logger.info('开始检查数据库连接状态...');
574
- // 执行一个简单的数据库查询来测试连接
575
- await ctx.database.get('wordpress_config', {}, { limit: 1 });
576
- ctx.logger.info('数据库连接正常');
577
- databaseConnected = true;
578
- lastDatabaseCheck = now;
579
- return true;
580
- }
581
- catch (error) {
582
- ctx.logger.error(`数据库连接异常: ${error}`);
583
- databaseConnected = false;
584
- lastDatabaseCheck = now;
585
- return false;
586
- }
587
- }
588
- // 确保数据库连接正常
589
- async function ensureDatabaseConnection() {
590
- const connected = await checkDatabaseConnection();
591
- if (!connected) {
592
- ctx.logger.warn('数据库连接异常,暂停推送任务');
593
- // 记录失败
594
- recordPushFailure('数据库连接异常');
595
- // 等待3秒后再次尝试
596
- await new Promise(resolve => setTimeout(resolve, 3000));
597
- return await checkDatabaseConnection();
598
- }
599
- return true;
600
- }
601
- // 记录推送失败并发送通知
602
- async function recordPushFailure(reason) {
603
- const now = Date.now();
604
- // 检查是否需要重置失败计数
605
- if (now - lastFailureTime > FAILURE_RESET_INTERVAL) {
606
- consecutiveFailureCount = 0;
607
- }
608
- // 增加失败计数
609
- consecutiveFailureCount++;
610
- lastFailureTime = now;
611
- ctx.logger.error(`推送失败,原因: ${reason},连续失败次数: ${consecutiveFailureCount}`);
612
- // 检查是否需要发送通知
613
- if (config.failureNotification.enable &&
614
- consecutiveFailureCount >= config.failureNotification.threshold) {
615
- await sendFailureNotification(reason);
616
- }
617
- }
618
- // 发送失败通知
619
- async function sendFailureNotification(reason) {
620
- try {
621
- const bot = getValidBot();
622
- if (!bot) {
623
- ctx.logger.error('无法发送失败通知,没有可用的 Bot 实例');
624
- return;
625
- }
626
- // 确定通知目标
627
- const notificationTargets = config.failureNotification.notificationTargets.length > 0
628
- ? config.failureNotification.notificationTargets
629
- : config.superAdmins;
630
- if (notificationTargets.length === 0) {
631
- ctx.logger.warn('无法发送失败通知,未配置通知目标');
632
- return;
633
- }
634
- // 构建通知消息
635
- const message = `🚨 WordPress 推送插件告警\n\n` +
636
- `📅 时间: ${new Date().toLocaleString()}\n` +
637
- `❌ 连续失败次数: ${consecutiveFailureCount}\n` +
638
- `🔍 失败原因: ${reason}\n` +
639
- `🌐 站点数: ${config.sites.length}\n` +
640
- `📡 总推送目标数: ${config.sites.reduce((total, site) => total + site.targets.length, 0)}\n\n` +
641
- `请及时检查插件状态和相关配置!`;
642
- // 发送通知
643
- for (const target of notificationTargets) {
644
- try {
645
- await bot.sendMessage(target, message);
646
- ctx.logger.info(`已向 ${target} 发送推送失败通知`);
647
- }
648
- catch (error) {
649
- ctx.logger.error(`发送通知到 ${target} 失败: ${error}`);
650
- }
651
- }
652
- // 重置失败计数
653
- consecutiveFailureCount = 0;
654
- }
655
- catch (error) {
656
- ctx.logger.error(`发送失败通知时发生错误: ${error}`);
657
- }
658
- }
659
- // 重置失败计数
660
- function resetFailureCount() {
661
- consecutiveFailureCount = 0;
662
- lastFailureTime = 0;
663
- ctx.logger.info('推送失败计数已重置');
664
- }
665
- // 检查数据库表结构的函数
666
- async function checkAndFixTableStructure() {
667
- try {
668
- ctx.logger.info('开始检查数据库表结构...');
669
- ctx.logger.info('所有群聊现在共用一个文章标记,不再区分群聊');
670
- ctx.logger.info('wordpress_group_pushes 表已不再使用,已移除相关功能');
671
- // 尝试获取一些记录来验证表结构是否正确
672
- try {
673
- ctx.logger.info('验证 wordpress_post_updates 表结构...');
674
- const testPostUpdate = await ctx.database.get('wordpress_post_updates', {}, {
675
- limit: 1
676
- });
677
- ctx.logger.info(`wordpress_post_updates 表验证成功,现有记录数:${testPostUpdate.length}`);
678
- }
679
- catch (error) {
680
- ctx.logger.warn(`wordpress_post_updates 表可能结构不正确,尝试重新初始化...`);
681
- // 尝试删除旧表并重新初始化
682
- try {
683
- ctx.logger.info('尝试删除旧的 wordpress_post_updates 表...');
684
- await ctx.database.drop('wordpress_post_updates');
685
- ctx.logger.info('旧表删除成功,重新初始化表结构...');
686
- // 重新扩展模型
687
- ctx.model.extend('wordpress_post_updates', {
688
- id: { type: 'integer', nullable: false },
689
- siteId: { type: 'string', nullable: false },
690
- postId: { type: 'integer', nullable: false },
691
- lastModified: { type: 'timestamp', nullable: false },
692
- pushedAt: { type: 'timestamp', nullable: false }
693
- }, {
694
- primary: 'id',
695
- autoInc: true,
696
- unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
697
- });
698
- ctx.logger.info('wordpress_post_updates 表重新初始化成功');
699
- }
700
- catch (dropError) {
701
- ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
702
- }
703
- }
704
- try {
705
- ctx.logger.info('验证 wordpress_user_registrations 表结构...');
706
- const testUserRegistration = await ctx.database.get('wordpress_user_registrations', {}, {
707
- limit: 1
708
- });
709
- ctx.logger.info(`wordpress_user_registrations 表验证成功,现有记录数:${testUserRegistration.length}`);
710
- }
711
- catch (error) {
712
- ctx.logger.warn(`wordpress_user_registrations 表可能结构不正确,尝试重新初始化...`);
713
- // 尝试删除旧表并重新初始化
714
- try {
715
- ctx.logger.info('尝试删除旧的 wordpress_user_registrations 表...');
716
- await ctx.database.drop('wordpress_user_registrations');
717
- ctx.logger.info('旧表删除成功,重新初始化表结构...');
718
- // 重新扩展模型
719
- ctx.model.extend('wordpress_user_registrations', {
720
- id: { type: 'integer', nullable: false },
721
- siteId: { type: 'string', nullable: false },
722
- userId: { type: 'integer', nullable: false },
723
- pushedAt: { type: 'timestamp', nullable: false }
724
- }, {
725
- primary: 'id',
726
- autoInc: true,
727
- unique: ['siteId', 'userId'] // 每个站点的用户 ID 唯一
89
+ await storageService.setPushConfig({
90
+ groupId,
91
+ enabled: true,
92
+ enableUpdatePush: config.enableUpdatePush || true,
93
+ enableAtAll: config.allowAtAll || false,
94
+ lastPushTime: new Date()
728
95
  });
729
- ctx.logger.info('wordpress_user_registrations 表重新初始化成功');
730
- }
731
- catch (dropError) {
732
- ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
733
- }
734
- }
735
- try {
736
- ctx.logger.info('验证 wordpress_config 表结构...');
737
- const testConfig = await ctx.database.get('wordpress_config', {}, {
738
- limit: 1
739
- });
740
- ctx.logger.info(`wordpress_config 表验证成功,现有记录数:${testConfig.length}`);
741
- }
742
- catch (error) {
743
- ctx.logger.warn(`wordpress_config 表可能结构不正确,尝试重新初始化...`);
744
- // 尝试删除旧表并重新初始化
745
- try {
746
- ctx.logger.info('尝试删除旧的 wordpress_config 表...');
747
- await ctx.database.drop('wordpress_config');
748
- ctx.logger.info('旧表删除成功,重新初始化表结构...');
749
- // 重新扩展模型
750
- ctx.model.extend('wordpress_config', {
751
- id: { type: 'string', nullable: false },
752
- key: { type: 'string', nullable: false },
753
- value: { type: 'string', nullable: false },
754
- updatedAt: { type: 'timestamp', nullable: false }
755
- }, {
756
- primary: 'id',
757
- unique: ['key']
758
- });
759
- ctx.logger.info('wordpress_config 表重新初始化成功');
760
- }
761
- catch (dropError) {
762
- ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
763
- }
764
- }
765
- try {
766
- ctx.logger.info('验证 wordpress_version 表结构...');
767
- const testVersion = await ctx.database.get('wordpress_version', {}, {
768
- limit: 1
769
- });
770
- ctx.logger.info(`wordpress_version 表验证成功,现有记录数:${testVersion.length}`);
771
- }
772
- catch (error) {
773
- ctx.logger.warn(`wordpress_version 表可能结构不正确,尝试重新初始化...`);
774
- // 尝试删除旧表并重新初始化
775
- try {
776
- ctx.logger.info('尝试删除旧的 wordpress_version 表...');
777
- await ctx.database.drop('wordpress_version');
778
- ctx.logger.info('旧表删除成功,重新初始化表结构...');
779
- // 重新扩展模型
780
- ctx.model.extend('wordpress_version', {
781
- id: { type: 'string', nullable: false },
782
- version: { type: 'string', nullable: false },
783
- updatedAt: { type: 'timestamp', nullable: false }
784
- }, {
785
- primary: 'id',
786
- unique: ['version']
787
- });
788
- ctx.logger.info('wordpress_version 表重新初始化成功');
789
- }
790
- catch (dropError) {
791
- ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
792
- }
793
- }
794
- ctx.logger.info('表结构检查和修复完成');
795
- }
796
- catch (error) {
797
- const errorMessage = error instanceof Error ? error.message : String(error);
798
- ctx.logger.error(`检查表结构失败:${errorMessage}`);
799
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
800
- }
801
- }
802
- // 通用HTTP请求包装函数,添加超时和重试机制
803
- async function httpRequest(url, config = {}, maxRetries = CONSTANTS.MAX_RETRIES) {
804
- const requestConfig = {
805
- ...config,
806
- timeout: CONSTANTS.HTTP_TIMEOUT, // 10秒超时
807
- };
808
- let retries = 0;
809
- while (retries <= maxRetries) {
810
- try {
811
- // API 请求频率控制
812
- const now = Date.now();
813
- const timeSinceLastRequest = now - lastRequestTime;
814
- if (timeSinceLastRequest < CONSTANTS.API_RATE_LIMIT) {
815
- const waitTime = CONSTANTS.API_RATE_LIMIT - timeSinceLastRequest;
816
- ctx.logger.info(`API 请求频率限制,等待 ${waitTime}ms 后继续请求`);
817
- await new Promise(resolve => setTimeout(resolve, waitTime));
96
+ ctx.logger.info(`Default push config initialized for group: ${groupId}`);
818
97
  }
819
- // 更新上次请求时间
820
- lastRequestTime = Date.now();
821
- ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
822
- const response = await ctx.http.get(url, requestConfig);
823
- ctx.logger.info(`HTTP请求成功: ${url}`);
824
- return response;
825
- }
826
- catch (error) {
827
- const errorMessage = error instanceof Error ? error.message : String(error);
828
- ctx.logger.warn(`HTTP请求失败 (${retries + 1}/${maxRetries + 1}): ${errorMessage}`);
829
- retries++;
830
- if (retries > maxRetries) {
831
- ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
832
- // 记录推送失败
833
- recordPushFailure(`API请求失败: ${errorMessage}`);
834
- return null;
98
+ catch (error) {
99
+ ctx.logger.error(`Failed to initialize default push config for group ${groupId}:`, error);
835
100
  }
836
- // 重试前等待
837
- await new Promise(resolve => setTimeout(resolve, CONSTANTS.RETRY_DELAY));
838
- }
839
- }
840
- return null;
841
- }
842
- async function fetchLatestPosts(site) {
843
- try {
844
- const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=date&order=desc`;
845
- ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的文章: ${url}`);
846
- // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
847
- const requestConfig = {};
848
- if (site.username && site.applicationPassword) {
849
- // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
850
- const username = site.username;
851
- const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
852
- const auth = Buffer.from(`${username}:${password}`).toString('base64');
853
- requestConfig.headers = {
854
- Authorization: `Basic ${auth}`
855
- };
856
- }
857
- const response = await httpRequest(url, requestConfig);
858
- if (!response) {
859
- const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`;
860
- ctx.logger.error(errorMessage);
861
- // 记录失败
862
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
863
- return { success: false, data: [], error: errorMessage };
864
- }
865
- ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
866
- return { success: true, data: response, error: '' };
867
- }
868
- catch (error) {
869
- const errorMessage = error instanceof Error ? error.message : String(error);
870
- const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`;
871
- ctx.logger.error(fullErrorMessage);
872
- // 记录失败
873
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
874
- return { success: false, data: [], error: fullErrorMessage };
875
- }
876
- }
877
- async function fetchLatestUsers(site) {
878
- try {
879
- // 修改API请求,添加_fields参数明确请求注册日期字段
880
- // WordPress REST API 默认可能不会返回注册日期,需要明确请求
881
- const fields = 'id,name,slug,date,date_registered,registered_date,created_at,registeredAt,email,roles,url,description,link,avatar_urls';
882
- const url = `${site.url}/wp-json/wp/v2/users?per_page=${site.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
883
- ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的用户: ${url}`);
884
- // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
885
- const requestConfig = {};
886
- if (site.username && site.applicationPassword) {
887
- // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
888
- const username = site.username;
889
- const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
890
- const auth = Buffer.from(`${username}:${password}`).toString('base64');
891
- requestConfig.headers = {
892
- Authorization: `Basic ${auth}`
893
- };
894
- }
895
- const response = await httpRequest(url, requestConfig);
896
- if (!response) {
897
- const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`;
898
- ctx.logger.error(errorMessage);
899
- ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
900
- // 记录失败
901
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
902
- // 返回空数组,确保插件继续运行
903
- return { success: false, data: [], error: errorMessage };
904
- }
905
- ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
906
- // 添加调试日志,查看API返回的实际数据结构
907
- if (response.length > 0) {
908
- ctx.logger.info(`用户数据示例: ${JSON.stringify(response[0], null, 2)}`);
909
- // 打印所有可能的日期相关字段
910
- const user = response[0];
911
- ctx.logger.info(`用户日期字段: date=${user.date}, date_registered=${user.date_registered}, registered_date=${user.registered_date}, created_at=${user.created_at}`);
912
- }
913
- return { success: true, data: response, error: '' };
914
- }
915
- catch (error) {
916
- const errorMessage = error instanceof Error ? error.message : String(error);
917
- const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`;
918
- ctx.logger.error(fullErrorMessage);
919
- ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
920
- // 记录失败
921
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
922
- // 返回空数组,确保插件继续运行
923
- return { success: false, data: [], error: fullErrorMessage };
924
- }
925
- }
926
- async function fetchUpdatedPosts(site) {
927
- try {
928
- const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=modified&order=desc`;
929
- ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的更新文章: ${url}`);
930
- // 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
931
- const requestConfig = {};
932
- if (site.username && site.applicationPassword) {
933
- // 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
934
- const username = site.username;
935
- const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
936
- const auth = Buffer.from(`${username}:${password}`).toString('base64');
937
- requestConfig.headers = {
938
- Authorization: `Basic ${auth}`
939
- };
940
- }
941
- const response = await httpRequest(url, requestConfig);
942
- if (!response) {
943
- const errorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`;
944
- ctx.logger.error(errorMessage);
945
- // 记录失败
946
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
947
- return { success: false, data: [], error: errorMessage };
948
- }
949
- ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
950
- return { success: true, data: response, error: '' };
951
- }
952
- catch (error) {
953
- const errorMessage = error instanceof Error ? error.message : String(error);
954
- const fullErrorMessage = `获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`;
955
- ctx.logger.error(fullErrorMessage);
956
- // 记录失败
957
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
958
- return { success: false, data: [], error: fullErrorMessage };
959
- }
960
- }
961
- async function isUserPushed(siteId, userId) {
962
- try {
963
- // 确保数据库连接正常
964
- const connected = await ensureDatabaseConnection();
965
- if (!connected) {
966
- ctx.logger.warn('数据库连接异常,跳过检查用户推送记录');
967
- return false;
968
- }
969
- ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
970
- const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId: parseInt(userId) });
971
- const result = record.length > 0;
972
- ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
973
- return result;
974
- }
975
- catch (error) {
976
- const errorMessage = error instanceof Error ? error.message : String(error);
977
- ctx.logger.error(`检查用户推送记录失败:${errorMessage}`);
978
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
979
- // 发生错误时,默认返回 false,避免阻塞推送流程
980
- return false;
981
- }
982
- }
983
- async function getPostUpdateRecord(siteId, postId) {
984
- try {
985
- // 确保数据库连接正常
986
- const connected = await ensureDatabaseConnection();
987
- if (!connected) {
988
- ctx.logger.warn('数据库连接异常,跳过获取文章更新记录');
989
- return null;
990
- }
991
- ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
992
- const records = await ctx.database.get('wordpress_post_updates', { siteId, postId: parseInt(postId) });
993
- const result = records.length > 0 ? records[0] : null;
994
- ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
995
- return result;
996
- }
997
- catch (error) {
998
- const errorMessage = error instanceof Error ? error.message : String(error);
999
- ctx.logger.error(`获取文章更新记录失败:${errorMessage}`);
1000
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1001
- // 发生错误时,返回 null,避免阻塞推送流程
1002
- return null;
1003
- }
1004
- }
1005
- async function markUserAsPushed(siteId, userId) {
1006
- try {
1007
- // 确保数据库连接正常
1008
- const connected = await ensureDatabaseConnection();
1009
- if (!connected) {
1010
- ctx.logger.warn('数据库连接异常,跳过标记用户推送记录');
1011
- return;
1012
- }
1013
- ctx.logger.info(`开始标记用户已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
1014
- // 创建新记录,不手动指定id,让数据库自动生成
1015
- const newRecord = {
1016
- siteId,
1017
- userId: parseInt(userId),
1018
- pushedAt: new Date()
1019
- };
1020
- ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
1021
- await ctx.database.create('wordpress_user_registrations', newRecord);
1022
- ctx.logger.info(`已成功标记站点 ${siteId} 用户 ${userId} 为已推送`);
1023
- }
1024
- catch (error) {
1025
- const errorMessage = error instanceof Error ? error.message : String(error);
1026
- if (errorMessage.includes('UNIQUE constraint failed')) {
1027
- ctx.logger.warn(`用户推送记录已存在,跳过重复插入:站点 ${siteId} 用户 ${userId}`);
1028
- ctx.logger.warn(`完整错误信息:${errorMessage}`);
1029
- }
1030
- else {
1031
- ctx.logger.error(`标记用户推送记录失败:${errorMessage}`);
1032
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1033
- ctx.logger.error(`插入参数:siteId=${siteId}, userId=${userId}`);
1034
- // 非约束冲突错误,不抛出,确保插件继续运行
1035
101
  }
1036
102
  }
1037
103
  }
1038
- async function updatePostUpdateRecord(siteId, postId, modifiedDate) {
104
+ // 清理服务
105
+ function cleanupServices() {
106
+ ctx.logger.info('Cleaning up WordPress push plugin services...');
1039
107
  try {
1040
- // 确保数据库连接正常
1041
- const connected = await ensureDatabaseConnection();
1042
- if (!connected) {
1043
- ctx.logger.warn('数据库连接异常,跳过更新文章更新记录');
1044
- return;
1045
- }
1046
- ctx.logger.info(`开始更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId},修改时间: ${modifiedDate}`);
1047
- const record = await getPostUpdateRecord(siteId, postId);
1048
- if (record) {
1049
- ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
1050
- // Koishi database API 不支持 update 方法,使用 remove + create 代替
1051
- await ctx.database.remove('wordpress_post_updates', { siteId, postId: parseInt(postId) });
1052
- ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
108
+ if (scheduleService) {
109
+ scheduleService.dispose();
110
+ ctx.logger.info('ScheduleService disposed successfully');
1053
111
  }
1054
- // 创建新记录,不指定 id 字段,让数据库自动生成
1055
- const newRecord = {
1056
- siteId,
1057
- postId: parseInt(postId),
1058
- lastModified: modifiedDate,
1059
- pushedAt: new Date()
1060
- };
1061
- ctx.logger.info(`准备创建新记录,站点 ID: ${siteId},文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
1062
- await ctx.database.create('wordpress_post_updates', newRecord);
1063
- ctx.logger.info(`已成功更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
1064
112
  }
1065
113
  catch (error) {
1066
- const errorMessage = error instanceof Error ? error.message : String(error);
1067
- ctx.logger.error(`更新文章更新记录失败,站点 ID: ${siteId},文章 ID: ${postId}`);
1068
- ctx.logger.error(`错误信息: ${errorMessage}`);
1069
- ctx.logger.error(`错误栈: ${error instanceof Error ? error.stack : '无'}`);
1070
- // 不再抛出错误,确保推送流程继续运行
1071
- // 发生错误时,默认返回,避免阻塞推送流程
1072
- ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,站点 ID: ${siteId},文章 ID: ${postId}`);
1073
- }
1074
- }
1075
- // 1. 新增强清洗函数:针对性解决敏感字符问题
1076
- function sanitizeContent(content) {
1077
- let sanitized = content
1078
- .replace(/<[^>]*>/g, '') // 移除所有 HTML 标签
1079
- .replace(/[\x00-\x1F\x7F]/g, '') // 移除不可见控制符,QQ 接口明确禁止
1080
- .replace(/\u3000/g, ' ') // 全角空格转半角空格,解决适配器编码缺陷
1081
- .replace(/\s+/g, ' ') // 标准化所有空白符为单个半角空格
1082
- .trim(); // 移除首尾空格
1083
- // 添加敏感词过滤
1084
- sanitized = filterSensitiveWords(sanitized);
1085
- return sanitized;
1086
- }
1087
- // 敏感词过滤函数
1088
- function filterSensitiveWords(content) {
1089
- let filteredContent = content;
1090
- let hasSensitiveWords = false;
1091
- // 遍历敏感词列表进行替换
1092
- for (const word of CONSTANTS.SENSITIVE_WORDS) {
1093
- if (filteredContent.includes(word)) {
1094
- hasSensitiveWords = true;
1095
- const regex = new RegExp(word, 'gi'); // 不区分大小写
1096
- filteredContent = filteredContent.replace(regex, CONSTANTS.SENSITIVE_REPLACEMENT);
1097
- }
1098
- }
1099
- // 如果检测到敏感词,记录日志
1100
- if (hasSensitiveWords) {
1101
- ctx.logger.info('消息内容包含敏感词,已进行过滤处理');
1102
- // 仅记录处理信息,不记录具体内容,保护隐私
1103
- }
1104
- return filteredContent;
1105
- }
1106
- function formatPostMessage(post, mention = false, isUpdate = false, siteConfig) {
1107
- // 使用默认站点配置或传入的站点配置
1108
- const configToUse = siteConfig || config.sites[0];
1109
- if (!configToUse) {
1110
- ctx.logger.error('无可用的站点配置');
1111
- return '';
1112
- }
1113
- // 生成缓存键,包含推送模板配置信息
1114
- const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${configToUse.mentionAll}_${JSON.stringify(configToUse.pushTemplate)}`;
1115
- // 检查缓存
1116
- const cached = formatCache.post.get(cacheKey);
1117
- if (cached && isCacheValid(cached.timestamp)) {
1118
- ctx.logger.info(`使用缓存的文章消息格式,文章 ID: ${post.id}`);
1119
- return cached.value;
1120
- }
1121
- // 使用统一的强清洗函数
1122
- let title = sanitizeContent(post.title.rendered);
1123
- // 严格截断标题为最大长度
1124
- if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
1125
- title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
1126
- }
1127
- // 根据配置格式化日期
1128
- const formatDate = (dateString) => {
1129
- // 使用统一的时间处理工具函数
1130
- const date = parseWPDate(dateString);
1131
- if (!date) {
1132
- return '未知时间';
1133
- }
1134
- const year = date.getFullYear();
1135
- const month = String(date.getMonth() + 1).padStart(2, '0');
1136
- const day = String(date.getDate()).padStart(2, '0');
1137
- const hours = String(date.getHours()).padStart(2, '0');
1138
- const minutes = String(date.getMinutes()).padStart(2, '0');
1139
- // 根据配置的日期格式进行替换
1140
- let formattedDate = configToUse.pushTemplate.dateFormat
1141
- .replace('YYYY', year.toString())
1142
- .replace('MM', month)
1143
- .replace('DD', day)
1144
- .replace('HH', hours)
1145
- .replace('mm', minutes);
1146
- return formattedDate;
1147
- };
1148
- const date = formatDate(post.date);
1149
- // 链接强制编码
1150
- const encodedLink = encodeURI(post.link);
1151
- // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
1152
- const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
1153
- // 只使用一个极简表情
1154
- const messageType = isUpdate ? '📝' : '📰';
1155
- // 构建消息内容
1156
- let messageParts = [];
1157
- // 添加头部
1158
- messageParts.push(`${messageType} ${atAllText}${date} - ${title}`);
1159
- // 根据配置添加作者
1160
- if (configToUse.pushTemplate.showAuthor && post.author) {
1161
- messageParts.push(`👤 作者: ${post.author}`);
1162
- }
1163
- // 根据配置添加摘要
1164
- if (configToUse.pushTemplate.showExcerpt && post.excerpt) {
1165
- let excerpt = sanitizeContent(post.excerpt.rendered);
1166
- // 截断摘要长度
1167
- if (excerpt.length > 100) {
1168
- excerpt = excerpt.substring(0, 97) + '...';
1169
- }
1170
- if (excerpt) {
1171
- messageParts.push(`📄 摘要: ${excerpt}`);
1172
- }
1173
- }
1174
- // 根据配置添加链接
1175
- if (configToUse.pushTemplate.linkPosition === 'top') {
1176
- messageParts.unshift(`🔗 ${encodedLink}`);
1177
- }
1178
- else if (configToUse.pushTemplate.linkPosition === 'bottom') {
1179
- messageParts.push(`🔗 ${encodedLink}`);
1180
- }
1181
- // 合并消息部分
1182
- let message = messageParts.join('\n');
1183
- // 双级长度控制:整体消息兜底最大长度
1184
- if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
1185
- message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
1186
- ctx.logger.warn(`文章消息超长,已截断,文章 ID: ${post.id}`);
114
+ ctx.logger.error('Error disposing ScheduleService:', error);
1187
115
  }
1188
- // 缓存结果
1189
- formatCache.post.set(cacheKey, {
1190
- value: message,
1191
- timestamp: Date.now()
1192
- });
1193
- // 直接返回纯字符串,跳过适配器复杂编码
1194
- return message;
1195
116
  }
1196
- function formatUserMessage(user, mention = false, siteConfig) {
1197
- // 使用默认站点配置或传入的站点配置
1198
- const configToUse = siteConfig || config.sites[0];
1199
- if (!configToUse) {
1200
- ctx.logger.error('无可用的站点配置');
1201
- return '';
117
+ // 验证配置
118
+ function validateConfig(config) {
119
+ if (!config.wordpressUrl) {
120
+ throw new Error('WordPress URL is required');
1202
121
  }
1203
- // 生成缓存键
1204
- const cacheKey = `user_${user.id}_${mention}_${configToUse.mentionAll}`;
1205
- // 检查缓存
1206
- const cached = formatCache.user.get(cacheKey);
1207
- if (cached && isCacheValid(cached.timestamp)) {
1208
- ctx.logger.info(`使用缓存的用户消息格式,用户 ID: ${user.id}`);
1209
- return cached.value;
122
+ // 验证认证配置
123
+ if ((config.authType === 'basic' || config.authType === 'application-password') && (!config.username || !config.password)) {
124
+ throw new Error(`Authentication type ${config.authType} requires both username and password`);
1210
125
  }
1211
- // 使用统一的强清洗函数
1212
- let username = sanitizeContent(user.name);
1213
- // 严格截断用户名为最大长度
1214
- if (username.length > CONSTANTS.MAX_USERNAME_LENGTH) {
1215
- username = username.substring(0, CONSTANTS.MAX_USERNAME_LENGTH - 3) + '...';
1216
- }
1217
- // 安全处理日期,避免显示 "Invalid Date",自定义格式
1218
- let registerDate = '未知时间';
1219
- try {
1220
- ctx.logger.info(`正在处理用户 ${username} 的注册日期`);
1221
- // 尝试所有可能的日期字段,按优先级排序
1222
- const dateFields = [
1223
- 'registered_date',
1224
- 'user_registered',
1225
- 'date_registered',
1226
- 'created_at',
1227
- 'registeredAt',
1228
- 'date'
1229
- ];
1230
- let dateStr;
1231
- // 遍历所有可能的日期字段
1232
- for (const field of dateFields) {
1233
- if (user[field]) {
1234
- dateStr = user[field];
1235
- ctx.logger.info(`找到日期字段 ${field}: ${dateStr}`);
1236
- break;
1237
- }
1238
- }
1239
- // 如果没有找到已知字段,尝试打印所有字段以便调试
1240
- if (!dateStr) {
1241
- ctx.logger.info(`用户 ${username} 的所有字段: ${JSON.stringify(Object.keys(user))}`);
1242
- ctx.logger.info(`用户 ${username} 的原始数据: ${JSON.stringify(user)}`);
1243
- }
1244
- if (dateStr) {
1245
- // 使用统一的时间处理工具函数
1246
- const date = parseWPDate(dateStr);
1247
- if (date) {
1248
- const year = date.getFullYear();
1249
- const month = String(date.getMonth() + 1).padStart(2, '0');
1250
- const day = String(date.getDate()).padStart(2, '0');
1251
- const hours = String(date.getHours()).padStart(2, '0');
1252
- const minutes = String(date.getMinutes()).padStart(2, '0');
1253
- registerDate = `${year}-${month}-${day} ${hours}:${minutes}`;
1254
- ctx.logger.info(`格式化后的日期: ${registerDate}`);
126
+ // 验证群聊 ID 格式
127
+ if (config.defaultGroups) {
128
+ for (const groupId of config.defaultGroups) {
129
+ if (!groupId || typeof groupId !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(groupId)) {
130
+ throw new Error(`Invalid group ID format: ${groupId}`);
1255
131
  }
1256
132
  }
1257
133
  }
1258
- catch (error) {
1259
- // 捕获任何异常,确保消息能正常生成
1260
- ctx.logger.error(`处理用户 ${username} 日期时出错: ${error}`);
1261
- }
1262
- // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
1263
- const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
1264
- // 只使用一个极简表情
1265
- const messageType = '👤';
1266
- // 构建核心消息内容,严格控制格式和换行
1267
- // 格式:[表情] [@全体] 新用户注册 - [用户名]
1268
- // 注册时间: [时间]
1269
- let message = `${messageType} ${atAllText}新用户注册 - ${username}\n注册时间: ${registerDate}`;
1270
- // 严格控制整体消息长度为最大长度
1271
- if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
1272
- message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
1273
- ctx.logger.warn(`用户消息超长,已截断,用户 ID: ${user.id}`);
134
+ // 验证 WordPress URL 格式
135
+ if (!/^https?:\/\//i.test(config.wordpressUrl)) {
136
+ throw new Error('WordPress URL must start with http:// or https://');
1274
137
  }
1275
- // 缓存结果
1276
- formatCache.user.set(cacheKey, {
1277
- value: message,
1278
- timestamp: Date.now()
1279
- });
1280
- // 直接返回纯字符串,跳过适配器复杂编码
1281
- return message;
1282
138
  }
1283
- async function pushNewPosts() {
1284
- const bot = getValidBot();
1285
- if (!bot) {
1286
- ctx.logger.error('没有可用的 Bot 实例');
1287
- recordPushFailure('没有可用的 Bot 实例');
1288
- return;
1289
- }
1290
- // 修复 Bot 标识 undefined 问题
1291
- const botId = bot.selfId || 'unknown';
1292
- ctx.logger.info(`使用 bot ${bot.platform}:${botId} 进行推送`);
1293
- // 遍历所有站点
1294
- for (const site of config.sites) {
1295
- ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
1296
- // 推送新文章
1297
- if (site.enableAutoPush) {
1298
- const postsResult = await fetchLatestPosts(site);
1299
- if (!postsResult.success) {
1300
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的文章失败: ${postsResult.error}`);
1301
- // 记录失败
1302
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${postsResult.error}`);
1303
- continue;
1304
- }
1305
- const posts = postsResult.data;
1306
- ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
1307
- if (posts.length > 0) {
1308
- for (const post of posts) {
1309
- ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
1310
- ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
1311
- // 检查文章是否已推送过(所有群聊共用一个标记)
1312
- const postRecord = await getPostUpdateRecord(site.id, String(post.id));
1313
- const hasPushed = !!postRecord;
1314
- ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
1315
- if (!hasPushed) {
1316
- // 推送到该站点的所有目标群聊
1317
- const failedTargets = [];
1318
- for (const target of site.targets) {
1319
- try {
1320
- ctx.logger.info(`正在处理目标: ${target}`);
1321
- // 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
1322
- const stringTarget = target;
1323
- // 检查 Bot 是否有发送消息的权限
1324
- const hasPermission = await checkBotPermission(bot, stringTarget);
1325
- if (!hasPermission) {
1326
- ctx.logger.warn(`Bot 没有权限向 ${stringTarget} 发送消息,跳过推送`);
1327
- failedTargets.push(target);
1328
- continue;
1329
- }
1330
- const message = formatPostMessage(post, site.mentionAll, false, site);
1331
- ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
1332
- await bot.sendMessage(stringTarget, message);
1333
- ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
1334
- }
1335
- catch (error) {
1336
- const errorMessage = error instanceof Error ? error.message : String(error);
1337
- ctx.logger.error(`推送新文章到 ${target} 失败: ${errorMessage}`);
1338
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1339
- failedTargets.push(target);
1340
- // 记录推送失败
1341
- recordPushFailure(`推送消息失败到 ${target}: ${errorMessage}`);
1342
- }
1343
- }
1344
- // 如果有失败的目标,添加到失败队列
1345
- if (failedTargets.length > 0) {
1346
- addToFailedQueue('post', post, failedTargets, site);
1347
- }
1348
- // 标记文章已推送(所有群聊共用一个标记)
1349
- await updatePostUpdateRecord(site.id, String(post.id), new Date(post.modified));
1350
- ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
1351
- }
1352
- else {
1353
- ctx.logger.info(`跳过推送: 站点 ${site.id} 文章 ${post.id} 已推送过,所有群聊将不再推送`);
1354
- }
1355
- }
1356
- }
1357
- }
1358
- // 推送文章更新
1359
- if (site.enableUpdatePush) {
1360
- const postsResult = await fetchUpdatedPosts(site);
1361
- if (!postsResult.success) {
1362
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${postsResult.error}`);
1363
- // 记录失败
1364
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${postsResult.error}`);
1365
- continue;
1366
- }
1367
- const posts = postsResult.data;
1368
- if (posts.length > 0) {
1369
- for (const post of posts) {
1370
- const updateRecord = await getPostUpdateRecord(site.id, String(post.id));
1371
- const postModifiedDate = new Date(post.modified);
1372
- // 检查文章是否有更新
1373
- if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
1374
- ctx.logger.info(`站点 ${site.id} 文章 ${post.id} 有更新,准备推送更新通知`);
1375
- // 推送到该站点的所有目标群聊
1376
- const failedTargets = [];
1377
- for (const target of site.targets) {
1378
- try {
1379
- ctx.logger.info(`正在处理目标: ${target}`);
1380
- const stringTarget = target;
1381
- // 检查 Bot 是否有发送消息的权限
1382
- const hasPermission = await checkBotPermission(bot, stringTarget);
1383
- if (!hasPermission) {
1384
- ctx.logger.warn(`Bot 没有权限向 ${stringTarget} 发送消息,跳过推送`);
1385
- failedTargets.push(target);
1386
- continue;
1387
- }
1388
- const message = formatPostMessage(post, site.mentionAll, true, site);
1389
- ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
1390
- await bot.sendMessage(stringTarget, message);
1391
- ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
1392
- }
1393
- catch (error) {
1394
- const errorMessage = error instanceof Error ? error.message : String(error);
1395
- ctx.logger.error(`推送文章更新到 ${target} 失败: ${errorMessage}`);
1396
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1397
- failedTargets.push(target);
1398
- // 记录推送失败
1399
- recordPushFailure(`推送文章更新失败到 ${target}: ${errorMessage}`);
1400
- }
1401
- }
1402
- // 如果有失败的目标,添加到失败队列
1403
- if (failedTargets.length > 0) {
1404
- addToFailedQueue('update', post, failedTargets, site);
1405
- }
1406
- // 更新文章更新记录(所有群聊共用一个标记)
1407
- await updatePostUpdateRecord(site.id, String(post.id), postModifiedDate);
1408
- ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
1409
- }
1410
- }
1411
- }
1412
- }
1413
- // 推送新用户注册
1414
- if (site.enableUserPush) {
1415
- const usersResult = await fetchLatestUsers(site);
1416
- if (!usersResult.success) {
1417
- ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的用户失败: ${usersResult.error}`);
1418
- // 记录失败
1419
- recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${usersResult.error}`);
1420
- continue;
1421
- }
1422
- const users = usersResult.data;
1423
- if (users.length > 0) {
1424
- for (const user of users) {
1425
- if (!(await isUserPushed(site.id, String(user.id)))) {
1426
- const failedTargets = [];
1427
- for (const target of site.targets) {
1428
- try {
1429
- ctx.logger.info(`正在处理目标: ${target}`);
1430
- // 直接使用原始目标字符串,与新文章推送逻辑保持一致
1431
- const stringTarget = target;
1432
- // 检查 Bot 是否有发送消息的权限
1433
- const hasPermission = await checkBotPermission(bot, stringTarget);
1434
- if (!hasPermission) {
1435
- ctx.logger.warn(`Bot 没有权限向 ${stringTarget} 发送消息,跳过推送`);
1436
- failedTargets.push(target);
1437
- continue;
1438
- }
1439
- const message = formatUserMessage(user, site.mentionAll, site);
1440
- ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
1441
- await bot.sendMessage(stringTarget, message);
1442
- ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
1443
- }
1444
- catch (error) {
1445
- const errorMessage = error instanceof Error ? error.message : String(error);
1446
- ctx.logger.error(`推送新用户到 ${target} 失败: ${errorMessage}`);
1447
- ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
1448
- failedTargets.push(target);
1449
- // 记录推送失败
1450
- recordPushFailure(`推送新用户失败到 ${target}: ${errorMessage}`);
1451
- }
1452
- }
1453
- // 如果有失败的目标,添加到失败队列
1454
- if (failedTargets.length > 0) {
1455
- addToFailedQueue('user', user, failedTargets, site);
1456
- }
1457
- // 标记用户已推送
1458
- await markUserAsPushed(site.id, String(user.id));
1459
- }
1460
- }
1461
- }
1462
- }
1463
- }
1464
- }
1465
- ctx.command('wordpress.latest [siteId]', '查看最新文章,可选站点 ID')
1466
- .action(async ({ session }, siteId) => {
1467
- ctx.logger.info(`命令 wordpress.latest 被调用,站点 ID: ${siteId || '默认'}`);
1468
- // 选择站点
1469
- const targetSite = siteId
1470
- ? config.sites.find(site => site.id === siteId)
1471
- : config.sites[0];
1472
- if (!targetSite) {
1473
- return `未找到站点 ID: ${siteId}`;
1474
- }
1475
- const postsResult = await fetchLatestPosts(targetSite);
1476
- if (!postsResult.success) {
1477
- ctx.logger.error(`获取最新文章失败: ${postsResult.error}`);
1478
- return `获取文章失败: ${postsResult.error}`;
1479
- }
1480
- const posts = postsResult.data;
1481
- if (posts.length === 0) {
1482
- ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
1483
- return `站点 ${targetSite.name} 暂无文章`;
1484
- }
1485
- // 动态添加文章,确保消息长度不超过500字符
1486
- let message = `📰 ${targetSite.name} 最新文章:\n`;
1487
- let addedCount = 0;
1488
- for (const post of posts) {
1489
- const title = sanitizeContent(post.title.rendered);
1490
- // 自定义日期格式,避免过长
1491
- const date = new Date(post.date);
1492
- const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
1493
- const encodedLink = encodeURI(post.link);
1494
- // 截断标题,避免单条过长
1495
- const truncatedTitle = title.length > CONSTANTS.MAX_POST_TITLE_DISPLAY_LENGTH ? title.substring(0, CONSTANTS.MAX_POST_TITLE_DISPLAY_LENGTH - 3) + '...' : title;
1496
- // 单篇文章的消息片段
1497
- const postMessage = `${truncatedTitle}\n📅 ${formattedDate}\n🔗 ${encodedLink}\n`;
1498
- // 检查添加后是否超过500字符,如果超过则停止添加
1499
- if (message.length + postMessage.length > 500) {
1500
- break;
1501
- }
1502
- message += postMessage;
1503
- addedCount++;
1504
- }
1505
- // 如果有更多文章未显示,添加提示
1506
- if (addedCount < posts.length) {
1507
- message += `... 共 ${posts.length} 篇文章,显示前 ${addedCount} 篇`;
1508
- }
1509
- ctx.logger.info(`准备返回消息,长度: ${message.length},显示 ${addedCount}/${posts.length} 篇文章`);
1510
- return message;
1511
- });
1512
- ctx.command('wordpress.list [siteId]', '查看文章列表,可选站点 ID')
1513
- .action(async (_, siteId) => {
1514
- ctx.logger.info(`命令 wordpress.list 被调用,站点 ID: ${siteId || '默认'}`);
1515
- // 选择站点
1516
- const targetSite = siteId
1517
- ? config.sites.find(site => site.id === siteId)
1518
- : config.sites[0];
1519
- if (!targetSite) {
1520
- return `未找到站点 ID: ${siteId}`;
1521
- }
1522
- const postsResult = await fetchLatestPosts(targetSite);
1523
- if (!postsResult.success) {
1524
- ctx.logger.error(`获取文章列表失败: ${postsResult.error}`);
1525
- return `获取文章失败: ${postsResult.error}`;
1526
- }
1527
- const posts = postsResult.data;
1528
- if (posts.length === 0) {
1529
- return `站点 ${targetSite.name} 暂无文章`;
1530
- }
1531
- // 使用数组拼接消息,便于控制格式和长度
1532
- const messageParts = [`📚 ${targetSite.name} 文章列表:`];
1533
- for (const post of posts) {
1534
- const title = sanitizeContent(post.title.rendered);
1535
- // 截断标题,避免单条过长
1536
- const truncatedTitle = title.length > 50 ? title.substring(0, 47) + '...' : title;
1537
- messageParts.push(`${post.id}. ${truncatedTitle}`);
1538
- }
1539
- let message = messageParts.join('\n');
1540
- // 长度验证,超过 500 字符则精简
1541
- if (message.length > 500) {
1542
- ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1543
- // 只保留前10篇文章
1544
- const shortParts = messageParts.slice(0, 11); // 1个标题 + 10篇文章
1545
- shortParts.push('... 更多文章请查看完整列表');
1546
- message = shortParts.join('\n');
1547
- }
1548
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1549
- return message;
1550
- });
1551
- ctx.command('wordpress.push', '手动推送最新文章')
1552
- .action(async () => {
1553
- ctx.logger.info('命令 wordpress.push 被调用');
1554
- await pushNewPosts();
1555
- return '已检查并推送最新文章';
1556
- });
1557
- ctx.command('wordpress.status [siteId]', '查看插件状态,可选站点 ID')
1558
- .action(({ session }, siteId) => {
1559
- ctx.logger.info(`命令 wordpress.status 被调用,站点 ID: ${siteId || '所有'}`);
1560
- // 获取当前群号,如果有的话
1561
- const currentGroup = session?.channelId || '未知群聊';
1562
- if (siteId) {
1563
- // 显示单个站点状态
1564
- const targetSite = config.sites.find(site => site.id === siteId);
1565
- if (!targetSite) {
1566
- return `未找到站点 ID: ${siteId}`;
1567
- }
1568
- // 使用数组拼接消息,便于控制格式和长度
1569
- const messageParts = [
1570
- `📊 WordPress 插件状态 - ${targetSite.name}`,
1571
- `🌐 站点: ${targetSite.url}`,
1572
- `⏰ 间隔: ${targetSite.interval / 1000} 秒`,
1573
- `🎯 推送目标: ${targetSite.targets.join(', ') || '无'}`,
1574
- `🔔 自动推送: ${targetSite.enableAutoPush ? '开启' : '关闭'}`,
1575
- `🔄 更新推送: ${targetSite.enableUpdatePush ? '开启' : '关闭'}`,
1576
- `👤 用户推送: ${targetSite.enableUserPush ? '开启' : '关闭'}`,
1577
- `📢 @全体: ${targetSite.mentionAll ? '开启' : '关闭'}`,
1578
- `📝 最多推送: ${targetSite.maxArticles} 篇`
1579
- ];
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, 5).join('\n') + '\n... 更多配置请查看完整状态';
1586
- }
1587
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1588
- // 直接返回纯字符串,跳过适配器复杂编码
1589
- return message;
1590
- }
1591
- else {
1592
- // 显示所有站点状态
1593
- let message = '📊 WordPress 插件状态\n\n';
1594
- config.sites.forEach((site, index) => {
1595
- const siteMessage = `站点 ${index + 1}: ${site.name} (${site.id})\n` +
1596
- `🌐 URL: ${site.url}\n` +
1597
- `⏰ 间隔: ${site.interval / 1000} 秒\n` +
1598
- `🎯 目标: ${site.targets.join(', ') || '无'}\n` +
1599
- `🔔 自动: ${site.enableAutoPush ? '开启' : '关闭'}\n` +
1600
- `🔄 更新: ${site.enableUpdatePush ? '开启' : '关闭'}\n` +
1601
- `👤 用户: ${site.enableUserPush ? '开启' : '关闭'}\n\n`;
1602
- // 检查添加后是否超过500字符
1603
- if (message.length + siteMessage.length > 500) {
1604
- message += '... 更多站点请使用站点 ID 查看详细状态';
1605
- return false;
1606
- }
1607
- message += siteMessage;
1608
- });
1609
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1610
- return message;
1611
- }
1612
- });
1613
- ctx.command('wordpress.site.toggle-update <siteId>', '切换指定站点的文章更新推送开关')
1614
- .action(async ({ session }, siteId) => {
1615
- // 检查权限
1616
- const authResult = checkSuperAdmin(session);
1617
- if (!authResult.valid) {
1618
- return authResult.message;
1619
- }
1620
- ctx.logger.info(`命令 wordpress.site.toggle-update 被调用,站点 ID: ${siteId}`);
1621
- // 查找站点
1622
- const site = config.sites.find(s => s.id === siteId);
1623
- if (!site) {
1624
- return `未找到站点 ID: ${siteId}`;
1625
- }
1626
- // 切换开关
1627
- site.enableUpdatePush = !site.enableUpdatePush;
1628
- await saveSiteConfig(siteId, site);
1629
- return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
1630
- });
1631
- ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
1632
- .action(async ({ session }, siteId) => {
1633
- // 检查权限
1634
- const authResult = checkSuperAdmin(session);
1635
- if (!authResult.valid) {
1636
- return authResult.message;
1637
- }
1638
- ctx.logger.info(`命令 wordpress.site.toggle-user 被调用,站点 ID: ${siteId}`);
1639
- // 查找站点
1640
- const site = config.sites.find(s => s.id === siteId);
1641
- if (!site) {
1642
- return `未找到站点 ID: ${siteId}`;
1643
- }
1644
- // 切换开关
1645
- site.enableUserPush = !site.enableUserPush;
1646
- await saveSiteConfig(siteId, site);
1647
- return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
1648
- });
1649
- ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
1650
- .action(async ({ session }, siteId) => {
1651
- // 检查权限
1652
- const authResult = checkSuperAdmin(session);
1653
- if (!authResult.valid) {
1654
- return authResult.message;
1655
- }
1656
- ctx.logger.info(`命令 wordpress.site.toggle 被调用,站点 ID: ${siteId}`);
1657
- // 查找站点
1658
- const site = config.sites.find(s => s.id === siteId);
1659
- if (!site) {
1660
- return `未找到站点 ID: ${siteId}`;
1661
- }
1662
- // 切换开关
1663
- site.enableAutoPush = !site.enableAutoPush;
1664
- await saveSiteConfig(siteId, site);
1665
- return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
1666
- });
1667
- ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
1668
- .action(async ({ session }, siteId) => {
1669
- // 检查权限
1670
- const authResult = checkSuperAdmin(session);
1671
- if (!authResult.valid) {
1672
- return authResult.message;
1673
- }
1674
- ctx.logger.info(`命令 wordpress.site.mention 被调用,站点 ID: ${siteId}`);
1675
- // 查找站点
1676
- const site = config.sites.find(s => s.id === siteId);
1677
- if (!site) {
1678
- return `未找到站点 ID: ${siteId}`;
1679
- }
1680
- // 切换开关
1681
- site.mentionAll = !site.mentionAll;
1682
- await saveSiteConfig(siteId, site);
1683
- return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
1684
- });
1685
- ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
1686
- .action(async ({ session }, siteId, url) => {
1687
- // 检查权限
1688
- const authResult = checkSuperAdmin(session);
1689
- if (!authResult.valid) {
1690
- return authResult.message;
1691
- }
1692
- ctx.logger.info(`命令 wordpress.site.set-url 被调用,站点 ID: ${siteId},新地址:${url}`);
1693
- // 查找站点
1694
- const site = config.sites.find(s => s.id === siteId);
1695
- if (!site) {
1696
- return `未找到站点 ID: ${siteId}`;
1697
- }
1698
- // 修改站点地址
1699
- site.url = url;
1700
- await saveSiteConfig(siteId, site);
1701
- ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
1702
- return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
1703
- });
1704
- ctx.command('wordpress.pushed', '查看已推送的文章列表')
1705
- .action(async () => {
1706
- ctx.logger.info('命令 wordpress.pushed 被调用');
1707
- // 获取已推送的文章记录,使用 wordpress_post_updates 表
1708
- const records = await ctx.database.get('wordpress_post_updates', {}, {
1709
- sort: {
1710
- pushedAt: 'desc'
1711
- }
1712
- });
1713
- if (records.length === 0) {
1714
- return '暂无已推送文章记录';
1715
- }
1716
- // 使用数组拼接消息,便于控制格式和长度
1717
- const messageParts = ['📋 已推送文章列表(按时间倒序):'];
1718
- for (const record of records) {
1719
- // 自定义时间格式,避免特殊分隔符
1720
- const formattedDate = new Date(record.pushedAt);
1721
- const dateStr = `${formattedDate.getFullYear()}-${String(formattedDate.getMonth() + 1).padStart(2, '0')}-${String(formattedDate.getDate()).padStart(2, '0')} ${String(formattedDate.getHours()).padStart(2, '0')}:${String(formattedDate.getMinutes()).padStart(2, '0')}`;
1722
- messageParts.push(`${record.id}. 文章 ID: ${record.postId}`);
1723
- messageParts.push(`📅 推送时间: ${dateStr}`);
1724
- messageParts.push(''); // 空行分隔
1725
- }
1726
- let message = messageParts.join('\n');
1727
- // 长度验证,超过 500 字符则精简,符合 QQ 接口限制
1728
- if (message.length > 500) {
1729
- ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1730
- message = messageParts.slice(0, 8).join('\n') + '\n... 更多记录请查看完整列表';
1731
- }
1732
- return message;
1733
- });
1734
- ctx.command('wordpress.clean [days]', '清理指定天数前的推送记录(默认 30 天)')
1735
- .action(async ({ session }, days) => {
1736
- // 检查权限
1737
- const authResult = checkSuperAdmin(session);
1738
- if (!authResult.valid) {
1739
- return authResult.message;
1740
- }
1741
- ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'},调用者:${authResult.userId}`);
1742
- // 设置默认天数
1743
- const daysToKeep = days ? parseInt(days) : CONSTANTS.DEFAULT_CLEAN_DAYS;
1744
- if (isNaN(daysToKeep) || daysToKeep <= 0 || daysToKeep > 365) {
1745
- return '请输入有效的天数(1-365天)';
1746
- }
1747
- // 计算清理时间点
1748
- const cutoffDate = new Date();
1749
- cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
1750
- // 批量删除旧记录,使用条件删除减少数据库请求次数
1751
- let result = 0;
1752
- // 批量删除 wordpress_post_updates 中的旧记录
1753
- try {
1754
- // 使用分页查询,每次处理 100 条记录,避免内存溢出
1755
- let processed = 0;
1756
- let hasMore = true;
1757
- const queryBatchSize = 100;
1758
- while (hasMore) {
1759
- // 分页查询记录
1760
- const updateRecords = await ctx.database.get('wordpress_post_updates', {}, {
1761
- limit: queryBatchSize,
1762
- offset: processed
1763
- });
1764
- if (updateRecords.length === 0) {
1765
- hasMore = false;
1766
- break;
1767
- }
1768
- // 过滤需要删除的记录
1769
- const recordsToRemove = updateRecords.filter(record => {
1770
- return new Date(record.pushedAt) < cutoffDate;
1771
- });
1772
- if (recordsToRemove.length === 0) {
1773
- processed += updateRecords.length;
1774
- continue;
1775
- }
1776
- // 批量删除,每批最多删除 10 条记录
1777
- const deleteBatchSize = 10;
1778
- for (let i = 0; i < recordsToRemove.length; i += deleteBatchSize) {
1779
- const batch = recordsToRemove.slice(i, i + deleteBatchSize);
1780
- // 并行删除,提高效率
1781
- await Promise.all(batch.map(record => ctx.database.remove('wordpress_post_updates', { id: record.id })));
1782
- }
1783
- result += recordsToRemove.length;
1784
- processed += updateRecords.length;
1785
- ctx.logger.info(`已处理 ${processed} 条记录,删除了 ${recordsToRemove.length} 条旧记录`);
1786
- // 避免数据库压力过大,每批处理后稍作延迟
1787
- await new Promise(resolve => setTimeout(resolve, 100));
1788
- }
1789
- ctx.logger.info(`批量删除 wordpress_post_updates 旧记录完成,共删除 ${result} 条记录`);
1790
- }
1791
- catch (error) {
1792
- ctx.logger.error(`批量删除 wordpress_post_updates 旧记录失败: ${error}`);
1793
- }
1794
- // 批量删除 wordpress_user_registrations 中的旧记录
1795
- try {
1796
- // 使用分页查询,每次处理 100 条记录,避免内存溢出
1797
- let processed = 0;
1798
- let hasMore = true;
1799
- const queryBatchSize = 100;
1800
- while (hasMore) {
1801
- // 分页查询记录
1802
- const userRecords = await ctx.database.get('wordpress_user_registrations', {}, {
1803
- limit: queryBatchSize,
1804
- offset: processed
1805
- });
1806
- if (userRecords.length === 0) {
1807
- hasMore = false;
1808
- break;
1809
- }
1810
- // 过滤需要删除的记录
1811
- const recordsToRemove = userRecords.filter(record => {
1812
- return new Date(record.pushedAt) < cutoffDate;
1813
- });
1814
- if (recordsToRemove.length === 0) {
1815
- processed += userRecords.length;
1816
- continue;
1817
- }
1818
- // 批量删除,每批最多删除 10 条记录
1819
- const deleteBatchSize = 10;
1820
- for (let i = 0; i < recordsToRemove.length; i += deleteBatchSize) {
1821
- const batch = recordsToRemove.slice(i, i + deleteBatchSize);
1822
- // 并行删除,提高效率
1823
- await Promise.all(batch.map(record => ctx.database.remove('wordpress_user_registrations', { id: record.id })));
1824
- }
1825
- result += recordsToRemove.length;
1826
- processed += userRecords.length;
1827
- ctx.logger.info(`已处理 ${processed} 条记录,删除了 ${recordsToRemove.length} 条旧记录`);
1828
- // 避免数据库压力过大,每批处理后稍作延迟
1829
- await new Promise(resolve => setTimeout(resolve, 100));
1830
- }
1831
- ctx.logger.info(`批量删除 wordpress_user_registrations 旧记录完成,共删除 ${result} 条记录`);
1832
- }
1833
- catch (error) {
1834
- ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
1835
- }
1836
- });
1837
- ctx.command('wordpress.stats', '查看插件运行指标统计')
1838
- .action(() => {
1839
- ctx.logger.info('命令 wordpress.stats 被调用');
1840
- // 计算 API 调用成功率
1841
- const apiSuccessRate = runtimeStats.apiCallCount > 0
1842
- ? (runtimeStats.apiSuccessCount / runtimeStats.apiCallCount * 100).toFixed(2)
1843
- : '0.00';
1844
- // 计算推送成功率
1845
- const pushSuccessRate = (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) > 0
1846
- ? (runtimeStats.pushSuccessCount / (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) * 100).toFixed(2)
1847
- : '0.00';
1848
- // 计算统计时间范围
1849
- const startTime = new Date(runtimeStats.lastResetTime);
1850
- 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')}`;
1851
- // 构建统计消息
1852
- const messageParts = [
1853
- '📊 WordPress 插件运行统计',
1854
- `📅 统计开始时间: ${formattedStartTime}`,
1855
- `📈 API 调用: ${runtimeStats.apiCallCount} 次`,
1856
- `✅ API 成功: ${runtimeStats.apiSuccessCount} 次`,
1857
- `❌ API 失败: ${runtimeStats.apiFailureCount} 次`,
1858
- `📊 API 成功率: ${apiSuccessRate}%`,
1859
- `📤 推送成功: ${runtimeStats.pushSuccessCount} 次`,
1860
- `📥 推送失败: ${runtimeStats.pushFailureCount} 次`,
1861
- `📊 推送成功率: ${pushSuccessRate}%`
1862
- ];
1863
- let message = messageParts.join('\n');
1864
- // 长度验证,超过 500 字符则精简
1865
- if (message.length > 500) {
1866
- ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1867
- message = messageParts.slice(0, 6).join('\n') + '\n... 更多统计信息请查看完整数据';
1868
- }
1869
- return message;
1870
- });
1871
- ctx.command('wordpress.health', '查看插件健康状态')
1872
- .action(async () => {
1873
- ctx.logger.info('命令 wordpress.health 被调用');
1874
- // 检查数据库连接
1875
- const dbConnected = await checkDatabaseConnection();
1876
- // 检查 Bot 在线状态
1877
- const bot = getValidBot();
1878
- const botOnline = !!bot;
1879
- // 检查 API 可达性(尝试访问第一个站点的 API)
1880
- let apiReachable = false;
1881
- if (config.sites.length > 0) {
1882
- const firstSite = config.sites[0];
139
+ // 生命周期管理
140
+ ctx.on('ready', async () => {
141
+ ctx.logger.info('WordPress 推送插件 is ready');
142
+ // 插件就绪后执行异步初始化操作
143
+ if (storageService) {
144
+ // 1. 初始化默认推送配置
145
+ await initializeDefaultGroups();
146
+ // 2. 执行清理操作
1883
147
  try {
1884
- const url = `${firstSite.url}/wp-json/wp/v2/posts?per_page=1`;
1885
- const response = await ctx.http.get(url, {
1886
- timeout: 5000
1887
- });
1888
- apiReachable = true;
148
+ await storageService.cleanup();
149
+ ctx.logger.info('Initial cleanup completed');
1889
150
  }
1890
151
  catch (error) {
1891
- ctx.logger.warn(`API 可达性检查失败: ${error}`);
152
+ ctx.logger.error('Error during initial cleanup:', error);
1892
153
  }
1893
154
  }
1894
- // 构建健康状态消息
1895
- const messageParts = [
1896
- '🏥 WordPress 插件健康状态',
1897
- `🗄️ 数据库连接: ${dbConnected ? '✅ 正常' : '❌ 异常'}`,
1898
- `🤖 Bot 在线状态: ${botOnline ? '✅ 在线' : '❌ 离线'}`,
1899
- `🌐 API 可达性: ${apiReachable ? '✅ 可达' : '❌ 不可达'}`
1900
- ];
1901
- // 添加站点状态
1902
- if (config.sites.length > 0) {
1903
- messageParts.push('\n📋 站点状态:');
1904
- config.sites.forEach((site, index) => {
1905
- if (index < 3) { // 只显示前3个站点
1906
- messageParts.push(`${index + 1}. ${site.name}: ${site.enableAutoPush ? '✅ 启用' : '❌ 禁用'}`);
1907
- }
1908
- });
1909
- if (config.sites.length > 3) {
1910
- messageParts.push(`... 共 ${config.sites.length} 个站点`);
1911
- }
1912
- }
1913
- let message = messageParts.join('\n');
1914
- // 长度验证,超过 500 字符则精简
1915
- if (message.length > 500) {
1916
- ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
1917
- message = messageParts.slice(0, 5).join('\n') + '\n... 更多健康状态信息请查看完整数据';
1918
- }
1919
- return message;
1920
155
  });
1921
- ctx.command('wordpress', 'WordPress 推送插件菜单')
1922
- .action(() => {
1923
- ctx.logger.info('命令 wordpress 被调用');
1924
- // 使用数组拼接消息,便于控制格式和长度
1925
- const messageParts = [
1926
- '📚 WordPress 推送插件菜单:',
1927
- '🔹 /wordpress.status - 查看插件状态',
1928
- '🔹 /wordpress.latest - 查看最新文章',
1929
- '🔹 /wordpress.list - 查看文章列表',
1930
- '🔹 /wordpress.push - 手动推送最新文章',
1931
- '🔹 /wordpress.set-url <url> - 修改 WordPress 站点地址',
1932
- '🔹 /wordpress.pushed - 查看已推送文章列表',
1933
- '🔹 /wordpress.clean [days] - 清理旧推送记录',
1934
- '🔹 /wordpress.toggle - 切换自动推送开关(仅超级管理员)',
1935
- '🔹 /wordpress.toggle-update - 切换文章更新推送开关(仅超级管理员)',
1936
- '🔹 /wordpress.toggle-user - 切换新用户注册推送开关(仅超级管理员)',
1937
- '🔹 /wordpress.mention - 切换 @全体 开关(仅超级管理员)',
1938
- '💡 提示:所有命令都需要加 / 前缀'
1939
- ];
1940
- let message = messageParts.join('\n');
1941
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
1942
- return message;
1943
- });
1944
- // 为每个站点设置独立的定时任务,绑定到 ctx 生命周期
1945
- const siteTimers = [];
1946
- config.sites.forEach(site => {
1947
- const timer = ctx.setInterval(() => {
1948
- try {
1949
- pushNewPosts();
1950
- }
1951
- catch (error) {
1952
- const errorMessage = error instanceof Error ? error.message : String(error);
1953
- ctx.logger.error(`站点 ${site.id} 定时任务执行失败:${errorMessage}`);
1954
- ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
1955
- ctx.logger.warn('定时任务将在下一个周期继续执行');
1956
- }
1957
- }, site.interval);
1958
- siteTimers.push(timer);
1959
- });
1960
- // 绑定到 ctx 生命周期,插件卸载时清理所有定时器
1961
- ctx.on('dispose', () => {
1962
- ctx.logger.info('WordPress 插件开始清理资源...');
1963
- // 清理缓存清理定时器
1964
- if (typeof cacheCleanupTimer === 'function') {
1965
- cacheCleanupTimer();
1966
- ctx.logger.info('缓存清理定时器已清理');
1967
- }
1968
- // 清理失败队列定时器
1969
- if (typeof failedQueueTimer === 'function') {
1970
- failedQueueTimer();
1971
- ctx.logger.info('失败队列定时器已清理');
1972
- }
1973
- // 清理站点定时任务
1974
- siteTimers.forEach((timer, index) => {
1975
- if (typeof timer === 'function') {
1976
- timer();
1977
- ctx.logger.info(`站点定时任务 ${index + 1} 已清理`);
1978
- }
1979
- });
1980
- ctx.logger.info('WordPress 插件资源清理完成');
156
+ ctx.on('dispose', async () => {
157
+ ctx.logger.info('WordPress 推送插件 is disposing...');
158
+ cleanupServices();
159
+ ctx.logger.info('WordPress 推送插件 has been disposed');
1981
160
  });
161
+ // 立即初始化核心服务(包括命令注册)
162
+ initializeCoreServices();
1982
163
  }