koishi-plugin-wordpress-notifier 2.8.0 → 2.8.2
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 +127 -38
- package/lib/index.d.ts +26 -2
- package/lib/index.js +839 -266
- 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) {
|
|
@@ -114,13 +219,24 @@ function apply(ctx, config) {
|
|
|
114
219
|
ctx.logger.info('持久化配置加载完成');
|
|
115
220
|
}
|
|
116
221
|
catch (error) {
|
|
117
|
-
|
|
222
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
223
|
+
ctx.logger.error(`加载持久化配置失败: ${errorMessage}`);
|
|
224
|
+
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
225
|
+
// 发生错误时,不抛出异常,确保插件继续运行
|
|
226
|
+
// 使用默认配置
|
|
227
|
+
ctx.logger.warn('使用默认配置继续运行,持久化配置将在下次保存时重新创建');
|
|
118
228
|
}
|
|
119
229
|
}
|
|
120
230
|
// 保存配置到数据库
|
|
121
231
|
async function saveConfig(key, value) {
|
|
122
232
|
try {
|
|
123
233
|
ctx.logger.info(`保存配置到数据库: ${key} = ${JSON.stringify(value)}`);
|
|
234
|
+
// 确保数据库连接正常
|
|
235
|
+
const connected = await ensureDatabaseConnection();
|
|
236
|
+
if (!connected) {
|
|
237
|
+
ctx.logger.warn('数据库连接异常,跳过保存配置');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
124
240
|
// 检查配置是否已存在
|
|
125
241
|
const existingRecords = await ctx.database.get('wordpress_config', { key });
|
|
126
242
|
if (existingRecords.length > 0) {
|
|
@@ -207,14 +323,15 @@ function apply(ctx, config) {
|
|
|
207
323
|
const MAX_RETRIES = CONSTANTS.MAX_PUSH_RETRIES;
|
|
208
324
|
const RETRY_INTERVAL = CONSTANTS.PUSH_RETRY_INTERVAL; // 5分钟
|
|
209
325
|
// 添加到失败队列
|
|
210
|
-
function addToFailedQueue(type, data, targets) {
|
|
326
|
+
function addToFailedQueue(type, data, targets, siteConfig) {
|
|
211
327
|
const item = {
|
|
212
328
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
213
329
|
type,
|
|
214
330
|
data,
|
|
215
331
|
targets,
|
|
216
332
|
retries: 0,
|
|
217
|
-
createdAt: new Date()
|
|
333
|
+
createdAt: new Date(),
|
|
334
|
+
siteConfig
|
|
218
335
|
};
|
|
219
336
|
failedPushQueue.push(item);
|
|
220
337
|
ctx.logger.info(`添加到失败队列,类型: ${type},目标数: ${targets.length},队列长度: ${failedPushQueue.length}`);
|
|
@@ -243,10 +360,10 @@ function apply(ctx, config) {
|
|
|
243
360
|
// 根据类型格式化消息
|
|
244
361
|
let message;
|
|
245
362
|
if (item.type === 'post' || item.type === 'update') {
|
|
246
|
-
message = formatPostMessage(item.data, true, item.type === 'update');
|
|
363
|
+
message = formatPostMessage(item.data, true, item.type === 'update', item.siteConfig);
|
|
247
364
|
}
|
|
248
365
|
else {
|
|
249
|
-
message = formatUserMessage(item.data, true);
|
|
366
|
+
message = formatUserMessage(item.data, true, item.siteConfig);
|
|
250
367
|
}
|
|
251
368
|
// 推送到所有目标
|
|
252
369
|
for (const target of item.targets) {
|
|
@@ -292,6 +409,106 @@ function apply(ctx, config) {
|
|
|
292
409
|
// 权限检查通过,继续执行命令
|
|
293
410
|
return { valid: true, userId };
|
|
294
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
|
+
}
|
|
295
512
|
// 检查数据库表结构的函数
|
|
296
513
|
async function checkAndFixTableStructure() {
|
|
297
514
|
try {
|
|
@@ -308,7 +525,27 @@ function apply(ctx, config) {
|
|
|
308
525
|
}
|
|
309
526
|
catch (error) {
|
|
310
527
|
ctx.logger.warn(`wordpress_post_updates 表可能结构不正确,尝试重新初始化...`);
|
|
311
|
-
//
|
|
528
|
+
// 尝试删除旧表并重新初始化
|
|
529
|
+
try {
|
|
530
|
+
ctx.logger.info('尝试删除旧的 wordpress_post_updates 表...');
|
|
531
|
+
await ctx.database.drop('wordpress_post_updates');
|
|
532
|
+
ctx.logger.info('旧表删除成功,重新初始化表结构...');
|
|
533
|
+
// 重新扩展模型
|
|
534
|
+
ctx.model.extend('wordpress_post_updates', {
|
|
535
|
+
id: 'integer',
|
|
536
|
+
postId: 'integer',
|
|
537
|
+
lastModified: 'timestamp',
|
|
538
|
+
pushedAt: 'timestamp'
|
|
539
|
+
}, {
|
|
540
|
+
primary: 'id',
|
|
541
|
+
autoInc: true,
|
|
542
|
+
unique: ['postId']
|
|
543
|
+
});
|
|
544
|
+
ctx.logger.info('wordpress_post_updates 表重新初始化成功');
|
|
545
|
+
}
|
|
546
|
+
catch (dropError) {
|
|
547
|
+
ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
|
|
548
|
+
}
|
|
312
549
|
}
|
|
313
550
|
try {
|
|
314
551
|
ctx.logger.info('验证 wordpress_user_registrations 表结构...');
|
|
@@ -319,7 +556,57 @@ function apply(ctx, config) {
|
|
|
319
556
|
}
|
|
320
557
|
catch (error) {
|
|
321
558
|
ctx.logger.warn(`wordpress_user_registrations 表可能结构不正确,尝试重新初始化...`);
|
|
322
|
-
//
|
|
559
|
+
// 尝试删除旧表并重新初始化
|
|
560
|
+
try {
|
|
561
|
+
ctx.logger.info('尝试删除旧的 wordpress_user_registrations 表...');
|
|
562
|
+
await ctx.database.drop('wordpress_user_registrations');
|
|
563
|
+
ctx.logger.info('旧表删除成功,重新初始化表结构...');
|
|
564
|
+
// 重新扩展模型
|
|
565
|
+
ctx.model.extend('wordpress_user_registrations', {
|
|
566
|
+
id: 'integer',
|
|
567
|
+
userId: 'integer',
|
|
568
|
+
pushedAt: 'timestamp'
|
|
569
|
+
}, {
|
|
570
|
+
primary: 'id',
|
|
571
|
+
autoInc: true,
|
|
572
|
+
unique: ['userId']
|
|
573
|
+
});
|
|
574
|
+
ctx.logger.info('wordpress_user_registrations 表重新初始化成功');
|
|
575
|
+
}
|
|
576
|
+
catch (dropError) {
|
|
577
|
+
ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
ctx.logger.info('验证 wordpress_config 表结构...');
|
|
582
|
+
const testConfig = await ctx.database.get('wordpress_config', {}, {
|
|
583
|
+
limit: 1
|
|
584
|
+
});
|
|
585
|
+
ctx.logger.info(`wordpress_config 表验证成功,现有记录数:${testConfig.length}`);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
ctx.logger.warn(`wordpress_config 表可能结构不正确,尝试重新初始化...`);
|
|
589
|
+
// 尝试删除旧表并重新初始化
|
|
590
|
+
try {
|
|
591
|
+
ctx.logger.info('尝试删除旧的 wordpress_config 表...');
|
|
592
|
+
await ctx.database.drop('wordpress_config');
|
|
593
|
+
ctx.logger.info('旧表删除成功,重新初始化表结构...');
|
|
594
|
+
// 重新扩展模型
|
|
595
|
+
ctx.model.extend('wordpress_config', {
|
|
596
|
+
id: 'integer',
|
|
597
|
+
key: 'string',
|
|
598
|
+
value: 'string',
|
|
599
|
+
updatedAt: 'timestamp'
|
|
600
|
+
}, {
|
|
601
|
+
primary: 'id',
|
|
602
|
+
autoInc: true,
|
|
603
|
+
unique: ['key']
|
|
604
|
+
});
|
|
605
|
+
ctx.logger.info('wordpress_config 表重新初始化成功');
|
|
606
|
+
}
|
|
607
|
+
catch (dropError) {
|
|
608
|
+
ctx.logger.warn(`删除旧表失败,可能表不存在:${dropError}`);
|
|
609
|
+
}
|
|
323
610
|
}
|
|
324
611
|
ctx.logger.info('表结构检查和修复完成');
|
|
325
612
|
}
|
|
@@ -338,6 +625,16 @@ function apply(ctx, config) {
|
|
|
338
625
|
let retries = 0;
|
|
339
626
|
while (retries <= maxRetries) {
|
|
340
627
|
try {
|
|
628
|
+
// API 请求频率控制
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
const timeSinceLastRequest = now - lastRequestTime;
|
|
631
|
+
if (timeSinceLastRequest < CONSTANTS.API_RATE_LIMIT) {
|
|
632
|
+
const waitTime = CONSTANTS.API_RATE_LIMIT - timeSinceLastRequest;
|
|
633
|
+
ctx.logger.info(`API 请求频率限制,等待 ${waitTime}ms 后继续请求`);
|
|
634
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
635
|
+
}
|
|
636
|
+
// 更新上次请求时间
|
|
637
|
+
lastRequestTime = Date.now();
|
|
341
638
|
ctx.logger.info(`HTTP请求: ${url} (尝试 ${retries + 1}/${maxRetries + 1})`);
|
|
342
639
|
const response = await ctx.http.get(url, requestConfig);
|
|
343
640
|
ctx.logger.info(`HTTP请求成功: ${url}`);
|
|
@@ -349,6 +646,8 @@ function apply(ctx, config) {
|
|
|
349
646
|
retries++;
|
|
350
647
|
if (retries > maxRetries) {
|
|
351
648
|
ctx.logger.error(`HTTP请求最终失败,已达到最大重试次数: ${url}`);
|
|
649
|
+
// 记录推送失败
|
|
650
|
+
recordPushFailure(`API请求失败: ${errorMessage}`);
|
|
352
651
|
return null;
|
|
353
652
|
}
|
|
354
653
|
// 重试前等待
|
|
@@ -357,16 +656,16 @@ function apply(ctx, config) {
|
|
|
357
656
|
}
|
|
358
657
|
return null;
|
|
359
658
|
}
|
|
360
|
-
async function fetchLatestPosts() {
|
|
659
|
+
async function fetchLatestPosts(site) {
|
|
361
660
|
try {
|
|
362
|
-
const url = `${
|
|
363
|
-
ctx.logger.info(
|
|
661
|
+
const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=date&order=desc`;
|
|
662
|
+
ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的文章: ${url}`);
|
|
364
663
|
// 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
|
|
365
664
|
const requestConfig = {};
|
|
366
|
-
if (
|
|
665
|
+
if (site.username && site.applicationPassword) {
|
|
367
666
|
// 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
|
|
368
|
-
const username =
|
|
369
|
-
const password =
|
|
667
|
+
const username = site.username;
|
|
668
|
+
const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
|
|
370
669
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
371
670
|
requestConfig.headers = {
|
|
372
671
|
Authorization: `Basic ${auth}`
|
|
@@ -374,30 +673,33 @@ function apply(ctx, config) {
|
|
|
374
673
|
}
|
|
375
674
|
const response = await httpRequest(url, requestConfig);
|
|
376
675
|
if (!response) {
|
|
377
|
-
ctx.logger.error(
|
|
676
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败,已达到最大重试次数`);
|
|
378
677
|
return [];
|
|
379
678
|
}
|
|
380
|
-
ctx.logger.info(
|
|
679
|
+
ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇文章`);
|
|
381
680
|
return response;
|
|
382
681
|
}
|
|
383
682
|
catch (error) {
|
|
384
|
-
|
|
683
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
684
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 文章失败: ${errorMessage}`);
|
|
685
|
+
// 记录失败
|
|
686
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的文章失败: ${errorMessage}`);
|
|
385
687
|
return [];
|
|
386
688
|
}
|
|
387
689
|
}
|
|
388
|
-
async function fetchLatestUsers() {
|
|
690
|
+
async function fetchLatestUsers(site) {
|
|
389
691
|
try {
|
|
390
692
|
// 修改API请求,添加_fields参数明确请求注册日期字段
|
|
391
693
|
// WordPress REST API 默认可能不会返回注册日期,需要明确请求
|
|
392
694
|
const fields = 'id,name,slug,date,date_registered,registered_date,created_at,registeredAt,email,roles,url,description,link,avatar_urls';
|
|
393
|
-
const url = `${
|
|
394
|
-
ctx.logger.info(
|
|
695
|
+
const url = `${site.url}/wp-json/wp/v2/users?per_page=${site.maxArticles}&orderby=registered_date&order=desc&_fields=${fields}`;
|
|
696
|
+
ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的用户: ${url}`);
|
|
395
697
|
// 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
|
|
396
698
|
const requestConfig = {};
|
|
397
|
-
if (
|
|
699
|
+
if (site.username && site.applicationPassword) {
|
|
398
700
|
// 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
|
|
399
|
-
const username =
|
|
400
|
-
const password =
|
|
701
|
+
const username = site.username;
|
|
702
|
+
const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
|
|
401
703
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
402
704
|
requestConfig.headers = {
|
|
403
705
|
Authorization: `Basic ${auth}`
|
|
@@ -405,12 +707,14 @@ function apply(ctx, config) {
|
|
|
405
707
|
}
|
|
406
708
|
const response = await httpRequest(url, requestConfig);
|
|
407
709
|
if (!response) {
|
|
408
|
-
ctx.logger.error(
|
|
409
|
-
ctx.logger.error(`WordPress REST API 的 users
|
|
710
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败,已达到最大重试次数`);
|
|
711
|
+
ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
|
|
712
|
+
// 记录失败
|
|
713
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: API 认证失败`);
|
|
410
714
|
// 返回空数组,确保插件继续运行
|
|
411
715
|
return [];
|
|
412
716
|
}
|
|
413
|
-
ctx.logger.info(
|
|
717
|
+
ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 位用户`);
|
|
414
718
|
// 添加调试日志,查看API返回的实际数据结构
|
|
415
719
|
if (response.length > 0) {
|
|
416
720
|
ctx.logger.info(`用户数据示例: ${JSON.stringify(response[0], null, 2)}`);
|
|
@@ -421,22 +725,25 @@ function apply(ctx, config) {
|
|
|
421
725
|
return response;
|
|
422
726
|
}
|
|
423
727
|
catch (error) {
|
|
424
|
-
|
|
425
|
-
ctx.logger.error(
|
|
728
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
729
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 用户失败: ${errorMessage}`);
|
|
730
|
+
ctx.logger.error(`WordPress REST API 的 users 端点需要认证才能访问,请在站点配置中添加 WordPress 用户名和应用程序密码`);
|
|
731
|
+
// 记录失败
|
|
732
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的用户失败: ${errorMessage}`);
|
|
426
733
|
// 返回空数组,确保插件继续运行
|
|
427
734
|
return [];
|
|
428
735
|
}
|
|
429
736
|
}
|
|
430
|
-
async function fetchUpdatedPosts() {
|
|
737
|
+
async function fetchUpdatedPosts(site) {
|
|
431
738
|
try {
|
|
432
|
-
const url = `${
|
|
433
|
-
ctx.logger.info(
|
|
739
|
+
const url = `${site.url}/wp-json/wp/v2/posts?per_page=${site.maxArticles}&orderby=modified&order=desc`;
|
|
740
|
+
ctx.logger.info(`正在获取站点 ${site.id} (${site.name}) 的更新文章: ${url}`);
|
|
434
741
|
// 准备请求配置,添加认证头(如果配置了用户名和应用程序密码)
|
|
435
742
|
const requestConfig = {};
|
|
436
|
-
if (
|
|
743
|
+
if (site.username && site.applicationPassword) {
|
|
437
744
|
// 处理WordPress应用程序密码,移除空格(WordPress生成的应用密码格式为:hGR2 sPFu Yncl xHc4 AvJq cUtB)
|
|
438
|
-
const username =
|
|
439
|
-
const password =
|
|
745
|
+
const username = site.username;
|
|
746
|
+
const password = site.applicationPassword.replace(/\s+/g, ''); // 移除所有空格
|
|
440
747
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
441
748
|
requestConfig.headers = {
|
|
442
749
|
Authorization: `Basic ${auth}`
|
|
@@ -444,23 +751,34 @@ function apply(ctx, config) {
|
|
|
444
751
|
}
|
|
445
752
|
const response = await httpRequest(url, requestConfig);
|
|
446
753
|
if (!response) {
|
|
447
|
-
ctx.logger.error(
|
|
754
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败,已达到最大重试次数`);
|
|
755
|
+
// 记录失败
|
|
756
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败`);
|
|
448
757
|
return [];
|
|
449
758
|
}
|
|
450
|
-
ctx.logger.info(
|
|
759
|
+
ctx.logger.info(`成功获取站点 ${site.id} (${site.name}) 的 ${response.length} 篇更新文章`);
|
|
451
760
|
return response;
|
|
452
761
|
}
|
|
453
762
|
catch (error) {
|
|
454
|
-
|
|
763
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
764
|
+
ctx.logger.error(`获取站点 ${site.id} (${site.name}) 的 WordPress 更新文章失败: ${errorMessage}`);
|
|
765
|
+
// 记录失败
|
|
766
|
+
recordPushFailure(`获取站点 ${site.id} (${site.name}) 的更新文章失败: ${errorMessage}`);
|
|
455
767
|
return [];
|
|
456
768
|
}
|
|
457
769
|
}
|
|
458
|
-
async function isUserPushed(userId) {
|
|
770
|
+
async function isUserPushed(siteId, userId) {
|
|
459
771
|
try {
|
|
460
|
-
|
|
461
|
-
const
|
|
772
|
+
// 确保数据库连接正常
|
|
773
|
+
const connected = await ensureDatabaseConnection();
|
|
774
|
+
if (!connected) {
|
|
775
|
+
ctx.logger.warn('数据库连接异常,跳过检查用户推送记录');
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
ctx.logger.info(`检查用户是否已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
|
|
779
|
+
const record = await ctx.database.get('wordpress_user_registrations', { siteId, userId });
|
|
462
780
|
const result = record.length > 0;
|
|
463
|
-
ctx.logger.info(
|
|
781
|
+
ctx.logger.info(`检查结果:站点 ${siteId} 用户 ${userId} 已推送:${result ? '是' : '否'}`);
|
|
464
782
|
return result;
|
|
465
783
|
}
|
|
466
784
|
catch (error) {
|
|
@@ -471,12 +789,18 @@ function apply(ctx, config) {
|
|
|
471
789
|
return false;
|
|
472
790
|
}
|
|
473
791
|
}
|
|
474
|
-
async function getPostUpdateRecord(postId) {
|
|
792
|
+
async function getPostUpdateRecord(siteId, postId) {
|
|
475
793
|
try {
|
|
476
|
-
|
|
477
|
-
const
|
|
794
|
+
// 确保数据库连接正常
|
|
795
|
+
const connected = await ensureDatabaseConnection();
|
|
796
|
+
if (!connected) {
|
|
797
|
+
ctx.logger.warn('数据库连接异常,跳过获取文章更新记录');
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
ctx.logger.info(`获取文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
801
|
+
const records = await ctx.database.get('wordpress_post_updates', { siteId, postId });
|
|
478
802
|
const result = records.length > 0 ? records[0] : null;
|
|
479
|
-
ctx.logger.info(
|
|
803
|
+
ctx.logger.info(`获取结果:站点 ${siteId} 文章 ${postId} 更新记录:${result ? '找到' : '未找到'}`);
|
|
480
804
|
return result;
|
|
481
805
|
}
|
|
482
806
|
catch (error) {
|
|
@@ -487,72 +811,116 @@ function apply(ctx, config) {
|
|
|
487
811
|
return null;
|
|
488
812
|
}
|
|
489
813
|
}
|
|
490
|
-
async function markUserAsPushed(userId) {
|
|
814
|
+
async function markUserAsPushed(siteId, userId) {
|
|
491
815
|
try {
|
|
492
|
-
|
|
816
|
+
// 确保数据库连接正常
|
|
817
|
+
const connected = await ensureDatabaseConnection();
|
|
818
|
+
if (!connected) {
|
|
819
|
+
ctx.logger.warn('数据库连接异常,跳过标记用户推送记录');
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
ctx.logger.info(`开始标记用户已推送,站点 ID: ${siteId},用户 ID: ${userId}`);
|
|
493
823
|
// 创建新记录,不手动指定id,让数据库自动生成
|
|
494
824
|
const newRecord = {
|
|
825
|
+
siteId,
|
|
495
826
|
userId,
|
|
496
827
|
pushedAt: new Date()
|
|
497
828
|
};
|
|
498
829
|
ctx.logger.info(`准备创建用户推送记录:${JSON.stringify(newRecord)}`);
|
|
499
830
|
await ctx.database.create('wordpress_user_registrations', newRecord);
|
|
500
|
-
ctx.logger.info(
|
|
831
|
+
ctx.logger.info(`已成功标记站点 ${siteId} 用户 ${userId} 为已推送`);
|
|
501
832
|
}
|
|
502
833
|
catch (error) {
|
|
503
834
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
504
835
|
if (errorMessage.includes('UNIQUE constraint failed')) {
|
|
505
|
-
ctx.logger.warn(
|
|
836
|
+
ctx.logger.warn(`用户推送记录已存在,跳过重复插入:站点 ${siteId} 用户 ${userId}`);
|
|
506
837
|
ctx.logger.warn(`完整错误信息:${errorMessage}`);
|
|
507
838
|
}
|
|
508
839
|
else {
|
|
509
840
|
ctx.logger.error(`标记用户推送记录失败:${errorMessage}`);
|
|
510
841
|
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
511
|
-
ctx.logger.error(`插入参数:userId=${userId}`);
|
|
842
|
+
ctx.logger.error(`插入参数:siteId=${siteId}, userId=${userId}`);
|
|
512
843
|
// 非约束冲突错误,不抛出,确保插件继续运行
|
|
513
844
|
}
|
|
514
845
|
}
|
|
515
846
|
}
|
|
516
|
-
async function updatePostUpdateRecord(postId, modifiedDate) {
|
|
847
|
+
async function updatePostUpdateRecord(siteId, postId, modifiedDate) {
|
|
517
848
|
try {
|
|
518
|
-
|
|
519
|
-
const
|
|
849
|
+
// 确保数据库连接正常
|
|
850
|
+
const connected = await ensureDatabaseConnection();
|
|
851
|
+
if (!connected) {
|
|
852
|
+
ctx.logger.warn('数据库连接异常,跳过更新文章更新记录');
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
ctx.logger.info(`开始更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId},修改时间: ${modifiedDate}`);
|
|
856
|
+
const record = await getPostUpdateRecord(siteId, postId);
|
|
520
857
|
if (record) {
|
|
521
|
-
ctx.logger.info(
|
|
858
|
+
ctx.logger.info(`发现现有记录,站点 ID: ${siteId},文章 ID: ${postId},上次修改时间: ${record.lastModified}`);
|
|
522
859
|
// Koishi database API 不支持 update 方法,使用 remove + create 代替
|
|
523
|
-
await ctx.database.remove('wordpress_post_updates', { postId });
|
|
524
|
-
ctx.logger.info(
|
|
860
|
+
await ctx.database.remove('wordpress_post_updates', { siteId, postId });
|
|
861
|
+
ctx.logger.info(`已删除旧记录,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
525
862
|
}
|
|
526
|
-
//
|
|
863
|
+
// 创建新记录,不指定 id 字段,让数据库自动生成
|
|
527
864
|
const newRecord = {
|
|
865
|
+
siteId,
|
|
528
866
|
postId,
|
|
529
867
|
lastModified: modifiedDate,
|
|
530
868
|
pushedAt: new Date()
|
|
531
869
|
};
|
|
532
|
-
ctx.logger.info(
|
|
870
|
+
ctx.logger.info(`准备创建新记录,站点 ID: ${siteId},文章 ID: ${postId},记录内容: ${JSON.stringify(newRecord)}`);
|
|
533
871
|
await ctx.database.create('wordpress_post_updates', newRecord);
|
|
534
|
-
ctx.logger.info(
|
|
872
|
+
ctx.logger.info(`已成功更新文章更新记录,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
535
873
|
}
|
|
536
874
|
catch (error) {
|
|
537
875
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
538
|
-
ctx.logger.error(
|
|
876
|
+
ctx.logger.error(`更新文章更新记录失败,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
539
877
|
ctx.logger.error(`错误信息: ${errorMessage}`);
|
|
540
878
|
ctx.logger.error(`错误栈: ${error instanceof Error ? error.stack : '无'}`);
|
|
541
|
-
|
|
879
|
+
// 不再抛出错误,确保推送流程继续运行
|
|
880
|
+
// 发生错误时,默认返回,避免阻塞推送流程
|
|
881
|
+
ctx.logger.warn(`更新文章更新记录失败,但推送流程将继续运行,站点 ID: ${siteId},文章 ID: ${postId}`);
|
|
542
882
|
}
|
|
543
883
|
}
|
|
544
884
|
// 1. 新增强清洗函数:针对性解决敏感字符问题
|
|
545
885
|
function sanitizeContent(content) {
|
|
546
|
-
|
|
886
|
+
let sanitized = content
|
|
547
887
|
.replace(/<[^>]*>/g, '') // 移除所有 HTML 标签
|
|
548
888
|
.replace(/[\x00-\x1F\x7F]/g, '') // 移除不可见控制符,QQ 接口明确禁止
|
|
549
889
|
.replace(/\u3000/g, ' ') // 全角空格转半角空格,解决适配器编码缺陷
|
|
550
890
|
.replace(/\s+/g, ' ') // 标准化所有空白符为单个半角空格
|
|
551
891
|
.trim(); // 移除首尾空格
|
|
892
|
+
// 添加敏感词过滤
|
|
893
|
+
sanitized = filterSensitiveWords(sanitized);
|
|
894
|
+
return sanitized;
|
|
552
895
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
896
|
+
// 敏感词过滤函数
|
|
897
|
+
function filterSensitiveWords(content) {
|
|
898
|
+
let filteredContent = content;
|
|
899
|
+
let hasSensitiveWords = false;
|
|
900
|
+
// 遍历敏感词列表进行替换
|
|
901
|
+
for (const word of CONSTANTS.SENSITIVE_WORDS) {
|
|
902
|
+
if (filteredContent.includes(word)) {
|
|
903
|
+
hasSensitiveWords = true;
|
|
904
|
+
const regex = new RegExp(word, 'gi'); // 不区分大小写
|
|
905
|
+
filteredContent = filteredContent.replace(regex, CONSTANTS.SENSITIVE_REPLACEMENT);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// 如果检测到敏感词,记录日志
|
|
909
|
+
if (hasSensitiveWords) {
|
|
910
|
+
ctx.logger.info('消息内容包含敏感词,已进行过滤处理');
|
|
911
|
+
// 仅记录处理信息,不记录具体内容,保护隐私
|
|
912
|
+
}
|
|
913
|
+
return filteredContent;
|
|
914
|
+
}
|
|
915
|
+
function formatPostMessage(post, mention = false, isUpdate = false, siteConfig) {
|
|
916
|
+
// 使用默认站点配置或传入的站点配置
|
|
917
|
+
const configToUse = siteConfig || config.sites[0];
|
|
918
|
+
if (!configToUse) {
|
|
919
|
+
ctx.logger.error('无可用的站点配置');
|
|
920
|
+
return '';
|
|
921
|
+
}
|
|
922
|
+
// 生成缓存键,包含推送模板配置信息
|
|
923
|
+
const cacheKey = `post_${post.id}_${mention}_${isUpdate}_${configToUse.mentionAll}_${JSON.stringify(configToUse.pushTemplate)}`;
|
|
556
924
|
// 检查缓存
|
|
557
925
|
const cached = formatCache.post.get(cacheKey);
|
|
558
926
|
if (cached && isCacheValid(cached.timestamp)) {
|
|
@@ -565,7 +933,7 @@ function apply(ctx, config) {
|
|
|
565
933
|
if (title.length > CONSTANTS.MAX_TITLE_LENGTH) {
|
|
566
934
|
title = title.substring(0, CONSTANTS.MAX_TITLE_LENGTH - 3) + '...';
|
|
567
935
|
}
|
|
568
|
-
//
|
|
936
|
+
// 根据配置格式化日期
|
|
569
937
|
const formatDate = (dateString) => {
|
|
570
938
|
const date = new Date(dateString);
|
|
571
939
|
const year = date.getFullYear();
|
|
@@ -573,19 +941,50 @@ function apply(ctx, config) {
|
|
|
573
941
|
const day = String(date.getDate()).padStart(2, '0');
|
|
574
942
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
575
943
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
576
|
-
|
|
944
|
+
// 根据配置的日期格式进行替换
|
|
945
|
+
let formattedDate = configToUse.pushTemplate.dateFormat
|
|
946
|
+
.replace('YYYY', year.toString())
|
|
947
|
+
.replace('MM', month)
|
|
948
|
+
.replace('DD', day)
|
|
949
|
+
.replace('HH', hours)
|
|
950
|
+
.replace('mm', minutes);
|
|
951
|
+
return formattedDate;
|
|
577
952
|
};
|
|
578
953
|
const date = formatDate(post.date);
|
|
579
954
|
// 链接强制编码
|
|
580
955
|
const encodedLink = encodeURI(post.link);
|
|
581
956
|
// 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
|
|
582
|
-
const atAllText = mention &&
|
|
957
|
+
const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
|
|
583
958
|
// 只使用一个极简表情
|
|
584
959
|
const messageType = isUpdate ? '📝' : '📰';
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
//
|
|
588
|
-
|
|
960
|
+
// 构建消息内容
|
|
961
|
+
let messageParts = [];
|
|
962
|
+
// 添加头部
|
|
963
|
+
messageParts.push(`${messageType} ${atAllText}${date} - ${title}`);
|
|
964
|
+
// 根据配置添加作者
|
|
965
|
+
if (configToUse.pushTemplate.showAuthor && post.author) {
|
|
966
|
+
messageParts.push(`👤 作者: ${post.author}`);
|
|
967
|
+
}
|
|
968
|
+
// 根据配置添加摘要
|
|
969
|
+
if (configToUse.pushTemplate.showExcerpt && post.excerpt) {
|
|
970
|
+
let excerpt = sanitizeContent(post.excerpt.rendered);
|
|
971
|
+
// 截断摘要长度
|
|
972
|
+
if (excerpt.length > 100) {
|
|
973
|
+
excerpt = excerpt.substring(0, 97) + '...';
|
|
974
|
+
}
|
|
975
|
+
if (excerpt) {
|
|
976
|
+
messageParts.push(`📄 摘要: ${excerpt}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
// 根据配置添加链接
|
|
980
|
+
if (configToUse.pushTemplate.linkPosition === 'top') {
|
|
981
|
+
messageParts.unshift(`🔗 ${encodedLink}`);
|
|
982
|
+
}
|
|
983
|
+
else if (configToUse.pushTemplate.linkPosition === 'bottom') {
|
|
984
|
+
messageParts.push(`🔗 ${encodedLink}`);
|
|
985
|
+
}
|
|
986
|
+
// 合并消息部分
|
|
987
|
+
let message = messageParts.join('\n');
|
|
589
988
|
// 双级长度控制:整体消息兜底最大长度
|
|
590
989
|
if (message.length > CONSTANTS.MAX_MESSAGE_LENGTH) {
|
|
591
990
|
message = message.substring(0, CONSTANTS.MAX_MESSAGE_LENGTH - 3) + '...';
|
|
@@ -599,9 +998,15 @@ function apply(ctx, config) {
|
|
|
599
998
|
// 直接返回纯字符串,跳过适配器复杂编码
|
|
600
999
|
return message;
|
|
601
1000
|
}
|
|
602
|
-
function formatUserMessage(user, mention = false) {
|
|
1001
|
+
function formatUserMessage(user, mention = false, siteConfig) {
|
|
1002
|
+
// 使用默认站点配置或传入的站点配置
|
|
1003
|
+
const configToUse = siteConfig || config.sites[0];
|
|
1004
|
+
if (!configToUse) {
|
|
1005
|
+
ctx.logger.error('无可用的站点配置');
|
|
1006
|
+
return '';
|
|
1007
|
+
}
|
|
603
1008
|
// 生成缓存键
|
|
604
|
-
const cacheKey = `user_${user.id}_${mention}_${
|
|
1009
|
+
const cacheKey = `user_${user.id}_${mention}_${configToUse.mentionAll}`;
|
|
605
1010
|
// 检查缓存
|
|
606
1011
|
const cached = formatCache.user.get(cacheKey);
|
|
607
1012
|
if (cached && isCacheValid(cached.timestamp)) {
|
|
@@ -661,7 +1066,7 @@ function apply(ctx, config) {
|
|
|
661
1066
|
ctx.logger.error(`处理用户 ${username} 日期时出错: ${error}`);
|
|
662
1067
|
}
|
|
663
1068
|
// 构建 @全体成员 文本(适配 QQ 官方 bot 和其他适配器)
|
|
664
|
-
const atAllText = mention &&
|
|
1069
|
+
const atAllText = mention && configToUse.mentionAll ? '@全体成员 ' : '';
|
|
665
1070
|
// 只使用一个极简表情
|
|
666
1071
|
const messageType = '👤';
|
|
667
1072
|
// 构建核心消息内容,严格控制格式和换行
|
|
@@ -685,138 +1090,159 @@ function apply(ctx, config) {
|
|
|
685
1090
|
const bot = getValidBot();
|
|
686
1091
|
if (!bot) {
|
|
687
1092
|
ctx.logger.error('没有可用的 Bot 实例');
|
|
1093
|
+
recordPushFailure('没有可用的 Bot 实例');
|
|
688
1094
|
return;
|
|
689
1095
|
}
|
|
690
1096
|
// 修复 Bot 标识 undefined 问题
|
|
691
1097
|
const botId = bot.selfId || 'unknown';
|
|
692
1098
|
ctx.logger.info(`使用 bot ${bot.platform}:${botId} 进行推送`);
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1099
|
+
// 遍历所有站点
|
|
1100
|
+
for (const site of config.sites) {
|
|
1101
|
+
ctx.logger.info(`开始处理站点: ${site.id} (${site.name})`);
|
|
1102
|
+
// 推送新文章
|
|
1103
|
+
if (site.enableAutoPush) {
|
|
1104
|
+
const posts = await fetchLatestPosts(site);
|
|
1105
|
+
ctx.logger.info(`站点 ${site.id} (${site.name}) 开始检查 ${posts.length} 篇文章是否需要推送`);
|
|
1106
|
+
if (posts.length > 0) {
|
|
1107
|
+
for (const post of posts) {
|
|
1108
|
+
ctx.logger.info(`正在处理文章: ${post.id} - ${post.title.rendered}`);
|
|
1109
|
+
ctx.logger.info(`文章 ID: ${post.id}, 发布时间: ${post.date}, 修改时间: ${post.modified}`);
|
|
1110
|
+
// 检查文章是否已推送过(所有群聊共用一个标记)
|
|
1111
|
+
const postRecord = await getPostUpdateRecord(site.id, post.id);
|
|
1112
|
+
const hasPushed = !!postRecord;
|
|
1113
|
+
ctx.logger.info(`检查结果: 站点 ${site.id} 文章 ${post.id} 是否已推送:${hasPushed ? '是' : '否'}`);
|
|
1114
|
+
if (!hasPushed) {
|
|
1115
|
+
// 推送到该站点的所有目标群聊
|
|
1116
|
+
const failedTargets = [];
|
|
1117
|
+
for (const target of site.targets) {
|
|
1118
|
+
try {
|
|
1119
|
+
ctx.logger.info(`正在处理目标: ${target}`);
|
|
1120
|
+
// 直接使用原始目标字符串,不进行数字转换,避免丢失平台前缀等信息
|
|
1121
|
+
const stringTarget = target;
|
|
1122
|
+
const message = formatPostMessage(post, site.mentionAll, false, site);
|
|
1123
|
+
ctx.logger.info(`准备推送新文章到目标: ${stringTarget}`);
|
|
1124
|
+
await bot.sendMessage(stringTarget, message);
|
|
1125
|
+
ctx.logger.info(`已推送新文章到 ${stringTarget}: ${post.title.rendered}`);
|
|
1126
|
+
}
|
|
1127
|
+
catch (error) {
|
|
1128
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1129
|
+
ctx.logger.error(`推送新文章到 ${target} 失败: ${errorMessage}`);
|
|
1130
|
+
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
1131
|
+
failedTargets.push(target);
|
|
1132
|
+
// 记录推送失败
|
|
1133
|
+
recordPushFailure(`推送消息失败到 ${target}: ${errorMessage}`);
|
|
1134
|
+
}
|
|
717
1135
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
failedTargets.push(target);
|
|
1136
|
+
// 如果有失败的目标,添加到失败队列
|
|
1137
|
+
if (failedTargets.length > 0) {
|
|
1138
|
+
addToFailedQueue('post', post, failedTargets, site);
|
|
722
1139
|
}
|
|
1140
|
+
// 标记文章已推送(所有群聊共用一个标记)
|
|
1141
|
+
await updatePostUpdateRecord(site.id, post.id, new Date(post.modified));
|
|
1142
|
+
ctx.logger.info(`已标记站点 ${site.id} 文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
|
|
723
1143
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
addToFailedQueue('post', post, failedTargets);
|
|
1144
|
+
else {
|
|
1145
|
+
ctx.logger.info(`跳过推送: 站点 ${site.id} 文章 ${post.id} 已推送过,所有群聊将不再推送`);
|
|
727
1146
|
}
|
|
728
|
-
// 标记文章已推送(所有群聊共用一个标记)
|
|
729
|
-
await updatePostUpdateRecord(post.id, new Date(post.modified));
|
|
730
|
-
ctx.logger.info(`已标记文章 ${post.id} 为已推送,所有群聊将不再推送此文章`);
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
ctx.logger.info(`跳过推送: 文章 ${post.id} 已推送过,所有群聊将不再推送`);
|
|
734
1147
|
}
|
|
735
1148
|
}
|
|
736
1149
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1150
|
+
// 推送文章更新
|
|
1151
|
+
if (site.enableUpdatePush) {
|
|
1152
|
+
const posts = await fetchUpdatedPosts(site);
|
|
1153
|
+
if (posts.length > 0) {
|
|
1154
|
+
for (const post of posts) {
|
|
1155
|
+
const updateRecord = await getPostUpdateRecord(site.id, post.id);
|
|
1156
|
+
const postModifiedDate = new Date(post.modified);
|
|
1157
|
+
// 检查文章是否有更新
|
|
1158
|
+
if (updateRecord && postModifiedDate > new Date(updateRecord.lastModified)) {
|
|
1159
|
+
ctx.logger.info(`站点 ${site.id} 文章 ${post.id} 有更新,准备推送更新通知`);
|
|
1160
|
+
// 推送到该站点的所有目标群聊
|
|
1161
|
+
const failedTargets = [];
|
|
1162
|
+
for (const target of site.targets) {
|
|
1163
|
+
try {
|
|
1164
|
+
ctx.logger.info(`正在处理目标: ${target}`);
|
|
1165
|
+
const stringTarget = target;
|
|
1166
|
+
const message = formatPostMessage(post, site.mentionAll, true, site);
|
|
1167
|
+
ctx.logger.info(`准备推送文章更新到目标: ${stringTarget}`);
|
|
1168
|
+
await bot.sendMessage(stringTarget, message);
|
|
1169
|
+
ctx.logger.info(`已推送文章更新到 ${stringTarget}: ${post.title.rendered}`);
|
|
1170
|
+
}
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1173
|
+
ctx.logger.error(`推送文章更新到 ${target} 失败: ${errorMessage}`);
|
|
1174
|
+
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
1175
|
+
failedTargets.push(target);
|
|
1176
|
+
// 记录推送失败
|
|
1177
|
+
recordPushFailure(`推送文章更新失败到 ${target}: ${errorMessage}`);
|
|
1178
|
+
}
|
|
758
1179
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
failedTargets.push(target);
|
|
1180
|
+
// 如果有失败的目标,添加到失败队列
|
|
1181
|
+
if (failedTargets.length > 0) {
|
|
1182
|
+
addToFailedQueue('update', post, failedTargets, site);
|
|
763
1183
|
}
|
|
1184
|
+
// 更新文章更新记录(所有群聊共用一个标记)
|
|
1185
|
+
await updatePostUpdateRecord(site.id, post.id, postModifiedDate);
|
|
1186
|
+
ctx.logger.info(`已更新站点 ${site.id} 文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
|
|
764
1187
|
}
|
|
765
|
-
// 如果有失败的目标,添加到失败队列
|
|
766
|
-
if (failedTargets.length > 0) {
|
|
767
|
-
addToFailedQueue('update', post, failedTargets);
|
|
768
|
-
}
|
|
769
|
-
// 更新文章更新记录(所有群聊共用一个标记)
|
|
770
|
-
await updatePostUpdateRecord(post.id, postModifiedDate);
|
|
771
|
-
ctx.logger.info(`已更新文章 ${post.id} 的推送记录,所有群聊将使用此更新时间作为新的推送基准`);
|
|
772
1188
|
}
|
|
773
1189
|
}
|
|
774
1190
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1191
|
+
// 推送新用户注册
|
|
1192
|
+
if (site.enableUserPush) {
|
|
1193
|
+
const users = await fetchLatestUsers(site);
|
|
1194
|
+
if (users.length > 0) {
|
|
1195
|
+
for (const user of users) {
|
|
1196
|
+
if (!(await isUserPushed(site.id, user.id))) {
|
|
1197
|
+
const failedTargets = [];
|
|
1198
|
+
for (const target of site.targets) {
|
|
1199
|
+
try {
|
|
1200
|
+
ctx.logger.info(`正在处理目标: ${target}`);
|
|
1201
|
+
// 直接使用原始目标字符串,与新文章推送逻辑保持一致
|
|
1202
|
+
const stringTarget = target;
|
|
1203
|
+
const message = formatUserMessage(user, site.mentionAll, site);
|
|
1204
|
+
ctx.logger.info(`准备推送新用户到目标: ${stringTarget}`);
|
|
1205
|
+
await bot.sendMessage(stringTarget, message);
|
|
1206
|
+
ctx.logger.info(`已推送新用户到 ${stringTarget}: ${user.name}`);
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1210
|
+
ctx.logger.error(`推送新用户到 ${target} 失败: ${errorMessage}`);
|
|
1211
|
+
ctx.logger.error(`错误详情: ${JSON.stringify(error)}`);
|
|
1212
|
+
failedTargets.push(target);
|
|
1213
|
+
// 记录推送失败
|
|
1214
|
+
recordPushFailure(`推送新用户失败到 ${target}: ${errorMessage}`);
|
|
1215
|
+
}
|
|
792
1216
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
failedTargets.push(target);
|
|
1217
|
+
// 如果有失败的目标,添加到失败队列
|
|
1218
|
+
if (failedTargets.length > 0) {
|
|
1219
|
+
addToFailedQueue('user', user, failedTargets, site);
|
|
797
1220
|
}
|
|
1221
|
+
// 标记用户已推送
|
|
1222
|
+
await markUserAsPushed(site.id, user.id);
|
|
798
1223
|
}
|
|
799
|
-
// 如果有失败的目标,添加到失败队列
|
|
800
|
-
if (failedTargets.length > 0) {
|
|
801
|
-
addToFailedQueue('user', user, failedTargets);
|
|
802
|
-
}
|
|
803
|
-
// 标记用户已推送
|
|
804
|
-
await markUserAsPushed(user.id);
|
|
805
1224
|
}
|
|
806
1225
|
}
|
|
807
1226
|
}
|
|
808
1227
|
}
|
|
809
1228
|
}
|
|
810
|
-
ctx.command('wordpress.latest', '
|
|
811
|
-
.action(async ({ session }) => {
|
|
812
|
-
ctx.logger.info(
|
|
813
|
-
|
|
1229
|
+
ctx.command('wordpress.latest [siteId]', '查看最新文章,可选站点 ID')
|
|
1230
|
+
.action(async ({ session }, siteId) => {
|
|
1231
|
+
ctx.logger.info(`命令 wordpress.latest 被调用,站点 ID: ${siteId || '默认'}`);
|
|
1232
|
+
// 选择站点
|
|
1233
|
+
const targetSite = siteId
|
|
1234
|
+
? config.sites.find(site => site.id === siteId)
|
|
1235
|
+
: config.sites[0];
|
|
1236
|
+
if (!targetSite) {
|
|
1237
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1238
|
+
}
|
|
1239
|
+
const posts = await fetchLatestPosts(targetSite);
|
|
814
1240
|
if (posts.length === 0) {
|
|
815
|
-
ctx.logger.info(
|
|
816
|
-
return
|
|
1241
|
+
ctx.logger.info(`站点 ${targetSite.id} 没有找到文章`);
|
|
1242
|
+
return `站点 ${targetSite.name} 暂无文章`;
|
|
817
1243
|
}
|
|
818
1244
|
// 动态添加文章,确保消息长度不超过500字符
|
|
819
|
-
let message =
|
|
1245
|
+
let message = `📰 ${targetSite.name} 最新文章:\n`;
|
|
820
1246
|
let addedCount = 0;
|
|
821
1247
|
for (const post of posts) {
|
|
822
1248
|
const title = sanitizeContent(post.title.rendered);
|
|
@@ -842,15 +1268,22 @@ function apply(ctx, config) {
|
|
|
842
1268
|
ctx.logger.info(`准备返回消息,长度: ${message.length},显示 ${addedCount}/${posts.length} 篇文章`);
|
|
843
1269
|
return message;
|
|
844
1270
|
});
|
|
845
|
-
ctx.command('wordpress.list', '
|
|
846
|
-
.action(async () => {
|
|
847
|
-
ctx.logger.info(
|
|
848
|
-
|
|
1271
|
+
ctx.command('wordpress.list [siteId]', '查看文章列表,可选站点 ID')
|
|
1272
|
+
.action(async (_, siteId) => {
|
|
1273
|
+
ctx.logger.info(`命令 wordpress.list 被调用,站点 ID: ${siteId || '默认'}`);
|
|
1274
|
+
// 选择站点
|
|
1275
|
+
const targetSite = siteId
|
|
1276
|
+
? config.sites.find(site => site.id === siteId)
|
|
1277
|
+
: config.sites[0];
|
|
1278
|
+
if (!targetSite) {
|
|
1279
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1280
|
+
}
|
|
1281
|
+
const posts = await fetchLatestPosts(targetSite);
|
|
849
1282
|
if (posts.length === 0) {
|
|
850
|
-
return
|
|
1283
|
+
return `站点 ${targetSite.name} 暂无文章`;
|
|
851
1284
|
}
|
|
852
1285
|
// 使用数组拼接消息,便于控制格式和长度
|
|
853
|
-
const messageParts = [
|
|
1286
|
+
const messageParts = [`📚 ${targetSite.name} 文章列表:`];
|
|
854
1287
|
for (const post of posts) {
|
|
855
1288
|
const title = sanitizeContent(post.title.rendered);
|
|
856
1289
|
// 截断标题,避免单条过长
|
|
@@ -875,97 +1308,152 @@ function apply(ctx, config) {
|
|
|
875
1308
|
await pushNewPosts();
|
|
876
1309
|
return '已检查并推送最新文章';
|
|
877
1310
|
});
|
|
878
|
-
ctx.command('wordpress.status', '
|
|
879
|
-
.action(({ session }) => {
|
|
880
|
-
ctx.logger.info(
|
|
1311
|
+
ctx.command('wordpress.status [siteId]', '查看插件状态,可选站点 ID')
|
|
1312
|
+
.action(({ session }, siteId) => {
|
|
1313
|
+
ctx.logger.info(`命令 wordpress.status 被调用,站点 ID: ${siteId || '所有'}`);
|
|
881
1314
|
// 获取当前群号,如果有的话
|
|
882
1315
|
const currentGroup = session?.channelId || '未知群聊';
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
message = messageParts.
|
|
1316
|
+
if (siteId) {
|
|
1317
|
+
// 显示单个站点状态
|
|
1318
|
+
const targetSite = config.sites.find(site => site.id === siteId);
|
|
1319
|
+
if (!targetSite) {
|
|
1320
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1321
|
+
}
|
|
1322
|
+
// 使用数组拼接消息,便于控制格式和长度
|
|
1323
|
+
const messageParts = [
|
|
1324
|
+
`📊 WordPress 插件状态 - ${targetSite.name}`,
|
|
1325
|
+
`🌐 站点: ${targetSite.url}`,
|
|
1326
|
+
`⏰ 间隔: ${targetSite.interval / 1000} 秒`,
|
|
1327
|
+
`🎯 推送目标: ${targetSite.targets.join(', ') || '无'}`,
|
|
1328
|
+
`🔔 自动推送: ${targetSite.enableAutoPush ? '开启' : '关闭'}`,
|
|
1329
|
+
`🔄 更新推送: ${targetSite.enableUpdatePush ? '开启' : '关闭'}`,
|
|
1330
|
+
`👤 用户推送: ${targetSite.enableUserPush ? '开启' : '关闭'}`,
|
|
1331
|
+
`📢 @全体: ${targetSite.mentionAll ? '开启' : '关闭'}`,
|
|
1332
|
+
`📝 最多推送: ${targetSite.maxArticles} 篇`
|
|
1333
|
+
];
|
|
1334
|
+
// 合并为单行文本,统一换行符
|
|
1335
|
+
let message = messageParts.join('\n');
|
|
1336
|
+
// 长度验证,超过 500 字符则精简
|
|
1337
|
+
if (message.length > 500) {
|
|
1338
|
+
ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
|
|
1339
|
+
message = messageParts.slice(0, 5).join('\n') + '\n... 更多配置请查看完整状态';
|
|
1340
|
+
}
|
|
1341
|
+
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
1342
|
+
// 直接返回纯字符串,跳过适配器复杂编码
|
|
1343
|
+
return message;
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
// 显示所有站点状态
|
|
1347
|
+
let message = '📊 WordPress 插件状态\n\n';
|
|
1348
|
+
config.sites.forEach((site, index) => {
|
|
1349
|
+
const siteMessage = `站点 ${index + 1}: ${site.name} (${site.id})\n` +
|
|
1350
|
+
`🌐 URL: ${site.url}\n` +
|
|
1351
|
+
`⏰ 间隔: ${site.interval / 1000} 秒\n` +
|
|
1352
|
+
`🎯 目标: ${site.targets.join(', ') || '无'}\n` +
|
|
1353
|
+
`🔔 自动: ${site.enableAutoPush ? '开启' : '关闭'}\n` +
|
|
1354
|
+
`🔄 更新: ${site.enableUpdatePush ? '开启' : '关闭'}\n` +
|
|
1355
|
+
`👤 用户: ${site.enableUserPush ? '开启' : '关闭'}\n\n`;
|
|
1356
|
+
// 检查添加后是否超过500字符
|
|
1357
|
+
if (message.length + siteMessage.length > 500) {
|
|
1358
|
+
message += '... 更多站点请使用站点 ID 查看详细状态';
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
message += siteMessage;
|
|
1362
|
+
});
|
|
1363
|
+
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
1364
|
+
return message;
|
|
903
1365
|
}
|
|
904
|
-
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
905
|
-
// 直接返回纯字符串,跳过适配器复杂编码
|
|
906
|
-
return message;
|
|
907
1366
|
});
|
|
908
|
-
ctx.command('wordpress.toggle-update', '
|
|
909
|
-
.action(async ({ session }) => {
|
|
1367
|
+
ctx.command('wordpress.site.toggle-update <siteId>', '切换指定站点的文章更新推送开关')
|
|
1368
|
+
.action(async ({ session }, siteId) => {
|
|
910
1369
|
// 检查权限
|
|
911
1370
|
const authResult = checkSuperAdmin(session);
|
|
912
1371
|
if (!authResult.valid) {
|
|
913
1372
|
return authResult.message;
|
|
914
1373
|
}
|
|
915
|
-
ctx.logger.info(
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1374
|
+
ctx.logger.info(`命令 wordpress.site.toggle-update 被调用,站点 ID: ${siteId}`);
|
|
1375
|
+
// 查找站点
|
|
1376
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1377
|
+
if (!site) {
|
|
1378
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1379
|
+
}
|
|
1380
|
+
// 切换开关
|
|
1381
|
+
site.enableUpdatePush = !site.enableUpdatePush;
|
|
1382
|
+
await saveConfig('sites', config.sites);
|
|
1383
|
+
return `站点 ${site.name} 的文章更新推送已${site.enableUpdatePush ? '开启' : '关闭'}`;
|
|
919
1384
|
});
|
|
920
|
-
ctx.command('wordpress.toggle-user', '
|
|
921
|
-
.action(async ({ session }) => {
|
|
1385
|
+
ctx.command('wordpress.site.toggle-user <siteId>', '切换指定站点的新用户注册推送开关')
|
|
1386
|
+
.action(async ({ session }, siteId) => {
|
|
922
1387
|
// 检查权限
|
|
923
1388
|
const authResult = checkSuperAdmin(session);
|
|
924
1389
|
if (!authResult.valid) {
|
|
925
1390
|
return authResult.message;
|
|
926
1391
|
}
|
|
927
|
-
ctx.logger.info(
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1392
|
+
ctx.logger.info(`命令 wordpress.site.toggle-user 被调用,站点 ID: ${siteId}`);
|
|
1393
|
+
// 查找站点
|
|
1394
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1395
|
+
if (!site) {
|
|
1396
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1397
|
+
}
|
|
1398
|
+
// 切换开关
|
|
1399
|
+
site.enableUserPush = !site.enableUserPush;
|
|
1400
|
+
await saveConfig('sites', config.sites);
|
|
1401
|
+
return `站点 ${site.name} 的新用户注册推送已${site.enableUserPush ? '开启' : '关闭'}`;
|
|
931
1402
|
});
|
|
932
|
-
ctx.command('wordpress.toggle', '
|
|
933
|
-
.action(async ({ session }) => {
|
|
1403
|
+
ctx.command('wordpress.site.toggle <siteId>', '切换指定站点的自动推送开关')
|
|
1404
|
+
.action(async ({ session }, siteId) => {
|
|
934
1405
|
// 检查权限
|
|
935
1406
|
const authResult = checkSuperAdmin(session);
|
|
936
1407
|
if (!authResult.valid) {
|
|
937
1408
|
return authResult.message;
|
|
938
1409
|
}
|
|
939
|
-
ctx.logger.info(
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1410
|
+
ctx.logger.info(`命令 wordpress.site.toggle 被调用,站点 ID: ${siteId}`);
|
|
1411
|
+
// 查找站点
|
|
1412
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1413
|
+
if (!site) {
|
|
1414
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1415
|
+
}
|
|
1416
|
+
// 切换开关
|
|
1417
|
+
site.enableAutoPush = !site.enableAutoPush;
|
|
1418
|
+
await saveConfig('sites', config.sites);
|
|
1419
|
+
return `站点 ${site.name} 的自动推送已${site.enableAutoPush ? '开启' : '关闭'}`;
|
|
943
1420
|
});
|
|
944
|
-
ctx.command('wordpress.mention', '
|
|
945
|
-
.action(async ({ session }) => {
|
|
1421
|
+
ctx.command('wordpress.site.mention <siteId>', '切换指定站点的 @全体成员 开关')
|
|
1422
|
+
.action(async ({ session }, siteId) => {
|
|
946
1423
|
// 检查权限
|
|
947
1424
|
const authResult = checkSuperAdmin(session);
|
|
948
1425
|
if (!authResult.valid) {
|
|
949
1426
|
return authResult.message;
|
|
950
1427
|
}
|
|
951
|
-
ctx.logger.info(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1428
|
+
ctx.logger.info(`命令 wordpress.site.mention 被调用,站点 ID: ${siteId}`);
|
|
1429
|
+
// 查找站点
|
|
1430
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1431
|
+
if (!site) {
|
|
1432
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1433
|
+
}
|
|
1434
|
+
// 切换开关
|
|
1435
|
+
site.mentionAll = !site.mentionAll;
|
|
1436
|
+
await saveConfig('sites', config.sites);
|
|
1437
|
+
return `站点 ${site.name} 的 @全体成员 已${site.mentionAll ? '开启' : '关闭'}`;
|
|
955
1438
|
});
|
|
956
|
-
ctx.command('wordpress.set-url <url>', '
|
|
957
|
-
.action(async ({ session }, url) => {
|
|
1439
|
+
ctx.command('wordpress.site.set-url <siteId> <url>', '修改指定站点的 WordPress 地址')
|
|
1440
|
+
.action(async ({ session }, siteId, url) => {
|
|
958
1441
|
// 检查权限
|
|
959
1442
|
const authResult = checkSuperAdmin(session);
|
|
960
1443
|
if (!authResult.valid) {
|
|
961
1444
|
return authResult.message;
|
|
962
1445
|
}
|
|
963
|
-
ctx.logger.info(`命令 wordpress.set-url
|
|
1446
|
+
ctx.logger.info(`命令 wordpress.site.set-url 被调用,站点 ID: ${siteId},新地址:${url}`);
|
|
1447
|
+
// 查找站点
|
|
1448
|
+
const site = config.sites.find(s => s.id === siteId);
|
|
1449
|
+
if (!site) {
|
|
1450
|
+
return `未找到站点 ID: ${siteId}`;
|
|
1451
|
+
}
|
|
964
1452
|
// 修改站点地址
|
|
965
|
-
|
|
966
|
-
await saveConfig('
|
|
967
|
-
ctx.logger.info(
|
|
968
|
-
return
|
|
1453
|
+
site.url = url;
|
|
1454
|
+
await saveConfig('sites', config.sites);
|
|
1455
|
+
ctx.logger.info(`站点 ${site.name} 的地址已修改为:${url}`);
|
|
1456
|
+
return `站点 ${site.name} 的 WordPress 地址已修改为:${url}`;
|
|
969
1457
|
});
|
|
970
1458
|
ctx.command('wordpress.pushed', '查看已推送的文章列表')
|
|
971
1459
|
.action(async () => {
|
|
@@ -1061,8 +1549,90 @@ function apply(ctx, config) {
|
|
|
1061
1549
|
catch (error) {
|
|
1062
1550
|
ctx.logger.error(`批量删除 wordpress_user_registrations 旧记录失败: ${error}`);
|
|
1063
1551
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1552
|
+
});
|
|
1553
|
+
ctx.command('wordpress.stats', '查看插件运行指标统计')
|
|
1554
|
+
.action(() => {
|
|
1555
|
+
ctx.logger.info('命令 wordpress.stats 被调用');
|
|
1556
|
+
// 计算 API 调用成功率
|
|
1557
|
+
const apiSuccessRate = runtimeStats.apiCallCount > 0
|
|
1558
|
+
? (runtimeStats.apiSuccessCount / runtimeStats.apiCallCount * 100).toFixed(2)
|
|
1559
|
+
: '0.00';
|
|
1560
|
+
// 计算推送成功率
|
|
1561
|
+
const pushSuccessRate = (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) > 0
|
|
1562
|
+
? (runtimeStats.pushSuccessCount / (runtimeStats.pushSuccessCount + runtimeStats.pushFailureCount) * 100).toFixed(2)
|
|
1563
|
+
: '0.00';
|
|
1564
|
+
// 计算统计时间范围
|
|
1565
|
+
const startTime = new Date(runtimeStats.lastResetTime);
|
|
1566
|
+
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')}`;
|
|
1567
|
+
// 构建统计消息
|
|
1568
|
+
const messageParts = [
|
|
1569
|
+
'📊 WordPress 插件运行统计',
|
|
1570
|
+
`📅 统计开始时间: ${formattedStartTime}`,
|
|
1571
|
+
`📈 API 调用: ${runtimeStats.apiCallCount} 次`,
|
|
1572
|
+
`✅ API 成功: ${runtimeStats.apiSuccessCount} 次`,
|
|
1573
|
+
`❌ API 失败: ${runtimeStats.apiFailureCount} 次`,
|
|
1574
|
+
`📊 API 成功率: ${apiSuccessRate}%`,
|
|
1575
|
+
`📤 推送成功: ${runtimeStats.pushSuccessCount} 次`,
|
|
1576
|
+
`📥 推送失败: ${runtimeStats.pushFailureCount} 次`,
|
|
1577
|
+
`📊 推送成功率: ${pushSuccessRate}%`
|
|
1578
|
+
];
|
|
1579
|
+
let message = messageParts.join('\n');
|
|
1580
|
+
// 长度验证,超过 500 字符则精简
|
|
1581
|
+
if (message.length > 500) {
|
|
1582
|
+
ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
|
|
1583
|
+
message = messageParts.slice(0, 6).join('\n') + '\n... 更多统计信息请查看完整数据';
|
|
1584
|
+
}
|
|
1585
|
+
return message;
|
|
1586
|
+
});
|
|
1587
|
+
ctx.command('wordpress.health', '查看插件健康状态')
|
|
1588
|
+
.action(async () => {
|
|
1589
|
+
ctx.logger.info('命令 wordpress.health 被调用');
|
|
1590
|
+
// 检查数据库连接
|
|
1591
|
+
const dbConnected = await checkDatabaseConnection();
|
|
1592
|
+
// 检查 Bot 在线状态
|
|
1593
|
+
const bot = getValidBot();
|
|
1594
|
+
const botOnline = !!bot;
|
|
1595
|
+
// 检查 API 可达性(尝试访问第一个站点的 API)
|
|
1596
|
+
let apiReachable = false;
|
|
1597
|
+
if (config.sites.length > 0) {
|
|
1598
|
+
const firstSite = config.sites[0];
|
|
1599
|
+
try {
|
|
1600
|
+
const url = `${firstSite.url}/wp-json/wp/v2/posts?per_page=1`;
|
|
1601
|
+
const response = await ctx.http.get(url, {
|
|
1602
|
+
timeout: 5000
|
|
1603
|
+
});
|
|
1604
|
+
apiReachable = true;
|
|
1605
|
+
}
|
|
1606
|
+
catch (error) {
|
|
1607
|
+
ctx.logger.warn(`API 可达性检查失败: ${error}`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
// 构建健康状态消息
|
|
1611
|
+
const messageParts = [
|
|
1612
|
+
'🏥 WordPress 插件健康状态',
|
|
1613
|
+
`🗄️ 数据库连接: ${dbConnected ? '✅ 正常' : '❌ 异常'}`,
|
|
1614
|
+
`🤖 Bot 在线状态: ${botOnline ? '✅ 在线' : '❌ 离线'}`,
|
|
1615
|
+
`🌐 API 可达性: ${apiReachable ? '✅ 可达' : '❌ 不可达'}`
|
|
1616
|
+
];
|
|
1617
|
+
// 添加站点状态
|
|
1618
|
+
if (config.sites.length > 0) {
|
|
1619
|
+
messageParts.push('\n📋 站点状态:');
|
|
1620
|
+
config.sites.forEach((site, index) => {
|
|
1621
|
+
if (index < 3) { // 只显示前3个站点
|
|
1622
|
+
messageParts.push(`${index + 1}. ${site.name}: ${site.enableAutoPush ? '✅ 启用' : '❌ 禁用'}`);
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
if (config.sites.length > 3) {
|
|
1626
|
+
messageParts.push(`... 共 ${config.sites.length} 个站点`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
let message = messageParts.join('\n');
|
|
1630
|
+
// 长度验证,超过 500 字符则精简
|
|
1631
|
+
if (message.length > 500) {
|
|
1632
|
+
ctx.logger.warn(`消息过长,长度: ${message.length},将进行精简`);
|
|
1633
|
+
message = messageParts.slice(0, 5).join('\n') + '\n... 更多健康状态信息请查看完整数据';
|
|
1634
|
+
}
|
|
1635
|
+
return message;
|
|
1066
1636
|
});
|
|
1067
1637
|
ctx.command('wordpress', 'WordPress 推送插件菜单')
|
|
1068
1638
|
.action(() => {
|
|
@@ -1087,15 +1657,18 @@ function apply(ctx, config) {
|
|
|
1087
1657
|
ctx.logger.info(`准备返回消息,长度: ${message.length}`);
|
|
1088
1658
|
return message;
|
|
1089
1659
|
});
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1660
|
+
// 为每个站点设置独立的定时任务
|
|
1661
|
+
config.sites.forEach(site => {
|
|
1662
|
+
ctx.setInterval(() => {
|
|
1663
|
+
try {
|
|
1664
|
+
pushNewPosts();
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1668
|
+
ctx.logger.error(`站点 ${site.id} 定时任务执行失败:${errorMessage}`);
|
|
1669
|
+
ctx.logger.error(`错误栈:${error instanceof Error ? error.stack : '无'}`);
|
|
1670
|
+
ctx.logger.warn('定时任务将在下一个周期继续执行');
|
|
1671
|
+
}
|
|
1672
|
+
}, site.interval);
|
|
1673
|
+
});
|
|
1101
1674
|
}
|