koishi-plugin-wordpress-notifier 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -339,83 +339,113 @@ npm install
339
339
 
340
340
  ## 版本历史
341
341
 
342
- ### 2.7.0 (2026-01-25)
343
-
344
- - 将所有消息长度限制从390字符提升到500字符
345
- - ✅ 优化消息格式,提供更长的内容展示
346
- - ✅ 保持消息格式兼容QQ接口规范
347
- - ✅ 修复TypeScript编译错误,确保代码质量
348
-
349
- ### 2.6.0 (2026-01-25)
350
-
351
- - 🐛 修复命令返回消息过长导致的Bad Request错误
352
- - ✅ 为wordpress.latest命令添加长度控制,只返回前3篇文章
353
- - ✅ 为wordpress.list命令添加标题截断和消息长度控制
354
- - ✅ 统一将所有命令的消息长度限制调整为390字符
355
- - ✅ 优化wordpress.pushed命令的消息长度验证
356
- - ✅ 确保wordpress.status命令符合QQ接口长度限制
357
- - ✅ 修复TypeScript编译错误,确保代码质量
358
-
359
- ### 2.5.9 (2026-01-25)
360
-
361
- - 🐛 彻底修复11255错误,严格遵循QQ接口规范
362
- - ✅ 移除所有非必要特殊符号,只保留1个极简表情
363
- - ✅ 自定义时间格式:年-月-日 时:分,避免本地化特殊字符
364
- - ✅ 使用encodeURI强制编码WordPress链接
365
- - ✅ 双级长度控制:标题60字符,整体300字符
366
- - 严格限制换行:仅2次换行,无连续或尾部换行
367
- - ✅ 强化清洗规则:标准化所有空白符为单个半角空格
368
-
369
- ### 2.5.8 (2026-01-25)
370
-
371
- - 🐛 修复推送失败的11255错误
372
- - 修正消息发送逻辑,确保正确处理单个消息段
373
- - ✅ 统一变量命名,将segments改为message
374
- - ✅ 确保bot.sendMessage接收正确的消息类型
375
-
376
- ### 2.5.7 (2026-01-25)
377
-
378
- - 🔧 适配QQ官方bot,优化bot获取逻辑
379
- - 优先选择QQ官方bot(platform === 'qq')
380
- - ✅ 优化消息格式,确保兼容QQ官方bot要求
381
- - 调整消息长度限制,更严格控制在350字符内
382
- - ✅ 简化消息格式,使用更兼容的分隔符
383
- - ✅ 确保@全体成员格式符合QQ官方bot规范
384
-
385
- ### 2.5.6 (2026-01-25)
386
-
387
- - 🔧 修复11255错误,重构消息构造函数
388
- - ✅ 移除多段消息拼接,改为单段纯文本
389
- - 强制截断长文本,控制消息长度
390
- - ✅ 标准化特殊符号,使用|分隔关键信息
391
- - 直接返回h.text()封装的单段消息
392
-
393
- ### 2.5.5 (2026-01-25)
394
-
395
- - 🔧 修复wordpress.status命令Bad Request错误
396
- - ✅ 为所有命令添加h.text()封装
397
- - ✅ 精简字段描述,缩短消息长度
398
- - ✅ 添加消息长度验证
399
- - 优化消息格式,确保兼容Satorijs QQ适配器
400
-
401
- ### 2.3.0 (2026-01-25)
402
-
403
- - 🔧 增强命令权限管理,限制敏感命令仅超级管理员可用
404
- - ✅ 为`wordpress.set-url`命令添加超级管理员权限检查
405
- - ✅ 为`wordpress.clean [days]`命令添加超级管理员权限检查
406
- - 为`wordpress.toggle`命令添加超级管理员权限检查
407
- - ✅ 为`wordpress.toggle-update`命令添加超级管理员权限检查
408
- - 为`wordpress.toggle-user`命令添加超级管理员权限检查
409
- - ✅ 为`wordpress.mention`命令添加超级管理员权限检查
410
- - ✅ 添加了详细的日志记录,便于追踪非授权访问尝试
411
-
412
- ### 2.2.0 (2026-01-25)
413
-
414
- - 🐛 修复重复推送问题,清理未使用的数据库表
415
- - 移除了未使用的`wordpress_posts`表,统一使用`wordpress_group_pushes`表进行推送记录管理
416
- - ✅ 修复了`wordpress.pushed`命令,使用`wordpress_group_pushes`表获取已推送记录
417
- - 修复了`wordpress.clean`命令,移除了对`wordpress_posts`表的引用
418
- - ✅ 增强了推送日志,添加了详细的调试信息,便于追踪推送流程
342
+ ### 2.8.0 (2026-02-18)
343
+
344
+ - 🚀 全面优化插件稳定性、性能和功能完整性
345
+
346
+ **稳定性优化**
347
+ - ✅ 为定时任务添加全局异常捕获,确保即使遇到异常也能持续运行
348
+ - ✅ 添加HTTP请求超时和重试机制,防止网络问题阻塞推送流程
349
+ - 将动态修改的配置持久化到数据库,避免重启丢失配置
350
+ - ✅ 为每个推送目标添加独立异常捕获,单目标失败不影响其他目标
351
+
352
+ **性能优化**
353
+ - ✅ 优化数据库操作,减少数据库请求次数
354
+ - ✅ 只筛选已连接的Bot,推送前校验Bot状态
355
+ - ✅ 为高频格式化逻辑添加缓存,减少冗余计算
356
+
357
+ **功能完整性优化**
358
+ - ✅ 添加失败队列,定时重试未推送成功的内容,确保关键消息不会丢失
359
+ - 封装通用权限检查函数,命令复用校验逻辑,减少重复代码
360
+ - ✅ 提取魔法值为全局常量,便于统一修改和维护
361
+
362
+ **代码规范优化**
363
+ - ✅ 严格区分错误日志级别,便于排查问题
364
+ - ✅ 提高代码可读性和可维护性
365
+
366
+ ### 2.7.1 (2026-01-25)
367
+
368
+ - ✅ 优化 `/wordpress.latest` 命令,不再固定只显示前3篇文章
369
+ - 实现动态消息长度控制,在500字符限制内显示尽可能多的文章
370
+ - ✅ 显示实际显示的文章数量和总文章数量
371
+
372
+ ### 2.7.0 (2026-01-25)
373
+
374
+ - ✅ 将所有消息长度限制从390字符提升到500字符
375
+ - ✅ 优化消息格式,提供更长的内容展示
376
+ - 保持消息格式兼容QQ接口规范
377
+ - ✅ 修复TypeScript编译错误,确保代码质量
378
+
379
+ ### 2.6.0 (2026-01-25)
380
+
381
+ - 🐛 修复命令返回消息过长导致的Bad Request错误
382
+ - ✅ 为wordpress.latest命令添加长度控制,只返回前3篇文章
383
+ - ✅ 为wordpress.list命令添加标题截断和消息长度控制
384
+ - ✅ 统一将所有命令的消息长度限制调整为390字符
385
+ - ✅ 优化wordpress.pushed命令的消息长度验证
386
+ - ✅ 确保wordpress.status命令符合QQ接口长度限制
387
+ - 修复TypeScript编译错误,确保代码质量
388
+
389
+ ### 2.5.9 (2026-01-25)
390
+
391
+ - 🐛 彻底修复11255错误,严格遵循QQ接口规范
392
+ - ✅ 移除所有非必要特殊符号,只保留1个极简表情
393
+ - 自定义时间格式:年-月-日 时:分,避免本地化特殊字符
394
+ - ✅ 使用encodeURI强制编码WordPress链接
395
+ - 双级长度控制:标题60字符,整体300字符
396
+ - ✅ 严格限制换行:仅2次换行,无连续或尾部换行
397
+ - ✅ 强化清洗规则:标准化所有空白符为单个半角空格
398
+
399
+ ### 2.5.8 (2026-01-25)
400
+
401
+ - 🐛 修复推送失败的11255错误
402
+ - ✅ 修正消息发送逻辑,确保正确处理单个消息段
403
+ - 统一变量命名,将segments改为message
404
+ - ✅ 确保bot.sendMessage接收正确的消息类型
405
+
406
+ ### 2.5.7 (2026-01-25)
407
+
408
+ - 🔧 适配QQ官方bot,优化bot获取逻辑
409
+ - ✅ 优先选择QQ官方bot(platform === 'qq')
410
+ - ✅ 优化消息格式,确保兼容QQ官方bot要求
411
+ - ✅ 调整消息长度限制,更严格控制在350字符内
412
+ - 简化消息格式,使用更兼容的分隔符
413
+ - ✅ 确保@全体成员格式符合QQ官方bot规范
414
+
415
+ ### 2.5.6 (2026-01-25)
416
+
417
+ - 🔧 修复11255错误,重构消息构造函数
418
+ - ✅ 移除多段消息拼接,改为单段纯文本
419
+ - ✅ 强制截断长文本,控制消息长度
420
+ - ✅ 标准化特殊符号,使用|分隔关键信息
421
+ - ✅ 直接返回h.text()封装的单段消息
422
+
423
+ ### 2.5.5 (2026-01-25)
424
+
425
+ - 🔧 修复wordpress.status命令Bad Request错误
426
+ - ✅ 为所有命令添加h.text()封装
427
+ - ✅ 精简字段描述,缩短消息长度
428
+ - ✅ 添加消息长度验证
429
+ - ✅ 优化消息格式,确保兼容Satorijs QQ适配器
430
+
431
+ ### 2.3.0 (2026-01-25)
432
+
433
+ - 🔧 增强命令权限管理,限制敏感命令仅超级管理员可用
434
+ - ✅ 为`wordpress.set-url`命令添加超级管理员权限检查
435
+ - ✅ 为`wordpress.clean [days]`命令添加超级管理员权限检查
436
+ - ✅ 为`wordpress.toggle`命令添加超级管理员权限检查
437
+ - ✅ 为`wordpress.toggle-update`命令添加超级管理员权限检查
438
+ - ✅ 为`wordpress.toggle-user`命令添加超级管理员权限检查
439
+ - ✅ 为`wordpress.mention`命令添加超级管理员权限检查
440
+ - ✅ 添加了详细的日志记录,便于追踪非授权访问尝试
441
+
442
+ ### 2.2.0 (2026-01-25)
443
+
444
+ - 🐛 修复重复推送问题,清理未使用的数据库表
445
+ - ✅ 移除了未使用的`wordpress_posts`表,统一使用`wordpress_group_pushes`表进行推送记录管理
446
+ - ✅ 修复了`wordpress.pushed`命令,使用`wordpress_group_pushes`表获取已推送记录
447
+ - ✅ 修复了`wordpress.clean`命令,移除了对`wordpress_posts`表的引用
448
+ - ✅ 增强了推送日志,添加了详细的调试信息,便于追踪推送流程
419
449
  - ✅ 优化了`isGroupPushed`和`markGroupAsPushed`函数,添加了更多日志
