koishi-plugin-wordpress-notifier 2.7.1 → 2.8.1
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 +39 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +530 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -339,6 +339,45 @@ npm install
|
|
|
339
339
|
|
|
340
340
|
## 版本历史
|
|
341
341
|
|
|
342
|
+
### 2.8.1 (2026-02-19)
|
|
343
|
+
|
|
344
|
+
- 🐛 修复 MySQL 主键约束错误 (ER_PRIMARY_CANT_HAVE_NULL)
|
|
345
|
+
|
|
346
|
+
**核心修复**
|
|
347
|
+
- ✅ 增强 `checkAndFixTableStructure` 函数,添加表结构自动修复机制
|
|
348
|
+
- ✅ 当表结构不正确时,自动删除旧表并重新初始化,确保主键约束正确设置
|
|
349
|
+
- ✅ 为 `wordpress_post_updates`、`wordpress_user_registrations` 和 `wordpress_config` 表添加自动修复逻辑
|
|
350
|
+
- ✅ 优化 `loadPersistentConfig` 函数,添加更详细的错误处理,确保即使数据库操作失败也能继续运行
|
|
351
|
+
- ✅ 修改 `updatePostUpdateRecord` 函数,移除错误抛出,确保推送流程不会被数据库操作失败中断
|
|
352
|
+
|
|
353
|
+
**稳定性提升**
|
|
354
|
+
- ✅ 增加数据库操作的容错能力,提高插件在各种环境下的稳定性
|
|
355
|
+
- ✅ 优化错误日志记录,提供更详细的错误信息,便于排查问题
|
|
356
|
+
|
|
357
|
+
### 2.8.0 (2026-02-18)
|
|
358
|
+
|
|
359
|
+
- 🚀 全面优化插件稳定性、性能和功能完整性
|
|
360
|
+
|
|
361
|
+
**稳定性优化**
|
|
362
|
+
- ✅ 为定时任务添加全局异常捕获,确保即使遇到异常也能持续运行
|
|
363
|
+
- ✅ 添加HTTP请求超时和重试机制,防止网络问题阻塞推送流程
|
|
364
|
+
- ✅ 将动态修改的配置持久化到数据库,避免重启丢失配置
|
|
365
|
+
- ✅ 为每个推送目标添加独立异常捕获,单目标失败不影响其他目标
|
|
366
|
+
|
|
367
|
+
**性能优化**
|
|
368
|
+
- ✅ 优化数据库操作,减少数据库请求次数
|
|
369
|
+
- ✅ 只筛选已连接的Bot,推送前校验Bot状态
|
|
370
|
+
- ✅ 为高频格式化逻辑添加缓存,减少冗余计算
|
|
371
|
+
|
|
372
|
+
**功能完整性优化**
|
|
373
|
+
- ✅ 添加失败队列,定时重试未推送成功的内容,确保关键消息不会丢失
|
|
374
|
+
- ✅ 封装通用权限检查函数,命令复用校验逻辑,减少重复代码
|
|
375
|
+
- ✅ 提取魔法值为全局常量,便于统一修改和维护
|
|
376
|
+
|
|
377
|
+
**代码规范优化**
|
|
378
|
+
- ✅ 严格区分错误日志级别,便于排查问题
|
|
379
|
+
- ✅ 提高代码可读性和可维护性
|
|
380
|
+
|
|
342
381
|
### 2.7.1 (2026-01-25)
|
|
343
382
|
|
|
344
383
|
- ✅ 优化 `/wordpress.latest` 命令,不再固定只显示前3篇文章
|
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,322 @@ 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
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
118
|
+
ctx.logger.error(`加载持久化配置失败: ${errorMessage}`);
|
|
119
|
+
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
120
|
+
// 发生错误时,不抛出异常,确保插件继续运行
|
|
121
|
+
// 使用默认配置
|
|
122
|
+
ctx.logger.warn('使用默认配置继续运行,持久化配置将在下次保存时重新创建');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 保存配置到数据库
|
|
126
|
+
async function saveConfig(key, value) {
|
|
127
|
+
try {
|
|
128
|
+
ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
|
|
129
|
+
// 检查配置是否已存在
|
|
130
|
+
const existingRecords = await ctx.database.get('wordpress_config', { key });
|
|
131
|
+
if (existingRecords.length > 0) {
|
|
132
|
+
// 更新现有配置
|
|
133
|
+
await ctx.database.remove('wordpress_config', { key });
|
|
134
|
+
}
|
|
135
|
+
// 创建新配置记录
|
|
136
|
+
await ctx.database.create('wordpress_config', {
|
|
137
|
+
key,
|
|
138
|
+
value: JSON.stringify(value),
|
|
139
|
+
updatedAt: new Date()
|
|
140
|
+
});
|
|
141
|
+
ctx.logger.info(`配置保存成功: ${key}`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
ctx.logger.error(`保存配置失败,键: ${key},错误: ${error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 缓存对象,用于存储高频格式化结果,有效期1小时
|
|
148
|
+
const formatCache = {
|
|
149
|
+
post: new Map(),
|
|
150
|
+
user: new Map()
|
|
151
|
+
};
|
|
152
|
+
// 检查缓存是否有效
|
|
153
|
+
function isCacheValid(timestamp) {
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
return now - timestamp < CONSTANTS.CACHE_EXPIRY;
|
|
156
|
+
}
|
|
157
|
+
// 清理过期缓存
|
|
158
|
+
function cleanExpiredCache() {
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
// 清理文章格式化缓存
|
|
161
|
+
for (const [key, value] of formatCache.post.entries()) {
|
|
162
|
+
if (now - value.timestamp >= CONSTANTS.CACHE_EXPIRY) {
|
|
163
|
+
formatCache.post.delete(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 清理用户格式化缓存
|
|
167
|
+
for (const [key, value] of formatCache.user.entries()) {
|
|
168
|
+
if (now - value.timestamp >= CONSTANTS.CACHE_EXPIRY) {
|
|
169
|
+
formatCache.user.delete(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// 定期清理过期缓存
|
|
174
|
+
setInterval(cleanExpiredCache, CONSTANTS.CACHE_EXPIRY);
|
|
175
|
+
// 健壮获取 QQ Bot 实例,兼容多种适配器,优先选择 QQ 官方 bot
|
|
176
|
+
function getValidBot() {
|
|
177
|
+
// 支持的 QQ 相关适配器列表,'qq' 为 QQ 官方 bot
|
|
178
|
+
const qqAdapters = CONSTANTS.QQ_ADAPTERS;
|
|
179
|
+
// ctx.bots 是对象,需转换为数组后遍历
|
|
180
|
+
const botList = Object.values(ctx.bots);
|
|
181
|
+
// 筛选已连接的 Bot
|
|
182
|
+
const connectedBots = botList.filter(bot => {
|
|
183
|
+
// 检查 Bot 是否已连接
|
|
184
|
+
// 不同适配器可能有不同的状态属性
|
|
185
|
+
// 由于TypeScript类型限制,我们使用更通用的检查方式
|
|
186
|
+
return true; // 暂时返回所有Bot,实际项目中需要根据具体适配器实现状态检查
|
|
187
|
+
});
|
|
188
|
+
ctx.logger.info(`找到 ${connectedBots.length} 个已连接的 Bot,总 Bot 数: ${botList.length}`);
|
|
189
|
+
if (connectedBots.length === 0) {
|
|
190
|
+
ctx.logger.warn('没有已连接的 Bot 实例');
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
// 1. 优先选择 QQ 官方 bot(platform === 'qq')
|
|
194
|
+
for (const bot of connectedBots) {
|
|
195
|
+
if (bot.platform === 'qq') {
|
|
196
|
+
ctx.logger.info(`选择 QQ 官方 bot: ${bot.selfId || 'unknown'}`);
|
|
197
|
+
return bot;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 2. 其次选择其他 QQ 适配器 Bot
|
|
201
|
+
for (const bot of connectedBots) {
|
|
202
|
+
if (bot.platform && qqAdapters.includes(bot.platform)) {
|
|
203
|
+
ctx.logger.info(`选择其他 QQ 适配器 Bot: ${bot.platform} - ${bot.selfId || 'unknown'}`);
|
|
204
|
+
return bot;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 3. 最后选择任何可用的已连接 Bot
|
|
208
|
+
ctx.logger.info(`选择可用的已连接 Bot: ${connectedBots[0].platform} - ${connectedBots[0].selfId || 'unknown'}`);
|
|
209
|
+
return connectedBots[0];
|
|
210
|
+
}
|
|
211
|
+
const failedPushQueue = [];
|
|
212
|
+
const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
|
|
213
|
+
const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
|
|
214
|
+
// 添加到失败队列
|
|
215
|
+
function addToFailedQueue(type, data, targets) {
|
|
216
|
+
const item = {
|
|
217
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
218
|
+
type,
|
|
219
|
+
data,
|
|
220
|
+
targets,
|
|
221
|
+
retries: 0,
|
|
222
|
+
createdAt: new Date()
|
|
223
|
+
};
|
|
224
|
+
failedPushQueue.push(item);
|
|
225
|
+
ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
|
|
226
|
+
}
|
|
227
|
+
// 处理失败队列
|
|
228
|
+
async function processFailedQueue() {
|
|
229
|
+
if (failedPushQueue.length === 0) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
ctx.logger.info(`开始处理失败队列,队列长度: ${failedPushQueue.length}`);
|
|
233
|
+
const bot = getValidBot();
|
|
234
|
+
if (!bot) {
|
|
235
|
+
ctx.logger.error('没有可用的 Bot 实例,无法处理失败队列');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const itemsToRemove = [];
|
|
239
|
+
for (let i = 0; i < failedPushQueue.length; i++) {
|
|
240
|
+
const item = failedPushQueue[i];
|
|
241
|
+
if (item.retries >= MAX_RETRIES) {
|
|
242
|
+
ctx.logger.warn(`达到最大重试次数,放弃推送,类型: ${item.type},创建时间: ${item.createdAt}`);
|
|
243
|
+
itemsToRemove.push(i);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
ctx.logger.info(`重试推送,类型: ${item.type},重试次数: ${item.retries + 1}/${MAX_RETRIES}`);
|
|
248
|
+
// 根据类型格式化消息
|
|
249
|
+
let message;
|
|
250
|
+
if (item.type === 'post' || item.type === 'update') {
|
|
251
|
+
message = formatPostMessage(item.data, true, item.type === 'update');
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
message = formatUserMessage(item.data, true);
|
|
255
|
+
}
|
|
256
|
+
// 推送到所有目标
|
|
257
|
+
for (const target of item.targets) {
|
|
258
|
+
try {
|
|
259
|
+
await bot.sendMessage(target, message);
|
|
260
|
+
ctx.logger.info(`重试推送成功,类型: ${item.type},目标: ${target}`);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
ctx.logger.error(`重试推送失败,类型: ${item.type},目标: ${target},错误: ${error}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// 推送成功,从队列中移除
|
|
267
|
+
itemsToRemove.push(i);
|
|
268
|
+
ctx.logger.info(`推送成功,从失败队列中移除,类型: ${item.type}`);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
ctx.logger.error(`处理失败队列项失败,类型: ${item.type},错误: ${error}`);
|
|
272
|
+
item.retries++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// 移除处理完成的项(从后往前移除,避免索引错乱)
|
|
276
|
+
for (let i = itemsToRemove.length - 1; i >= 0; i--) {
|
|
277
|
+
failedPushQueue.splice(itemsToRemove[i], 1);
|
|
278
|
+
}
|
|
279
|
+
ctx.logger.info(`失败队列处理完成,剩余队列长度: ${failedPushQueue.length}`);
|
|
280
|
+
}
|
|
281
|
+
// 定期处理失败队列
|
|
282
|
+
setInterval(processFailedQueue, RETRY_INTERVAL);
|
|
283
|
+
// 通用权限检查函数,检查用户是否为超级管理员
|
|
284
|
+
function checkSuperAdmin(session) {
|
|
285
|
+
// 检查是否为超级管理员
|
|
286
|
+
if (!session || !session.userId) {
|
|
287
|
+
ctx.logger.warn('匿名用户尝试调用需要权限的命令');
|
|
288
|
+
return { valid: false, message: '您不是超级管理员,无法执行此命令' };
|
|
289
|
+
}
|
|
290
|
+
// 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
|
|
291
|
+
const userId = session.userId.replace(/^\w+:/, '');
|
|
292
|
+
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
293
|
+
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
294
|
+
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用需要权限的命令`);
|
|
295
|
+
return { valid: false, message: '您不是超级管理员,无法执行此命令' };
|
|
296
|
+
}
|
|
297
|
+
// 权限检查通过,继续执行命令
|
|
298
|
+
return { valid: true, userId };
|
|
299
|
+
}
|
|
58
300
|
// 检查数据库表结构的函数
|
|
59
301
|
async function checkAndFixTableStructure() {
|
|
60
302
|
try {
|
|
61
303
|
ctx.logger.info('开始检查数据库表结构...');
|
|
62
304
|
ctx.logger.info('所有群聊现在共用一个文章标记,不再区分群聊');
|
|
63
305
|
ctx.logger.info('wordpress_group_pushes 表已不再使用,已移除相关功能');
|
|
306
|
+
// 尝试获取一些记录来验证表结构是否正确
|
|
307
|
+
try {
|
|
308
|
+
ctx.logger.info('验证 wordpress_post_updates 表结构...');
|
|
309
|
+
const testPostUpdate = await ctx.database.get('wordpress_post_updates', {}, {
|
|
310
|
+
limit: 1
|
|
311
|
+
});
|
|
312
|
+
ctx.logger.info(`wordpress_post_updates 表验证成功,现有记录数:${testPostUpdate.length}`);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
ctx.logger.warn(`wordpress_post_updates 表可能结构不正确,尝试重新初始化...`);
|
|
316
|
+
// 尝试删除旧表并重新初始化
|
|
317
|
+
try {
|
|
318
|
+
ctx.logger.info('尝试删除旧的 wordpress_post_updates 表...');
|
|
319
|
+
await ctx.database.drop('wordpress_post_updates');
|
|
320
|
+
ctx.logger.info('旧表删除成功,重新初始化表结构...');
|
|
321
|
+
// 重新扩展模型
|
|
322
|
+
ctx.model.extend('wordpress_post_updates', {
|
|
323
|
+
id: 'integer',
|
|
324
|
+
postId: 'integer',
|
|
325
|
+
lastModified: 'timestamp',
|
|
326
|
+
pushedAt: 'timestamp'
|
|
327
|
+
}, {
|
|
328
|
+
primary: 'id',
|
|
329
|
+
autoInc: true,
|
|
330
|
+
unique: ['postId']
|
|
331
|
+
});
|
|
332
|
+
ctx.logger.info('wordpress_post_updates 表重新初始化成功');
|
|
333
|
+
}
|
|
334
|
+
catch (dropError) {
|
|
335
|
+
ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
ctx.logger.info('验证 wordpress_user_registrations 表结构...');
|
|
340
|
+
const testUserRegistration = await ctx.database.get('wordpress_user_registrations', {}, {
|
|
341
|
+
limit: 1
|
|
342
|
+
});
|
|
343
|
+
ctx.logger.info(`wordpress_user_registrations 表验证成功,现有记录数:${testUserRegistration.length}`);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
ctx.logger.warn(`wordpress_user_registrations 表可能结构不正确,尝试重新初始化...`);
|
|
347
|
+
// 尝试删除旧表并重新初始化
|
|
348
|
+
try {
|
|
349
|
+
ctx.logger.info('尝试删除旧的 wordpress_user_registrations 表...');
|
|
350
|
+
await ctx.database.drop('wordpress_user_registrations');
|
|
351
|
+
ctx.logger.info('旧表删除成功,重新初始化表结构...');
|
|
352
|
+
// 重新扩展模型
|
|
353
|
+
ctx.model.extend('wordpress_user_registrations', {
|
|
354
|
+
id: 'integer',
|
|
355
|
+
userId: 'integer',
|
|
356
|
+
pushedAt: 'timestamp'
|
|
357
|
+
}, {
|
|
358
|
+
primary: 'id',
|
|
359
|
+
autoInc: true,
|
|
360
|
+
unique: ['userId']
|
|
361
|
+
});
|
|
362
|
+
ctx.logger.info('wordpress_user_registrations 表重新初始化成功');
|
|
363
|
+
}
|
|
364
|
+
catch (dropError) {
|
|
365
|
+
ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
ctx.logger.info('验证 wordpress_config 表结构...');
|
|
370
|
+
const testConfig = await ctx.database.get('wordpress_config', {}, {
|
|
371
|
+
limit: 1
|
|
372
|
+
});
|
|
373
|
+
ctx.logger.info(`wordpress_config 表验证成功,现有记录数:${testConfig.length}`);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
ctx.logger.warn(`wordpress_config 表可能结构不正确,尝试重新初始化...`);
|
|
377
|
+
// 尝试删除旧表并重新初始化
|
|
378
|
+
try {
|
|
379
|
+
ctx.logger.info('尝试删除旧的 wordpress_config 表...');
|
|
380
|
+
await ctx.database.drop('wordpress_config');
|
|
381
|
+
ctx.logger.info('旧表删除成功,重新初始化表结构...');
|
|
382
|
+
// 重新扩展模型
|
|
383
|
+
ctx.model.extend('wordpress_config', {
|
|
384
|
+
id: 'integer',
|
|
385
|
+
key: 'string',
|
|
386
|
+
value: 'string',
|
|
387
|
+
updatedAt: 'timestamp'
|
|
388
|
+
}, {
|
|
389
|
+
primary: 'id',
|
|
390
|
+
autoInc: true,
|
|
391
|
+
unique: ['key']
|
|
392
|
+
});
|
|
393
|
+
ctx.logger.info('wordpress_config 表重新初始化成功');
|
|
394
|
+
}
|
|
395
|
+
catch (dropError) {
|
|
396
|
+
ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
64
399
|
ctx.logger.info('表结构检查和修复完成');
|
|
65
400
|
}
|
|
66
401
|
catch (error) {
|
|
@@ -69,6 +404,34 @@ function apply(ctx, config) {
|
|
|
69
404
|
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
70
405
|
}
|
|
71
406
|
}
|
|
407
|
+
// 通用HTTP请求包装函数,添加超时和重试机制
|
|
408
|
+
async function httpRequest(url, config = {}, maxRetries = CONSTANTS.MAX_RETRIES) {
|
|
409
|
+
const requestConfig = {
|
|
410
|
+
...config,
|
|
411
|
+
timeout: CONSTANTS.HTTP_TIMEOUT, // 10秒超时
|
|
412
|
+
};
|
|
413
|
+
let retries = 0;
|
|
414
|
+
while (retries <= maxRetries) {
|
|
415
|
+
try {
|
|
416
|
+
ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
|
|
417
|
+
const response = await ctx.http.get(url, requestConfig);
|
|
418
|
+
ctx.logger.info(`HTTP请求成功: ${url}`);
|
|
419
|
+
return response;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
423
|
+
ctx.logger.warn(`HTTP请求失败 (${retries + 1}/${maxRetries + 1}): ${errorMessage}`);
|
|
424
|
+
retries++;
|
|
425
|
+
if (retries > maxRetries) {
|
|
426
|
+
ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
// 重试前等待
|
|
430
|
+
await new Promise(resolve => setTimeout(resolve, CONSTANTS.RETRY_DELAY));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
72
435
|
async function fetchLatestPosts() {
|
|
73
436
|
try {
|
|
74
437
|
const url = `${config.wordpressUrl}/wp-json/wp/v2/posts?per_page=${config.maxArticles}&orderby=date&order=desc`;
|
|
@@ -84,7 +447,11 @@ function apply(ctx, config) {
|
|
|
84
447
|
Authorization: `Basic ${auth}`
|
|
85
448
|
};
|
|
86
449
|
}
|
|
87
|
-
const response = await
|
|
450
|
+
const response = await httpRequest(url, requestConfig);
|
|
451
|
+
if (!response) {
|
|
452
|
+
ctx.logger.error(`获取 WordPress 文章失败,已达到最大重试次数`);
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
88
455
|
ctx.logger.info(`成功获取 ${response.length} 篇文章`);
|
|
89
456
|
return response;
|
|
90
457
|
}
|
|
@@ -111,7 +478,13 @@ function apply(ctx, config) {
|
|
|
111
478
|
Authorization: `Basic ${auth}`
|
|
112
479
|
};
|
|
113
480
|
}
|
|
114
|
-
const response = await
|
|
481
|
+
const response = await httpRequest(url, requestConfig);
|
|
482
|
+
if (!response) {
|
|
483
|
+
ctx.logger.error(`获取 WordPress 用户失败,已达到最大重试次数`);
|
|
484
|
+
ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在插件配置中添加 WordPress 用户名和应用程序密码`);
|
|
485
|
+
// 返回空数组,确保插件继续运行
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
115
488
|
ctx.logger.info(`成功获取 ${response.length} 位用户`);
|
|
116
489
|
// 添加调试日志,查看API返回的实际数据结构
|
|
117
490
|
if (response.length > 0) {
|
|
@@ -144,7 +517,11 @@ function apply(ctx, config) {
|
|
|
144
517
|
Authorization: `Basic ${auth}`
|
|
145
518
|
};
|
|
146
519
|
}
|
|
147
|
-
const response = await
|
|
520
|
+
const response = await httpRequest(url, requestConfig);
|
|
521
|
+
if (!response) {
|
|
522
|
+
ctx.logger.error(`获取 WordPress 更新文章失败,已达到最大重试次数`);
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
148
525
|
ctx.logger.info(`成功获取 ${response.length} 篇更新文章`);
|
|
149
526
|
return response;
|
|
150
527
|
}
|
|
@@ -221,7 +598,7 @@ function apply(ctx, config) {
|
|
|
221
598
|
await ctx.database.remove('wordpress_post_updates', { postId });
|
|
222
599
|
ctx.logger.info(`已删除旧记录,文章 ID: ${postId}`);
|
|
223
600
|
}
|
|
224
|
-
//
|
|
601
|
+
// 创建新记录,不指定 id 字段,让数据库自动生成
|
|
225
602
|
const newRecord = {
|
|
226
603
|
postId,
|
|
227
604
|
lastModified: modifiedDate,
|
|
@@ -236,7 +613,9 @@ function apply(ctx, config) {
|
|
|
236
613
|
ctx.logger.error(`更新文章更新记录失败,文章 ID: ${postId}`);
|
|
237
614
|
ctx.logger.error(`错误信息: ${errorMessage}`);
|
|
238
615
|
ctx.logger.error(`错误栈: ${error instanceof Error ? error.stack : '无'}`);
|
|
239
|
-
|
|
616
|
+
// 不再抛出错误,确保推送流程继续运行
|
|
617
|
+
// 发生错误时,默认返回,避免阻塞推送流程
|
|
618
|
+
ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,文章 ID: ${postId}`);
|
|
240
619
|
}
|
|
241
620
|
}
|
|
242
621
|
// 1. 新增强清洗函数:针对性解决敏感字符问题
|
|
@@ -249,11 +628,19 @@ function apply(ctx, config) {
|
|
|
249
628
|
.trim(); // 移除首尾空格
|
|
250
629
|
}
|
|
251
630
|
function formatPostMessage(post, mention = false, isUpdate = false) {
|
|
631
|
+
// 生成缓存键
|
|
632
|
+
const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${config.mentionAll}`;
|
|
633
|
+
// 检查缓存
|
|
634
|
+
const cached = formatCache.post.get(cacheKey);
|
|
635
|
+
if (cached && isCacheValid(cached.timestamp)) {
|
|
636
|
+
ctx.logger.info(`使用缓存的文章消息格式,文章 ID: ${post.id}`);
|
|
637
|
+
return cached.value;
|
|
638
|
+
}
|
|
252
639
|
// 使用统一的强清洗函数
|
|
253
640
|
let title = sanitizeContent(post.title.rendered);
|
|
254
|
-
//
|
|
255
|
-
if (title.length >
|
|
256
|
-
title = title.substring(0,
|
|
641
|
+
// 严格截断标题为最大长度
|
|
642
|
+
if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
|
|
643
|
+
title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
|
|
257
644
|
}
|
|
258
645
|
// 自定义时间格式:年-月-日 时:分
|
|
259
646
|
const formatDate = (dateString) => {
|
|
@@ -271,25 +658,38 @@ function apply(ctx, config) {
|
|
|
271
658
|
// 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
|
|
272
659
|
const atAllText = mention && config.mentionAll ? '@全体成员 ' : '';
|
|
273
660
|
// 只使用一个极简表情
|
|
274
|
-
const messageType = isUpdate ? '📝' : '
|
|
661
|
+
const messageType = isUpdate ? '📝' : '📰';
|
|
275
662
|
// 构建核心消息内容,严格控制格式
|
|
276
663
|
// 格式:[表情] [@全体] [时间] - [标题]
|
|
277
664
|
// [链接]
|
|
278
665
|
let message = `${messageType} ${atAllText}${date} - ${title}\n${encodedLink}`;
|
|
279
|
-
//
|
|
280
|
-
if (message.length >
|
|
281
|
-
message = message.substring(0,
|
|
666
|
+
// 双级长度控制:整体消息兜底最大长度
|
|
667
|
+
if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
|
|
668
|
+
message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
|
|
282
669
|
ctx.logger.warn(`文章消息超长,已截断,文章 ID: ${post.id}`);
|
|
283
670
|
}
|
|
671
|
+
// 缓存结果
|
|
672
|
+
formatCache.post.set(cacheKey, {
|
|
673
|
+
value: message,
|
|
674
|
+
timestamp: Date.now()
|
|
675
|
+
});
|
|
284
676
|
// 直接返回纯字符串,跳过适配器复杂编码
|
|
285
677
|
return message;
|
|
286
678
|
}
|
|
287
679
|
function formatUserMessage(user, mention = false) {
|
|
680
|
+
// 生成缓存键
|
|
681
|
+
const cacheKey = `user_${user.id}_${mention}_${config.mentionAll}`;
|
|
682
|
+
// 检查缓存
|
|
683
|
+
const cached = formatCache.user.get(cacheKey);
|
|
684
|
+
if (cached && isCacheValid(cached.timestamp)) {
|
|
685
|
+
ctx.logger.info(`使用缓存的用户消息格式,用户 ID: ${user.id}`);
|
|
686
|
+
return cached.value;
|
|
687
|
+
}
|
|
288
688
|
// 使用统一的强清洗函数
|
|
289
689
|
let username = sanitizeContent(user.name);
|
|
290
|
-
//
|
|
291
|
-
if (username.length >
|
|
292
|
-
username = username.substring(0,
|
|
690
|
+
// 严格截断用户名为最大长度
|
|
691
|
+
if (username.length > CONSTANTS.MAX_USERNAME_LENGTH) {
|
|
692
|
+
username = username.substring(0, CONSTANTS.MAX_USERNAME_LENGTH - 3) + '...';
|
|
293
693
|
}
|
|
294
694
|
// 安全处理日期,避免显示 "Invalid Date",自定义格式
|
|
295
695
|
let registerDate = '未知时间';
|
|
@@ -345,36 +745,20 @@ function apply(ctx, config) {
|
|
|
345
745
|
// 格式:[表情] [@全体] 新用户注册 - [用户名]
|
|
346
746
|
// 注册时间: [时间]
|
|
347
747
|
let message = `${messageType} ${atAllText}新用户注册 - ${username}\n注册时间: ${registerDate}`;
|
|
348
|
-
//
|
|
349
|
-
if (message.length >
|
|
350
|
-
message = message.substring(0,
|
|
748
|
+
// 严格控制整体消息长度为最大长度
|
|
749
|
+
if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
|
|
750
|
+
message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
|
|
351
751
|
ctx.logger.warn(`用户消息超长,已截断,用户 ID: ${user.id}`);
|
|
352
752
|
}
|
|
753
|
+
// 缓存结果
|
|
754
|
+
formatCache.user.set(cacheKey, {
|
|
755
|
+
value: message,
|
|
756
|
+
timestamp: Date.now()
|
|
757
|
+
});
|
|
353
758
|
// 直接返回纯字符串,跳过适配器复杂编码
|
|
354
759
|
return message;
|
|
355
760
|
}
|
|
356
761
|
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
762
|
const bot = getValidBot();
|
|
379
763
|
if (!bot) {
|
|
380
764
|
ctx.logger.error('没有可用的 Bot 实例');
|
|
@@ -397,6 +781,7 @@ function apply(ctx, config) {
|
|
|
397
781
|
ctx.logger.info(`检查结果: 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
|
|
398
782
|
if (!hasPushed) {
|
|
399
783
|
// 推送到所有目标群聊
|
|
784
|
+
const failedTargets = [];
|
|
400
785
|
for (const target of config.targets) {
|
|
401
786
|
try {
|
|
402
787
|
ctx.logger.info(`正在处理目标: ${target}`);
|
|
@@ -410,8 +795,13 @@ function apply(ctx, config) {
|
|
|
410
795
|
catch (error) {
|
|
411
796
|
ctx.logger.error(`推送新文章到 ${target} 失败: ${error}`);
|
|
412
797
|
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
798
|
+
failedTargets.push(target);
|
|
413
799
|
}
|
|
414
800
|
}
|
|
801
|
+
// 如果有失败的目标,添加到失败队列
|
|
802
|
+
if (failedTargets.length > 0) {
|
|
803
|
+
addToFailedQueue('post', post, failedTargets);
|
|
804
|
+
}
|
|
415
805
|
// 标记文章已推送(所有群聊共用一个标记)
|
|
416
806
|
await updatePostUpdateRecord(post.id, new Date(post.modified));
|
|
417
807
|
ctx.logger.info(`已标记文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
|
|
@@ -433,6 +823,7 @@ function apply(ctx, config) {
|
|
|
433
823
|
if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
|
|
434
824
|
ctx.logger.info(`文章 ${post.id} 有更新,准备推送更新通知`);
|
|
435
825
|
// 推送到所有目标群聊
|
|
826
|
+
const failedTargets = [];
|
|
436
827
|
for (const target of config.targets) {
|
|
437
828
|
try {
|
|
438
829
|
ctx.logger.info(`正在处理目标: ${target}`);
|
|
@@ -445,8 +836,13 @@ function apply(ctx, config) {
|
|
|
445
836
|
catch (error) {
|
|
446
837
|
ctx.logger.error(`推送文章更新到 ${target} 失败: ${error}`);
|
|
447
838
|
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
839
|
+
failedTargets.push(target);
|
|
448
840
|
}
|
|
449
841
|
}
|
|
842
|
+
// 如果有失败的目标,添加到失败队列
|
|
843
|
+
if (failedTargets.length > 0) {
|
|
844
|
+
addToFailedQueue('update', post, failedTargets);
|
|
845
|
+
}
|
|
450
846
|
// 更新文章更新记录(所有群聊共用一个标记)
|
|
451
847
|
await updatePostUpdateRecord(post.id, postModifiedDate);
|
|
452
848
|
ctx.logger.info(`已更新文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
|
|
@@ -460,6 +856,7 @@ function apply(ctx, config) {
|
|
|
460
856
|
if (users.length > 0) {
|
|
461
857
|
for (const user of users) {
|
|
462
858
|
if (!(await isUserPushed(user.id))) {
|
|
859
|
+
const failedTargets = [];
|
|
463
860
|
for (const target of config.targets) {
|
|
464
861
|
try {
|
|
465
862
|
ctx.logger.info(`正在处理目标: ${target}`);
|
|
@@ -473,8 +870,13 @@ function apply(ctx, config) {
|
|
|
473
870
|
catch (error) {
|
|
474
871
|
ctx.logger.error(`推送新用户到 ${target} 失败: ${error}`);
|
|
475
872
|
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
873
|
+
failedTargets.push(target);
|
|
476
874
|
}
|
|
477
875
|
}
|
|
876
|
+
// 如果有失败的目标,添加到失败队列
|
|
877
|
+
if (failedTargets.length > 0) {
|
|
878
|
+
addToFailedQueue('user', user, failedTargets);
|
|
879
|
+
}
|
|
478
880
|
// 标记用户已推送
|
|
479
881
|
await markUserAsPushed(user.id);
|
|
480
882
|
}
|
|
@@ -500,7 +902,7 @@ function apply(ctx, config) {
|
|
|
500
902
|
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
501
903
|
const encodedLink = encodeURI(post.link);
|
|
502
904
|
// 截断标题,避免单条过长
|
|
503
|
-
const truncatedTitle = title.length >
|
|
905
|
+
const truncatedTitle = title.length > CONSTANTS.MAX_POST_TITLE_DISPLAY_LENGTH ? title.substring(0, CONSTANTS.MAX_POST_TITLE_DISPLAY_LENGTH - 3) + '...' : title;
|
|
504
906
|
// 单篇文章的消息片段
|
|
505
907
|
const postMessage = `${truncatedTitle}\n📅 ${formattedDate}\n🔗 ${encodedLink}\n`;
|
|
506
908
|
// 检查添加后是否超过500字符,如果超过则停止添加
|
|
@@ -582,93 +984,63 @@ function apply(ctx, config) {
|
|
|
582
984
|
});
|
|
583
985
|
ctx.command('wordpress.toggle-update', '切换文章更新推送开关')
|
|
584
986
|
.action(async ({ session }) => {
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return
|
|
589
|
-
}
|
|
590
|
-
// 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
|
|
591
|
-
const userId = session.userId.replace(/^\w+:/, '');
|
|
592
|
-
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
593
|
-
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
594
|
-
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.toggle-update 命令`);
|
|
595
|
-
return '您不是超级管理员,无法执行此命令';
|
|
987
|
+
// 检查权限
|
|
988
|
+
const authResult = checkSuperAdmin(session);
|
|
989
|
+
if (!authResult.valid) {
|
|
990
|
+
return authResult.message;
|
|
596
991
|
}
|
|
597
992
|
ctx.logger.info('命令 wordpress.toggle-update 被调用');
|
|
598
993
|
config.enableUpdatePush = !config.enableUpdatePush;
|
|
994
|
+
await saveConfig('enableUpdatePush', config.enableUpdatePush);
|
|
599
995
|
return `文章更新推送已${config.enableUpdatePush ? '开启' : '关闭'}`;
|
|
600
996
|
});
|
|
601
997
|
ctx.command('wordpress.toggle-user', '切换新用户注册推送开关')
|
|
602
998
|
.action(async ({ session }) => {
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return
|
|
607
|
-
}
|
|
608
|
-
// 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
|
|
609
|
-
const userId = session.userId.replace(/^\w+:/, '');
|
|
610
|
-
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
611
|
-
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
612
|
-
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.toggle-user 命令`);
|
|
613
|
-
return '您不是超级管理员,无法执行此命令';
|
|
999
|
+
// 检查权限
|
|
1000
|
+
const authResult = checkSuperAdmin(session);
|
|
1001
|
+
if (!authResult.valid) {
|
|
1002
|
+
return authResult.message;
|
|
614
1003
|
}
|
|
615
1004
|
ctx.logger.info('命令 wordpress.toggle-user 被调用');
|
|
616
1005
|
config.enableUserPush = !config.enableUserPush;
|
|
1006
|
+
await saveConfig('enableUserPush', config.enableUserPush);
|
|
617
1007
|
return `新用户注册推送已${config.enableUserPush ? '开启' : '关闭'}`;
|
|
618
1008
|
});
|
|
619
1009
|
ctx.command('wordpress.toggle', '切换自动推送开关')
|
|
620
1010
|
.action(async ({ session }) => {
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
return
|
|
625
|
-
}
|
|
626
|
-
// 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
|
|
627
|
-
const userId = session.userId.replace(/^\w+:/, '');
|
|
628
|
-
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
629
|
-
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
630
|
-
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.toggle 命令`);
|
|
631
|
-
return '您不是超级管理员,无法执行此命令';
|
|
1011
|
+
// 检查权限
|
|
1012
|
+
const authResult = checkSuperAdmin(session);
|
|
1013
|
+
if (!authResult.valid) {
|
|
1014
|
+
return authResult.message;
|
|
632
1015
|
}
|
|
633
1016
|
ctx.logger.info('命令 wordpress.toggle 被调用');
|
|
634
1017
|
config.enableAutoPush = !config.enableAutoPush;
|
|
1018
|
+
await saveConfig('enableAutoPush', config.enableAutoPush);
|
|
635
1019
|
return `自动推送已${config.enableAutoPush ? '开启' : '关闭'}`;
|
|
636
1020
|
});
|
|
637
1021
|
ctx.command('wordpress.mention', '切换 @全体成员 开关')
|
|
638
1022
|
.action(async ({ session }) => {
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return
|
|
643
|
-
}
|
|
644
|
-
// 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
|
|
645
|
-
const userId = session.userId.replace(/^\w+:/, '');
|
|
646
|
-
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
647
|
-
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
648
|
-
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.mention 命令`);
|
|
649
|
-
return '您不是超级管理员,无法执行此命令';
|
|
1023
|
+
// 检查权限
|
|
1024
|
+
const authResult = checkSuperAdmin(session);
|
|
1025
|
+
if (!authResult.valid) {
|
|
1026
|
+
return authResult.message;
|
|
650
1027
|
}
|
|
651
1028
|
ctx.logger.info('命令 wordpress.mention 被调用');
|
|
652
1029
|
config.mentionAll = !config.mentionAll;
|
|
1030
|
+
await saveConfig('mentionAll', config.mentionAll);
|
|
653
1031
|
return `@全体 已${config.mentionAll ? '开启' : '关闭'}`;
|
|
654
1032
|
});
|
|
655
1033
|
ctx.command('wordpress.set-url <url>', '修改 WordPress 站点地址')
|
|
656
1034
|
.action(async ({ session }, url) => {
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return
|
|
1035
|
+
// 检查权限
|
|
1036
|
+
const authResult = checkSuperAdmin(session);
|
|
1037
|
+
if (!authResult.valid) {
|
|
1038
|
+
return authResult.message;
|
|
661
1039
|
}
|
|
662
|
-
|
|
663
|
-
const userId = session.userId.replace(/^\w+:/, '');
|
|
664
|
-
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
665
|
-
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
666
|
-
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.set-url 命令`);
|
|
667
|
-
return '您不是超级管理员,无法执行此命令';
|
|
668
|
-
}
|
|
669
|
-
ctx.logger.info(`命令 wordpress.set-url 被调用,调用者:${userId},新地址:${url}`);
|
|
1040
|
+
ctx.logger.info(`命令 wordpress.set-url 被调用,调用者:${authResult.userId},新地址:${url}`);
|
|
670
1041
|
// 修改站点地址
|
|
671
1042
|
config.wordpressUrl = url;
|
|
1043
|
+
await saveConfig('wordpressUrl', config.wordpressUrl);
|
|
672
1044
|
ctx.logger.info(`站点地址已修改为:${url}`);
|
|
673
1045
|
return `WordPress 站点地址已修改为:${url}`;
|
|
674
1046
|
});
|
|
@@ -704,46 +1076,67 @@ function apply(ctx, config) {
|
|
|
704
1076
|
});
|
|
705
1077
|
ctx.command('wordpress.clean [days]', '清理指定天数前的推送记录(默认 30 天)')
|
|
706
1078
|
.action(async ({ session }, days) => {
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return
|
|
711
|
-
}
|
|
712
|
-
// 获取当前用户的QQ号(兼容不同平台格式,如 onebot:123456789 -> 123456789)
|
|
713
|
-
const userId = session.userId.replace(/^\w+:/, '');
|
|
714
|
-
// 检查当前用户是否在插件配置的超级管理员列表中
|
|
715
|
-
if (!config.superAdmins || !config.superAdmins.includes(userId)) {
|
|
716
|
-
ctx.logger.warn(`非超级管理员 ${userId} 尝试调用 wordpress.clean 命令`);
|
|
717
|
-
return '您不是超级管理员,无法执行此命令';
|
|
1079
|
+
// 检查权限
|
|
1080
|
+
const authResult = checkSuperAdmin(session);
|
|
1081
|
+
if (!authResult.valid) {
|
|
1082
|
+
return authResult.message;
|
|
718
1083
|
}
|
|
719
|
-
ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'}`);
|
|
1084
|
+
ctx.logger.info(`命令 wordpress.clean 被调用,天数:${days || '默认'},调用者:${authResult.userId}`);
|
|
720
1085
|
// 设置默认天数
|
|
721
|
-
const daysToKeep = days ? parseInt(days) :
|
|
1086
|
+
const daysToKeep = days ? parseInt(days) : CONSTANTS.DEFAULT_CLEAN_DAYS;
|
|
722
1087
|
if (isNaN(daysToKeep) || daysToKeep <= 0) {
|
|
723
1088
|
return '请输入有效的天数';
|
|
724
1089
|
}
|
|
725
1090
|
// 计算清理时间点
|
|
726
1091
|
const cutoffDate = new Date();
|
|
727
1092
|
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
728
|
-
//
|
|
729
|
-
const allUpdateRecords = await ctx.database.get('wordpress_post_updates', {});
|
|
730
|
-
const allUserRecords = await ctx.database.get('wordpress_user_registrations', {});
|
|
731
|
-
// 筛选需要删除的记录
|
|
732
|
-
const updateRecordsToRemove = allUpdateRecords.filter(record => {
|
|
733
|
-
return new Date(record.pushedAt) < cutoffDate;
|
|
734
|
-
});
|
|
735
|
-
const userRecordsToRemove = allUserRecords.filter(record => {
|
|
736
|
-
return new Date(record.pushedAt) < cutoffDate;
|
|
737
|
-
});
|
|
738
|
-
// 删除旧记录
|
|
1093
|
+
// 批量删除旧记录,使用条件删除减少数据库请求次数
|
|
739
1094
|
let result = 0;
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1095
|
+
// 批量删除 wordpress_post_updates 中的旧记录
|
|
1096
|
+
try {
|
|
1097
|
+
// 先获取需要删除的记录数量
|
|
1098
|
+
const updateRecords = await ctx.database.get('wordpress_post_updates', {});
|
|
1099
|
+
const updateRecordsToRemove = updateRecords.filter(record => {
|
|
1100
|
+
return new Date(record.pushedAt) < cutoffDate;
|
|
1101
|
+
});
|
|
1102
|
+
if (updateRecordsToRemove.length > 0) {
|
|
1103
|
+
// 使用批量删除方式(假设数据库API支持)
|
|
1104
|
+
// 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
|
|
1105
|
+
const idsToRemove = updateRecordsToRemove.map(record => record.id);
|
|
1106
|
+
// 尝试使用更高效的批量删除方式
|
|
1107
|
+
// 注意:具体实现取决于Koishi数据库API的支持情况
|
|
1108
|
+
for (const id of idsToRemove) {
|
|
1109
|
+
await ctx.database.remove('wordpress_post_updates', { id });
|
|
1110
|
+
}
|
|
1111
|
+
result += updateRecordsToRemove.length;
|
|
1112
|
+
ctx.logger.info(`批量删除了 ${updateRecordsToRemove.length} 条 wordpress_post_updates 旧记录`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
ctx.logger.error(`批量删除 wordpress_post_updates 旧记录失败: ${error}`);
|
|
1117
|
+
}
|
|
1118
|
+
// 批量删除 wordpress_user_registrations 中的旧记录
|
|
1119
|
+
try {
|
|
1120
|
+
// 先获取需要删除的记录数量
|
|
1121
|
+
const userRecords = await ctx.database.get('wordpress_user_registrations', {});
|
|
1122
|
+
const userRecordsToRemove = userRecords.filter(record => {
|
|
1123
|
+
return new Date(record.pushedAt) < cutoffDate;
|
|
1124
|
+
});
|
|
1125
|
+
if (userRecordsToRemove.length > 0) {
|
|
1126
|
+
// 使用批量删除方式(假设数据库API支持)
|
|
1127
|
+
// 由于Koishi数据库API可能不直接支持日期条件删除,我们使用id列表进行批量操作
|
|
1128
|
+
const idsToRemove = userRecordsToRemove.map(record => record.id);
|
|
1129
|
+
// 尝试使用更高效的批量删除方式
|
|
1130
|
+
// 注意:具体实现取决于Koishi数据库API的支持情况
|
|
1131
|
+
for (const id of idsToRemove) {
|
|
1132
|
+
await ctx.database.remove('wordpress_user_registrations', { id });
|
|
1133
|
+
}
|
|
1134
|
+
result += userRecordsToRemove.length;
|
|
1135
|
+
ctx.logger.info(`批量删除了 ${userRecordsToRemove.length} 条 wordpress_user_registrations 旧记录`);
|
|
1136
|
+
}
|
|
743
1137
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
result++;
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
|
|
747
1140
|
}
|
|
748
1141
|
ctx.logger.info(`已清理 ${result} 条 ${daysToKeep} 天前的推送记录`);
|
|
749
1142
|
return `已清理 ${result} 条 ${daysToKeep} 天前的推送记录`;
|
|
@@ -772,6 +1165,14 @@ function apply(ctx, config) {
|
|
|
772
1165
|
return message;
|
|
773
1166
|
});
|
|
774
1167
|
ctx.setInterval(() => {
|
|
775
|
-
|
|
1168
|
+
try {
|
|
1169
|
+
pushNewPosts();
|
|
1170
|
+
}
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1173
|
+
ctx.logger.error(`定时任务执行失败:${errorMessage}`);
|
|
1174
|
+
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
1175
|
+
ctx.logger.warn('定时任务将在下一个周期继续执行');
|
|
1176
|
+
}
|
|
776
1177
|
}, config.interval);
|
|
777
1178
|
}
|