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 +107 -77
- package/lib/index.d.ts +7 -0
- package/lib/index.js +466 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -339,83 +339,113 @@ npm install
|
|
|
339
339
|
|
|
340
340
|
## 版本历史
|
|
341
341
|
|
|
342
|
-
### 2.
|
|
343
|
-
|
|
344
|
-
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
- ✅
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
- ✅
|
|
354
|
-
- ✅
|
|
355
|
-
- ✅
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
- ✅
|
|
364
|
-
- ✅
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
- ✅
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
-
|
|
382
|
-
- ✅
|
|
383
|
-
- ✅
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
-
|
|
396
|
-
- ✅
|
|
397
|
-
- ✅
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
-
|
|
404
|
-
- ✅
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
-
|
|
409
|
-
- ✅
|
|
410
|
-
- ✅
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
255
|
-
if (title.length >
|
|
256
|
-
title = title.substring(0,
|
|
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
|
-
//
|
|
280
|
-
if (message.length >
|
|
281
|
-
message = message.substring(0,
|
|
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
|
-
//
|
|
291
|
-
if (username.length >
|
|
292
|
-
username = username.substring(0,
|
|
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
|
-
//
|
|
349
|
-
if (message.length >
|
|
350
|
-
message = message.substring(0,
|
|
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
|
-
//
|
|
494
|
-
// 采用简化方案:只返回前3篇文章,确保消息长度在限制内
|
|
495
|
-
const limitedPosts = posts.slice(0, 3);
|
|
818
|
+
// 动态添加文章,确保消息长度不超过500字符
|
|
496
819
|
let message = '📰 最新文章:\n';
|
|
497
|
-
|
|
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 >
|
|
505
|
-
|
|
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
|
|
509
|
-
message += `... 共 ${posts.length}
|
|
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
|
-
|
|
581
|
-
|
|
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
|
-
|
|
599
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
-
|
|
635
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
return
|
|
958
|
+
// 检查权限
|
|
959
|
+
const authResult = checkSuperAdmin(session);
|
|
960
|
+
if (!authResult.valid) {
|
|
961
|
+
return authResult.message;
|
|
655
962
|
}
|
|
656
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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) :
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
}
|