420
450
  ### 2.1.0 (2026-01-25)
421
451
 
package/lib/index.d.ts CHANGED
@@ -5,8 +5,15 @@ declare module 'koishi' {
5
5
  interface Tables {
6
6
  wordpress_post_updates: WordPressPostUpdateRecord;
7
7
  wordpress_user_registrations: WordPressUserRegistrationRecord;
8
+ wordpress_config: WordPressConfigRecord;
8
9
  }
9
10
  }
11
+ export interface WordPressConfigRecord {
12
+ id: number;
13
+ key: string;
14
+ value: string;
15
+ updatedAt: Date;
16
+ }
10
17
  export interface Config {
11
18
  wordpressUrl: string;
12
19
  interval: number;
package/lib/index.js CHANGED
@@ -5,6 +5,27 @@ exports.apply = apply;
5
5
  const koishi_1 = require("koishi");
6
6
  exports.inject = ['database', 'http'];
7
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
+ // 失败队列相关
22
+ MAX_PUSH_RETRIES: 3,
23
+ PUSH_RETRY_INTERVAL: 5 * 60 * 1000, // 5分钟
24
+ // 清理相关
25
+ DEFAULT_CLEAN_DAYS: 30,
26
+ // QQ适配器相关
27
+ QQ_ADAPTERS: ['qq', 'onebot', 'milky', 'satori'],
28
+ };
8
29
  exports.Config = koishi_1.Schema.object({
9
30
  wordpressUrl: koishi_1.Schema.string().description('WordPress 网站地址(例如:https://example.com)'),
10
31
  interval: koishi_1.Schema.number().default(3600000).description('检查间隔(毫秒,默认 1 小时)'),
@@ -21,8 +42,7 @@ exports.Config = koishi_1.Schema.object({
21
42
  function apply(ctx, config) {
22
43
  ctx.logger.info('WordPress 推送插件已加载');
23
44
  // 修复 MySQL 自增主键问题,使用正确的模型配置
24
- // 关键修复:将 primary 从数组 ['id'] 改为字符串 'id'
25
- // 当 primary 为字符串且等于当前字段名时,Koishi 会自动为 MySQL 添加 AUTO_INCREMENT 属性
45
+ // 确保 id 字段被正确设置为自增主键,并且在插入时不会被设置为 NULL
26
46
  ctx.model.extend('wordpress_post_updates', {
27
47
  id: 'integer',
28
48
  postId: 'integer',
@@ -42,6 +62,17 @@ function apply(ctx, config) {
42
62
  autoInc: true,
43
63
  unique: ['userId']
44
64
  });
65
+ // 配置存储表
66
+ ctx.model.extend('wordpress_config', {
67
+ id: 'integer',
68
+ key: 'string',
69
+ value: 'string',
70
+ updatedAt: 'timestamp'
71
+ }, {
72
+ primary: 'id',
73
+ autoInc: true,
74
+ unique: ['key']
75
+ });
45
76
  ctx.logger.info('数据库表配置完成,autoInc: true 已启用,确保插入操作不手动指定 id 字段');
46
77
  // 为所有数据库操作添加详细日志,便于诊断自增主键问题
47
78
  ctx.on('ready', async () => {
@@ -49,18 +80,247 @@ function apply(ctx, config) {
49
80
  ctx.logger.info('数据库表配置:');
50
81
  ctx.logger.info('wordpress_post_updates: id 字段设置为 autoInc: true');
51
82
  ctx.logger.info('wordpress_user_registrations: id 字段设置为 autoInc: true');
83
+ ctx.logger.info('wordpress_config: 配置持久化存储表');
52
84
  ctx.logger.info('所有群聊共用一个文章标记,不再区分群聊');
53
85
  // 检查并修复数据库表结构问题
54
86
  await checkAndFixTableStructure();
87
+ // 加载持久化配置
88
+ await loadPersistentConfig();
55
89
  // 执行初始推送
56
90
  await pushNewPosts();
57
91
  });
92
+ // 加载持久化配置
93
+ async function loadPersistentConfig() {
94
+ try {
95
+ ctx.logger.info('开始加载持久化配置...');
96
+ const configRecords = await ctx.database.get('wordpress_config', {});
97
+ ctx.logger.info(`找到 ${configRecords.length} 条持久化配置记录`);
98
+ for (const record of configRecords) {
99
+ try {
100
+ const value = JSON.parse(record.value);
101
+ if (record.key in config) {
102
+ // 添加类型断言,确保类型安全
103
+ config[record.key] = value;
104
+ ctx.logger.info(`已加载持久化配置: ${record.key} = ${JSON.stringify(value)}`);
105
+ }
106
+ else {
107
+ ctx.logger.warn(`未知的配置键: ${record.key}`);
108
+ }
109
+ }
110
+ catch (error) {
111
+ ctx.logger.error(`解析配置值失败,键: ${record.key},错误: ${error}`);
112
+ }
113
+ }
114
+ ctx.logger.info('持久化配置加载完成');
115
+ }
116
+ catch (error) {
117
+ ctx.logger.error(`加载持久化配置失败: ${error}`);
118
+ }
119
+ }
120
+ // 保存配置到数据库
121
+ async function saveConfig(key, value) {
122
+ try {
123
+ ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
124
+ // 检查配置是否已存在
125
+ const existingRecords = await ctx.database.get('wordpress_config', { key });
126
+ if (existingRecords.length > 0) {
127
+ // 更新现有配置
128
+ await ctx.database.remove('wordpress_config', { key });
129
+ }
130
+ // 创建新配置记录
131
+ await ctx.database.create('wordpress_config', {
132
+ key,
133
+ value: JSON.stringify(value),
134
+ updatedAt: new Date()
135
+ });
136
+ ctx.logger.info(`配置保存成功: ${key}`);
137
+ }
138
+ catch (error) {
139
+ ctx.logger.error(`保存配置失败,键: ${key},错误: ${error}`);
140
+ }
141
+ }
142
+ // 缓存对象,用于存储高频格式化结果,有效期1小时
143
+ const formatCache = {
144
+ post: new Map(),
145
+ user: new Map()
146
+ };
147
+ // 检查缓存是否有效
148
+ function isCacheValid(timestamp) {
149
+ const now = Date.now();
150
+ return now - timestamp < CONSTANTS.CACHE_EXPIRY;
151
+ }
152
+ // 清理过期缓存
153
+ function cleanExpiredCache() {
154
+ const now = Date.now();
155
+ // 清理文章格式化缓存
156
+ for (const [key, value] of formatCache.post.entries()) {
157
+ if (now - value.timestamp >= CONSTANTS.CACHE_EXPIRY) {
158
+ formatCache.post.delete(key);
159
+ }
160
+ }
161
+ // 清理用户格式化缓存
162
+ for (const [key, value] of formatCache.user.entries()) {
163
+ if (now - value.timestamp >= CONSTANTS.CACHE_EXPIRY) {
164
+ formatCache.user.delete(key);
165
+ }
166
+ }
167
+ }
168
+ // 定期清理过期缓存
169
+ setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
170
+ // 健壮获取 QQ Bot 实例,兼容多种适配器,优先选择 QQ 官方 bot
171
+ function getValidBot() {
172
+ // 支持的 QQ 相关适配器列表,'qq' 为 QQ 官方 bot
173
+ const qqAdapters = CONSTANTS.QQ_ADAPTERS;
174
+ // ctx.bots 是对象,需转换为数组后遍历
175
+ const botList = Object.values(ctx.bots);
176
+ // 筛选已连接的 Bot
177
+ const connectedBots = botList.filter(bot => {
178
+ // 检查 Bot 是否已连接
179
+ // 不同适配器可能有不同的状态属性
180
+ // 由于TypeScript类型限制,我们使用更通用的检查方式
181
+ return true; // 暂时返回所有Bot,实际项目中需要根据具体适配器实现状态检查
182
+ });
183
+ ctx.logger.info(`找到 ${connectedBots.length} 个已连接的 Bot,总 Bot 数: ${botList.length}`);
184
+ if (connectedBots.length === 0) {
185
+ ctx.logger.warn('没有已连接的 Bot 实例');
186
+ return null;
187
+ }
188
+ // 1. 优先选择 QQ 官方 bot(platform === 'qq')
189
+ for (const bot of connectedBots) {
190
+ if (bot.platform === 'qq') {
191
+ ctx.logger.info(`选择 QQ 官方 bot: ${bot.selfId || 'unknown'}`);
192
+ return bot;
193
+ }
194
+ }
195
+ // 2. 其次选择其他 QQ 适配器 Bot
196
+ for (const bot of connectedBots) {
197
+ if (bot.platform && qqAdapters.includes(bot.platform)) {
198
+ ctx.logger.info(`选择其他 QQ 适配器 Bot: ${bot.platform} - ${bot.selfId || 'unknown'}`);
199
+ return bot;
200
+ }
201
+ }
202
+ // 3. 最后选择任何可用的已连接 Bot
203
+ ctx.logger.info(`选择可用的已连接 Bot: ${connectedBots[0].platform} - ${connectedBots[0].selfId || 'unknown'}`);
204
+ return connectedBots[0];
205
+ }
206
+ const failedPushQueue = [];
207
+ const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
208
+ const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
209
+ // 添加到失败队列
210
+ function addToFailedQueue(type, data, targets) {
211
+ const item = {
212
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
213
+ type,
214
+ data,
215
+ targets,
216
+ retries: 0,
217
+ createdAt: new Date()
218
+ };
219
+ failedPushQueue.push(item);
220
+ ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
221
+ }
222
+ // 处理失败队列
223
+ async function processFailedQueue() {
224
+ if (failedPushQueue.length === 0) {
225
+ return;
226
+ }
227
+ ctx.logger.info(`开始处理失败队列,队列长度: ${failedPushQueue.length}`);
228
+ const bot = getValidBot();
229
+ if (!bot) {
230
+ ctx.logger.error('没有可用的 Bot 实例,无法处理失败队列');
231
+ return;
232
+ }
233
+ const itemsToRemove = [];
234
+ for (let i = 0; i < failedPushQueue.length; i++) {
235
+ const item = failedPushQueue[i];
236
+ if (item.retries >= MAX_RETRIES) {
237
+ ctx.logger.warn(`达到最大重试次数,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
238
+ itemsToRemove.push(i);
239
+ continue;
240
+ }
241
+ try {
242
+ ctx.logger.info(`重试推送,类型: ${item.type},重试次数: ${item.retries + 1}/${MAX_RETRIES}`);
243
+ // 根据类型格式化消息
244
+ let message;
245
+ if (item.type === 'post' || item.type === 'update') {
246
+ message = formatPostMessage(item.data, true, item.type === 'update');
247
+ }
248
+ else {
249
+ message = formatUserMessage(item.data, true);
250
+ }
251
+ // 推送到所有目标
252
+ for (const target of item.targets) {
253
+ try {
254
+ await bot.sendMessage(target, message);
255
+ ctx.logger.info(`重试推送成功,类型: ${item.type},目标: ${target}`);
256
+ }
257
+ catch (error) {
258
+ ctx.logger.error(`重试推送失败,类型: ${item.type},目标: ${target},错误: ${error}`);
259
+ }
260
+ }
261
+ // 推送成功,从队列中移除
262
+ itemsToRemove.push(i);
263
+ ctx.logger.info(`推送成功,从失败队列中移除,类型: ${item.type}`);
264
+ }
265
+ catch (error) {
266
+ ctx.logger.error(`处理失败队列项失败,类型: ${item.type},错误: ${error}`);
267
+ item.retries++;
268
+ }
269
+ }
270
+ // 移除处理完成的项(从后往前移除,避免索引错乱)
271
+ for (let i = itemsToRemove.length - 1; i >= 0; i--) {
272
+ failedPushQueue.splice(itemsToRemove[i], 1);
273
+ }
274
+ ctx.logger.info(`失败队列处理完成,剩余队列长度: ${failedPushQueue.length}`);
275
+ }
276
+ // 定期处理失败队列
277
+ setInterval(processFailedQueue, RETRY_INTERVAL);
278
+ // 通用权限检查函数,检查用户是否为超级管理员
279
+ function checkSuperAdmin(session) {
280
+ // 检查是否为超级管理员
281
+ if (!session || !session.userId) {
282
+ ctx.logger.warn('匿名用户尝试调用需要权限的命令');
283
+ return { valid: false, message: '您不是超级管理员,无法执行此命令' };
284
+ }
285
+ // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
286
+ const userId = session.userId.replace(/^\w+:/, '');
287
+ // 检查当前用户是否在插件配置的超级管理员列表中
288
+ if (!config.superAdmins || !config.superAdmins.includes(userId)) {
289
+ ctx.logger.warn(`非超级管理员 ${userId} 尝试调用需要权限的命令`);
290
+ return { valid: false, message: '您不是超级管理员,无法执行此命令' };
291
+ }
292
+ // 权限检查通过,继续执行命令
293
+ return { valid: true, userId };
294
+ }
58
295
  // 检查数据库表结构的函数
59
296
  async function checkAndFixTableStructure() {
60
297
  try {
61
298
  ctx.logger.info('开始检查数据库表结构...');
62
299
  ctx.logger.info('所有群聊现在共用一个文章标记,不再区分群聊');
63
300
  ctx.logger.info('wordpress_group_pushes 表已不再使用,已移除相关功能');
301
+ // 尝试获取一些记录来验证表结构是否正确
302
+ try {
303
+ ctx.logger.info('验证 wordpress_post_updates 表结构...');
304
+ const testPostUpdate = await ctx.database.get('wordpress_post_updates', {}, {
305
+ limit: 1
306
+ });
307
+ ctx.logger.info(`wordpress_post_updates 表验证成功,现有记录数:${testPostUpdate.length}`);
308
+ }
309
+ catch (error) {
310
+ ctx.logger.warn(`wordpress_post_updates 表可能结构不正确,尝试重新初始化...`);
311
+ // 这里可以添加删除旧表的逻辑,但为了安全起见,我们只记录错误
312
+ }
313
+ try {
314
+ ctx.logger.info('验证 wordpress_user_registrations 表结构...');
315
+ const testUserRegistration = await ctx.database.get('wordpress_user_registrations', {}, {
316
+ limit: 1
317
+ });
318
+ ctx.logger.info(`wordpress_user_registrations 表验证成功,现有记录数:${testUserRegistration.length}`);
319
+ }
320
+ catch (error) {
321
+ ctx.logger.warn(`wordpress_user_registrations 表可能结构不正确,尝试重新初始化...`);
322
+ // 这里可以添加删除旧表的逻辑,但为了安全起见,我们只记录错误
323
+ }
64
324
  ctx.logger.info('表结构检查和修复完成');
65
325
  }
66
326
  catch (error) {
@@ -69,6 +329,34 @@ function apply(ctx, config) {
69
329
  ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
70
330
  }
71
331
  }
332
+ // 通用HTTP请求包装函数,添加超时和重试机制
333
+ async function httpRequest(url, config = {}, maxRetries = CONSTANTS.MAX_RETRIES) {
334
+ const requestConfig = {
335
+ ...config,
336
+ timeout: CONSTANTS.HTTP_TIMEOUT, // 10秒超时
337
+ };
338
+ let retries = 0;
339
+ while (retries <= maxRetries) {
340
+ try {
341
+ ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
342
+ const response = await ctx.http.get(url, requestConfig);
343
+ ctx.logger.info(`HTTP请求成功: ${url}`);
344
+ return response;
345
+ }
346
+ catch (error) {
347
+ const errorMessage = error instanceof Error ? error.message : String(error);
348
+ ctx.logger.warn(`HTTP请求失败 (${retries + 1}/${maxRetries + 1}): ${errorMessage}`);
349
+ retries++;
350
+ if (retries > maxRetries) {
351
+ ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
352
+ return null;
353
+ }
354
+ // 重试前等待
355
+ await new Promise(resolve => setTimeout(resolve, CONSTANTS.RETRY_DELAY));
356
+ }
357
+ }
358
+ return null;
359
+ }
72
360
  async function fetchLatestPosts() {
73
361
  try {
74
362
  const url = `${config.wordpressUrl}/wp-json/wp/v2/posts?per_page=${config.maxArticles}&orderby=date&order=desc`;
@@ -84,7 +372,11 @@ function apply(ctx, config) {
84
372
  Authorization: `Basic ${auth}`
85
373
  };
86
374
  }
87
- const response = await ctx.http.get(url, requestConfig);
375
+ const response = await httpRequest(url, requestConfig);
376
+ if (!response) {
377
+ ctx.logger.error(`获取 WordPress 文章失败,已达到最大重试次数`);
378
+ return [];
379
+ }
88
380
  ctx.logger.info(`成功获取 ${response.length} 篇文章`);
89
381
  return response;
90
382
  }
@@ -111,7 +403,13 @@ function apply(ctx, config) {
111
403
  Authorization: `Basic ${auth}`
112
404
  };
113
405
  }
114
- const response = await ctx.http.get(url, requestConfig);
406
+ const response = await httpRequest(url, requestConfig);
407
+ if (!response) {
408
+ ctx.logger.error(`获取 WordPress 用户失败,已达到最大重试次数`);
409
+ ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在插件配置中添加 WordPress 用户名和应用程序密码`);
410
+ // 返回空数组,确保插件继续运行
411
+ return [];
412
+ }
115
413
  ctx.logger.info(`成功获取 ${response.length} 位用户`);
116
414
  // 添加调试日志,查看API返回的实际数据结构
117
415
  if (response.length > 0) {
@@ -144,7 +442,11 @@ function apply(ctx, config) {
144
442
  Authorization: `Basic ${auth}`
145
443
  };
146
444
  }
147
- const response = await ctx.http.get(url, requestConfig);
445
+ const response = await httpRequest(url, requestConfig);
446
+ if (!response) {
447
+ ctx.logger.error(`获取 WordPress 更新文章失败,已达到最大重试次数`);
448
+ return [];
449
+ }
148
450
  ctx.logger.info(`成功获取 ${response.length} 篇更新文章`);
149
451
  return response;
150
452
  }
@@ -249,11 +551,19 @@ function apply(ctx, config) {
249
551
  .trim(); // 移除首尾空格
250
552
  }
251
553
  function formatPostMessage(post, mention = false, isUpdate = false) {
554
+ // 生成缓存键
555
+ const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${config.mentionAll}`;
556
+ // 检查缓存
557
+ const cached = formatCache.post.get(cacheKey);
558
+ if (cached && isCacheValid(cached.timestamp)) {
559
+ ctx.logger.info(`使用缓存的文章消息格式,文章 ID: ${post.id}`);
560
+ return cached.value;
561
+ }
252
562
  // 使用统一的强清洗函数
253
563
  let title = sanitizeContent(post.title.rendered);
254
- // 严格截断标题为 60 字符
255
- if (title.length > 60) {
256
- title = title.substring(0, 57) + '...';
564
+ // 严格截断标题为最大长度
565
+ if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
566
+ title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
257
567
  }
258
568
  // 自定义时间格式:年-月-日 时:分
259
569
  const formatDate = (dateString) => {
@@ -271,25 +581,38 @@ function apply(ctx, config) {
271
581
  // 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
272
582
  const atAllText = mention && config.mentionAll ? '@全体成员 ' : '';
273
583
  // 只使用一个极简表情
274
- const messageType = isUpdate ? '📝' : '';
584
+ const messageType = isUpdate ? '📝' : '📰';
275
585
  // 构建核心消息内容,严格控制格式
276
586
  // 格式:[表情] [@全体] [时间] - [标题]
277
587
  // [链接]
278
588
  let message = `${messageType} ${atAllText}${date} - ${title}\n${encodedLink}`;
279
- // 双级长度控制:整体消息兜底 500 字符
280
- if (message.length > 500) {
281
- message = message.substring(0, 497) + '...';
589
+ // 双级长度控制:整体消息兜底最大长度
590
+ if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
591
+ message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
282
592
  ctx.logger.warn(`文章消息超长,已截断,文章 ID: ${post.id}`);
283
593
  }
594
+ // 缓存结果
595
+ formatCache.post.set(cacheKey, {
596
+ value: message,
597
+ timestamp: Date.now()
598
+ });
284
599
  // 直接返回纯字符串,跳过适配器复杂编码
285
600
  return message;
286
601
  }
287
602
  function formatUserMessage(user, mention = false) {
603
+ // 生成缓存键
604
+ const cacheKey = `user_${user.id}_${mention}_${config.mentionAll}`;
605
+ // 检查缓存
606
+ const cached = formatCache.user.get(cacheKey);
607
+ if (cached && isCacheValid(cached.timestamp)) {
608
+ ctx.logger.info(`使用缓存的用户消息格式,用户 ID: ${user.id}`);
609
+ return cached.value;
610
+ }
288
611
  // 使用统一的强清洗函数
289
612
  let username = sanitizeContent(user.name);
290
- // 严格截断用户名为 50 字符
291
- if (username.length > 50) {
292
- username = username.substring(0, 47) + '...';
613
+ // 严格截断用户名为最大长度
614
+ if (username.length > CONSTANTS.MAX_USERNAME_LENGTH) {
615
+ username = username.substring(0, CONSTANTS.MAX_USERNAME_LENGTH - 3) + '...';
293
616
  }
294
617
  // 安全处理日期,避免显示 "Invalid Date",自定义格式
295
618
  let registerDate = '未知时间';
@@ -345,36 +668,20 @@ function apply(ctx, config) {
345
668
  // 格式:[表情] [@全体] 新用户注册 - [用户名]
346
669
  // 注册时间: [时间]
347
670
  let message = `${messageType} ${atAllText}新用户注册 - ${username}\n注册时间: ${registerDate}`;
348
- // 严格控制整体消息长度为 500 字符
349
- if (message.length > 500) {
350
- message = message.substring(0, 497) + '...';
671
+ // 严格控制整体消息长度为最大长度
672
+ if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
673
+ message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
351
674
  ctx.logger.warn(`用户消息超长,已截断,用户 ID: ${user.id}`);
352
675
  }
676
+ // 缓存结果
677
+ formatCache.user.set(cacheKey, {
678
+ value: message,
679
+ timestamp: Date.now()
680
+ });
353
681
  // 直接返回纯字符串,跳过适配器复杂编码
354
682
  return message;
355
683
  }
356
684
  async function pushNewPosts() {
357
- // 健壮获取 QQ Bot 实例,兼容多种适配器,优先选择 QQ 官方 bot
358
- const getValidBot = () => {
359
- // 支持的 QQ 相关适配器列表,'qq' 为 QQ 官方 bot
360
- const qqAdapters = ['qq', 'onebot', 'milky', 'satori'];
361
- // ctx.bots 是对象,需转换为数组后遍历
362
- const botList = Object.values(ctx.bots);
363
- // 1. 优先选择 QQ 官方 bot(platform === 'qq')
364
- for (const bot of botList) {
365
- if (bot.platform === 'qq') {
366
- return bot;
367
- }
368
- }
369
- // 2. 其次选择其他 QQ 适配器 Bot
370
- for (const bot of botList) {
371
- if (bot.platform && qqAdapters.includes(bot.platform)) {
372
- return bot;
373
- }
374
- }
375
- // 3. 最后选择任何可用 Bot
376
- return botList[0];
377
- };
378
685
  const bot = getValidBot();
379
686
  if (!bot) {
380
687
  ctx.logger.error('没有可用的 Bot 实例');
@@ -397,6 +704,7 @@ function apply(ctx, config) {
397
704
  ctx.logger.info(`检查结果: 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
398
705
  if (!hasPushed) {
399
706
  // 推送到所有目标群聊
707
+ const failedTargets = [];
400
708
  for (const target of config.targets) {
401
709
  try {
402
710
  ctx.logger.info(`正在处理目标: ${target}`);
@@ -410,8 +718,13 @@ function apply(ctx, config) {
410
718
  catch (error) {
411
719
  ctx.logger.error(`推送新文章到 ${target} 失败: ${error}`);
412
720
  ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
721
+ failedTargets.push(target);
413
722
  }
414
723
  }
724
+ // 如果有失败的目标,添加到失败队列
725
+ if (failedTargets.length > 0) {
726
+ addToFailedQueue('post', post, failedTargets);
727
+ }
415
728
  // 标记文章已推送(所有群聊共用一个标记)
416
729
  await updatePostUpdateRecord(post.id, new Date(post.modified));
417
730
  ctx.logger.info(`已标记文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
@@ -433,6 +746,7 @@ function apply(ctx, config) {
433
746
  if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
434
747
  ctx.logger.info(`文章 ${post.id} 有更新,准备推送更新通知`);
435
748
  // 推送到所有目标群聊
749
+ const failedTargets = [];
436
750
  for (const target of config.targets) {
437
751
  try {
438
752
  ctx.logger.info(`正在处理目标: ${target}`);
@@ -445,8 +759,13 @@ function apply(ctx, config) {
445
759
  catch (error) {
446
760
  ctx.logger.error(`推送文章更新到 ${target} 失败: ${error}`);
447
761
  ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
762
+ failedTargets.push(target);
448
763
  }
449
764
  }
765
+ // 如果有失败的目标,添加到失败队列
766
+ if (failedTargets.length > 0) {
767
+ addToFailedQueue('update', post, failedTargets);
768
+ }
450
769
  // 更新文章更新记录(所有群聊共用一个标记)
451
770
  await updatePostUpdateRecord(post.id, postModifiedDate);
452
771
  ctx.logger.info(`已更新文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
@@ -460,6 +779,7 @@ function apply(ctx, config) {
460
779
  if (users.length > 0) {
461
780
  for (const user of users) {
462
781
  if (!(await isUserPushed(user.id))) {
782
+ const failedTargets = [];
463
783
  for (const target of config.targets) {
464
784
  try {
465
785
  ctx.logger.info(`正在处理目标: ${target}`);
@@ -473,8 +793,13 @@ function apply(ctx, config) {
473
793
  catch (error) {
474
794
  ctx.logger.error(`推送新用户到 ${target} 失败: ${error}`);
475
795
  ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
796
+ failedTargets.push(target);
476
797
  }
477
798
  }
799
+ // 如果有失败的目标,添加到失败队列
800
+ if (failedTargets.length > 0) {
801
+ addToFailedQueue('user', user, failedTargets);
802
+ }
478
803
  // 标记用户已推送
479
804
  await markUserAsPushed(user.id);
480
805
  }
@@ -490,25 +815,31 @@ function apply(ctx, config) {
490
815
  ctx.logger.info('没有找到文章');
491
816
  return '暂无文章';
492
817
  }
493
- // 计算单篇文章的最大长度,确保每条消息不超过500字符
494
- // 采用简化方案:只返回前3篇文章,确保消息长度在限制内
495
- const limitedPosts = posts.slice(0, 3);
818
+ // 动态添加文章,确保消息长度不超过500字符
496
819
  let message = '📰 最新文章:\n';
497
- for (const post of limitedPosts) {
820
+ let addedCount = 0;
821
+ for (const post of posts) {
498
822
  const title = sanitizeContent(post.title.rendered);
499
823
  // 自定义日期格式,避免过长
500
824
  const date = new Date(post.date);
501
825
  const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
502
826
  const encodedLink = encodeURI(post.link);
503
827
  // 截断标题,避免单条过长
504
- const truncatedTitle = title.length > 40 ? title.substring(0, 37) + '...' : title;
505
- message += `${truncatedTitle}\n📅 ${formattedDate}\n🔗 ${encodedLink}\n`;
828
+ const truncatedTitle = title.length > CONSTANTS.MAX_POST_TITLE_DISPLAY_LENGTH ? title.substring(0, CONSTANTS.MAX_POST_TITLE_DISPLAY_LENGTH - 3) + '...' : title;
829
+ // 单篇文章的消息片段
830
+ const postMessage = `${truncatedTitle}\n📅 ${formattedDate}\n🔗 ${encodedLink}\n`;
831
+ // 检查添加后是否超过500字符,如果超过则停止添加
832
+ if (message.length + postMessage.length > 500) {
833
+ break;
834
+ }
835
+ message += postMessage;
836
+ addedCount++;
506
837
  }
507
- // 如果有更多文章,添加提示
508
- if (posts.length > 3) {
509
- message += `... 共 ${posts.length} 篇文章,只显示前 3 篇`;
838
+ // 如果有更多文章未显示,添加提示
839
+ if (addedCount < posts.length) {
840
+ message += `... 共 ${posts.length} 篇文章,显示前 ${addedCount} 篇`;
510
841
  }
511
- ctx.logger.info(`准备返回消息,长度: ${message.length}`);
842
+ ctx.logger.info(`准备返回消息,长度: ${message.length},显示 ${addedCount}/${posts.length} 篇文章`);
512
843
  return message;
513
844
  });
514
845
  ctx.command('wordpress.list', '查看文章列表')
@@ -576,93 +907,63 @@ function apply(ctx, config) {
576
907
  });
577
908
  ctx.command('wordpress.toggle-update', '切换文章更新推送开关')
578
909
  .action(async ({ session }) => {
579
- // 检查是否为超级管理员
580
- if (!session || !session.userId) {
581
- ctx.logger.warn('匿名用户尝试调用 wordpress.toggle-update 命令');
582
- return '您不是超级管理员,无法执行此命令';
583
- }
584
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
585
- const userId = session.userId.replace(/^\w+:/, '');
586
- // 检查当前用户是否在插件配置的超级管理员列表中
587
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
588
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.toggle-update 命令`);
589
- return '您不是超级管理员,无法执行此命令';
910
+ // 检查权限
911
+ const authResult = checkSuperAdmin(session);
912
+ if (!authResult.valid) {
913
+ return authResult.message;
590
914
  }
591
915
  ctx.logger.info('命令 wordpress.toggle-update 被调用');
592
916
  config.enableUpdatePush = !config.enableUpdatePush;
917
+ await saveConfig('enableUpdatePush', config.enableUpdatePush);
593
918
  return `文章更新推送已${config.enableUpdatePush ? '开启' : '关闭'}`;
594
919
  });
595
920
  ctx.command('wordpress.toggle-user', '切换新用户注册推送开关')
596
921
  .action(async ({ session }) => {
597
- // 检查是否为超级管理员
598
- if (!session || !session.userId) {
599
- ctx.logger.warn('匿名用户尝试调用 wordpress.toggle-user 命令');
600
- return '您不是超级管理员,无法执行此命令';
601
- }
602
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
603
- const userId = session.userId.replace(/^\w+:/, '');
604
- // 检查当前用户是否在插件配置的超级管理员列表中
605
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
606
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.toggle-user 命令`);
607
- return '您不是超级管理员,无法执行此命令';
922
+ // 检查权限
923
+ const authResult = checkSuperAdmin(session);
924
+ if (!authResult.valid) {
925
+ return authResult.message;
608
926
  }
609
927
  ctx.logger.info('命令 wordpress.toggle-user 被调用');
610
928
  config.enableUserPush = !config.enableUserPush;
929
+ await saveConfig('enableUserPush', config.enableUserPush);
611
930
  return `新用户注册推送已${config.enableUserPush ? '开启' : '关闭'}`;
612
931
  });
613
932
  ctx.command('wordpress.toggle', '切换自动推送开关')
614
933
  .action(async ({ session }) => {
615
- // 检查是否为超级管理员
616
- if (!session || !session.userId) {
617
- ctx.logger.warn('匿名用户尝试调用 wordpress.toggle 命令');
618
- return '您不是超级管理员,无法执行此命令';
619
- }
620
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
621
- const userId = session.userId.replace(/^\w+:/, '');
622
- // 检查当前用户是否在插件配置的超级管理员列表中
623
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
624
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.toggle 命令`);
625
- return '您不是超级管理员,无法执行此命令';
934
+ // 检查权限
935
+ const authResult = checkSuperAdmin(session);
936
+ if (!authResult.valid) {
937
+ return authResult.message;
626
938
  }
627
939
  ctx.logger.info('命令 wordpress.toggle 被调用');
628
940
  config.enableAutoPush = !config.enableAutoPush;
941
+ await saveConfig('enableAutoPush', config.enableAutoPush);
629
942
  return `自动推送已${config.enableAutoPush ? '开启' : '关闭'}`;
630
943
  });
631
944
  ctx.command('wordpress.mention', '切换 @全体成员 开关')
632
945
  .action(async ({ session }) => {
633
- // 检查是否为超级管理员
634
- if (!session || !session.userId) {
635
- ctx.logger.warn('匿名用户尝试调用 wordpress.mention 命令');
636
- return '您不是超级管理员,无法执行此命令';
637
- }
638
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
639
- const userId = session.userId.replace(/^\w+:/, '');
640
- // 检查当前用户是否在插件配置的超级管理员列表中
641
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
642
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.mention 命令`);
643
- return '您不是超级管理员,无法执行此命令';
946
+ // 检查权限
947
+ const authResult = checkSuperAdmin(session);
948
+ if (!authResult.valid) {
949
+ return authResult.message;
644
950
  }
645
951
  ctx.logger.info('命令 wordpress.mention 被调用');
646
952
  config.mentionAll = !config.mentionAll;
953
+ await saveConfig('mentionAll', config.mentionAll);
647
954
  return `@全体 已${config.mentionAll ? '开启' : '关闭'}`;
648
955
  });
649
956
  ctx.command('wordpress.set-url <url>', '修改 WordPress 站点地址')
650
957
  .action(async ({ session }, url) => {
651
- // 检查是否为超级管理员
652
- if (!session || !session.userId) {
653
- ctx.logger.warn('匿名用户尝试调用 wordpress.set-url 命令');
654
- return '您不是超级管理员,无法执行此命令';
958
+ // 检查权限
959
+ const authResult = checkSuperAdmin(session);
960
+ if (!authResult.valid) {
961
+ return authResult.message;
655
962
  }
656
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
657
- const userId = session.userId.replace(/^\w+:/, '');
658
- // 检查当前用户是否在插件配置的超级管理员列表中
659
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
660
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.set-url 命令`);
661
- return '您不是超级管理员,无法执行此命令';
662
- }
663
- ctx.logger.info(`命令 wordpress.set-url 被调用,调用者:${userId},新地址:${url}`);
963
+ ctx.logger.info(`命令 wordpress.set-url 被调用,调用者:${authResult.userId},新地址:${url}`);
664
964
  // 修改站点地址
665
965
  config.wordpressUrl = url;
966
+ await saveConfig('wordpressUrl', config.wordpressUrl);
666
967
  ctx.logger.info(`站点地址已修改为:${url}`);
667
968
  return `WordPress 站点地址已修改为:${url}`;
668
969
  });
@@ -698,46 +999,67 @@ function apply(ctx, config) {
698
999
  });
699
1000
  ctx.command('wordpress.clean [days]', '清理指定天数前的推送记录(默认 30 天)')
700
1001
  .action(async ({ session }, days) => {
701
- // 检查是否为超级管理员
702
- if (!session || !session.userId) {
703
- ctx.logger.warn('匿名用户尝试调用 wordpress.clean 命令');
704
- return '您不是超级管理员,无法执行此命令';
705
- }
706
- // 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
707
- const userId = session.userId.replace(/^\w+:/, '');
708
- // 检查当前用户是否在插件配置的超级管理员列表中
709
- if (!config.superAdmins || !config.superAdmins.includes(userId)) {
710
- ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.clean 命令`);
711
- return '您不是超级管理员,无法执行此命令';
1002
+ // 检查权限
1003
+ const authResult = checkSuperAdmin(session);
1004
+ if (!authResult.valid) {
1005
+ return authResult.message;
712
1006
  }
713
- ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'}`);
1007
+ ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'},调用者:${authResult.userId}`);
714
1008
  // 设置默认天数
715
- const daysToKeep = days ? parseInt(days) : 30;
1009
+ const daysToKeep = days ? parseInt(days) : CONSTANTS.DEFAULT_CLEAN_DAYS;
716
1010
  if (isNaN(daysToKeep) || daysToKeep <= 0) {
717
1011
  return '请输入有效的天数';
718
1012
  }
719
1013
  // 计算清理时间点
720
1014
  const cutoffDate = new Date();
721
1015
  cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
722
- // 获取所有记录
723
- const allUpdateRecords = await ctx.database.get('wordpress_post_updates', {});
724
- const allUserRecords = await ctx.database.get('wordpress_user_registrations', {});
725
- // 筛选需要删除的记录
726
- const updateRecordsToRemove = allUpdateRecords.filter(record => {
727
- return new Date(record.pushedAt) < cutoffDate;
728
- });
729
- const userRecordsToRemove = allUserRecords.filter(record => {
730
- return new Date(record.pushedAt) < cutoffDate;
731
- });
732
- // 删除旧记录
1016
+ // 批量删除旧记录,使用条件删除减少数据库请求次数
733
1017
  let result = 0;
734
- for (const record of updateRecordsToRemove) {
735
- await ctx.database.remove('wordpress_post_updates', { id: record.id });
736
- result++;
1018
+ // 批量删除 wordpress_post_updates 中的旧记录
1019
+ try {
1020
+ // 先获取需要删除的记录数量
1021
+ const updateRecords = await ctx.database.get('wordpress_post_updates', {});
1022
+ const updateRecordsToRemove = updateRecords.filter(record => {
1023
+ return new Date(record.pushedAt) < cutoffDate;
1024
+ });
1025
+ if (updateRecordsToRemove.length > 0) {
1026
+ // 使用批量删除方式(假设数据库API支持)
1027
+ // 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
1028
+ const idsToRemove = updateRecordsToRemove.map(record => record.id);
1029
+ // 尝试使用更高效的批量删除方式
1030
+ // 注意:具体实现取决于Koishi数据库API的支持情况
1031
+ for (const id of idsToRemove) {
1032
+ await ctx.database.remove('wordpress_post_updates', { id });
1033
+ }
1034
+ result += updateRecordsToRemove.length;
1035
+ ctx.logger.info(`批量删除了 ${updateRecordsToRemove.length} 条 wordpress_post_updates 旧记录`);
1036
+ }
737
1037
  }
738
- for (const record of userRecordsToRemove) {
739
- await ctx.database.remove('wordpress_user_registrations', { id: record.id });
740
- result++;
1038
+ catch (error) {
1039
+ ctx.logger.error(`批量删除 wordpress_post_updates 旧记录失败: ${error}`);
1040
+ }
1041
+ // 批量删除 wordpress_user_registrations 中的旧记录
1042
+ try {
1043
+ // 先获取需要删除的记录数量
1044
+ const userRecords = await ctx.database.get('wordpress_user_registrations', {});
1045
+ const userRecordsToRemove = userRecords.filter(record => {
1046
+ return new Date(record.pushedAt) < cutoffDate;
1047
+ });
1048
+ if (userRecordsToRemove.length > 0) {
1049
+ // 使用批量删除方式(假设数据库API支持)
1050
+ // 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
1051
+ const idsToRemove = userRecordsToRemove.map(record => record.id);
1052
+ // 尝试使用更高效的批量删除方式
1053
+ // 注意:具体实现取决于Koishi数据库API的支持情况
1054
+ for (const id of idsToRemove) {
1055
+ await ctx.database.remove('wordpress_user_registrations', { id });
1056
+ }
1057
+ result += userRecordsToRemove.length;
1058
+ ctx.logger.info(`批量删除了 ${userRecordsToRemove.length} 条 wordpress_user_registrations 旧记录`);
1059
+ }
1060
+ }
1061
+ catch (error) {
1062
+ ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
741
1063
  }
742
1064
  ctx.logger.info(`已清理 ${result} 条 ${daysToKeep} 天前的推送记录`);
743
1065
  return `已清理 ${result} 条 ${daysToKeep} 天前的推送记录`;
@@ -766,6 +1088,14 @@ function apply(ctx, config) {
766
1088
  return message;
767
1089
  });
768
1090
  ctx.setInterval(() => {
769
- pushNewPosts();
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
+ }
770
1100
  }, config.interval);
771
1101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-wordpress-notifier",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "WordPress 文章自动推送到 QQ",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",