@anyul/koishi-plugin-rss 5.2.48 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/lib/commands/index.d.ts +32 -0
- package/lib/commands/index.js +361 -218
- package/lib/commands/subscription-create.d.ts +18 -5
- package/lib/commands/subscription-create.js +40 -28
- package/lib/commands/subscription-edit.js +5 -5
- package/lib/commands/subscription-management.js +9 -9
- package/lib/commands/web-monitor.d.ts +20 -1
- package/lib/commands/web-monitor.js +21 -19
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +2 -6
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/message-cache.d.ts +9 -0
- package/lib/utils/message-cache.js +36 -21
- package/package.json +1 -1
package/lib/commands/index.js
CHANGED
|
@@ -2,221 +2,335 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createCommandRuntimeDeps = exports.registerWebMonitorCommands = exports.registerSubscriptionCreateCommand = exports.registerSubscriptionEditCommand = exports.registerSubscriptionManagementCommands = void 0;
|
|
4
4
|
exports.registerManagementCommands = registerManagementCommands;
|
|
5
|
+
exports.resolveCacheMessageById = resolveCacheMessageById;
|
|
6
|
+
exports.handleCacheList = handleCacheList;
|
|
7
|
+
exports.handleCacheMessage = handleCacheMessage;
|
|
8
|
+
exports.handleCacheSearch = handleCacheSearch;
|
|
9
|
+
exports.handleCacheStats = handleCacheStats;
|
|
10
|
+
exports.handleCacheClear = handleCacheClear;
|
|
11
|
+
exports.handleCacheCleanup = handleCacheCleanup;
|
|
12
|
+
exports.handleCachePull = handleCachePull;
|
|
5
13
|
const error_handler_1 = require("../utils/error-handler");
|
|
6
14
|
const error_tracker_1 = require("../utils/error-tracker");
|
|
7
|
-
const message_cache_1 = require("../utils/message-cache");
|
|
8
15
|
const logger_1 = require("../utils/logger");
|
|
16
|
+
const message_cache_1 = require("../utils/message-cache");
|
|
9
17
|
const utils_1 = require("./utils");
|
|
10
18
|
/**
|
|
11
19
|
* 注册管理类命令
|
|
20
|
+
*
|
|
21
|
+
* 设计要点(命令系统统一):
|
|
22
|
+
* - cache/queue 父命令【不带 .action()】,由 Koishi 原生「无 action → 显示 help」接管,
|
|
23
|
+
* 自动在 help 菜单/网页控制台列出子命令树。
|
|
24
|
+
* - 子命令注册为 rssowl.cache.list / rssowl.cache.stats 等真正的嵌套命令;
|
|
25
|
+
* 用户在聊天里输入 "rsso.cache list"(带空格)时,Koishi 核心路由
|
|
26
|
+
* (inferCommand, core index.mjs:1377-1405)会自动解析到 rssowl.cache.list 子命令,
|
|
27
|
+
* 输入方式与旧版完全一致,但获得了菜单可见性。
|
|
28
|
+
* - 父命令 rssowl.cache 必须无位置参数(`ctx.command('rssowl.cache', ...)`),
|
|
29
|
+
* 否则路由会在父命令处 break,导致 list 被当作位置参数而非子命令。
|
|
12
30
|
*/
|
|
13
31
|
function registerManagementCommands(deps) {
|
|
14
|
-
registerCacheCommands(deps.ctx, deps.config);
|
|
32
|
+
registerCacheCommands(deps.ctx, deps.config, deps.queueManager);
|
|
15
33
|
registerQueueCommands(deps.ctx, deps.config, deps.queueManager);
|
|
16
34
|
}
|
|
17
|
-
|
|
35
|
+
// 设计决策(审查 #9):cache/queue 命令通过 ctx.guild() 限定为「仅群组可用」。
|
|
36
|
+
// 这是有意为之——缓存数据天然按 platform+guildId 分组,私聊没有有意义的 guildId,
|
|
37
|
+
// extractSessionInfo 依赖 session.event.guild.id;若放开到私聊会导致 guild 为 undefined 而崩溃,
|
|
38
|
+
// 且私聊也无"本群缓存"这一语义。因此保持 ctx.guild(),不在私聊降级。
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// cache 命令树
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function registerCacheCommands(ctx, config, queueManager) {
|
|
43
|
+
// 父命令:无 action,Koishi 自动显示 help + 子命令列表
|
|
18
44
|
ctx.guild()
|
|
19
45
|
.command('rssowl.cache', '消息缓存管理')
|
|
20
|
-
.alias('rsso.cache')
|
|
21
|
-
.usage(
|
|
22
|
-
|
|
46
|
+
.alias('rsso.cache', 'rsc')
|
|
47
|
+
.usage(`查看和管理已推送的 RSS 消息缓存(仅限当前群组)。
|
|
48
|
+
|
|
49
|
+
输入 rsso.cache 不带子命令可查看所有子命令,或直接使用:
|
|
50
|
+
rsso.cache list [页数] 查看缓存消息列表
|
|
51
|
+
rsso.cache search <关键词> 搜索缓存消息
|
|
52
|
+
rsso.cache stats 查看缓存统计
|
|
53
|
+
rsso.cache message <ID> 查看消息详情(真实 ID)
|
|
54
|
+
rsso.cache pull <ID> 重新推送缓存消息(真实 ID)
|
|
55
|
+
rsso.cache clear 清空本群缓存(需权限)
|
|
56
|
+
rsso.cache cleanup [N] 清理并保留最新 N 条(需权限)
|
|
23
57
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
注意:
|
|
59
|
+
- 仅显示/操作【当前群组】的缓存
|
|
60
|
+
- message/pull 使用 list 中方括号 [ID:xxx] 显示的真实数据库 ID`);
|
|
61
|
+
// 子命令:list
|
|
62
|
+
ctx.guild()
|
|
63
|
+
.command('rssowl.cache.list [page:number]', '查看缓存消息列表')
|
|
64
|
+
.alias('rsso.cache.list')
|
|
65
|
+
.usage(`查看当前群组的缓存消息列表(分页,每页 10 条)。
|
|
32
66
|
|
|
33
67
|
示例:
|
|
34
|
-
rsso.cache list
|
|
35
|
-
rsso.cache list 2
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
rsso.cache list 查看第 1 页
|
|
69
|
+
rsso.cache list 2 查看第 2 页`)
|
|
70
|
+
.example('rsso.cache list')
|
|
71
|
+
.action(async ({ session }, page) => {
|
|
72
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'list');
|
|
73
|
+
if (typeof cmd === 'string')
|
|
74
|
+
return cmd;
|
|
75
|
+
return handleCacheList(cmd, page ? [String(page)] : [], config);
|
|
76
|
+
});
|
|
77
|
+
// 子命令:search
|
|
78
|
+
ctx.guild()
|
|
79
|
+
.command('rssowl.cache.search <keyword:text>', '搜索缓存消息')
|
|
80
|
+
.alias('rsso.cache.search')
|
|
81
|
+
.usage(`在当前群组的缓存中搜索包含关键词的消息。
|
|
41
82
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
示例:
|
|
84
|
+
rsso.cache search 新闻`)
|
|
85
|
+
.example('rsso.cache search 新闻')
|
|
86
|
+
.action(async ({ session }, keyword) => {
|
|
87
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'search');
|
|
88
|
+
if (typeof cmd === 'string')
|
|
89
|
+
return cmd;
|
|
90
|
+
if (!keyword)
|
|
91
|
+
return '请提供搜索关键词\n用法: rsso.cache search <关键词>';
|
|
92
|
+
return handleCacheSearch(cmd, [keyword], config);
|
|
93
|
+
});
|
|
94
|
+
// 子命令:stats
|
|
95
|
+
ctx.guild()
|
|
96
|
+
.command('rssowl.cache.stats', '查看缓存统计')
|
|
97
|
+
.alias('rsso.cache.stats')
|
|
98
|
+
.usage('查看当前群组的缓存统计信息(总数、按订阅分布、时间范围)。')
|
|
99
|
+
.example('rsso.cache stats')
|
|
100
|
+
.action(async ({ session }) => {
|
|
101
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'stats');
|
|
102
|
+
if (typeof cmd === 'string')
|
|
103
|
+
return cmd;
|
|
104
|
+
return handleCacheStats(cmd, config);
|
|
105
|
+
});
|
|
106
|
+
// 子命令:message
|
|
107
|
+
ctx.guild()
|
|
108
|
+
.command('rssowl.cache.message <id:number>', '查看缓存消息详情')
|
|
109
|
+
.alias('rsso.cache.message')
|
|
110
|
+
.usage(`按真实 ID 查看缓存消息详情(ID 来自 list 列表中的 [ID:xxx])。
|
|
52
111
|
|
|
53
|
-
|
|
54
|
-
rsso.cache
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
112
|
+
示例:
|
|
113
|
+
rsso.cache message 42`)
|
|
114
|
+
.example('rsso.cache message 42')
|
|
115
|
+
.action(async ({ session }, id) => {
|
|
116
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'message');
|
|
117
|
+
if (typeof cmd === 'string')
|
|
118
|
+
return cmd;
|
|
119
|
+
return handleCacheMessage(cmd, id != null ? [String(id)] : [], config);
|
|
120
|
+
});
|
|
121
|
+
// 子命令:pull
|
|
122
|
+
ctx.guild()
|
|
123
|
+
.command('rssowl.cache.pull <id:number>', '重新推送缓存消息')
|
|
124
|
+
.alias('rsso.cache.pull')
|
|
125
|
+
.usage(`按真实 ID 重新推送一条缓存消息(ID 来自 list 列表中的 [ID:xxx])。
|
|
126
|
+
推送经发送队列,与正常 feed 推送路径一致(含重试/降级)。
|
|
61
127
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
示例:
|
|
129
|
+
rsso.cache pull 42`)
|
|
130
|
+
.example('rsso.cache pull 42')
|
|
131
|
+
.action(async ({ session }, id) => {
|
|
132
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'pull');
|
|
133
|
+
if (typeof cmd === 'string')
|
|
134
|
+
return cmd;
|
|
135
|
+
return handleCachePull(ctx, queueManager, session, cmd, id != null ? [String(id)] : [], config);
|
|
136
|
+
});
|
|
137
|
+
// 子命令:clear(需权限)
|
|
138
|
+
ctx.guild()
|
|
139
|
+
.command('rssowl.cache.clear', '清空本群缓存')
|
|
140
|
+
.alias('rsso.cache.clear')
|
|
141
|
+
.usage('清空当前群组的所有缓存消息(需要基础权限)。')
|
|
142
|
+
.example('rsso.cache clear')
|
|
143
|
+
.action(async ({ session }) => {
|
|
144
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'clear');
|
|
145
|
+
if (typeof cmd === 'string')
|
|
146
|
+
return cmd;
|
|
147
|
+
const ac = (0, utils_1.checkAuthority)(cmd.authority, config.basic.authority, permissionDeniedMsg(cmd.authority, config));
|
|
148
|
+
if (!ac.success)
|
|
149
|
+
return ac.message;
|
|
150
|
+
return handleCacheClear(cmd, config);
|
|
151
|
+
});
|
|
152
|
+
// 子命令:cleanup(需权限)
|
|
153
|
+
ctx.guild()
|
|
154
|
+
.command('rssowl.cache.cleanup [keepLatest:number]', '清理缓存')
|
|
155
|
+
.alias('rsso.cache.cleanup')
|
|
156
|
+
.usage(`清理当前群组缓存,保留最新 N 条(需要基础权限)。
|
|
157
|
+
不传 N 时使用配置的最大缓存限制。
|
|
158
|
+
|
|
159
|
+
示例:
|
|
160
|
+
rsso.cache cleanup 按配置上限清理
|
|
161
|
+
rsso.cache cleanup 50 保留最新 50 条`)
|
|
162
|
+
.example('rsso.cache cleanup 50')
|
|
163
|
+
.action(async ({ session }, keepLatest) => {
|
|
164
|
+
const cmd = buildCacheCmd(session, 'rsso.cache', 'cleanup');
|
|
165
|
+
if (typeof cmd === 'string')
|
|
166
|
+
return cmd;
|
|
167
|
+
const ac = (0, utils_1.checkAuthority)(cmd.authority, config.basic.authority, permissionDeniedMsg(cmd.authority, config));
|
|
168
|
+
if (!ac.success)
|
|
169
|
+
return ac.message;
|
|
170
|
+
return handleCacheCleanup(cmd, keepLatest != null ? [String(keepLatest)] : [], config);
|
|
89
171
|
});
|
|
90
172
|
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// queue 命令树
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
91
176
|
function registerQueueCommands(ctx, config, queueManager) {
|
|
177
|
+
// 父命令:无 action,Koishi 自动显示 help + 子命令列表
|
|
92
178
|
ctx.guild()
|
|
93
179
|
.command('rssowl.queue', '发送队列管理')
|
|
94
|
-
.alias('rsso.queue')
|
|
95
|
-
.usage(
|
|
96
|
-
发送队列管理功能,查看和管理待发送的消息队列。
|
|
180
|
+
.alias('rsso.queue', 'rsq')
|
|
181
|
+
.usage(`查看和管理发送队列。
|
|
97
182
|
|
|
98
|
-
|
|
99
|
-
rsso.queue stats
|
|
100
|
-
rsso.queue retry [id]
|
|
101
|
-
rsso.queue retry --all
|
|
102
|
-
rsso.queue cleanup [hours]
|
|
183
|
+
输入 rsso.queue 不带子命令可查看所有子命令,或直接使用:
|
|
184
|
+
rsso.queue stats 查看队列统计
|
|
185
|
+
rsso.queue retry [id] 重试失败任务(需权限)
|
|
186
|
+
rsso.queue retry --all 重试所有失败任务(需权限)
|
|
187
|
+
rsso.queue cleanup [hours] 清理旧的成功任务(默认 24 小时,需权限)`);
|
|
188
|
+
// 子命令:stats
|
|
189
|
+
ctx.guild()
|
|
190
|
+
.command('rssowl.queue.stats', '查看发送队列统计')
|
|
191
|
+
.alias('rsso.queue.stats')
|
|
192
|
+
.usage('查看发送队列各状态的任务数量。')
|
|
193
|
+
.example('rsso.queue stats')
|
|
194
|
+
.action(async ({ session }) => {
|
|
195
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.queue', 'stats');
|
|
196
|
+
return handleQueueStats(queueManager, config, logContext);
|
|
197
|
+
});
|
|
198
|
+
// 子命令:retry(需权限)
|
|
199
|
+
ctx.guild()
|
|
200
|
+
.command('rssowl.queue.retry [id:number]', '重试发送队列失败任务')
|
|
201
|
+
.alias('rsso.queue.retry')
|
|
202
|
+
.option('all', '--all 重试所有失败任务')
|
|
203
|
+
.usage(`重试发送队列中失败的任务(需要基础权限)。
|
|
103
204
|
|
|
104
205
|
示例:
|
|
105
|
-
rsso.queue
|
|
106
|
-
rsso.queue retry
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
rsso.queue retry [id] - 重试失败的任务
|
|
124
|
-
rsso.queue retry --all - 重试所有失败任务
|
|
125
|
-
rsso.queue cleanup [hours] - 清理旧的成功任务(默认24小时)
|
|
206
|
+
rsso.queue retry 5 重试 ID 为 5 的任务
|
|
207
|
+
rsso.queue retry --all 重试所有失败任务`)
|
|
208
|
+
.example('rsso.queue retry --all')
|
|
209
|
+
.action(async ({ session, options }, id) => {
|
|
210
|
+
const { authority } = (0, utils_1.extractSessionInfo)(session);
|
|
211
|
+
const ac = (0, utils_1.checkAuthority)(authority, config.basic.authority, permissionDeniedMsg(authority, config));
|
|
212
|
+
if (!ac.success)
|
|
213
|
+
return ac.message;
|
|
214
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.queue', 'retry');
|
|
215
|
+
const args = options?.all ? ['--all'] : (id != null ? [String(id)] : []);
|
|
216
|
+
return handleQueueRetry(queueManager, args, config, logContext);
|
|
217
|
+
});
|
|
218
|
+
// 子命令:cleanup(需权限)
|
|
219
|
+
ctx.guild()
|
|
220
|
+
.command('rssowl.queue.cleanup [hours:number]', '清理发送队列成功任务')
|
|
221
|
+
.alias('rsso.queue.cleanup')
|
|
222
|
+
.usage(`清理超过指定小时数的成功任务(需要基础权限)。
|
|
223
|
+
不传 hours 时默认 24 小时。
|
|
126
224
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
case 'cleanup':
|
|
139
|
-
if (authority < config.basic.authority) {
|
|
140
|
-
return `权限不足!当前权限: ${authority},需要权限: ${config.basic.authority} 或以上`;
|
|
141
|
-
}
|
|
142
|
-
return handleQueueCleanup(queueManager, args, config, logContext);
|
|
143
|
-
default:
|
|
144
|
-
return `未知的子命令: ${subcommand}\n使用 "rsso.queue" 查看可用指令`;
|
|
145
|
-
}
|
|
225
|
+
示例:
|
|
226
|
+
rsso.queue cleanup 清理 24 小时前的成功任务
|
|
227
|
+
rsso.queue cleanup 48 清理 48 小时前的成功任务`)
|
|
228
|
+
.example('rsso.queue cleanup 48')
|
|
229
|
+
.action(async ({ session }, hours) => {
|
|
230
|
+
const { authority } = (0, utils_1.extractSessionInfo)(session);
|
|
231
|
+
const ac = (0, utils_1.checkAuthority)(authority, config.basic.authority, permissionDeniedMsg(authority, config));
|
|
232
|
+
if (!ac.success)
|
|
233
|
+
return ac.message;
|
|
234
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.queue', 'cleanup');
|
|
235
|
+
return handleQueueCleanup(queueManager, hours != null ? [String(hours)] : [], config, logContext);
|
|
146
236
|
});
|
|
147
237
|
}
|
|
148
|
-
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// cache 命令上下文构造
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
/**
|
|
242
|
+
* 从 session 构造 cache 命令上下文,并预先校验缓存是否启用。
|
|
243
|
+
* 返回 string 时表示出错消息(应直接 return),返回 CacheCommandContext 时继续。
|
|
244
|
+
*/
|
|
245
|
+
function buildCacheCmd(session, parent, sub) {
|
|
246
|
+
const { platform, guildId, authority } = (0, utils_1.extractSessionInfo)(session);
|
|
247
|
+
const cache = (0, message_cache_1.getMessageCache)();
|
|
248
|
+
if (!cache) {
|
|
249
|
+
return '消息缓存功能未启用,请在配置中启用 cache.enabled';
|
|
250
|
+
}
|
|
251
|
+
const logContext = (0, utils_1.buildCommandLogContext)(session, parent, sub);
|
|
252
|
+
return { cache, platform, guildId, authority, logContext };
|
|
253
|
+
}
|
|
254
|
+
function permissionDeniedMsg(authority, config) {
|
|
255
|
+
return `❌ 权限不足!当前权限: ${authority},需要权限: ${config.basic.authority} 或以上`;
|
|
256
|
+
}
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// cache handlers(业务逻辑不变,仅统一轻量 emoji 风格)
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
async function handleCacheList(cmdCtx, args, config) {
|
|
261
|
+
const { cache, platform, guildId, logContext } = cmdCtx;
|
|
149
262
|
const page = parseInt(args[0]) || 1;
|
|
150
263
|
const limit = 10;
|
|
151
264
|
const offset = (page - 1) * limit;
|
|
152
265
|
try {
|
|
153
|
-
|
|
266
|
+
// 限定到当前群组,杜绝跨群泄露
|
|
267
|
+
const messages = await cache.getMessages({ limit, offset, platform, guildId });
|
|
154
268
|
if (messages.length === 0) {
|
|
155
269
|
return '暂无缓存消息';
|
|
156
270
|
}
|
|
157
|
-
const stats = await cache.getStats();
|
|
158
|
-
let output =
|
|
271
|
+
const stats = await cache.getStats({ platform, guildId });
|
|
272
|
+
let output = `缓存消息列表 (第${page}页,共${Math.max(1, Math.ceil(stats.totalMessages / limit))}页,总计${stats.totalMessages}条)\n\n`;
|
|
159
273
|
output += messages.map((msg, index) => {
|
|
160
274
|
const date = new Date(msg.createdAt).toLocaleString('zh-CN', {
|
|
161
275
|
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
162
276
|
});
|
|
163
277
|
const title = msg.title.length > 30 ? msg.title.substring(0, 30) + '...' : msg.title;
|
|
164
|
-
return `${index + 1}. [ID:${msg.id}] [${msg.rssId}] ${title}\n 时间: ${date}\n 链接: ${msg.link}`;
|
|
278
|
+
return `${offset + index + 1}. [ID:${msg.id}] [${msg.rssId}] ${title}\n 时间: ${date}\n 链接: ${msg.link}`;
|
|
165
279
|
}).join('\n\n');
|
|
166
280
|
output += `\n\n💡 使用 "rsso.cache list ${page + 1}" 查看下一页`;
|
|
167
|
-
output += '\n💡 使用 "rsso.cache
|
|
168
|
-
output += '\n💡 使用 "rsso.cache
|
|
281
|
+
output += '\n💡 使用 "rsso.cache message <ID>" 查看详情(ID 为方括号中的真实 ID)';
|
|
282
|
+
output += '\n💡 使用 "rsso.cache pull <ID>" 推送消息(使用真实 ID)';
|
|
169
283
|
return output;
|
|
170
284
|
}
|
|
171
285
|
catch (error) {
|
|
172
|
-
|
|
173
|
-
return
|
|
286
|
+
logCacheError(config, error, 'cache list error', { ...logContext, page, limit });
|
|
287
|
+
return `❌ 获取消息列表失败: ${error.message}`;
|
|
174
288
|
}
|
|
175
289
|
}
|
|
176
|
-
async function handleCacheMessage(
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
290
|
+
async function handleCacheMessage(cmdCtx, args, config) {
|
|
291
|
+
const { cache, logContext } = cmdCtx;
|
|
292
|
+
const realId = parseInt(args[0]);
|
|
293
|
+
if (!realId || realId < 1) {
|
|
294
|
+
return '请提供真实 ID\n用法: rsso.cache message <ID>\n示例: rsso.cache message 42\n💡 提示:使用 "rsso.cache list" 查看方括号中的真实 ID';
|
|
180
295
|
}
|
|
181
296
|
try {
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
return `❌
|
|
297
|
+
const message = await resolveCacheMessageById(cmdCtx, realId);
|
|
298
|
+
if (!message) {
|
|
299
|
+
return `❌ 未找到 ID 为 ${args[0]} 的消息(或该消息不属于当前群组)\n💡 使用 "rsso.cache list" 查看可用的真实 ID`;
|
|
185
300
|
}
|
|
186
|
-
const pubDate = new Date(
|
|
301
|
+
const pubDate = new Date(message.pubDate).toLocaleString('zh-CN', {
|
|
187
302
|
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
188
303
|
});
|
|
189
|
-
const createdAt = new Date(
|
|
304
|
+
const createdAt = new Date(message.createdAt).toLocaleString('zh-CN', {
|
|
190
305
|
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
191
306
|
});
|
|
192
|
-
let output =
|
|
193
|
-
output +=
|
|
194
|
-
if (
|
|
195
|
-
const content =
|
|
196
|
-
output += `\n
|
|
307
|
+
let output = `消息详情 (ID:${message.id})\n\n`;
|
|
308
|
+
output += `标题: ${message.title}\n订阅: ${message.rssId}\n群组: ${message.platform}:${message.guildId}\n链接: ${message.link}\n发布时间: ${pubDate}\n缓存时间: ${createdAt}\n`;
|
|
309
|
+
if (message.content) {
|
|
310
|
+
const content = message.content.length > 200 ? message.content.substring(0, 200) + '...' : message.content;
|
|
311
|
+
output += `\n内容:\n${content}`;
|
|
197
312
|
}
|
|
198
|
-
if (
|
|
199
|
-
output += `\n\n
|
|
200
|
-
if (
|
|
201
|
-
output += `\n\n
|
|
313
|
+
if (message.imageUrl)
|
|
314
|
+
output += `\n\n图片: ${message.imageUrl}`;
|
|
315
|
+
if (message.videoUrl)
|
|
316
|
+
output += `\n\n视频: ${message.videoUrl}`;
|
|
202
317
|
return output;
|
|
203
318
|
}
|
|
204
319
|
catch (error) {
|
|
205
|
-
|
|
206
|
-
return
|
|
320
|
+
logCacheError(config, error, 'cache message error', { ...logContext, realId });
|
|
321
|
+
return `❌ 获取消息详情失败: ${error.message}`;
|
|
207
322
|
}
|
|
208
323
|
}
|
|
209
|
-
async function handleCacheSearch(
|
|
324
|
+
async function handleCacheSearch(cmdCtx, args, config) {
|
|
325
|
+
const { cache, platform, guildId, logContext } = cmdCtx;
|
|
210
326
|
const keyword = args[0];
|
|
211
|
-
if (!keyword) {
|
|
212
|
-
return '请提供搜索关键词\n使用方法: rsso.cache search <关键词>';
|
|
213
|
-
}
|
|
214
327
|
try {
|
|
215
|
-
|
|
328
|
+
// 限定到当前群组
|
|
329
|
+
const messages = await cache.searchMessages({ keyword, limit: 10, platform, guildId });
|
|
216
330
|
if (messages.length === 0) {
|
|
217
331
|
return `未找到包含 "${keyword}" 的消息`;
|
|
218
332
|
}
|
|
219
|
-
let output =
|
|
333
|
+
let output = `搜索结果 "${keyword}" (找到${messages.length}条)\n\n`;
|
|
220
334
|
output += messages.map((msg, index) => {
|
|
221
335
|
const date = new Date(msg.createdAt).toLocaleString('zh-CN', {
|
|
222
336
|
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
@@ -224,109 +338,134 @@ async function handleCacheSearch(cache, args, config, logContext) {
|
|
|
224
338
|
const title = msg.title.length > 30 ? msg.title.substring(0, 30) + '...' : msg.title;
|
|
225
339
|
return `${index + 1}. [ID:${msg.id}] [${msg.rssId}] ${title}\n 时间: ${date}`;
|
|
226
340
|
}).join('\n\n');
|
|
227
|
-
output += '\n\n💡 使用 "rsso.cache message
|
|
341
|
+
output += '\n\n💡 使用 "rsso.cache message <ID>" 查看详情(使用方括号中的真实 ID)';
|
|
228
342
|
return output;
|
|
229
343
|
}
|
|
230
344
|
catch (error) {
|
|
231
|
-
|
|
232
|
-
return
|
|
345
|
+
logCacheError(config, error, 'cache search error', { ...logContext, keyword });
|
|
346
|
+
return `❌ 搜索失败: ${error.message}`;
|
|
233
347
|
}
|
|
234
348
|
}
|
|
235
|
-
async function handleCacheStats(
|
|
349
|
+
async function handleCacheStats(cmdCtx, config) {
|
|
350
|
+
const { cache, platform, guildId, logContext } = cmdCtx;
|
|
236
351
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
352
|
+
// 限定到当前群组
|
|
353
|
+
const stats = await cache.getStats({ platform, guildId });
|
|
354
|
+
let output = `缓存统计信息(本群)\n\n总消息数: ${stats.totalMessages}\n`;
|
|
239
355
|
if (stats.oldestMessage)
|
|
240
|
-
output +=
|
|
356
|
+
output += `最早消息: ${new Date(stats.oldestMessage).toLocaleString('zh-CN')}\n`;
|
|
241
357
|
if (stats.newestMessage)
|
|
242
|
-
output +=
|
|
243
|
-
output += '\n
|
|
358
|
+
output += `最新消息: ${new Date(stats.newestMessage).toLocaleString('zh-CN')}\n`;
|
|
359
|
+
output += '\n按订阅统计:\n';
|
|
244
360
|
Object.entries(stats.bySubscription).sort(([, a], [, b]) => b - a).slice(0, 10).forEach(([rssId, count]) => {
|
|
245
361
|
output += ` ${rssId}: ${count}条\n`;
|
|
246
362
|
});
|
|
247
|
-
output +=
|
|
248
|
-
Object.entries(stats.byGuild).sort(([, a], [, b]) => b - a).slice(0, 10).forEach(([guild, count]) => {
|
|
249
|
-
output += ` ${guild}: ${count}条\n`;
|
|
250
|
-
});
|
|
251
|
-
output += `\n⚙️ 最大缓存限制: ${cache.getMaxCacheSize()}条`;
|
|
363
|
+
output += `\n最大缓存限制: ${cache.getMaxCacheSize()}条`;
|
|
252
364
|
return output;
|
|
253
365
|
}
|
|
254
366
|
catch (error) {
|
|
255
|
-
|
|
256
|
-
return
|
|
367
|
+
logCacheError(config, error, 'cache stats error', logContext);
|
|
368
|
+
return `❌ 获取统计信息失败: ${error.message}`;
|
|
257
369
|
}
|
|
258
370
|
}
|
|
259
|
-
async function handleCacheClear(
|
|
371
|
+
async function handleCacheClear(cmdCtx, config) {
|
|
372
|
+
const { cache, platform, guildId, logContext } = cmdCtx;
|
|
260
373
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
374
|
+
// 限定到当前群组
|
|
375
|
+
const deletedCount = await cache.clearAll({ platform, guildId });
|
|
376
|
+
return `✅ 已清空本群缓存,共删除 ${deletedCount} 条消息`;
|
|
263
377
|
}
|
|
264
378
|
catch (error) {
|
|
265
|
-
|
|
266
|
-
return
|
|
379
|
+
logCacheError(config, error, 'cache clear error', logContext);
|
|
380
|
+
return `❌ 清空缓存失败: ${error.message}`;
|
|
267
381
|
}
|
|
268
382
|
}
|
|
269
|
-
async function handleCacheCleanup(
|
|
383
|
+
async function handleCacheCleanup(cmdCtx, args, config) {
|
|
384
|
+
const { cache, platform, guildId, logContext } = cmdCtx;
|
|
270
385
|
const keepLatest = parseInt(args[0]) || cache.getMaxCacheSize();
|
|
271
386
|
try {
|
|
272
|
-
|
|
387
|
+
// 限定到当前群组
|
|
388
|
+
const deletedCount = await cache.cleanup({ keepLatest, platform, guildId });
|
|
273
389
|
if (deletedCount === 0) {
|
|
274
|
-
return '✅
|
|
390
|
+
return '✅ 当前群组缓存数量未超过限制,无需清理';
|
|
275
391
|
}
|
|
276
|
-
return `✅
|
|
392
|
+
return `✅ 已清理本群缓存,保留最新 ${keepLatest} 条,删除 ${deletedCount} 条消息`;
|
|
277
393
|
}
|
|
278
394
|
catch (error) {
|
|
279
|
-
|
|
280
|
-
return
|
|
395
|
+
logCacheError(config, error, 'cache cleanup error', { ...logContext, keepLatest });
|
|
396
|
+
return `❌ 清理缓存失败: ${error.message}`;
|
|
281
397
|
}
|
|
282
398
|
}
|
|
283
|
-
async function handleCachePull(ctx, session,
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
399
|
+
async function handleCachePull(ctx, queueManager, session, cmdCtx, args, config) {
|
|
400
|
+
const { logContext } = cmdCtx;
|
|
401
|
+
const realId = parseInt(args[0]);
|
|
402
|
+
if (!realId || realId < 1) {
|
|
403
|
+
return '请提供有效的真实 ID\n用法: rsso.cache pull <ID>\n示例: rsso.cache pull 42\n💡 提示:使用 "rsso.cache list" 查看方括号中的真实 ID';
|
|
287
404
|
}
|
|
288
|
-
let
|
|
289
|
-
const targetContext = {
|
|
290
|
-
...logContext,
|
|
291
|
-
serialNumber,
|
|
292
|
-
};
|
|
405
|
+
let message = null;
|
|
293
406
|
try {
|
|
294
|
-
|
|
295
|
-
if (!
|
|
296
|
-
return `❌
|
|
407
|
+
message = await resolveCacheMessageById(cmdCtx, realId);
|
|
408
|
+
if (!message) {
|
|
409
|
+
return `❌ 未找到 ID 为 ${args[0]} 的消息(或该消息不属于当前群组)\n💡 使用 "rsso.cache list" 查看可用的真实 ID`;
|
|
297
410
|
}
|
|
298
|
-
if (!
|
|
411
|
+
if (!message.finalMessage) {
|
|
299
412
|
return '❌ 该消息没有缓存的最终消息\n💡 这条消息可能是旧版本缓存,请重新订阅后重试';
|
|
300
413
|
}
|
|
301
414
|
const { id: guildId } = session.event.guild;
|
|
302
415
|
const { platform } = session.event;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
416
|
+
// 走发送队列而不是直接 ctx.broadcast,与 feeder 推送路径保持一致:
|
|
417
|
+
// - 经队列可获得重试 / 降级(OneBot 1200 等错误)保护
|
|
418
|
+
// - 统一的日志/追踪上下文
|
|
419
|
+
// - finalMessage 在缓存前已按 msg.censor 完成审查包裹,此处保持原样不重复包裹
|
|
420
|
+
// 为避免与原 feed 任务的 uid 去重冲突(队列按 subscribeId+uid+target 去重),
|
|
421
|
+
// 重新推送使用带 cache-pull 前缀与时间戳的唯一 uid。
|
|
422
|
+
const taskContent = {
|
|
423
|
+
message: message.finalMessage,
|
|
424
|
+
originalItem: undefined,
|
|
425
|
+
isDowngraded: false,
|
|
426
|
+
title: message.title,
|
|
427
|
+
description: message.content,
|
|
428
|
+
link: message.link,
|
|
429
|
+
pubDate: message.pubDate,
|
|
430
|
+
imageUrl: message.imageUrl,
|
|
431
|
+
};
|
|
432
|
+
await queueManager.addTask({
|
|
433
|
+
subscribeId: `cache-pull:${message.id}`,
|
|
434
|
+
rssId: message.rssId,
|
|
435
|
+
uid: `cache-pull:${message.id}:${Date.now()}`,
|
|
436
|
+
guildId,
|
|
437
|
+
platform,
|
|
438
|
+
content: taskContent,
|
|
439
|
+
});
|
|
440
|
+
return `✅ 已将消息(ID:${message.id})加入发送队列,即将重新推送`;
|
|
306
441
|
}
|
|
307
442
|
catch (error) {
|
|
308
443
|
const { id: guildId } = session.event.guild;
|
|
309
444
|
const { platform } = session.event;
|
|
310
|
-
|
|
311
|
-
...
|
|
445
|
+
logCacheError(config, error, 'cache pull error', {
|
|
446
|
+
...logContext,
|
|
447
|
+
realId,
|
|
312
448
|
guildId,
|
|
313
449
|
platform,
|
|
314
450
|
target: `${platform}:${guildId}`,
|
|
315
|
-
cachedMessageId:
|
|
316
|
-
rssId:
|
|
451
|
+
cachedMessageId: message?.id,
|
|
452
|
+
rssId: message?.rssId,
|
|
317
453
|
});
|
|
318
|
-
return
|
|
454
|
+
return `❌ 推送消息失败: ${error.message}`;
|
|
319
455
|
}
|
|
320
456
|
}
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// queue handlers(业务逻辑不变)
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
321
460
|
async function handleQueueStats(queueManager, config, logContext) {
|
|
322
461
|
try {
|
|
323
462
|
const stats = await queueManager.getStats();
|
|
324
463
|
const total = stats.pending + stats.retry + stats.failed + stats.success;
|
|
325
|
-
return
|
|
464
|
+
return `发送队列统计\n\n待发送: ${stats.pending}\n等待重试: ${stats.retry}\n❌ 发送失败: ${stats.failed}\n✅ 发送成功: ${stats.success}\n\n总计: ${total} 个任务`;
|
|
326
465
|
}
|
|
327
466
|
catch (error) {
|
|
328
|
-
|
|
329
|
-
return
|
|
467
|
+
logCacheError(config, error, 'queue stats error', logContext);
|
|
468
|
+
return `❌ 获取统计信息失败: ${error.message}`;
|
|
330
469
|
}
|
|
331
470
|
}
|
|
332
471
|
async function handleQueueRetry(queueManager, args, config, logContext) {
|
|
@@ -344,11 +483,11 @@ async function handleQueueRetry(queueManager, args, config, logContext) {
|
|
|
344
483
|
const count = await queueManager.retryFailedTasks(id);
|
|
345
484
|
return count > 0 ? `✅ 已重置任务 ${id}` : `❌ 未找到任务 ${id}`;
|
|
346
485
|
}
|
|
347
|
-
return '请指定任务ID或使用 --all 重试所有失败任务\n
|
|
486
|
+
return '请指定任务ID或使用 --all 重试所有失败任务\n用法: rsso.queue retry <id|--all>';
|
|
348
487
|
}
|
|
349
488
|
catch (error) {
|
|
350
|
-
|
|
351
|
-
return
|
|
489
|
+
logCacheError(config, error, 'queue retry error', { ...logContext, taskId: args[0] });
|
|
490
|
+
return `❌ 重试失败: ${error.message}`;
|
|
352
491
|
}
|
|
353
492
|
}
|
|
354
493
|
async function handleQueueCleanup(queueManager, args, config, logContext) {
|
|
@@ -361,25 +500,29 @@ async function handleQueueCleanup(queueManager, args, config, logContext) {
|
|
|
361
500
|
return `✅ 已清理 ${count} 个超过 ${hours} 小时的成功任务`;
|
|
362
501
|
}
|
|
363
502
|
catch (error) {
|
|
364
|
-
|
|
365
|
-
return
|
|
503
|
+
logCacheError(config, error, 'queue cleanup error', { ...logContext, hours: parseInt(args[0]) || 24 });
|
|
504
|
+
return `❌ 清理失败: ${error.message}`;
|
|
366
505
|
}
|
|
367
506
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// 辅助函数
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
/**
|
|
511
|
+
* 按真实数据库 ID 取缓存消息,并校验其归属当前群组(防跨群越权)。
|
|
512
|
+
* 即使攻击者猜到其他群的 ID,也会因 platform/guildId 不匹配而被拦截。
|
|
513
|
+
*/
|
|
514
|
+
async function resolveCacheMessageById(cmdCtx, realId) {
|
|
515
|
+
const { cache, platform, guildId } = cmdCtx;
|
|
516
|
+
const message = await cache.getMessage(realId);
|
|
517
|
+
if (!message)
|
|
518
|
+
return null;
|
|
519
|
+
// 归属校验:必须属于当前群组
|
|
520
|
+
if (message.platform !== platform || message.guildId !== guildId) {
|
|
521
|
+
return null;
|
|
379
522
|
}
|
|
380
|
-
return
|
|
523
|
+
return message;
|
|
381
524
|
}
|
|
382
|
-
function
|
|
525
|
+
function logCacheError(config, error, scope, context) {
|
|
383
526
|
const normalizedError = (0, error_handler_1.normalizeError)(error);
|
|
384
527
|
(0, logger_1.debug)(config, normalizedError, scope, 'error', context);
|
|
385
528
|
(0, error_tracker_1.trackError)(normalizedError, context);
|