koishi-plugin-wordpress-notifier 2.8.1 → 2.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -38
- package/lib/index.d.ts +26 -2
- package/lib/index.js +762 -264
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const CONSTANTS = {
|
|
|
18
18
|
HTTP_TIMEOUT: 10000, // 10秒
|
|
19
19
|
MAX_RETRIES: 2,
|
|
20
20
|
RETRY_DELAY: 1000, // 1秒
|
|
21
|
+
API_RATE_LIMIT: 1000, // API请求频率限制(毫秒),每秒最多1次
|
|
21
22
|
// 失败队列相关
|
|
22
23
|
MAX_PUSH_RETRIES: 3,
|
|
23
24
|
PUSH_RETRY_INTERVAL: 5 * 60 * 1000, // 5分钟
|
|
@@ -25,19 +26,104 @@ const CONSTANTS = {
|
|
|
25
26
|
DEFAULT_CLEAN_DAYS: 30,
|
|
26
27
|
// QQ适配器相关
|
|
27
28
|
QQ_ADAPTERS: ['qq', 'onebot', 'milky', 'satori'],
|
|
29
|
+
// 安全过滤相关
|
|
30
|
+
SENSITIVE_WORDS: [
|
|
31
|
+
// 常见敏感词列表(示例)
|
|
32
|
+
'敏感词1', '敏感词2', '敏感词3',
|
|
33
|
+
// 可以根据实际需求扩展
|
|
34
|
+
],
|
|
35
|
+
SENSITIVE_REPLACEMENT: '***', // 敏感词替换为***
|
|
28
36
|
};
|
|
37
|
+
// API 请求速率控制变量
|
|
38
|
+
let lastRequestTime = 0;
|
|
39
|
+
// 数据库连接状态
|
|
40
|
+
let databaseConnected = true;
|
|
41
|
+
let lastDatabaseCheck = 0;
|
|
42
|
+
const DATABASE_CHECK_INTERVAL = 30000; // 30秒检查一次数据库连接
|
|
43
|
+
// 推送失败统计
|
|
44
|
+
let consecutiveFailureCount = 0;
|
|
45
|
+
let lastFailureTime = 0;
|
|
46
|
+
const FAILURE_RESET_INTERVAL = 60 * 60 * 1000; // 1小时内无失败则重置计数
|
|
47
|
+
// 数据库版本
|
|
48
|
+
const DATABASE_VERSION = '2.0.0'; // 当前数据库结构版本
|
|
49
|
+
const runtimeStats = {
|
|
50
|
+
pushSuccessCount: 0,
|
|
51
|
+
pushFailureCount: 0,
|
|
52
|
+
apiCallCount: 0,
|
|
53
|
+
apiSuccessCount: 0,
|
|
54
|
+
apiFailureCount: 0,
|
|
55
|
+
lastResetTime: Date.now()
|
|
56
|
+
};
|
|
57
|
+
// 更新 API 调用统计
|
|
58
|
+
function updateApiStats(success) {
|
|
59
|
+
runtimeStats.apiCallCount++;
|
|
60
|
+
if (success) {
|
|
61
|
+
runtimeStats.apiSuccessCount++;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
runtimeStats.apiFailureCount++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 更新推送统计
|
|
68
|
+
function updatePushStats(success) {
|
|
69
|
+
if (success) {
|
|
70
|
+
runtimeStats.pushSuccessCount++;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
runtimeStats.pushFailureCount++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
29
76
|
exports.Config = koishi_1.Schema.object({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
77
|
+
// 多站点配置
|
|
78
|
+
sites: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
79
|
+
id: koishi_1.Schema.string().description('站点唯一标识(如 "blog"、"news" 等)'),
|
|
80
|
+
name: koishi_1.Schema.string().description('站点名称(用于显示)'),
|
|
81
|
+
url: koishi_1.Schema.string().description('WordPress 网站地址(例如:https://your-wordpress-site.com)'),
|
|
82
|
+
interval: koishi_1.Schema.number().default(3600000).description('检查间隔(毫秒,默认 1 小时)'),
|
|
83
|
+
targets: koishi_1.Schema.array(koishi_1.Schema.string()).description('推送目标(群号或 QQ 号)'),
|
|
84
|
+
enableAutoPush: koishi_1.Schema.boolean().default(true).description('是否启用自动推送'),
|
|
85
|
+
enableUpdatePush: koishi_1.Schema.boolean().default(false).description('是否启用文章更新推送'),
|
|
86
|
+
enableUserPush: koishi_1.Schema.boolean().default(false).description('是否启用新用户注册推送'),
|
|
87
|
+
mentionAll: koishi_1.Schema.boolean().default(false).description('是否 @全体成员'),
|
|
88
|
+
maxArticles: koishi_1.Schema.number().default(5).description('每次最多推送的文章数量'),
|
|
89
|
+
username: koishi_1.Schema.string().default('').description('WordPress 用户名(用于 Basic 认证,与应用程序密码配合使用)'),
|
|
90
|
+
applicationPassword: koishi_1.Schema.string().default('').description('WordPress 应用程序密码(用于 Basic 认证,例如:hGR2sPFuYnclxHc4AvJq cUtB)'),
|
|
91
|
+
// 站点特定的推送模板配置
|
|
92
|
+
pushTemplate: koishi_1.Schema.object({
|
|
93
|
+
showExcerpt: koishi_1.Schema.boolean().default(false).description('是否显示文章摘要'),
|
|
94
|
+
dateFormat: koishi_1.Schema.string().default('YYYY-MM-DD HH:mm').description('日期格式,支持 YYYY-MM-DD HH:mm 等格式'),
|
|
95
|
+
linkPosition: koishi_1.Schema.union(['top', 'bottom', 'none']).default('bottom').description('链接位置:顶部、底部或不显示'),
|
|
96
|
+
showAuthor: koishi_1.Schema.boolean().default(false).description('是否显示文章作者')
|
|
97
|
+
}).description('推送模板配置')
|
|
98
|
+
})).description('多站点配置列表').default([
|
|
99
|
+
{
|
|
100
|
+
id: 'default',
|
|
101
|
+
name: '默认站点',
|
|
102
|
+
url: 'https://your-wordpress-site.com',
|
|
103
|
+
interval: 3600000,
|
|
104
|
+
targets: [],
|
|
105
|
+
enableAutoPush: true,
|
|
106
|
+
enableUpdatePush: false,
|
|
107
|
+
enableUserPush: false,
|
|
108
|
+
mentionAll: false,
|
|
109
|
+
maxArticles: 5,
|
|
110
|
+
username: '',
|
|
111
|
+
applicationPassword: '',
|
|
112
|
+
pushTemplate: {
|
|
113
|
+
showExcerpt: false,
|
|
114
|
+
dateFormat: 'YYYY-MM-DD HH:mm',
|
|
115
|
+
linkPosition: 'bottom',
|
|
116
|
+
showAuthor: false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
]),
|
|
120
|
+
superAdmins: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description('超级管理员 QQ 号列表'),
|
|
121
|
+
// 全局推送失败通知配置
|
|
122
|
+
failureNotification: koishi_1.Schema.object({
|
|
123
|
+
enable: koishi_1.Schema.boolean().default(true).description('是否启用推送失败通知'),
|
|
124
|
+
threshold: koishi_1.Schema.number().default(3).description('连续失败阈值,达到此值时发送通知'),
|
|
125
|
+
notificationTargets: koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description('通知目标(超级管理员 QQ 号),留空则使用 superAdmins')
|
|
126
|
+
}).description('推送失败通知配置')
|
|
41
127
|
});
|
|
42
128
|
function apply(ctx, config) {
|
|
43
129
|
ctx.logger.info('WordPress 推送插件已加载');
|
|
@@ -45,22 +131,24 @@ function apply(ctx, config) {
|
|
|
45
131
|
// 确保 id 字段被正确设置为自增主键,并且在插入时不会被设置为 NULL
|
|
46
132
|
ctx.model.extend('wordpress_post_updates', {
|
|
47
133
|
id: 'integer',
|
|
134
|
+
siteId: 'string',
|
|
48
135
|
postId: 'integer',
|
|
49
136
|
lastModified: 'timestamp',
|
|
50
137
|
pushedAt: 'timestamp'
|
|
51
138
|
}, {
|
|
52
139
|
primary: 'id',
|
|
53
140
|
autoInc: true,
|
|
54
|
-
unique: ['postId']
|
|
141
|
+
unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
|
|
55
142
|
});
|
|
56
143
|
ctx.model.extend('wordpress_user_registrations', {
|
|
57
144
|
id: 'integer',
|
|
145
|
+
siteId: 'string',
|
|
58
146
|
userId: 'integer',
|
|
59
147
|
pushedAt: 'timestamp'
|
|
60
148
|
}, {
|
|
61
149
|
primary: 'id',
|
|
62
150
|
autoInc: true,
|
|
63
|
-
unique: ['userId']
|
|
151
|
+
unique: ['siteId', 'userId'] // 每个站点的用户 ID 唯一
|
|
64
152
|
});
|
|
65
153
|
// 配置存储表
|
|
66
154
|
ctx.model.extend('wordpress_config', {
|
|
@@ -73,6 +161,16 @@ function apply(ctx, config) {
|
|
|
73
161
|
autoInc: true,
|
|
74
162
|
unique: ['key']
|
|
75
163
|
});
|
|
164
|
+
// 版本记录表
|
|
165
|
+
ctx.model.extend('wordpress_version', {
|
|
166
|
+
id: 'integer',
|
|
167
|
+
version: 'string',
|
|
168
|
+
updatedAt: 'timestamp'
|
|
169
|
+
}, {
|
|
170
|
+
primary: 'id',
|
|
171
|
+
autoInc: true,
|
|
172
|
+
unique: ['version']
|
|
173
|
+
});
|
|
76
174
|
ctx.logger.info('数据库表配置完成,autoInc: true 已启用,确保插入操作不手动指定 id 字段');
|
|
77
175
|
// 为所有数据库操作添加详细日志,便于诊断自增主键问题
|
|
78
176
|
ctx.on('ready', async () => {
|
|
@@ -81,6 +179,7 @@ function apply(ctx, config) {
|
|
|
81
179
|
ctx.logger.info('wordpress_post_updates: id 字段设置为 autoInc: true');
|
|
82
180
|
ctx.logger.info('wordpress_user_registrations: id 字段设置为 autoInc: true');
|
|
83
181
|
ctx.logger.info('wordpress_config: 配置持久化存储表');
|
|
182
|
+
ctx.logger.info('wordpress_version: 数据库版本记录表');
|
|
84
183
|
ctx.logger.info('所有群聊共用一个文章标记,不再区分群聊');
|
|
85
184
|
// 检查并修复数据库表结构问题
|
|
86
185
|
await checkAndFixTableStructure();
|
|
@@ -93,6 +192,12 @@ function apply(ctx, config) {
|
|
|
93
192
|
async function loadPersistentConfig() {
|
|
94
193
|
try {
|
|
95
194
|
ctx.logger.info('开始加载持久化配置...');
|
|
195
|
+
// 确保数据库连接正常
|
|
196
|
+
const connected = await ensureDatabaseConnection();
|
|
197
|
+
if (!connected) {
|
|
198
|
+
ctx.logger.warn('数据库连接异常,跳过加载持久化配置');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
96
201
|
const configRecords = await ctx.database.get('wordpress_config', {});
|
|
97
202
|
ctx.logger.info(`找到 ${configRecords.length} 条持久化配置记录`);
|
|
98
203
|
for (const record of configRecords) {
|
|
@@ -126,6 +231,12 @@ function apply(ctx, config) {
|
|
|
126
231
|
async function saveConfig(key, value) {
|
|
127
232
|
try {
|
|
128
233
|
ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
|
|
234
|
+
// 确保数据库连接正常
|
|
235
|
+
const connected = await ensureDatabaseConnection();
|
|
236
|
+
if (!connected) {
|
|
237
|
+
ctx.logger.warn('数据库连接异常,跳过保存配置');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
129
240
|
// 检查配置是否已存在
|
|
130
241
|
const existingRecords = await ctx.database.get('wordpress_config', { key });
|
|
131
242
|
if (existingRecords.length > 0) {
|
|
@@ -212,14 +323,15 @@ function apply(ctx, config) {
|
|
|
212
323
|
const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
|
|
213
324
|
const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
|
|
214
325
|
// 添加到失败队列
|
|
215
|
-
function addToFailedQueue(type, data, targets) {
|
|
326
|
+
function addToFailedQueue(type, data, targets, siteConfig) {
|
|
216
327
|
const item = {
|
|
217
328
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
218
329
|
type,
|
|
219
330
|
data,
|
|
220
331
|
targets,
|
|
221
332
|
retries: 0,
|
|
222
|
-
createdAt: new Date()
|
|
333
|
+
createdAt: new Date(),
|
|
334
|
+
siteConfig
|
|
223
335
|
};
|
|
224
336
|
failedPushQueue.push(item);
|
|
225
337
|
ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
|
|
@@ -248,10 +360,10 @@ function apply(ctx, config) {
|
|
|
248
360
|
// 根据类型格式化消息
|
|
249
361
|
let message;
|
|
250
362
|
if (item.type === 'post' || item.type === 'update') {
|
|
251
|
-
message = formatPostMessage(item.data, true, item.type === 'update');
|
|
363
|
+
message = formatPostMessage(item.data, true, item.type === 'update', item.siteConfig);
|
|
252
364
|
}
|
|
253
365
|
else {
|
|
254
|
-
message = formatUserMessage(item.data, true);
|
|
366
|
+
message = formatUserMessage(item.data, true, item.siteConfig);
|
|
255
367
|
}
|
|
256
368
|
// 推送到所有目标
|
|
257
369
|
for (const target of item.targets) {
|
|
@@ -297,6 +409,106 @@ function apply(ctx, config) {
|
|
|
297
409
|
// 权限检查通过,继续执行命令
|
|
298
410
|
return { valid: true, userId };
|
|
299
411
|
}
|
|
412
|
+
// 检查数据库连接状态
|
|
413
|
+
async function checkDatabaseConnection() {
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
// 避免频繁检查,30秒内只检查一次
|
|
416
|
+
if (now - lastDatabaseCheck < DATABASE_CHECK_INTERVAL && databaseConnected) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
ctx.logger.info('开始检查数据库连接状态...');
|
|
421
|
+
// 执行一个简单的数据库查询来测试连接
|
|
422
|
+
await ctx.database.get('wordpress_config', {}, { limit: 1 });
|
|
423
|
+
ctx.logger.info('数据库连接正常');
|
|
424
|
+
databaseConnected = true;
|
|
425
|
+
lastDatabaseCheck = now;
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
ctx.logger.error(`数据库连接异常: ${error}`);
|
|
430
|
+
databaseConnected = false;
|
|
431
|
+
lastDatabaseCheck = now;
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// 确保数据库连接正常
|
|
436
|
+
async function ensureDatabaseConnection() {
|
|
437
|
+
const connected = await checkDatabaseConnection();
|
|
438
|
+
if (!connected) {
|
|
439
|
+
ctx.logger.warn('数据库连接异常,暂停推送任务');
|
|
440
|
+
// 记录失败
|
|
441
|
+
recordPushFailure('数据库连接异常');
|
|
442
|
+
// 等待3秒后再次尝试
|
|
443
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
444
|
+
return await checkDatabaseConnection();
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
// 记录推送失败并发送通知
|
|
449
|
+
async function recordPushFailure(reason) {
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
// 检查是否需要重置失败计数
|
|
452
|
+
if (now - lastFailureTime > FAILURE_RESET_INTERVAL) {
|
|
453
|
+
consecutiveFailureCount = 0;
|
|
454
|
+
}
|
|
455
|
+
// 增加失败计数
|
|
456
|
+
consecutiveFailureCount++;
|
|
457
|
+
lastFailureTime = now;
|
|
458
|
+
ctx.logger.error(`推送失败,原因: ${reason},连续失败次数: ${consecutiveFailureCount}`);
|
|
459
|
+
// 检查是否需要发送通知
|
|
460
|
+
if (config.failureNotification.enable &&
|
|
461
|
+
consecutiveFailureCount >= config.failureNotification.threshold) {
|
|
462
|
+
await sendFailureNotification(reason);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// 发送失败通知
|
|
466
|
+
async function sendFailureNotification(reason) {
|
|
467
|
+
try {
|
|
468
|
+
const bot = getValidBot();
|
|
469
|
+
if (!bot) {
|
|
470
|
+
ctx.logger.error('无法发送失败通知,没有可用的 Bot 实例');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// 确定通知目标
|
|
474
|
+
const notificationTargets = config.failureNotification.notificationTargets.length > 0
|
|
475
|
+
? config.failureNotification.notificationTargets
|
|
476
|
+
: config.superAdmins;
|
|
477
|
+
if (notificationTargets.length === 0) {
|
|
478
|
+
ctx.logger.warn('无法发送失败通知,未配置通知目标');
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// 构建通知消息
|
|
482
|
+
const message = `🚨 WordPress 推送插件告警\n\n` +
|
|
483
|
+
`📅 时间: ${new Date().toLocaleString()}\n` +
|
|
484
|
+
`❌ 连续失败次数: ${consecutiveFailureCount}\n` +
|
|
485
|
+
`🔍 失败原因: ${reason}\n` +
|
|
486
|
+
`🌐 站点数: ${config.sites.length}\n` +
|
|
487
|
+
`📡 总推送目标数: ${config.sites.reduce((total, site) => total + site.targets.length, 0)}\n\n` +
|
|
488
|
+
`请及时检查插件状态和相关配置!`;
|
|
489
|
+
// 发送通知
|
|
490
|
+
for (const target of notificationTargets) {
|
|
491
|
+
try {
|
|
492
|
+
await bot.sendMessage(target, message);
|
|
493
|
+
ctx.logger.info(`已向 ${target} 发送推送失败通知`);
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
ctx.logger.error(`发送通知到 ${target} 失败: ${error}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// 重置失败计数
|
|
500
|
+
consecutiveFailureCount = 0;
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
ctx.logger.error(`发送失败通知时发生错误: ${error}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// 重置失败计数
|
|
507
|
+
function resetFailureCount() {
|
|
508
|
+
consecutiveFailureCount = 0;
|
|
509
|
+
lastFailureTime = 0;
|
|
510
|
+
ctx.logger.info('推送失败计数已重置');
|
|
511
|
+
}
|
|
300
512
|
// 检查数据库表结构的函数
|
|
301
513
|
async function checkAndFixTableStructure() {
|
|
302
514
|
try {
|
|
@@ -321,13 +533,14 @@ function apply(ctx, config) {
|
|
|
321
533
|
// 重新扩展模型
|
|
322
534
|
ctx.model.extend('wordpress_post_updates', {
|
|
323
535
|
id: 'integer',
|
|
536
|
+
siteId: 'string',
|
|
324
537
|
postId: 'integer',
|
|
325
538
|
lastModified: 'timestamp',
|
|
326
539
|
pushedAt: 'timestamp'
|
|
327
540
|
}, {
|
|
328
541
|
primary: 'id',
|
|
329
542
|
autoInc: true,
|
|
330
|
-
unique: ['postId']
|
|
543
|
+
unique: ['siteId', 'postId'] // 每个站点的文章 ID 唯一
|
|
331
544
|
});
|
|
332
545
|
ctx.logger.info('wordpress_post_updates 表重新初始化成功');
|
|
333
546
|
}
|
|
@@ -352,12 +565,13 @@ function apply(ctx, config) {
|
|
|
352
565
|
// 重新扩展模型
|
|
353
566
|
ctx.model.extend('wordpress_user_registrations', {
|
|
354
567
|
id: 'integer',
|
|
568
|
+
siteId: 'string',
|
|
355
569
|
userId: 'integer',
|
|
356
570
|
pushedAt: 'timestamp'
|
|
357
571
|
}, {
|
|
358
572
|
primary: 'id',
|
|
359
573
|
autoInc: true,
|
|
360
|
-
unique: ['userId']
|
|
574
|
+
unique: ['siteId', 'userId'] // 每个站点的用户 ID 唯一
|
|
361
575
|
});
|
|
362
576
|
ctx.logger.info('wordpress_user_registrations 表重新初始化成功');
|
|
363
577
|
}
|
|
@@ -413,6 +627,16 @@ function apply(ctx, config) {
|
|
|
413
627
|
let retries = 0;
|
|
414
628
|
while (retries <= maxRetries) {
|
|
415
629
|
try {
|
|
630
|
+
// API 请求频率控制
|
|
631
|
+
const now = Date.now();
|
|
632
|
+
const timeSinceLastRequest = now - lastRequestTime;
|
|
633
|
+
if (timeSinceLastRequest < CONSTANTS.API_RATE_LIMIT) {
|
|
634
|
+
const waitTime = CONSTANTS.API_RATE_LIMIT - timeSinceLastRequest;
|
|
635
|
+
ctx.logger.info(`API 请求频率限制,等待 ${waitTime}ms 后继续请求`);
|
|
636
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
637
|
+
}
|
|
638
|
+
// 更新上次请求时间
|
|
639
|
+
lastRequestTime = Date.now();
|
|
416
640
|
ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
|
|
417
641
|
const response = await ctx.http.get(url, requestConfig);
|
|
418
642
|
ctx.logger.info(`HTTP请求成功: ${url}`);
|
|
@@ -424,6 +648,8 @@ function apply(ctx, config) {
|
|
|
424
648
|
retries++;
|
|
425
649
|
if (retries > maxRetries) {
|
|
426
650
|
ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
|
|
651
|
+
// 记录推送失败
|
|
652
|
+
recordPushFailure(`API请求失败: ${errorMessage}`);
|
|
427
653
|
return null;
|
|
428
654
|
}
|
|
429
655
|
// 重试前等待
|
|
@@ -432,16 +658,16 @@ function apply(ctx, config) {
|
|
|
432
658
|
}
|
|
433
659
|
return null;
|
|
434
660
|
}
|
|
435
|
-
async function fetchLatestPosts() {
|
|
661
|
+
async function fetchLatestPosts(site) {
|
|
436
662
|
try {
|
|
437
|
-
const url = `${
|
|
438
|
-
ctx.logger.info(
|
|
663
|
+
const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=date&order=desc`;
|
|
664
|
+
ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的文章: ${url}`);
|
|
439
665
|
// 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
|
|
440
666
|
const requestConfig = {};
|
|
441
|
-
if (
|
|
667
|
+
if (site.username && site.applicationPassword) {
|
|
442
668
|
// 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
|
|
443
|
-
const username =
|
|
444
|
-
const password =
|
|
669
|
+
const username = site.username;
|
|
670
|
+
const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
|
|
445
671
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
446
672
|
requestConfig.headers = {
|
|
447
673
|
Authorization: `Basic ${auth}`
|
|
@@ -449,30 +675,33 @@ function apply(ctx, config) {
|
|
|
449
675
|
}
|
|
450
676
|
const response = await httpRequest(url, requestConfig);
|
|
451
677
|
if (!response) {
|
|
452
|
-
ctx.logger.error(
|
|
678
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`);
|
|
453
679
|
return [];
|
|
454
680
|
}
|
|
455
|
-
ctx.logger.info(
|
|
681
|
+
ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
|
|
456
682
|
return response;
|
|
457
683
|
}
|
|
458
684
|
catch (error) {
|
|
459
|
-
|
|
685
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
686
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`);
|
|
687
|
+
// 记录失败
|
|
688
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
|
|
460
689
|
return [];
|
|
461
690
|
}
|
|
462
691
|
}
|
|
463
|
-
async function fetchLatestUsers() {
|
|
692
|
+
async function fetchLatestUsers(site) {
|
|
464
693
|
try {
|
|
465
694
|
// 修改API请求,添加_fields参数明确请求注册日期字段
|
|
466
695
|
// WordPress REST API 默认可能不会返回注册日期,需要明确请求
|
|
467
696
|
const fields = 'id,name,slug,date,date_registered,registered_date,created_at,registeredAt,email,roles,url,description,link,avatar_urls';
|
|
468
|
-
const url = `${
|
|
469
|
-
ctx.logger.info(
|
|
697
|
+
const url = `${site.url}/wp-json/wp/v2/users?per_page=${site.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
|
|
698
|
+
ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的用户: ${url}`);
|
|
470
699
|
// 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
|
|
471
700
|
const requestConfig = {};
|
|
472
|
-
if (
|
|
701
|
+
if (site.username && site.applicationPassword) {
|
|
473
702
|
// 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
|
|
474
|
-
const username =
|
|
475
|
-
const password =
|
|
703
|
+
const username = site.username;
|
|
704
|
+
const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
|
|
476
705
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
477
706
|
requestConfig.headers = {
|
|
478
707
|
Authorization: `Basic ${auth}`
|
|
@@ -480,12 +709,14 @@ function apply(ctx, config) {
|
|
|
480
709
|
}
|
|
481
710
|
const response = await httpRequest(url, requestConfig);
|
|
482
711
|
if (!response) {
|
|
483
|
-
ctx.logger.error(
|
|
484
|
-
ctx.logger.error(`WordPress REST API 的 users
|
|
712
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`);
|
|
713
|
+
ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
|
|
714
|
+
// 记录失败
|
|
715
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
|
|
485
716
|
// 返回空数组,确保插件继续运行
|
|
486
717
|
return [];
|
|
487
718
|
}
|
|
488
|
-
ctx.logger.info(
|
|
719
|
+
ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
|
|
489
720
|
// 添加调试日志,查看API返回的实际数据结构
|
|
490
721
|
if (response.length > 0) {
|
|
491
722
|
ctx.logger.info(`用户数据示例: ${JSON.stringify(response[0], null, 2)}`);
|
|
@@ -496,22 +727,25 @@ function apply(ctx, config) {
|
|
|
496
727
|
return response;
|
|
497
728
|
}
|
|
498
729
|
catch (error) {
|
|
499
|
-
|
|
500
|
-
ctx.logger.error(
|
|
730
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
731
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`);
|
|
732
|
+
ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
|
|
733
|
+
// 记录失败
|
|
734
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
|
|
501
735
|
// 返回空数组,确保插件继续运行
|
|
502
736
|
return [];
|
|
503
737
|
}
|
|
504
738
|
}
|
|
505
|
-
async function fetchUpdatedPosts() {
|
|
739
|
+
async function fetchUpdatedPosts(site) {
|
|
506
740
|
try {
|
|
507
|
-
const url = `${
|
|
508
|
-
ctx.logger.info(
|
|
741
|
+
const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=modified&order=desc`;
|
|
742
|
+
ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的更新文章: ${url}`);
|
|
509
743
|
// 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
|
|
510
744
|
const requestConfig = {};
|
|
511
|
-
if (
|
|
745
|
+
if (site.username && site.applicationPassword) {
|
|
512
746
|
// 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
|
|
513
|
-
const username =
|
|
514
|
-
const password =
|
|
747
|
+
const username = site.username;
|
|
748
|
+
const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
|
|
515
749
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
516
750
|
requestConfig.headers = {
|
|
517
751
|
Authorization: `Basic ${auth}`
|
|
@@ -519,23 +753,34 @@ function apply(ctx, config) {
|
|
|
519
753
|
}
|
|
520
754
|
const response = await httpRequest(url, requestConfig);
|
|
521
755
|
if (!response) {
|
|
522
|
-
ctx.logger.error(
|
|
756
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`);
|
|
757
|
+
// 记录失败
|
|
758
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
|
|
523
759
|
return [];
|
|
524
760
|
}
|
|
525
|
-
ctx.logger.info(
|
|
761
|
+
ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
|
|
526
762
|
return response;
|
|
527
763
|
}
|
|
528
764
|
catch (error) {
|
|
529
|
-
|
|
765
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
766
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`);
|
|
767
|
+
// 记录失败
|
|
768
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
|
|
530
769
|
return [];
|
|
531
770
|
}
|
|
532
771
|
}
|
|
533
|
-
async function isUserPushed(userId) {
|
|
772
|
+
async function isUserPushed(siteId, userId) {
|
|
534
773
|
try {
|
|
535
|
-
|
|
536
|
-
const
|
|
774
|
+
// 确保数据库连接正常
|
|
775
|
+
const connected = await ensureDatabaseConnection();
|
|
776
|
+
if (!connected) {
|
|
777
|
+
ctx.logger.warn('数据库连接异常,跳过检查用户推送记录');
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
|
|
781
|
+
const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId });
|
|
537
782
|
const result = record.length > 0;
|
|
538
|
-
ctx.logger.info(
|
|
783
|
+
ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
|
|
539
784
|
return result;
|
|
540
785
|
}
|
|
541
786
|
catch (error) {
|
|
@@ -546,12 +791,18 @@ function apply(ctx, config) {
|
|
|
546
791
|
return false;
|
|
547
792
|
}
|
|
548
793
|
}
|
|
549
|
-
async function getPostUpdateRecord(postId) {
|
|
794
|
+
async function getPostUpdateRecord(siteId, postId) {
|
|
550
795
|
try {
|
|
551
|
-
|
|
552
|
-
const
|
|
796
|
+
// 确保数据库连接正常
|
|
797
|
+
const connected = await ensureDatabaseConnection();
|
|
798
|
+
if (!connected) {
|
|
799
|
+
ctx.logger.warn('数据库连接异常,跳过获取文章更新记录');
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
803
|
+
const records = await ctx.database.get('wordpress_post_updates', { siteId, postId });
|
|
553
804
|
const result = records.length > 0 ? records[0] : null;
|
|
554
|
-
ctx.logger.info(
|
|
805
|
+
ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
|
|
555
806
|
return result;
|
|
556
807
|
}
|
|
557
808
|
catch (error) {
|
|
@@ -562,74 +813,116 @@ function apply(ctx, config) {
|
|
|
562
813
|
return null;
|
|
563
814
|
}
|
|
564
815
|
}
|
|
565
|
-
async function markUserAsPushed(userId) {
|
|
816
|
+
async function markUserAsPushed(siteId, userId) {
|
|
566
817
|
try {
|
|
567
|
-
|
|
818
|
+
// 确保数据库连接正常
|
|
819
|
+
const connected = await ensureDatabaseConnection();
|
|
820
|
+
if (!connected) {
|
|
821
|
+
ctx.logger.warn('数据库连接异常,跳过标记用户推送记录');
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
ctx.logger.info(`开始标记用户已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
|
|
568
825
|
// 创建新记录,不手动指定id,让数据库自动生成
|
|
569
826
|
const newRecord = {
|
|
827
|
+
siteId,
|
|
570
828
|
userId,
|
|
571
829
|
pushedAt: new Date()
|
|
572
830
|
};
|
|
573
831
|
ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
|
|
574
832
|
await ctx.database.create('wordpress_user_registrations', newRecord);
|
|
575
|
-
ctx.logger.info(
|
|
833
|
+
ctx.logger.info(`已成功标记站点 ${siteId} 用户 ${userId} 为已推送`);
|
|
576
834
|
}
|
|
577
835
|
catch (error) {
|
|
578
836
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
579
837
|
if (errorMessage.includes('UNIQUE constraint failed')) {
|
|
580
|
-
ctx.logger.warn(
|
|
838
|
+
ctx.logger.warn(`用户推送记录已存在,跳过重复插入:站点 ${siteId} 用户 ${userId}`);
|
|
581
839
|
ctx.logger.warn(`完整错误信息:${errorMessage}`);
|
|
582
840
|
}
|
|
583
841
|
else {
|
|
584
842
|
ctx.logger.error(`标记用户推送记录失败:${errorMessage}`);
|
|
585
843
|
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
586
|
-
ctx.logger.error(`插入参数:userId=${userId}`);
|
|
844
|
+
ctx.logger.error(`插入参数:siteId=${siteId}, userId=${userId}`);
|
|
587
845
|
// 非约束冲突错误,不抛出,确保插件继续运行
|
|
588
846
|
}
|
|
589
847
|
}
|
|
590
848
|
}
|
|
591
|
-
async function updatePostUpdateRecord(postId, modifiedDate) {
|
|
849
|
+
async function updatePostUpdateRecord(siteId, postId, modifiedDate) {
|
|
592
850
|
try {
|
|
593
|
-
|
|
594
|
-
const
|
|
851
|
+
// 确保数据库连接正常
|
|
852
|
+
const connected = await ensureDatabaseConnection();
|
|
853
|
+
if (!connected) {
|
|
854
|
+
ctx.logger.warn('数据库连接异常,跳过更新文章更新记录');
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
ctx.logger.info(`开始更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId},修改时间: ${modifiedDate}`);
|
|
858
|
+
const record = await getPostUpdateRecord(siteId, postId);
|
|
595
859
|
if (record) {
|
|
596
|
-
ctx.logger.info(
|
|
860
|
+
ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
|
|
597
861
|
// Koishi database API 不支持 update 方法,使用 remove + create 代替
|
|
598
|
-
await ctx.database.remove('wordpress_post_updates', { postId });
|
|
599
|
-
ctx.logger.info(
|
|
862
|
+
await ctx.database.remove('wordpress_post_updates', { siteId, postId });
|
|
863
|
+
ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
600
864
|
}
|
|
601
865
|
// 创建新记录,不指定 id 字段,让数据库自动生成
|
|
602
866
|
const newRecord = {
|
|
867
|
+
siteId,
|
|
603
868
|
postId,
|
|
604
869
|
lastModified: modifiedDate,
|
|
605
870
|
pushedAt: new Date()
|
|
606
871
|
};
|
|
607
|
-
ctx.logger.info(
|
|
872
|
+
ctx.logger.info(`准备创建新记录,站点 ID: ${siteId},文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
|
|
608
873
|
await ctx.database.create('wordpress_post_updates', newRecord);
|
|
609
|
-
ctx.logger.info(
|
|
874
|
+
ctx.logger.info(`已成功更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
610
875
|
}
|
|
611
876
|
catch (error) {
|
|
612
877
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
613
|
-
ctx.logger.error(
|
|
878
|
+
ctx.logger.error(`更新文章更新记录失败,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
614
879
|
ctx.logger.error(`错误信息: ${errorMessage}`);
|
|
615
880
|
ctx.logger.error(`错误栈: ${error instanceof Error ? error.stack : '无'}`);
|
|
616
881
|
// 不再抛出错误,确保推送流程继续运行
|
|
617
882
|
// 发生错误时,默认返回,避免阻塞推送流程
|
|
618
|
-
ctx.logger.warn(
|
|
883
|
+
ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
619
884
|
}
|
|
620
885
|
}
|
|
621
886
|
// 1. 新增强清洗函数:针对性解决敏感字符问题
|
|
622
887
|
function sanitizeContent(content) {
|
|
623
|
-
|
|
888
|
+
let sanitized = content
|
|
624
889
|
.replace(/<[^>]*>/g, '') // 移除所有 HTML 标签
|
|
625
890
|
.replace(/[\x00-\x1F\x7F]/g, '') // 移除不可见控制符,QQ 接口明确禁止
|
|
626
891
|
.replace(/\u3000/g, ' ') // 全角空格转半角空格,解决适配器编码缺陷
|
|
627
892
|
.replace(/\s+/g, ' ') // 标准化所有空白符为单个半角空格
|
|
628
893
|
.trim(); // 移除首尾空格
|
|
894
|
+
// 添加敏感词过滤
|
|
895
|
+
sanitized = filterSensitiveWords(sanitized);
|
|
896
|
+
return sanitized;
|
|
629
897
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
898
|
+
// 敏感词过滤函数
|
|
899
|
+
function filterSensitiveWords(content) {
|
|
900
|
+
let filteredContent = content;
|
|
901
|
+
let hasSensitiveWords = false;
|
|
902
|
+
// 遍历敏感词列表进行替换
|
|
903
|
+
for (const word of CONSTANTS.SENSITIVE_WORDS) {
|
|
904
|
+
if (filteredContent.includes(word)) {
|
|
905
|
+
hasSensitiveWords = true;
|
|
906
|
+
const regex = new RegExp(word, 'gi'); // 不区分大小写
|
|
907
|
+
filteredContent = filteredContent.replace(regex, CONSTANTS.SENSITIVE_REPLACEMENT);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
// 如果检测到敏感词,记录日志
|
|
911
|
+
if (hasSensitiveWords) {
|
|
912
|
+
ctx.logger.info('消息内容包含敏感词,已进行过滤处理');
|
|
913
|
+
// 仅记录处理信息,不记录具体内容,保护隐私
|
|
914
|
+
}
|
|
915
|
+
return filteredContent;
|
|
916
|
+
}
|
|
917
|
+
function formatPostMessage(post, mention = false, isUpdate = false, siteConfig) {
|
|
918
|
+
// 使用默认站点配置或传入的站点配置
|
|
919
|
+
const configToUse = siteConfig || config.sites[0];
|
|
920
|
+
if (!configToUse) {
|
|
921
|
+
ctx.logger.error('无可用的站点配置');
|
|
922
|
+
return '';
|
|
923
|
+
}
|
|
924
|
+
// 生成缓存键,包含推送模板配置信息
|
|
925
|
+
const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${configToUse.mentionAll}_${JSON.stringify(configToUse.pushTemplate)}`;
|
|
633
926
|
// 检查缓存
|
|
634
927
|
const cached = formatCache.post.get(cacheKey);
|
|
635
928
|
if (cached && isCacheValid(cached.timestamp)) {
|
|
@@ -642,7 +935,7 @@ function apply(ctx, config) {
|
|
|
642
935
|
if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
|
|
643
936
|
title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
|
|
644
937
|
}
|
|
645
|
-
//
|
|
938
|
+
// 根据配置格式化日期
|
|
646
939
|
const formatDate = (dateString) => {
|
|
647
940
|
const date = new Date(dateString);
|
|
648
941
|
const year = date.getFullYear();
|
|
@@ -650,19 +943,50 @@ function apply(ctx, config) {
|
|
|
650
943
|
const day = String(date.getDate()).padStart(2, '0');
|
|
651
944
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
652
945
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
653
|
-
|
|
946
|
+
// 根据配置的日期格式进行替换
|
|
947
|
+
let formattedDate = configToUse.pushTemplate.dateFormat
|
|
948
|
+
.replace('YYYY', year.toString())
|
|
949
|
+
.replace('MM', month)
|
|
950
|
+
.replace('DD', day)
|
|
951
|
+
.replace('HH', hours)
|
|
952
|
+
.replace('mm', minutes);
|
|
953
|
+
return formattedDate;
|
|
654
954
|
};
|
|
655
955
|
const date = formatDate(post.date);
|
|
656
956
|
// 链接强制编码
|
|
657
957
|
const encodedLink = encodeURI(post.link);
|
|
658
958
|
// 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
|
|
659
|
-
const atAllText = mention &&
|
|
959
|
+
const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
|
|
660
960
|
// 只使用一个极简表情
|
|
661
961
|
const messageType = isUpdate ? '📝' : '📰';
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
//
|
|
665
|
-
|
|
962
|
+
// 构建消息内容
|
|
963
|
+
let messageParts = [];
|
|
964
|
+
// 添加头部
|
|
965
|
+
messageParts.push(`${messageType} ${atAllText}${date} - ${title}`);
|
|
966
|
+
// 根据配置添加作者
|
|
967
|
+
if (configToUse.pushTemplate.showAuthor && post.author) {
|
|
968
|
+
messageParts.push(`👤 作者: ${post.author}`);
|
|
969
|
+
}
|
|
970
|
+
// 根据配置添加摘要
|
|
971
|
+
if (configToUse.pushTemplate.showExcerpt && post.excerpt) {
|
|
972
|
+
let excerpt = sanitizeContent(post.excerpt.rendered);
|
|
973
|
+
// 截断摘要长度
|
|
974
|
+
if (excerpt.length > 100) {
|
|
975
|
+
excerpt = excerpt.substring(0, 97) + '...';
|
|
976
|
+
}
|
|
977
|
+
if (excerpt) {
|
|
978
|
+
messageParts.push(`📄 摘要: ${excerpt}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// 根据配置添加链接
|
|
982
|
+
if (configToUse.pushTemplate.linkPosition === 'top') {
|
|
983
|
+
messageParts.unshift(`🔗 ${encodedLink}`);
|
|
984
|
+
}
|
|
985
|
+
else if (configToUse.pushTemplate.linkPosition === 'bottom') {
|
|
986
|
+
messageParts.push(`🔗 ${encodedLink}`);
|
|
987
|
+
}
|
|
988
|
+
// 合并消息部分
|
|
989
|
+
let message = messageParts.join('\n');
|
|
666
990
|
// 双级长度控制:整体消息兜底最大长度
|
|
667
991
|
if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
|
|
668
992
|
message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
|
|
@@ -676,9 +1000,15 @@ function apply(ctx, config) {
|
|
|
676
1000
|
// 直接返回纯字符串,跳过适配器复杂编码
|
|
677
1001
|
return message;
|
|
678
1002
|
}
|
|
679
|
-
function formatUserMessage(user, mention = false) {
|
|
1003
|
+
function formatUserMessage(user, mention = false, siteConfig) {
|
|
1004
|
+
// 使用默认站点配置或传入的站点配置
|
|
1005
|
+
const configToUse = siteConfig || config.sites[0];
|
|
1006
|
+
if (!configToUse) {
|
|
1007
|
+
ctx.logger.error('无可用的站点配置');
|
|
1008
|
+
return '';
|
|
1009
|
+
}
|
|
680
1010
|
// 生成缓存键
|
|
681
|
-
const cacheKey = `user_${user.id}_${mention}_${
|
|
1011
|
+
const cacheKey = `user_${user.id}_${mention}_${configToUse.mentionAll}`;
|
|
682
1012
|
// 检查缓存
|
|
683
1013
|
const cached = formatCache.user.get(cacheKey);
|
|
684
1014
|
if (cached && isCacheValid(cached.timestamp)) {
|
|
@@ -738,7 +1068,7 @@ function apply(ctx, config) {
|
|
|
738
1068
|
ctx.logger.error(`处理用户 ${username} 日期时出错: ${error}`);
|
|
739
1069
|
}
|
|
740
1070
|
// 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
|
|
741
|
-
const atAllText = mention &&
|
|
1071
|
+
const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
|
|
742
1072
|
// 只使用一个极简表情
|
|
743
1073
|
const messageType = '👤';
|
|
744
1074
|
// 构建核心消息内容,严格控制格式和换行
|
|
@@ -762,138 +1092,159 @@ function apply(ctx, config) {
|
|
|
762
1092
|
const bot = getValidBot();
|
|
763
1093
|
if (!bot) {
|
|
764
1094
|
ctx.logger.error('没有可用的 Bot 实例');
|
|
1095
|
+
recordPushFailure('没有可用的 Bot 实例');
|
|
765
1096
|
return;
|
|
766
1097
|
}
|
|
767
1098
|
// 修复 Bot 标识 undefined 问题
|
|
768
1099
|
const botId = bot.selfId || 'unknown';
|
|
769
1100
|
ctx.logger.info(`使用 bot ${bot.platform}:${botId} 进行推送`);
|
|
770
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1101
|
+
// 遍历所有站点
|
|
1102
|
+
for (const site of config.sites) {
|
|
1103
|
+
ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
|
|
1104
|
+
// 推送新文章
|
|
1105
|
+
if (site.enableAutoPush) {
|
|
1106
|
+
const posts = await fetchLatestPosts(site);
|
|
1107
|
+
ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
|
|
1108
|
+
if (posts.length > 0) {
|
|
1109
|
+
for (const post of posts) {
|
|
1110
|
+
ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
|
|
1111
|
+
ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
|
|
1112
|
+
// 检查文章是否已推送过(所有群聊共用一个标记)
|
|
1113
|
+
const postRecord = await getPostUpdateRecord(site.id, post.id);
|
|
1114
|
+
const hasPushed = !!postRecord;
|
|
1115
|
+
ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
|
|
1116
|
+
if (!hasPushed) {
|
|
1117
|
+
// 推送到该站点的所有目标群聊
|
|
1118
|
+
const failedTargets = [];
|
|
1119
|
+
for (const target of site.targets) {
|
|
1120
|
+
try {
|
|
1121
|
+
ctx.logger.info(`正在处理目标: ${target}`);
|
|
1122
|
+
// 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
|
|
1123
|
+
const stringTarget = target;
|
|
1124
|
+
const message = formatPostMessage(post, site.mentionAll, false, site);
|
|
1125
|
+
ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
|
|
1126
|
+
await bot.sendMessage(stringTarget, message);
|
|
1127
|
+
ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1131
|
+
ctx.logger.error(`推送新文章到 ${target} 失败: ${errorMessage}`);
|
|
1132
|
+
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
1133
|
+
failedTargets.push(target);
|
|
1134
|
+
// 记录推送失败
|
|
1135
|
+
recordPushFailure(`推送消息失败到 ${target}: ${errorMessage}`);
|
|
1136
|
+
}
|
|
794
1137
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
failedTargets.push(target);
|
|
1138
|
+
// 如果有失败的目标,添加到失败队列
|
|
1139
|
+
if (failedTargets.length > 0) {
|
|
1140
|
+
addToFailedQueue('post', post, failedTargets, site);
|
|
799
1141
|
}
|
|
1142
|
+
// 标记文章已推送(所有群聊共用一个标记)
|
|
1143
|
+
await updatePostUpdateRecord(site.id, post.id, new Date(post.modified));
|
|
1144
|
+
ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
|
|
800
1145
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
addToFailedQueue('post', post, failedTargets);
|
|
1146
|
+
else {
|
|
1147
|
+
ctx.logger.info(`跳过推送: 站点 ${site.id} 文章 ${post.id} 已推送过,所有群聊将不再推送`);
|
|
804
1148
|
}
|
|
805
|
-
// 标记文章已推送(所有群聊共用一个标记)
|
|
806
|
-
await updatePostUpdateRecord(post.id, new Date(post.modified));
|
|
807
|
-
ctx.logger.info(`已标记文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
|
|
808
|
-
}
|
|
809
|
-
else {
|
|
810
|
-
ctx.logger.info(`跳过推送: 文章 ${post.id} 已推送过,所有群聊将不再推送`);
|
|
811
1149
|
}
|
|
812
1150
|
}
|
|
813
1151
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1152
|
+
// 推送文章更新
|
|
1153
|
+
if (site.enableUpdatePush) {
|
|
1154
|
+
const posts = await fetchUpdatedPosts(site);
|
|
1155
|
+
if (posts.length > 0) {
|
|
1156
|
+
for (const post of posts) {
|
|
1157
|
+
const updateRecord = await getPostUpdateRecord(site.id, post.id);
|
|
1158
|
+
const postModifiedDate = new Date(post.modified);
|
|
1159
|
+
// 检查文章是否有更新
|
|
1160
|
+
if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
|
|
1161
|
+
ctx.logger.info(`站点 ${site.id} 文章 ${post.id} 有更新,准备推送更新通知`);
|
|
1162
|
+
// 推送到该站点的所有目标群聊
|
|
1163
|
+
const failedTargets = [];
|
|
1164
|
+
for (const target of site.targets) {
|
|
1165
|
+
try {
|
|
1166
|
+
ctx.logger.info(`正在处理目标: ${target}`);
|
|
1167
|
+
const stringTarget = target;
|
|
1168
|
+
const message = formatPostMessage(post, site.mentionAll, true, site);
|
|
1169
|
+
ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
|
|
1170
|
+
await bot.sendMessage(stringTarget, message);
|
|
1171
|
+
ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1175
|
+
ctx.logger.error(`推送文章更新到 ${target} 失败: ${errorMessage}`);
|
|
1176
|
+
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
1177
|
+
failedTargets.push(target);
|
|
1178
|
+
// 记录推送失败
|
|
1179
|
+
recordPushFailure(`推送文章更新失败到 ${target}: ${errorMessage}`);
|
|
1180
|
+
}
|
|
835
1181
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
failedTargets.push(target);
|
|
1182
|
+
// 如果有失败的目标,添加到失败队列
|
|
1183
|
+
if (failedTargets.length > 0) {
|
|
1184
|
+
addToFailedQueue('update', post, failedTargets, site);
|
|
840
1185
|
}
|
|
1186
|
+
// 更新文章更新记录(所有群聊共用一个标记)
|
|
1187
|
+
await updatePostUpdateRecord(site.id, post.id, postModifiedDate);
|
|
1188
|
+
ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
|
|
841
1189
|
}
|
|
842
|
-
// 如果有失败的目标,添加到失败队列
|
|
843
|
-
if (failedTargets.length > 0) {
|
|
844
|
-
addToFailedQueue('update', post, failedTargets);
|
|
845
|
-
}
|
|
846
|
-
// 更新文章更新记录(所有群聊共用一个标记)
|
|
847
|
-
await updatePostUpdateRecord(post.id, postModifiedDate);
|
|
848
|
-
ctx.logger.info(`已更新文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
|
|
849
1190
|
}
|
|
850
1191
|
}
|
|
851
1192
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1193
|
+
// 推送新用户注册
|
|
1194
|
+
if (site.enableUserPush) {
|
|
1195
|
+
const users = await fetchLatestUsers(site);
|
|
1196
|
+
if (users.length > 0) {
|
|
1197
|
+
for (const user of users) {
|
|
1198
|
+
if (!(await isUserPushed(site.id, user.id))) {
|
|
1199
|
+
const failedTargets = [];
|
|
1200
|
+
for (const target of site.targets) {
|
|
1201
|
+
try {
|
|
1202
|
+
ctx.logger.info(`正在处理目标: ${target}`);
|
|
1203
|
+
// 直接使用原始目标字符串,与新文章推送逻辑保持一致
|
|
1204
|
+
const stringTarget = target;
|
|
1205
|
+
const message = formatUserMessage(user, site.mentionAll, site);
|
|
1206
|
+
ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
|
|
1207
|
+
await bot.sendMessage(stringTarget, message);
|
|
1208
|
+
ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
|
|
1209
|
+
}
|
|
1210
|
+
catch (error) {
|
|
1211
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1212
|
+
ctx.logger.error(`推送新用户到 ${target} 失败: ${errorMessage}`);
|
|
1213
|
+
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
1214
|
+
failedTargets.push(target);
|
|
1215
|
+
// 记录推送失败
|
|
1216
|
+
recordPushFailure(`推送新用户失败到 ${target}: ${errorMessage}`);
|
|
1217
|
+
}
|
|
869
1218
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
failedTargets.push(target);
|
|
1219
|
+
// 如果有失败的目标,添加到失败队列
|
|
1220
|
+
if (failedTargets.length > 0) {
|
|
1221
|
+
addToFailedQueue('user', user, failedTargets, site);
|
|
874
1222
|
}
|
|
1223
|
+
// 标记用户已推送
|
|
1224
|
+
await markUserAsPushed(site.id, user.id);
|
|
875
1225
|
}
|
|
876
|
-
// 如果有失败的目标,添加到失败队列
|
|
877
|
-
if (failedTargets.length > 0) {
|
|
878
|
-
addToFailedQueue('user', user, failedTargets);
|
|
879
|
-
}
|
|
880
|
-
// 标记用户已推送
|
|
881
|
-
await markUserAsPushed(user.id);
|
|
882
1226
|
}
|
|
883
1227
|
}
|
|
884
1228
|
}
|
|
885
1229
|
}
|
|
886
1230
|
}
|
|
887
|
-
ctx.command('wordpress.latest', '
|
|
888
|
-
.action(async ({ session }) => {
|
|
889
|
-
ctx.logger.info(
|
|
890
|
-
|
|
1231
|
+
ctx.command('wordpress.latest [siteId]', '查看最新文章,可选站点 ID')
|
|
1232
|
+
.action(async ({ session }, siteId) => {
|
|
1233
|
+
ctx.logger.info(`命令 wordpress.latest 被调用,站点 ID: ${siteId || '默认'}`);
|
|
1234
|
+
// 选择站点
|
|
1235
|
+
const targetSite = siteId
|
|
1236
|
+
? config.sites.find(site => site.id === siteId)
|
|
1237
|
+
: config.sites[0];
|
|
1238
|
+
if (!targetSite) {
|
|
1239
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1240
|
+
}
|
|
1241
|
+
const posts = await fetchLatestPosts(targetSite);
|
|
891
1242
|
if (posts.length === 0) {
|
|
892
|
-
ctx.logger.info(
|
|
893
|
-
return
|
|
1243
|
+
ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
|
|
1244
|
+
return `站点 ${targetSite.name} 暂无文章`;
|
|
894
1245
|
}
|
|
895
1246
|
// 动态添加文章,确保消息长度不超过500字符
|
|
896
|
-
let message =
|
|
1247
|
+
let message = `📰 ${targetSite.name} 最新文章:\n`;
|
|
897
1248
|
let addedCount = 0;
|
|
898
1249
|
for (const post of posts) {
|
|
899
1250
|
const title = sanitizeContent(post.title.rendered);
|
|
@@ -919,15 +1270,22 @@ function apply(ctx, config) {
|
|
|
919
1270
|
ctx.logger.info(`准备返回消息,长度: ${message.length},显示 ${addedCount}/${posts.length} 篇文章`);
|
|
920
1271
|
return message;
|
|
921
1272
|
});
|
|
922
|
-
ctx.command('wordpress.list', '
|
|
923
|
-
.action(async () => {
|
|
924
|
-
ctx.logger.info(
|
|
925
|
-
|
|
1273
|
+
ctx.command('wordpress.list [siteId]', '查看文章列表,可选站点 ID')
|
|
1274
|
+
.action(async (_, siteId) => {
|
|
1275
|
+
ctx.logger.info(`命令 wordpress.list 被调用,站点 ID: ${siteId || '默认'}`);
|
|
1276
|
+
// 选择站点
|
|
1277
|
+
const targetSite = siteId
|
|
1278
|
+
? config.sites.find(site => site.id === siteId)
|
|
1279
|
+
: config.sites[0];
|
|
1280
|
+
if (!targetSite) {
|
|
1281
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1282
|
+
}
|
|
1283
|
+
const posts = await fetchLatestPosts(targetSite);
|
|
926
1284
|
if (posts.length === 0) {
|
|
927
|
-
return
|
|
1285
|
+
return `站点 ${targetSite.name} 暂无文章`;
|
|
928
1286
|
}
|
|
929
1287
|
// 使用数组拼接消息,便于控制格式和长度
|
|
930
|
-
const messageParts = [
|
|
1288
|
+
const messageParts = [`📚 ${targetSite.name} 文章列表:`];
|
|
931
1289
|
for (const post of posts) {
|
|
932
1290
|
const title = sanitizeContent(post.title.rendered);
|
|
933
1291
|
// 截断标题,避免单条过长
|
|
@@ -952,97 +1310,152 @@ function apply(ctx, config) {
|
|
|
952
1310
|
await pushNewPosts();
|
|
953
1311
|
return '已检查并推送最新文章';
|
|
954
1312
|
});
|
|
955
|
-
ctx.command('wordpress.status', '
|
|
956
|
-
.action(({ session }) => {
|
|
957
|
-
ctx.logger.info(
|
|
1313
|
+
ctx.command('wordpress.status [siteId]', '查看插件状态,可选站点 ID')
|
|
1314
|
+
.action(({ session }, siteId) => {
|
|
1315
|
+
ctx.logger.info(`命令 wordpress.status 被调用,站点 ID: ${siteId || '所有'}`);
|
|
958
1316
|
// 获取当前群号,如果有的话
|
|
959
1317
|
const currentGroup = session?.channelId || '未知群聊';
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
message = messageParts.
|
|
1318
|
+
if (siteId) {
|
|
1319
|
+
// 显示单个站点状态
|
|
1320
|
+
const targetSite = config.sites.find(site => site.id === siteId);
|
|
1321
|
+
if (!targetSite) {
|
|
1322
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1323
|
+
}
|
|
1324
|
+
// 使用数组拼接消息,便于控制格式和长度
|
|
1325
|
+
const messageParts = [
|
|
1326
|
+
`📊 WordPress 插件状态 - ${targetSite.name}`,
|
|
1327
|
+
`🌐 站点: ${targetSite.url}`,
|
|
1328
|
+
`⏰ 间隔: ${targetSite.interval / 1000} 秒`,
|
|
1329
|
+
`🎯 推送目标: ${targetSite.targets.join(', ') || '无'}`,
|
|
1330
|
+
`🔔 自动推送: ${targetSite.enableAutoPush ? '开启' : '关闭'}`,
|
|
1331
|
+
`🔄 更新推送: ${targetSite.enableUpdatePush ? '开启' : '关闭'}`,
|
|
1332
|
+
`👤 用户推送: ${targetSite.enableUserPush ? '开启' : '关闭'}`,
|
|
1333
|
+
`📢 @全体: ${targetSite.mentionAll ? '开启' : '关闭'}`,
|
|
1334
|
+
`📝 最多推送: ${targetSite.maxArticles} 篇`
|
|
1335
|
+
];
|
|
1336
|
+
// 合并为单行文本,统一换行符
|
|
1337
|
+
let message = messageParts.join('\n');
|
|
1338
|
+
// 长度验证,超过 500 字符则精简
|
|
1339
|
+
if (message.length > 500) {
|
|
1340
|
+
ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
|
|
1341
|
+
message = messageParts.slice(0, 5).join('\n') + '\n... 更多配置请查看完整状态';
|
|
1342
|
+
}
|
|
1343
|
+
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
1344
|
+
// 直接返回纯字符串,跳过适配器复杂编码
|
|
1345
|
+
return message;
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
// 显示所有站点状态
|
|
1349
|
+
let message = '📊 WordPress 插件状态\n\n';
|
|
1350
|
+
config.sites.forEach((site, index) => {
|
|
1351
|
+
const siteMessage = `站点 ${index + 1}: ${site.name} (${site.id})\n` +
|
|
1352
|
+
`🌐 URL: ${site.url}\n` +
|
|
1353
|
+
`⏰ 间隔: ${site.interval / 1000} 秒\n` +
|
|
1354
|
+
`🎯 目标: ${site.targets.join(', ') || '无'}\n` +
|
|
1355
|
+
`🔔 自动: ${site.enableAutoPush ? '开启' : '关闭'}\n` +
|
|
1356
|
+
`🔄 更新: ${site.enableUpdatePush ? '开启' : '关闭'}\n` +
|
|
1357
|
+
`👤 用户: ${site.enableUserPush ? '开启' : '关闭'}\n\n`;
|
|
1358
|
+
// 检查添加后是否超过500字符
|
|
1359
|
+
if (message.length + siteMessage.length > 500) {
|
|
1360
|
+
message += '... 更多站点请使用站点 ID 查看详细状态';
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
message += siteMessage;
|
|
1364
|
+
});
|
|
1365
|
+
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
1366
|
+
return message;
|
|
980
1367
|
}
|
|
981
|
-
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
982
|
-
// 直接返回纯字符串,跳过适配器复杂编码
|
|
983
|
-
return message;
|
|
984
1368
|
});
|
|
985
|
-
ctx.command('wordpress.toggle-update', '
|
|
986
|
-
.action(async ({ session }) => {
|
|
1369
|
+
ctx.command('wordpress.site.toggle-update <siteId>', '切换指定站点的文章更新推送开关')
|
|
1370
|
+
.action(async ({ session }, siteId) => {
|
|
987
1371
|
// 检查权限
|
|
988
1372
|
const authResult = checkSuperAdmin(session);
|
|
989
1373
|
if (!authResult.valid) {
|
|
990
1374
|
return authResult.message;
|
|
991
1375
|
}
|
|
992
|
-
ctx.logger.info(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1376
|
+
ctx.logger.info(`命令 wordpress.site.toggle-update 被调用,站点 ID: ${siteId}`);
|
|
1377
|
+
// 查找站点
|
|
1378
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1379
|
+
if (!site) {
|
|
1380
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1381
|
+
}
|
|
1382
|
+
// 切换开关
|
|
1383
|
+
site.enableUpdatePush = !site.enableUpdatePush;
|
|
1384
|
+
await saveConfig('sites', config.sites);
|
|
1385
|
+
return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
|
|
996
1386
|
});
|
|
997
|
-
ctx.command('wordpress.toggle-user', '
|
|
998
|
-
.action(async ({ session }) => {
|
|
1387
|
+
ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
|
|
1388
|
+
.action(async ({ session }, siteId) => {
|
|
999
1389
|
// 检查权限
|
|
1000
1390
|
const authResult = checkSuperAdmin(session);
|
|
1001
1391
|
if (!authResult.valid) {
|
|
1002
1392
|
return authResult.message;
|
|
1003
1393
|
}
|
|
1004
|
-
ctx.logger.info(
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1394
|
+
ctx.logger.info(`命令 wordpress.site.toggle-user 被调用,站点 ID: ${siteId}`);
|
|
1395
|
+
// 查找站点
|
|
1396
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1397
|
+
if (!site) {
|
|
1398
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1399
|
+
}
|
|
1400
|
+
// 切换开关
|
|
1401
|
+
site.enableUserPush = !site.enableUserPush;
|
|
1402
|
+
await saveConfig('sites', config.sites);
|
|
1403
|
+
return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
|
|
1008
1404
|
});
|
|
1009
|
-
ctx.command('wordpress.toggle', '
|
|
1010
|
-
.action(async ({ session }) => {
|
|
1405
|
+
ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
|
|
1406
|
+
.action(async ({ session }, siteId) => {
|
|
1011
1407
|
// 检查权限
|
|
1012
1408
|
const authResult = checkSuperAdmin(session);
|
|
1013
1409
|
if (!authResult.valid) {
|
|
1014
1410
|
return authResult.message;
|
|
1015
1411
|
}
|
|
1016
|
-
ctx.logger.info(
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1412
|
+
ctx.logger.info(`命令 wordpress.site.toggle 被调用,站点 ID: ${siteId}`);
|
|
1413
|
+
// 查找站点
|
|
1414
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1415
|
+
if (!site) {
|
|
1416
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1417
|
+
}
|
|
1418
|
+
// 切换开关
|
|
1419
|
+
site.enableAutoPush = !site.enableAutoPush;
|
|
1420
|
+
await saveConfig('sites', config.sites);
|
|
1421
|
+
return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
|
|
1020
1422
|
});
|
|
1021
|
-
ctx.command('wordpress.mention', '
|
|
1022
|
-
.action(async ({ session }) => {
|
|
1423
|
+
ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
|
|
1424
|
+
.action(async ({ session }, siteId) => {
|
|
1023
1425
|
// 检查权限
|
|
1024
1426
|
const authResult = checkSuperAdmin(session);
|
|
1025
1427
|
if (!authResult.valid) {
|
|
1026
1428
|
return authResult.message;
|
|
1027
1429
|
}
|
|
1028
|
-
ctx.logger.info(
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1430
|
+
ctx.logger.info(`命令 wordpress.site.mention 被调用,站点 ID: ${siteId}`);
|
|
1431
|
+
// 查找站点
|
|
1432
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1433
|
+
if (!site) {
|
|
1434
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1435
|
+
}
|
|
1436
|
+
// 切换开关
|
|
1437
|
+
site.mentionAll = !site.mentionAll;
|
|
1438
|
+
await saveConfig('sites', config.sites);
|
|
1439
|
+
return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
|
|
1032
1440
|
});
|
|
1033
|
-
ctx.command('wordpress.set-url <url>', '
|
|
1034
|
-
.action(async ({ session }, url) => {
|
|
1441
|
+
ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
|
|
1442
|
+
.action(async ({ session }, siteId, url) => {
|
|
1035
1443
|
// 检查权限
|
|
1036
1444
|
const authResult = checkSuperAdmin(session);
|
|
1037
1445
|
if (!authResult.valid) {
|
|
1038
1446
|
return authResult.message;
|
|
1039
1447
|
}
|
|
1040
|
-
ctx.logger.info(`命令 wordpress.set-url
|
|
1448
|
+
ctx.logger.info(`命令 wordpress.site.set-url 被调用,站点 ID: ${siteId},新地址:${url}`);
|
|
1449
|
+
// 查找站点
|
|
1450
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1451
|
+
if (!site) {
|
|
1452
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1453
|
+
}
|
|
1041
1454
|
// 修改站点地址
|
|
1042
|
-
|
|
1043
|
-
await saveConfig('
|
|
1044
|
-
ctx.logger.info(
|
|
1045
|
-
return
|
|
1455
|
+
site.url = url;
|
|
1456
|
+
await saveConfig('sites', config.sites);
|
|
1457
|
+
ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
|
|
1458
|
+
return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
|
|
1046
1459
|
});
|
|
1047
1460
|
ctx.command('wordpress.pushed', '查看已推送的文章列表')
|
|
1048
1461
|
.action(async () => {
|
|
@@ -1138,8 +1551,90 @@ function apply(ctx, config) {
|
|
|
1138
1551
|
catch (error) {
|
|
1139
1552
|
ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
|
|
1140
1553
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1554
|
+
});
|
|
1555
|
+
ctx.command('wordpress.stats', '查看插件运行指标统计')
|
|
1556
|
+
.action(() => {
|
|
1557
|
+
ctx.logger.info('命令 wordpress.stats 被调用');
|
|
1558
|
+
// 计算 API 调用成功率
|
|
1559
|
+
const apiSuccessRate = runtimeStats.apiCallCount > 0
|
|
1560
|
+
? (runtimeStats.apiSuccessCount / runtimeStats.apiCallCount * 100).toFixed(2)
|
|
1561
|
+
: '0.00';
|
|
1562
|
+
// 计算推送成功率
|
|
1563
|
+
const pushSuccessRate = (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) > 0
|
|
1564
|
+
? (runtimeStats.pushSuccessCount / (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) * 100).toFixed(2)
|
|
1565
|
+
: '0.00';
|
|
1566
|
+
// 计算统计时间范围
|
|
1567
|
+
const startTime = new Date(runtimeStats.lastResetTime);
|
|
1568
|
+
const formattedStartTime = `${startTime.getFullYear()}-${String(startTime.getMonth() + 1).padStart(2, '0')}-${String(startTime.getDate()).padStart(2, '0')} ${String(startTime.getHours()).padStart(2, '0')}:${String(startTime.getMinutes()).padStart(2, '0')}`;
|
|
1569
|
+
// 构建统计消息
|
|
1570
|
+
const messageParts = [
|
|
1571
|
+
'📊 WordPress 插件运行统计',
|
|
1572
|
+
`📅 统计开始时间: ${formattedStartTime}`,
|
|
1573
|
+
`📈 API 调用: ${runtimeStats.apiCallCount} 次`,
|
|
1574
|
+
`✅ API 成功: ${runtimeStats.apiSuccessCount} 次`,
|
|
1575
|
+
`❌ API 失败: ${runtimeStats.apiFailureCount} 次`,
|
|
1576
|
+
`📊 API 成功率: ${apiSuccessRate}%`,
|
|
1577
|
+
`📤 推送成功: ${runtimeStats.pushSuccessCount} 次`,
|
|
1578
|
+
`📥 推送失败: ${runtimeStats.pushFailureCount} 次`,
|
|
1579
|
+
`📊 推送成功率: ${pushSuccessRate}%`
|
|
1580
|
+
];
|
|
1581
|
+
let message = messageParts.join('\n');
|
|
1582
|
+
// 长度验证,超过 500 字符则精简
|
|
1583
|
+
if (message.length > 500) {
|
|
1584
|
+
ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
|
|
1585
|
+
message = messageParts.slice(0, 6).join('\n') + '\n... 更多统计信息请查看完整数据';
|
|
1586
|
+
}
|
|
1587
|
+
return message;
|
|
1588
|
+
});
|
|
1589
|
+
ctx.command('wordpress.health', '查看插件健康状态')
|
|
1590
|
+
.action(async () => {
|
|
1591
|
+
ctx.logger.info('命令 wordpress.health 被调用');
|
|
1592
|
+
// 检查数据库连接
|
|
1593
|
+
const dbConnected = await checkDatabaseConnection();
|
|
1594
|
+
// 检查 Bot 在线状态
|
|
1595
|
+
const bot = getValidBot();
|
|
1596
|
+
const botOnline = !!bot;
|
|
1597
|
+
// 检查 API 可达性(尝试访问第一个站点的 API)
|
|
1598
|
+
let apiReachable = false;
|
|
1599
|
+
if (config.sites.length > 0) {
|
|
1600
|
+
const firstSite = config.sites[0];
|
|
1601
|
+
try {
|
|
1602
|
+
const url = `${firstSite.url}/wp-json/wp/v2/posts?per_page=1`;
|
|
1603
|
+
const response = await ctx.http.get(url, {
|
|
1604
|
+
timeout: 5000
|
|
1605
|
+
});
|
|
1606
|
+
apiReachable = true;
|
|
1607
|
+
}
|
|
1608
|
+
catch (error) {
|
|
1609
|
+
ctx.logger.warn(`API 可达性检查失败: ${error}`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
// 构建健康状态消息
|
|
1613
|
+
const messageParts = [
|
|
1614
|
+
'🏥 WordPress 插件健康状态',
|
|
1615
|
+
`🗄️ 数据库连接: ${dbConnected ? '✅ 正常' : '❌ 异常'}`,
|
|
1616
|
+
`🤖 Bot 在线状态: ${botOnline ? '✅ 在线' : '❌ 离线'}`,
|
|
1617
|
+
`🌐 API 可达性: ${apiReachable ? '✅ 可达' : '❌ 不可达'}`
|
|
1618
|
+
];
|
|
1619
|
+
// 添加站点状态
|
|
1620
|
+
if (config.sites.length > 0) {
|
|
1621
|
+
messageParts.push('\n📋 站点状态:');
|
|
1622
|
+
config.sites.forEach((site, index) => {
|
|
1623
|
+
if (index < 3) { // 只显示前3个站点
|
|
1624
|
+
messageParts.push(`${index + 1}. ${site.name}: ${site.enableAutoPush ? '✅ 启用' : '❌ 禁用'}`);
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
if (config.sites.length > 3) {
|
|
1628
|
+
messageParts.push(`... 共 ${config.sites.length} 个站点`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
let message = messageParts.join('\n');
|
|
1632
|
+
// 长度验证,超过 500 字符则精简
|
|
1633
|
+
if (message.length > 500) {
|
|
1634
|
+
ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
|
|
1635
|
+
message = messageParts.slice(0, 5).join('\n') + '\n... 更多健康状态信息请查看完整数据';
|
|
1636
|
+
}
|
|
1637
|
+
return message;
|
|
1143
1638
|
});
|
|
1144
1639
|
ctx.command('wordpress', 'WordPress 推送插件菜单')
|
|
1145
1640
|
.action(() => {
|
|
@@ -1164,15 +1659,18 @@ function apply(ctx, config) {
|
|
|
1164
1659
|
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
1165
1660
|
return message;
|
|
1166
1661
|
});
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1662
|
+
// 为每个站点设置独立的定时任务
|
|
1663
|
+
config.sites.forEach(site => {
|
|
1664
|
+
ctx.setInterval(() => {
|
|
1665
|
+
try {
|
|
1666
|
+
pushNewPosts();
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1670
|
+
ctx.logger.error(`站点 ${site.id} 定时任务执行失败:${errorMessage}`);
|
|
1671
|
+
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
1672
|
+
ctx.logger.warn('定时任务将在下一个周期继续执行');
|
|
1673
|
+
}
|
|
1674
|
+
}, site.interval);
|
|
1675
|
+
});
|
|
1178
1676
|
}
|