@anyul/koishi-plugin-rss 5.0.0-beta → 5.0.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 CHANGED
@@ -661,8 +661,36 @@ npm run dev
661
661
 
662
662
  # 构建
663
663
  npm run build
664
+
665
+ # 运行测试
666
+ npm test
667
+
668
+ # 生成覆盖率报告
669
+ npm run test:coverage
670
+
671
+ # 监听模式(开发时使用)
672
+ npm run test:watch
664
673
  ```
665
674
 
675
+ ### 测试
676
+
677
+ 本项目拥有完善的测试套件,包含 165+ 个测试用例,覆盖核心功能模块。
678
+
679
+ **测试覆盖率**:
680
+ - 语句覆盖率: 90.83%
681
+ - 分支覆盖率: 74.86%
682
+ - 函数覆盖率: 97.43%
683
+ - 行覆盖率: 90.21%
684
+
685
+ **测试范围**:
686
+ - ✅ 工具函数测试(日期解析、URL处理、内容清理)
687
+ - ✅ HTTP 请求测试(RequestManager、createHttpFunction)
688
+ - ✅ 错误处理测试(友好错误消息、错误类型识别)
689
+ - ✅ 日志系统测试(debug输出、级别过滤)
690
+ - ✅ 集成测试(真实HTTP请求、代理配置)
691
+
692
+ 详见 [TESTING.md](./TESTING.md) 了解更多测试信息。
693
+
666
694
  ## 💬 致谢
667
695
 
668
696
  本项目基于以下优秀的开源项目:
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 命令错误处理中间件
3
+ * 提供统一的错误处理和用户友好的错误消息
4
+ */
5
+ import { Context } from 'koishi';
6
+ import { Config } from '../types';
7
+ /**
8
+ * 命令执行结果
9
+ */
10
+ export interface CommandResult {
11
+ success: boolean;
12
+ message: string;
13
+ error?: any;
14
+ }
15
+ /**
16
+ * 命令错误类型
17
+ */
18
+ export declare enum CommandErrorType {
19
+ PERMISSION_DENIED = "PERMISSION_DENIED",
20
+ INVALID_ARGUMENT = "INVALID_ARGUMENT",
21
+ NOT_FOUND = "NOT_FOUND",
22
+ ALREADY_EXISTS = "ALREADY_EXISTS",
23
+ NETWORK_ERROR = "NETWORK_ERROR",
24
+ INTERNAL_ERROR = "INTERNAL_ERROR"
25
+ }
26
+ /**
27
+ * 命令错误类
28
+ */
29
+ export declare class CommandError extends Error {
30
+ type: CommandErrorType;
31
+ details?: any;
32
+ constructor(type: CommandErrorType, message: string, details?: any);
33
+ }
34
+ /**
35
+ * 包装命令执行,提供统一的错误处理
36
+ */
37
+ export declare function executeCommand(ctx: Context, config: Config, operationName: string, handler: () => Promise<string>): Promise<string>;
38
+ /**
39
+ * 创建权限检查错误
40
+ */
41
+ export declare function permissionDenied(customMessage?: string): CommandError;
42
+ /**
43
+ * 创建参数错误
44
+ */
45
+ export declare function invalidArgument(message: string): CommandError;
46
+ /**
47
+ * 创建未找到错误
48
+ */
49
+ export declare function notFound(resource: string): CommandError;
50
+ /**
51
+ * 创建已存在错误
52
+ */
53
+ export declare function alreadyExists(resource: string): CommandError;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * 命令错误处理中间件
4
+ * 提供统一的错误处理和用户友好的错误消息
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.CommandError = exports.CommandErrorType = void 0;
8
+ exports.executeCommand = executeCommand;
9
+ exports.permissionDenied = permissionDenied;
10
+ exports.invalidArgument = invalidArgument;
11
+ exports.notFound = notFound;
12
+ exports.alreadyExists = alreadyExists;
13
+ const koishi_1 = require("koishi");
14
+ const error_handler_1 = require("../utils/error-handler");
15
+ const logger = new koishi_1.Logger('rss-owl-command');
16
+ /**
17
+ * 命令错误类型
18
+ */
19
+ var CommandErrorType;
20
+ (function (CommandErrorType) {
21
+ CommandErrorType["PERMISSION_DENIED"] = "PERMISSION_DENIED";
22
+ CommandErrorType["INVALID_ARGUMENT"] = "INVALID_ARGUMENT";
23
+ CommandErrorType["NOT_FOUND"] = "NOT_FOUND";
24
+ CommandErrorType["ALREADY_EXISTS"] = "ALREADY_EXISTS";
25
+ CommandErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
26
+ CommandErrorType["INTERNAL_ERROR"] = "INTERNAL_ERROR";
27
+ })(CommandErrorType || (exports.CommandErrorType = CommandErrorType = {}));
28
+ /**
29
+ * 命令错误类
30
+ */
31
+ class CommandError extends Error {
32
+ type;
33
+ details;
34
+ constructor(type, message, details) {
35
+ super(message);
36
+ this.type = type;
37
+ this.details = details;
38
+ this.name = 'CommandError';
39
+ }
40
+ }
41
+ exports.CommandError = CommandError;
42
+ /**
43
+ * 包装命令执行,提供统一的错误处理
44
+ */
45
+ async function executeCommand(ctx, config, operationName, handler) {
46
+ try {
47
+ return await handler();
48
+ }
49
+ catch (error) {
50
+ // 如果是 CommandError,使用自定义消息
51
+ if (error instanceof CommandError) {
52
+ logError(config, operationName, error);
53
+ return formatCommandError(error);
54
+ }
55
+ // 其他错误使用友好错误消息
56
+ logError(config, operationName, error);
57
+ const friendlyMessage = (0, error_handler_1.getFriendlyErrorMessage)(error, operationName);
58
+ return `${operationName}失败: ${friendlyMessage}`;
59
+ }
60
+ }
61
+ /**
62
+ * 记录命令错误
63
+ */
64
+ function logError(config, operation, error) {
65
+ if (config.debug === 'details' || config.debug === 'error') {
66
+ logger.error(`[${operation}]`, error);
67
+ }
68
+ }
69
+ /**
70
+ * 格式化命令错误消息
71
+ */
72
+ function formatCommandError(error) {
73
+ switch (error.type) {
74
+ case CommandErrorType.PERMISSION_DENIED:
75
+ return error.message;
76
+ case CommandErrorType.INVALID_ARGUMENT:
77
+ return `参数错误: ${error.message}`;
78
+ case CommandErrorType.NOT_FOUND:
79
+ return `未找到: ${error.message}`;
80
+ case CommandErrorType.ALREADY_EXISTS:
81
+ return `已存在: ${error.message}`;
82
+ case CommandErrorType.NETWORK_ERROR:
83
+ return `网络错误: ${error.message}`;
84
+ default:
85
+ return error.message || '操作失败,请稍后重试';
86
+ }
87
+ }
88
+ /**
89
+ * 创建权限检查错误
90
+ */
91
+ function permissionDenied(customMessage) {
92
+ return new CommandError(CommandErrorType.PERMISSION_DENIED, customMessage || '权限不足');
93
+ }
94
+ /**
95
+ * 创建参数错误
96
+ */
97
+ function invalidArgument(message) {
98
+ return new CommandError(CommandErrorType.INVALID_ARGUMENT, message);
99
+ }
100
+ /**
101
+ * 创建未找到错误
102
+ */
103
+ function notFound(resource) {
104
+ return new CommandError(CommandErrorType.NOT_FOUND, resource);
105
+ }
106
+ /**
107
+ * 创建已存在错误
108
+ */
109
+ function alreadyExists(resource) {
110
+ return new CommandError(CommandErrorType.ALREADY_EXISTS, resource);
111
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * RSS 订阅命令
3
+ * 处理 RSS 订阅的添加、删除、列表、关注等操作
4
+ */
5
+ import { Context } from 'koishi';
6
+ import { Config } from '../types';
7
+ import { RssItemProcessor } from '../core/item-processor';
8
+ /**
9
+ * 注册 RSS 订阅命令
10
+ */
11
+ export declare function registerRssSubscribeCommand(ctx: Context, config: Config, processor: RssItemProcessor, getRssDataLocal: (url: string, arg: any) => Promise<any[]>, parseRssItem: (item: any, arg: any, authorId: string | number) => Promise<string>, parseQuickUrlLocal: (url: string) => string, parsePubDateLocal: (pubDate: any) => Date): void;
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ /**
3
+ * RSS 订阅命令
4
+ * 处理 RSS 订阅的添加、删除、列表、关注等操作
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.registerRssSubscribeCommand = registerRssSubscribeCommand;
8
+ const constants_1 = require("../constants");
9
+ const feeder_1 = require("../core/feeder");
10
+ const common_1 = require("../utils/common");
11
+ const logger_1 = require("../utils/logger");
12
+ const utils_1 = require("./utils");
13
+ const error_handler_1 = require("./error-handler");
14
+ /**
15
+ * 注册 RSS 订阅命令
16
+ */
17
+ function registerRssSubscribeCommand(ctx, config, processor, getRssDataLocal, parseRssItem, parseQuickUrlLocal, parsePubDateLocal) {
18
+ const usage = 'RSS 订阅命令\n' +
19
+ '用法:\n' +
20
+ ' rsso <url> - 订阅 RSS\n' +
21
+ ' rsso -l - 查看订阅列表\n' +
22
+ ' rsso -r <id> - 删除订阅\n' +
23
+ ' rsso -p <id> - 拉取最新内容\n' +
24
+ ' rsso -f <id> - 关注订阅\n';
25
+ ctx.guild()
26
+ .command('rssowl <url:text>', '*订阅 RSS 链接*')
27
+ .alias('rsso')
28
+ .usage(usage)
29
+ .option('list', '-l [content] 查看订阅列表(详情)')
30
+ .option('remove', '-r <content> [订阅id|关键字] *删除订阅*')
31
+ .option('removeAll', '*删除全部订阅*')
32
+ .option('follow', '-f <content> [订阅id|关键字] 关注订阅,在该订阅更新时提醒你')
33
+ .option('followAll', '<content> [订阅id|关键字] **在该订阅更新时提醒所有人**')
34
+ .option('target', '<content> [群组id] **跨群订阅**')
35
+ .option('arg', '-a <content> 自定义配置')
36
+ .option('template', '-i <content> 消息模板[content(文字模板)|default(图片模板)],更多见readme')
37
+ .option('title', '-t <content> 自定义命名')
38
+ .option('pull', '-p <content> [订阅id|关键字]拉取订阅id最后更新')
39
+ .option('force', '强行写入')
40
+ .option('daily', '-d <content>')
41
+ .option('test', '-T 测试')
42
+ .option('quick', '-q [content] 查询快速订阅列表')
43
+ .example('rsso https://hub.slarker.me/qqorw')
44
+ .action(async ({ session, options }, url) => {
45
+ return (0, error_handler_1.executeCommand)(ctx, config, '订阅', () => handleRssCommand(ctx, config, processor, session, options, url, getRssDataLocal, parseRssItem, parseQuickUrlLocal, parsePubDateLocal));
46
+ });
47
+ }
48
+ /**
49
+ * 处理 RSS 订阅命令
50
+ */
51
+ async function handleRssCommand(ctx, config, processor, session, options, url, getRssDataLocal, parseRssItem, parseQuickUrlLocal, parsePubDateLocal) {
52
+ const { guildId, platform, authorId, authority } = (0, utils_1.extractSessionInfo)(session);
53
+ (0, logger_1.debug)(config, options, 'options', 'info');
54
+ (0, logger_1.debug)(config, `${platform}:${authorId}:${guildId}`, '', 'info');
55
+ // 处理快速订阅查询
56
+ if (options?.quick !== undefined) {
57
+ return handleQuickList(options.quick);
58
+ }
59
+ // 沙盒环境提示
60
+ if (platform.includes('sandbox') && !options.test && url) {
61
+ session.send('沙盒中无法推送更新,但RSS依然会被订阅,建议使用 -T 选项进行测试');
62
+ }
63
+ // 获取当前订阅列表
64
+ const rssList = await ctx.database.get('rssOwl', { platform, guildId });
65
+ // 处理列表选项
66
+ if (options?.list !== undefined) {
67
+ return handleList(rssList, options.list, parsePubDateLocal);
68
+ }
69
+ // 处理删除选项
70
+ if (options?.remove) {
71
+ return handleRemove(ctx, rssList, options.remove, authority);
72
+ }
73
+ // 处理删除全部选项
74
+ if (options?.removeAll) {
75
+ return handleRemoveAll(ctx, rssList, authority);
76
+ }
77
+ // 处理关注选项
78
+ if (options?.follow) {
79
+ return handleFollow(ctx, rssList, options.follow, authorId);
80
+ }
81
+ // 处理全员关注选项
82
+ if (options?.followAll) {
83
+ return handleFollowAll(ctx, rssList, options.followAll, authority);
84
+ }
85
+ // 处理拉取选项
86
+ if (options?.pull) {
87
+ return handlePull(ctx, config, rssList, options.pull, parseRssItem, getRssDataLocal, parseQuickUrlLocal, parsePubDateLocal, feeder_1.mixinArg);
88
+ }
89
+ // 处理订阅 URL
90
+ if (url) {
91
+ return handleSubscribe(ctx, config, session, rssList, url, options, platform, guildId, authorId, authority, getRssDataLocal, parseRssItem, parseQuickUrlLocal, parsePubDateLocal, feeder_1.formatArg);
92
+ }
93
+ return '用法: rsso <url> 或 rsso -l 查看订阅列表';
94
+ }
95
+ /**
96
+ * 处理快速订阅列表查询
97
+ */
98
+ function handleQuickList(quick) {
99
+ if (quick === '') {
100
+ return '输入 rsso -q [id] 查询详情\n' +
101
+ constants_1.quickList.map((v, i) => `${i + 1}.${v.name}`).join('\n');
102
+ }
103
+ const index = parseInt(quick) - 1;
104
+ if (index < 0 || index >= constants_1.quickList.length) {
105
+ return '无效的 ID';
106
+ }
107
+ const quickObj = constants_1.quickList[index];
108
+ return `${quickObj.name}\n${quickObj.detail}\n` +
109
+ `例:rsso -T ${quickObj.example}\n(${(0, common_1.parseQuickUrl)(quickObj.example, '', [])})`;
110
+ }
111
+ /**
112
+ * 处理列表查询
113
+ */
114
+ function handleList(rssList, listOption, parsePubDateLocal) {
115
+ (0, logger_1.debug)(config, rssList, 'rssList', 'info');
116
+ if (rssList.length === 0) {
117
+ return '当前没有任何订阅';
118
+ }
119
+ // 简单列表
120
+ if (listOption === '') {
121
+ return rssList.map((v, i) => `${i + 1}. ${v.title} [${v.id}]`).join('\n');
122
+ }
123
+ // 详细列表
124
+ const rssItem = (0, feeder_1.findRssItem)(rssList, listOption);
125
+ if (!rssItem) {
126
+ throw (0, error_handler_1.notFound)('未找到该订阅');
127
+ }
128
+ const lastPubDate = rssItem.lastPubDate
129
+ ? parsePubDateLocal(rssItem.lastPubDate).toLocaleString('zh-CN', { hour12: false })
130
+ : '未知';
131
+ return `标题: ${rssItem.title}\n` +
132
+ `链接: ${rssItem.url}\n` +
133
+ `更新时间: ${lastPubDate}`;
134
+ }
135
+ /**
136
+ * 处理删除订阅
137
+ */
138
+ async function handleRemove(ctx, rssList, identifier, authority) {
139
+ const authCheck = (0, utils_1.checkAuthority)(authority, ctx.config.basic.authority);
140
+ if (!authCheck.success) {
141
+ throw (0, error_handler_1.permissionDenied)();
142
+ }
143
+ const rssItem = (0, feeder_1.findRssItem)(rssList, identifier);
144
+ if (!rssItem) {
145
+ throw (0, error_handler_1.notFound)('未找到该订阅');
146
+ }
147
+ await ctx.database.remove('rssOwl', rssItem.id);
148
+ return '删除成功';
149
+ }
150
+ /**
151
+ * 处理删除全部订阅
152
+ */
153
+ async function handleRemoveAll(ctx, rssList, authority) {
154
+ const authCheck = (0, utils_1.checkAuthority)(authority, ctx.config.basic.authority);
155
+ if (!authCheck.success) {
156
+ throw (0, error_handler_1.permissionDenied)();
157
+ }
158
+ await ctx.database.remove('rssOwl', { platform, guildId });
159
+ return '删除成功';
160
+ }
161
+ /**
162
+ * 处理关注订阅
163
+ */
164
+ async function handleFollow(ctx, rssList, identifier, authorId) {
165
+ const rssItem = (0, feeder_1.findRssItem)(rssList, identifier);
166
+ if (!rssItem) {
167
+ throw (0, error_handler_1.notFound)('未找到该订阅');
168
+ }
169
+ if (!rssItem.followers) {
170
+ rssItem.followers = [];
171
+ }
172
+ if (rssItem.followers.includes(authorId)) {
173
+ return '已经关注过了';
174
+ }
175
+ rssItem.followers.push(authorId);
176
+ await ctx.database.set('rssOwl', { id: rssItem.id }, { followers: rssItem.followers });
177
+ return '关注成功';
178
+ }
179
+ /**
180
+ * 处理全员关注
181
+ */
182
+ async function handleFollowAll(ctx, rssList, identifier, authority) {
183
+ const authCheck = (0, utils_1.checkAuthority)(authority, ctx.config.basic.advancedAuthority);
184
+ if (!authCheck.success) {
185
+ throw (0, error_handler_1.permissionDenied)();
186
+ }
187
+ const rssItem = (0, feeder_1.findRssItem)(rssList, identifier);
188
+ if (!rssItem) {
189
+ throw (0, error_handler_1.notFound)('未找到该订阅');
190
+ }
191
+ if (!rssItem.followers) {
192
+ rssItem.followers = [];
193
+ }
194
+ rssItem.followers.push('all');
195
+ await ctx.database.set('rssOwl', { id: rssItem.id }, { followers: rssItem.followers });
196
+ return '关注成功';
197
+ }
198
+ /**
199
+ * 处理拉取最新内容
200
+ */
201
+ async function handlePull(ctx, config, rssList, identifier, parseRssItem, getRssDataLocal, parseQuickUrlLocal, parsePubDateLocal, mixinArg) {
202
+ const rssItem = (0, feeder_1.findRssItem)(rssList, identifier);
203
+ if (!rssItem) {
204
+ throw (0, error_handler_1.notFound)('未找到该订阅');
205
+ }
206
+ const arg = mixinArg(rssItem.arg || {});
207
+ const rssItemList = (await Promise.all(rssItem.url.split('|')
208
+ .map((url) => parseQuickUrlLocal(url))
209
+ .map(async (url) => await getRssDataLocal(url, arg)))).flat(1);
210
+ let itemArray = rssItemList.sort((a, b) => parsePubDateLocal(b.pubDate).getTime() - parsePubDateLocal(a.pubDate).getTime());
211
+ if (arg.reverse) {
212
+ itemArray = itemArray.reverse();
213
+ }
214
+ const maxItem = arg.forceLength || 1;
215
+ const messageList = await Promise.all(itemArray
216
+ .filter((v, i) => i < maxItem)
217
+ .map(i => parseRssItem(i, { ...rssItem, ...arg }, rssItem.author)));
218
+ return messageList.join('');
219
+ }
220
+ /**
221
+ * 处理新订阅
222
+ */
223
+ async function handleSubscribe(ctx, config, session, rssList, url, options, platform, guildId, authorId, authority, getRssDataLocal, parseRssItem, parseQuickUrlLocal, parsePubDateLocal, formatArg) {
224
+ // 检查是否已存在
225
+ if (rssList.find(i => i.url === url)) {
226
+ throw new Error('该订阅已存在');
227
+ }
228
+ const arg = formatArg(options);
229
+ let targetPlatform = platform;
230
+ let targetGuildId = guildId;
231
+ // 处理跨群订阅
232
+ if (options?.target) {
233
+ const authCheck = (0, utils_1.checkAuthority)(authority, config.basic.advancedAuthority);
234
+ if (!authCheck.success) {
235
+ throw (0, error_handler_1.permissionDenied)();
236
+ }
237
+ const target = (0, utils_1.parseTarget)(options.target);
238
+ if (!target) {
239
+ throw new Error('请输入正确的群号,格式为 platform:guildId 或 platform:guildId');
240
+ }
241
+ targetPlatform = target.platform;
242
+ targetGuildId = target.guildId;
243
+ }
244
+ let title = options?.title || '';
245
+ url = parseQuickUrlLocal(url);
246
+ url = (0, common_1.ensureUrlProtocol)(url);
247
+ // 获取 RSS 数据
248
+ const rssItemList = await getRssDataLocal(url, arg);
249
+ // 测试模式
250
+ if (options.test) {
251
+ return handleTestMode(rssItemList, arg, title, config, parseRssItem, authorId);
252
+ }
253
+ // 获取标题
254
+ if (!title) {
255
+ title = rssItemList[0]?.rss?.channel?.title;
256
+ if (!title) {
257
+ throw new Error('无法获取标题,请使用 -t 指定标题');
258
+ }
259
+ }
260
+ // 检查权限(force 模式)
261
+ if (options.force && authority < config.basic.authority) {
262
+ throw (0, error_handler_1.permissionDenied)();
263
+ }
264
+ // 检查去重
265
+ if (config.basic.urlDeduplication && rssList.find(i => i.rssId === title)) {
266
+ throw new Error(`订阅已存在: ${title}`);
267
+ }
268
+ // 创建订阅项
269
+ const lastPubDate = parsePubDateLocal(rssItemList[0]?.pubDate);
270
+ const rssItem = {
271
+ url,
272
+ platform: targetPlatform,
273
+ guildId: targetGuildId,
274
+ author: authorId,
275
+ rssId: rssItemList[0]?.rss?.channel?.title || title,
276
+ arg,
277
+ title,
278
+ lastPubDate,
279
+ lastContent: [],
280
+ followers: [],
281
+ firstime: lastPubDate
282
+ };
283
+ await ctx.database.create('rssOwl', rssItem);
284
+ // 首次加载
285
+ if (config.basic.firstLoad && arg.firstLoad !== false && rssItemList.length > 0) {
286
+ await loadInitialItems(ctx, rssItemList, rssItem, targetPlatform, targetGuildId, parsePubDateLocal, parseRssItem);
287
+ }
288
+ return `订阅成功: ${title}`;
289
+ }
290
+ /**
291
+ * 处理测试模式
292
+ */
293
+ async function handleTestMode(rssItemList, arg, title, config, parseRssItem, authorId) {
294
+ const testItem = rssItemList[0];
295
+ if (!testItem) {
296
+ throw new Error('未获取到数据');
297
+ }
298
+ const testArg = {
299
+ ...arg,
300
+ url: title || testItem.rss.channel.title,
301
+ title: title || testItem.rss.channel.title
302
+ };
303
+ if (!testArg.template) {
304
+ testArg.template = config.basic.defaultTemplate;
305
+ }
306
+ const msg = await parseRssItem(testItem, testArg, authorId);
307
+ return msg;
308
+ }
309
+ /**
310
+ * 加载初始条目
311
+ */
312
+ async function loadInitialItems(ctx, rssItemList, rssItem, targetPlatform, targetGuildId, parsePubDateLocal, parseRssItem) {
313
+ let itemArray = rssItemList.sort((a, b) => parsePubDateLocal(b.pubDate).getTime() - parsePubDateLocal(a.pubDate).getTime());
314
+ if (rssItem.arg.reverse) {
315
+ itemArray = itemArray.reverse();
316
+ }
317
+ const maxItem = rssItem.arg.forceLength || 1;
318
+ const messageList = await Promise.all(itemArray
319
+ .filter((v, i) => i < maxItem)
320
+ .map(i => parseRssItem(i, { ...rssItem, ...rssItem.arg }, rssItem.author)));
321
+ const message = messageList.join('');
322
+ await ctx.broadcast([`${targetPlatform}:${targetGuildId}`], message);
323
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 命令辅助函数
3
+ * 提供命令共享的工具函数
4
+ */
5
+ import { Context, Session } from 'koishi';
6
+ import { Config } from '../types';
7
+ /**
8
+ * 命令执行上下文
9
+ */
10
+ export interface CommandContext {
11
+ ctx: Context;
12
+ session: Session;
13
+ config: Config;
14
+ }
15
+ /**
16
+ * 会话信息提取
17
+ */
18
+ export interface SessionInfo {
19
+ guildId: string;
20
+ platform: string;
21
+ authorId: string;
22
+ authority: number;
23
+ }
24
+ /**
25
+ * 从会话中提取信息
26
+ */
27
+ export declare function extractSessionInfo(session: Session): SessionInfo;
28
+ /**
29
+ * 命令错误处理包装器
30
+ * 统一处理命令执行中的错误
31
+ */
32
+ export declare function withCommandErrorHandling(config: Config, operation: string, handler: () => Promise<string>): Promise<string>;
33
+ /**
34
+ * 权限检查辅助函数
35
+ */
36
+ export declare function checkAuthority(authority: number, required: number, customMessage?: string): {
37
+ success: boolean;
38
+ message?: string;
39
+ };
40
+ /**
41
+ * 解析目标群组
42
+ */
43
+ export declare function parseTarget(target: string): {
44
+ platform: string;
45
+ guildId: string;
46
+ } | null;
47
+ /**
48
+ * 验证 URL 格式
49
+ */
50
+ export declare function isValidUrl(url: string): boolean;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * 命令辅助函数
4
+ * 提供命令共享的工具函数
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.extractSessionInfo = extractSessionInfo;
8
+ exports.withCommandErrorHandling = withCommandErrorHandling;
9
+ exports.checkAuthority = checkAuthority;
10
+ exports.parseTarget = parseTarget;
11
+ exports.isValidUrl = isValidUrl;
12
+ const error_handler_1 = require("../utils/error-handler");
13
+ const logger_1 = require("../utils/logger");
14
+ /**
15
+ * 从会话中提取信息
16
+ */
17
+ function extractSessionInfo(session) {
18
+ const { id: guildId } = session.event.guild;
19
+ const { platform } = session.event;
20
+ const { id: authorId } = session.event.user;
21
+ const { authority } = session.user;
22
+ return { guildId, platform, authorId, authority };
23
+ }
24
+ /**
25
+ * 命令错误处理包装器
26
+ * 统一处理命令执行中的错误
27
+ */
28
+ function withCommandErrorHandling(config, operation, handler) {
29
+ return handler().catch((error) => {
30
+ (0, logger_1.debug)(config, error, `${operation} error`, 'error');
31
+ return Promise.resolve(`${operation}失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, operation)}`);
32
+ });
33
+ }
34
+ /**
35
+ * 权限检查辅助函数
36
+ */
37
+ function checkAuthority(authority, required, customMessage) {
38
+ if (authority >= required) {
39
+ return { success: true };
40
+ }
41
+ return {
42
+ success: false,
43
+ message: customMessage || '权限不足'
44
+ };
45
+ }
46
+ /**
47
+ * 解析目标群组
48
+ */
49
+ function parseTarget(target) {
50
+ const parts = target.split(/[::]/);
51
+ if (parts.length !== 2) {
52
+ return null;
53
+ }
54
+ return {
55
+ platform: parts[0],
56
+ guildId: parts[1]
57
+ };
58
+ }
59
+ /**
60
+ * 验证 URL 格式
61
+ */
62
+ function isValidUrl(url) {
63
+ try {
64
+ new URL(url);
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
package/lib/config.js CHANGED
@@ -24,6 +24,8 @@ exports.Config = koishi_1.Schema.object({
24
24
  autoSplitImage: koishi_1.Schema.boolean().description('垂直拆分大尺寸图片,解决部分适配器发不出长图的问题').default(true),
25
25
  cacheDir: koishi_1.Schema.string().description('File模式时使用的缓存路径').default('data/cache/rssOwl'),
26
26
  replaceDir: koishi_1.Schema.string().description('缓存替换路径,仅在使用docker部署时需要设置').default(''),
27
+ maxImageSize: koishi_1.Schema.number().description('图片最大文件大小限制(MB),超出限制的图片将被跳过').default(30),
28
+ maxVideoSize: koishi_1.Schema.number().description('视频最大文件大小限制(MB),超出限制的视频将被跳过').default(30),
27
29
  }).description('基础设置'),
28
30
  template: koishi_1.Schema.object({
29
31
  bodyWidth: koishi_1.Schema.number().description('puppeteer图片的宽度(px),较低的值可能导致排版错误,仅在非custom的模板生效').default(600),
@@ -92,6 +94,18 @@ exports.Config = koishi_1.Schema.object({
92
94
  maxInputLength: koishi_1.Schema.number().description('发送给 AI 的最大字数限制').default(2000),
93
95
  timeout: koishi_1.Schema.number().description('AI 请求超时时间(毫秒)').default(30000),
94
96
  }).description('AI 摘要设置'),
97
+ cache: koishi_1.Schema.object({
98
+ enabled: koishi_1.Schema.boolean().description('启用消息缓存').default(true),
99
+ maxSize: koishi_1.Schema.number().description('最大缓存消息条数').default(100),
100
+ }).description('消息缓存设置'),
95
101
  // customUrlEnable:Schema.boolean().description('开发中:允许使用自定义规则对网页进行提取,用于对非RSS链接抓取').default(false).experimental(),
96
- debug: koishi_1.Schema.union(["disable", "error", "info", "details"]).default("disable"),
102
+ debug: koishi_1.Schema.union(["disable", "error", "info", "details"]).default("disable").description('调试级别'),
103
+ logging: koishi_1.Schema.object({
104
+ structured: koishi_1.Schema.boolean().description('启用结构化日志(JSON格式)').default(false),
105
+ includeTimestamp: koishi_1.Schema.boolean().description('包含时间戳').default(true),
106
+ includeLevel: koishi_1.Schema.boolean().description('包含日志级别').default(true),
107
+ includeModule: koishi_1.Schema.boolean().description('包含模块名').default(true),
108
+ includeContext: koishi_1.Schema.boolean().description('包含额外上下文信息').default(false),
109
+ contextFields: koishi_1.Schema.array(koishi_1.Schema.string()).description('要包含的上下文字段(如 guildId, platform 等)').default([]),
110
+ }).description('日志设置'),
97
111
  });