@anyul/koishi-plugin-rss 5.2.2 → 5.2.4
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 +92 -37
- package/lib/commands/error-handler.js +13 -4
- package/lib/commands/index.d.ts +20 -1
- package/lib/commands/index.js +394 -2
- package/lib/commands/runtime.d.ts +17 -0
- package/lib/commands/runtime.js +27 -0
- package/lib/commands/subscription-create.d.ts +23 -0
- package/lib/commands/subscription-create.js +145 -0
- package/lib/commands/subscription-edit.d.ts +7 -0
- package/lib/commands/subscription-edit.js +177 -0
- package/lib/commands/subscription-management.d.ts +12 -0
- package/lib/commands/subscription-management.js +176 -0
- package/lib/commands/utils.d.ts +13 -1
- package/lib/commands/utils.js +43 -2
- package/lib/commands/web-monitor.d.ts +15 -0
- package/lib/commands/web-monitor.js +222 -0
- package/lib/config.js +25 -0
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +46 -83
- package/lib/core/ai-cache.d.ts +27 -0
- package/lib/core/ai-cache.js +169 -0
- package/lib/core/ai-client.d.ts +12 -0
- package/lib/core/ai-client.js +65 -0
- package/lib/core/ai-selector.d.ts +2 -0
- package/lib/core/ai-selector.js +80 -0
- package/lib/core/ai-summary.d.ts +10 -0
- package/lib/core/ai-summary.js +73 -0
- package/lib/core/ai-utils.d.ts +10 -0
- package/lib/core/ai-utils.js +104 -0
- package/lib/core/ai.d.ts +3 -77
- package/lib/core/ai.js +13 -455
- package/lib/core/feeder-arg.d.ts +17 -0
- package/lib/core/feeder-arg.js +234 -0
- package/lib/core/feeder-runtime.d.ts +96 -0
- package/lib/core/feeder-runtime.js +233 -0
- package/lib/core/feeder.d.ts +4 -6
- package/lib/core/feeder.js +120 -304
- package/lib/core/item-processor-runtime.d.ts +46 -0
- package/lib/core/item-processor-runtime.js +215 -0
- package/lib/core/item-processor-template.d.ts +16 -0
- package/lib/core/item-processor-template.js +158 -0
- package/lib/core/item-processor.d.ts +1 -10
- package/lib/core/item-processor.js +48 -393
- package/lib/core/notification-queue-retry.d.ts +25 -0
- package/lib/core/notification-queue-retry.js +78 -0
- package/lib/core/notification-queue-sender.d.ts +20 -0
- package/lib/core/notification-queue-sender.js +118 -0
- package/lib/core/notification-queue-store.d.ts +19 -0
- package/lib/core/notification-queue-store.js +137 -0
- package/lib/core/notification-queue-types.d.ts +49 -0
- package/lib/core/notification-queue-types.js +2 -0
- package/lib/core/notification-queue.d.ts +13 -72
- package/lib/core/notification-queue.js +132 -262
- package/lib/core/parser.js +12 -0
- package/lib/core/renderer.d.ts +15 -0
- package/lib/core/renderer.js +91 -23
- package/lib/core/search-format.d.ts +3 -0
- package/lib/core/search-format.js +36 -0
- package/lib/core/search-providers.d.ts +13 -0
- package/lib/core/search-providers.js +175 -0
- package/lib/core/search-rotation.d.ts +4 -0
- package/lib/core/search-rotation.js +55 -0
- package/lib/core/search-service.d.ts +3 -0
- package/lib/core/search-service.js +100 -0
- package/lib/core/search-types.d.ts +39 -0
- package/lib/core/search-types.js +2 -0
- package/lib/core/search.d.ts +4 -101
- package/lib/core/search.js +10 -508
- package/lib/index.js +50 -1160
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +51 -6
- package/lib/utils/common.js +52 -3
- package/lib/utils/error-handler.d.ts +8 -0
- package/lib/utils/error-handler.js +27 -0
- package/lib/utils/error-tracker.js +24 -8
- package/lib/utils/fetcher.js +68 -9
- package/lib/utils/legacy-config.d.ts +12 -0
- package/lib/utils/legacy-config.js +56 -0
- package/lib/utils/logger.d.ts +4 -2
- package/lib/utils/logger.js +193 -34
- package/lib/utils/media.js +3 -6
- package/lib/utils/proxy.d.ts +3 -0
- package/lib/utils/proxy.js +14 -0
- package/lib/utils/sanitizer.d.ts +58 -0
- package/lib/utils/sanitizer.js +227 -0
- package/lib/utils/security.d.ts +75 -0
- package/lib/utils/security.js +312 -0
- package/lib/utils/structured-logger.d.ts +7 -3
- package/lib/utils/structured-logger.js +29 -39
- package/package.json +2 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSubscriptionEditCommand = registerSubscriptionEditCommand;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
function registerSubscriptionEditCommand(deps) {
|
|
6
|
+
deps.ctx.guild()
|
|
7
|
+
.command('rssowl.edit <id:number>', '修改订阅配置')
|
|
8
|
+
.alias('rsso.edit')
|
|
9
|
+
.usage(`修改订阅的配置项
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
rsso.edit 1 -t "新标题" - 修改标题
|
|
13
|
+
rsso.edit 1 -i content - 修改模板
|
|
14
|
+
rsso.edit 1 -u https://... - 修改URL
|
|
15
|
+
rsso.edit 1 -s ".item" - 修改选择器(HTML监控)
|
|
16
|
+
rsso.edit 1 --target onebot:123 - 修改推送目标(高级权限)
|
|
17
|
+
rsso.edit 1 --target "onebot:123,telegram:456" - 多个推送目标(高级权限)⭐
|
|
18
|
+
rsso.edit 1 -t "新标题" --test - 测试修改(不保存)
|
|
19
|
+
|
|
20
|
+
示例:
|
|
21
|
+
rsso.edit 1 -t "我的订阅"
|
|
22
|
+
rsso.edit 1 -i custom
|
|
23
|
+
rsso.edit 1 --target onebot:123456
|
|
24
|
+
rsso.edit 1 --target "onebot:123,telegram:456"
|
|
25
|
+
|
|
26
|
+
💡 提示:
|
|
27
|
+
- 使用列表序号(1, 2, 3...)而不是数据库ID
|
|
28
|
+
- 推送目标格式: platform:guildId
|
|
29
|
+
- 多个目标用逗号分隔或多次使用 --target
|
|
30
|
+
`)
|
|
31
|
+
.option('title', '-t <title> 修改标题', { type: 'string' })
|
|
32
|
+
.option('url', '-u <url> 修改URL', { type: 'string' })
|
|
33
|
+
.option('template', '-i <template> 修改模板', { type: 'string' })
|
|
34
|
+
.option('selector', '-s <selector> 修改选择器(HTML监控)', { type: 'string' })
|
|
35
|
+
.option('target', '--target <target> 修改推送目标(高级权限)')
|
|
36
|
+
.option('test', '--test 测试修改(不保存)')
|
|
37
|
+
.example('rsso.edit 1 -t "新标题"')
|
|
38
|
+
.action(async ({ session, options }, id) => {
|
|
39
|
+
const { guildId, platform, authority } = (0, utils_1.extractSessionInfo)(session);
|
|
40
|
+
const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
|
|
41
|
+
const listIndex = id - 1;
|
|
42
|
+
if (listIndex < 0 || listIndex >= rssList.length) {
|
|
43
|
+
return `❌ 序号 ${id} 不存在\n当前共有 ${rssList.length} 个订阅\n\n使用 rsso.list 查看完整列表`;
|
|
44
|
+
}
|
|
45
|
+
const rssItem = rssList[listIndex];
|
|
46
|
+
const baseAuthorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.authority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.authority} 或以上`);
|
|
47
|
+
if (!baseAuthorityCheck.success)
|
|
48
|
+
return baseAuthorityCheck.message;
|
|
49
|
+
if (options.target) {
|
|
50
|
+
const targetAuthorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.advancedAuthority, `❌ 修改推送目标需要高级权限\n当前权限: ${authority},需要权限: ${deps.config.basic.advancedAuthority} 或以上`);
|
|
51
|
+
if (!targetAuthorityCheck.success)
|
|
52
|
+
return targetAuthorityCheck.message;
|
|
53
|
+
}
|
|
54
|
+
const hasChanges = options.title || options.url || options.template || options.selector || options.target;
|
|
55
|
+
if (!hasChanges) {
|
|
56
|
+
return '请指定要修改的内容\n可用选项: -t (标题), -u (URL), -i (模板), -s (选择器), --target (推送目标)\n使用 --help 查看详细帮助';
|
|
57
|
+
}
|
|
58
|
+
const parseResult = (0, utils_1.parseTargets)(options.target);
|
|
59
|
+
if (parseResult.invalidTarget) {
|
|
60
|
+
return `❌ 推送目标格式错误: "${parseResult.invalidTarget}"\n正确格式: platform:guildId\n示例: onebot:123456\n\n支持多个目标,用逗号分隔:\n --target "onebot:123,telegram:456"`;
|
|
61
|
+
}
|
|
62
|
+
if (options.test) {
|
|
63
|
+
return buildEditPreview(rssItem, id, options, parseResult.targets);
|
|
64
|
+
}
|
|
65
|
+
return saveSubscriptionChanges(deps.ctx, rssItem, id, options, parseResult.targets);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function buildEditPreview(rssItem, id, options, parsedTargets) {
|
|
69
|
+
let testOutput = `📝 修改预览 [序号:${id} | ID:${rssItem.id}]\n\n`;
|
|
70
|
+
testOutput += `当前标题: ${rssItem.title}\n`;
|
|
71
|
+
testOutput += `当前URL: ${rssItem.url}\n`;
|
|
72
|
+
testOutput += `当前推送目标: ${rssItem.platform}:${rssItem.guildId}\n`;
|
|
73
|
+
if (options.title)
|
|
74
|
+
testOutput += `→ 新标题: ${options.title}\n`;
|
|
75
|
+
if (options.url)
|
|
76
|
+
testOutput += `→ 新URL: ${options.url}\n`;
|
|
77
|
+
if (options.template)
|
|
78
|
+
testOutput += `→ 新模板: ${options.template}\n`;
|
|
79
|
+
if (options.selector)
|
|
80
|
+
testOutput += `→ 新选择器: ${options.selector}\n`;
|
|
81
|
+
if (parsedTargets.length > 0) {
|
|
82
|
+
if (parsedTargets.length === 1) {
|
|
83
|
+
testOutput += `→ 新推送目标: ${parsedTargets[0]}\n`;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
testOutput += `→ 新推送目标:\n ${parsedTargets.join('\n ')}\n`;
|
|
87
|
+
testOutput += '\n⚠️ 注意: 多个推送目标会创建多个订阅记录\n每个目标会复制当前的订阅配置\n';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
testOutput += '\n⚠️ 测试模式:不会保存修改\n去掉 --test 选项保存更改';
|
|
91
|
+
return testOutput;
|
|
92
|
+
}
|
|
93
|
+
async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets) {
|
|
94
|
+
const updates = {};
|
|
95
|
+
if (options.title)
|
|
96
|
+
updates.title = options.title;
|
|
97
|
+
if (options.url)
|
|
98
|
+
updates.url = options.url;
|
|
99
|
+
if (options.template) {
|
|
100
|
+
if (!rssItem.arg)
|
|
101
|
+
rssItem.arg = {};
|
|
102
|
+
rssItem.arg.template = options.template;
|
|
103
|
+
updates.arg = rssItem.arg;
|
|
104
|
+
}
|
|
105
|
+
if (options.selector) {
|
|
106
|
+
if (!rssItem.arg)
|
|
107
|
+
rssItem.arg = {};
|
|
108
|
+
rssItem.arg.selector = options.selector;
|
|
109
|
+
updates.arg = rssItem.arg;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
if (parsedTargets.length > 0) {
|
|
113
|
+
if (parsedTargets.length === 1) {
|
|
114
|
+
const [newPlatform, newGuildId] = parsedTargets[0].split(/[::]/);
|
|
115
|
+
updates.platform = newPlatform;
|
|
116
|
+
updates.guildId = newGuildId;
|
|
117
|
+
await ctx.database.set('rssOwl', rssItem.id, updates);
|
|
118
|
+
let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
|
|
119
|
+
if (options.title)
|
|
120
|
+
result += `标题: ${rssItem.title} → ${options.title}\n`;
|
|
121
|
+
if (options.url)
|
|
122
|
+
result += `URL: ${rssItem.url} → ${options.url}\n`;
|
|
123
|
+
if (options.template)
|
|
124
|
+
result += `模板: ${rssItem.arg?.template || 'default'} → ${options.template}\n`;
|
|
125
|
+
if (options.selector)
|
|
126
|
+
result += `选择器: ${rssItem.arg?.selector || '无'} → ${options.selector}\n`;
|
|
127
|
+
result += `推送目标: ${rssItem.platform}:${rssItem.guildId} → ${parsedTargets[0]}\n`;
|
|
128
|
+
return result.trim();
|
|
129
|
+
}
|
|
130
|
+
const originalTarget = `${rssItem.platform}:${rssItem.guildId}`;
|
|
131
|
+
let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
|
|
132
|
+
result += `已创建 ${parsedTargets.length} 个推送目标:\n\n`;
|
|
133
|
+
result += `1️⃣ 原订阅 (本群): ${originalTarget}\n`;
|
|
134
|
+
await ctx.database.set('rssOwl', rssItem.id, updates);
|
|
135
|
+
for (let i = 0; i < parsedTargets.length; i++) {
|
|
136
|
+
const [newPlatform, newGuildId] = parsedTargets[i].split(/[::]/);
|
|
137
|
+
const existing = await ctx.database.get('rssOwl', {
|
|
138
|
+
platform: newPlatform,
|
|
139
|
+
guildId: newGuildId,
|
|
140
|
+
url: rssItem.url
|
|
141
|
+
});
|
|
142
|
+
if (existing.length > 0) {
|
|
143
|
+
result += `${i + 2}. ⚠️ ${newPlatform}:${newGuildId} (订阅已存在,已跳过)\n`;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const newSubscription = {
|
|
147
|
+
...rssItem,
|
|
148
|
+
id: undefined,
|
|
149
|
+
platform: newPlatform,
|
|
150
|
+
guildId: newGuildId,
|
|
151
|
+
};
|
|
152
|
+
const created = await ctx.database.create('rssOwl', newSubscription);
|
|
153
|
+
result += `${i + 2}. ✅ ${newPlatform}:${newGuildId} (新订阅ID: ${created.id})\n`;
|
|
154
|
+
}
|
|
155
|
+
if (options.title)
|
|
156
|
+
result += `\n标题: ${rssItem.title} → ${options.title}\n`;
|
|
157
|
+
if (options.url)
|
|
158
|
+
result += `URL: ${rssItem.url} → ${options.url}\n`;
|
|
159
|
+
result += '\n💡 提示: 可以使用 rsso.list 查看所有订阅';
|
|
160
|
+
return result.trim();
|
|
161
|
+
}
|
|
162
|
+
await ctx.database.set('rssOwl', rssItem.id, updates);
|
|
163
|
+
let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
|
|
164
|
+
if (options.title)
|
|
165
|
+
result += `标题: ${rssItem.title} → ${options.title}\n`;
|
|
166
|
+
if (options.url)
|
|
167
|
+
result += `URL: ${rssItem.url} → ${options.url}\n`;
|
|
168
|
+
if (options.template)
|
|
169
|
+
result += `模板: ${rssItem.arg?.template || 'default'} → ${options.template}\n`;
|
|
170
|
+
if (options.selector)
|
|
171
|
+
result += `选择器: ${rssItem.arg?.selector || '无'} → ${options.selector}\n`;
|
|
172
|
+
return result.trim();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return `更新失败: ${error.message}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import type { Config } from '../types';
|
|
3
|
+
export interface SubscriptionCommandDeps {
|
|
4
|
+
ctx: Context;
|
|
5
|
+
config: Config;
|
|
6
|
+
parsePubDate: (pubDate: any) => Date;
|
|
7
|
+
parseQuickUrl: (url: string) => string;
|
|
8
|
+
getRssData: (url: string, arg: any) => Promise<any[]>;
|
|
9
|
+
parseRssItem: (item: any, arg: any, authorId: string | number) => Promise<string>;
|
|
10
|
+
mixinArg: (arg: any) => any;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerSubscriptionManagementCommands(deps: SubscriptionCommandDeps): void;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSubscriptionManagementCommands = registerSubscriptionManagementCommands;
|
|
4
|
+
const error_handler_1 = require("../utils/error-handler");
|
|
5
|
+
const error_tracker_1 = require("../utils/error-tracker");
|
|
6
|
+
const logger_1 = require("../utils/logger");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
function registerSubscriptionManagementCommands(deps) {
|
|
9
|
+
registerListCommand(deps);
|
|
10
|
+
registerRemoveCommand(deps);
|
|
11
|
+
registerPullCommand(deps);
|
|
12
|
+
registerFollowCommand(deps);
|
|
13
|
+
}
|
|
14
|
+
function registerListCommand(deps) {
|
|
15
|
+
deps.ctx.guild()
|
|
16
|
+
.command('rssowl.list [id:number]', '查看订阅列表')
|
|
17
|
+
.alias('rsso.list')
|
|
18
|
+
.usage(`查看订阅列表
|
|
19
|
+
|
|
20
|
+
用法:
|
|
21
|
+
rsso.list - 查看所有订阅
|
|
22
|
+
rsso.list 1 - 查看订阅 #1 的详情(使用列表序号)
|
|
23
|
+
`)
|
|
24
|
+
.example('rsso.list')
|
|
25
|
+
.action(async ({ session }, id) => {
|
|
26
|
+
const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
|
|
27
|
+
const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
|
|
28
|
+
if (id === undefined) {
|
|
29
|
+
if (rssList.length === 0)
|
|
30
|
+
return '当前没有任何订阅';
|
|
31
|
+
return rssList.map((item, index) => {
|
|
32
|
+
const isCrossGroup = item.platform !== platform || item.guildId !== guildId;
|
|
33
|
+
return `${index + 1}. ${item.title}${isCrossGroup ? ' [跨群]' : ''} [ID:${item.id}]`;
|
|
34
|
+
}).join('\n');
|
|
35
|
+
}
|
|
36
|
+
const rssItem = getSubscriptionByIndex(rssList, id);
|
|
37
|
+
if (!rssItem)
|
|
38
|
+
return getSubscriptionNotFoundMessage(id, rssList.length);
|
|
39
|
+
const followers = rssItem.followers?.length > 0 ? rssItem.followers.join(', ') : '无';
|
|
40
|
+
const pushTarget = `${rssItem.platform}:${rssItem.guildId}`;
|
|
41
|
+
const isCrossGroup = rssItem.platform !== platform || rssItem.guildId !== guildId;
|
|
42
|
+
const targetInfo = isCrossGroup
|
|
43
|
+
? `📤 推送目标: ${pushTarget} (跨群订阅)`
|
|
44
|
+
: `📤 推送目标: ${pushTarget} (本群)`;
|
|
45
|
+
return `📰 订阅详情 [序号:${id} | ID:${rssItem.id}]
|
|
46
|
+
标题: ${rssItem.title}
|
|
47
|
+
链接: ${rssItem.url}
|
|
48
|
+
类型: ${rssItem.arg?.type || 'RSS'}
|
|
49
|
+
模板: ${rssItem.arg?.template || deps.config.basic.defaultTemplate}
|
|
50
|
+
${targetInfo}
|
|
51
|
+
更新时间: ${rssItem.lastPubDate ? deps.parsePubDate(rssItem.lastPubDate).toLocaleString('zh-CN', { hour12: false }) : '未知'}
|
|
52
|
+
关注者: ${followers}`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function registerRemoveCommand(deps) {
|
|
56
|
+
deps.ctx.guild()
|
|
57
|
+
.command('rssowl.remove <id:number>', '删除订阅')
|
|
58
|
+
.alias('rsso.remove')
|
|
59
|
+
.usage(`删除订阅
|
|
60
|
+
|
|
61
|
+
用法:
|
|
62
|
+
rsso.remove 1 - 删除订阅 #1(使用列表序号)
|
|
63
|
+
rsso.remove --all - 删除全部订阅(需要权限)
|
|
64
|
+
`)
|
|
65
|
+
.option('all', '--all 删除全部订阅')
|
|
66
|
+
.example('rsso.remove 1')
|
|
67
|
+
.action(async ({ session, options }, id) => {
|
|
68
|
+
const { guildId, platform, authority } = (0, utils_1.extractSessionInfo)(session);
|
|
69
|
+
if (options.all) {
|
|
70
|
+
const authorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.authority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.authority} 或以上`);
|
|
71
|
+
if (!authorityCheck.success)
|
|
72
|
+
return authorityCheck.message;
|
|
73
|
+
await deps.ctx.database.remove('rssOwl', { platform, guildId });
|
|
74
|
+
return '✅ 已删除全部订阅';
|
|
75
|
+
}
|
|
76
|
+
const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
|
|
77
|
+
const rssItem = getSubscriptionByIndex(rssList, id);
|
|
78
|
+
if (!rssItem)
|
|
79
|
+
return getSubscriptionNotFoundMessage(id, rssList.length);
|
|
80
|
+
await deps.ctx.database.remove('rssOwl', { id: rssItem.id });
|
|
81
|
+
return `✅ 已删除订阅: ${rssItem.title}`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function registerPullCommand(deps) {
|
|
85
|
+
deps.ctx.guild()
|
|
86
|
+
.command('rssowl.pull <id:number>', '拉取订阅最新内容')
|
|
87
|
+
.alias('rsso.pull')
|
|
88
|
+
.usage(`拉取订阅最新内容
|
|
89
|
+
|
|
90
|
+
用法:
|
|
91
|
+
rsso.pull 1 - 拉取订阅 #1 的最新更新(使用列表序号)
|
|
92
|
+
`)
|
|
93
|
+
.example('rsso.pull 1')
|
|
94
|
+
.action(async ({ session }, id) => {
|
|
95
|
+
const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
|
|
96
|
+
const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
|
|
97
|
+
const rssItem = getSubscriptionByIndex(rssList, id);
|
|
98
|
+
if (!rssItem)
|
|
99
|
+
return getSubscriptionNotFoundMessage(id, rssList.length);
|
|
100
|
+
const logContext = {
|
|
101
|
+
...(0, utils_1.buildCommandLogContext)(session, 'rsso.pull', 'pull'),
|
|
102
|
+
subscriptionIndex: id,
|
|
103
|
+
subscribeId: String(rssItem.id),
|
|
104
|
+
rssId: rssItem.rssId || rssItem.title,
|
|
105
|
+
url: rssItem.url,
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
const arg = deps.mixinArg(rssItem.arg || {});
|
|
109
|
+
const rssItemList = (await Promise.all(String(rssItem.url).split('|')
|
|
110
|
+
.map(url => deps.parseQuickUrl(url))
|
|
111
|
+
.map(async (url) => await deps.getRssData(url, arg)))).flat(1);
|
|
112
|
+
let itemArray = rssItemList.sort((a, b) => deps.parsePubDate(b.pubDate).getTime() - deps.parsePubDate(a.pubDate).getTime());
|
|
113
|
+
if (arg.reverse)
|
|
114
|
+
itemArray = itemArray.reverse();
|
|
115
|
+
const maxItem = arg.forceLength || 1;
|
|
116
|
+
const messageList = await Promise.all(itemArray
|
|
117
|
+
.filter((_, index) => index < maxItem)
|
|
118
|
+
.map(async (item) => await deps.parseRssItem(item, { ...rssItem, ...arg }, rssItem.author)));
|
|
119
|
+
return messageList.join('');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const normalizedError = (0, error_handler_1.normalizeError)(error);
|
|
123
|
+
(0, logger_1.debug)(deps.config, normalizedError, 'pull error', 'error', logContext);
|
|
124
|
+
(0, error_tracker_1.trackError)(normalizedError, logContext);
|
|
125
|
+
return `拉取失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '获取订阅数据')}`;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function registerFollowCommand(deps) {
|
|
130
|
+
deps.ctx.guild()
|
|
131
|
+
.command('rssowl.follow <id:number>', '关注订阅')
|
|
132
|
+
.alias('rsso.follow')
|
|
133
|
+
.usage(`关注订阅,在该订阅更新时提醒你
|
|
134
|
+
|
|
135
|
+
用法:
|
|
136
|
+
rsso.follow 1 - 关注订阅 #1(仅提醒你)
|
|
137
|
+
rsso.follow 1 --all - 关注订阅 #1(提醒所有人,需要高级权限)
|
|
138
|
+
`)
|
|
139
|
+
.option('all', '--all 提醒所有人')
|
|
140
|
+
.example('rsso.follow 1')
|
|
141
|
+
.action(async ({ session, options }, id) => {
|
|
142
|
+
const { guildId, platform, authorId, authority } = (0, utils_1.extractSessionInfo)(session);
|
|
143
|
+
const rssList = await getGuildSubscriptions(deps.ctx, platform, guildId);
|
|
144
|
+
const rssItem = getSubscriptionByIndex(rssList, id);
|
|
145
|
+
if (!rssItem)
|
|
146
|
+
return getSubscriptionNotFoundMessage(id, rssList.length);
|
|
147
|
+
const followers = Array.isArray(rssItem.followers) ? [...rssItem.followers] : [];
|
|
148
|
+
if (options.all) {
|
|
149
|
+
const authorityCheck = (0, utils_1.checkAuthority)(authority, deps.config.basic.advancedAuthority, `权限不足!当前权限: ${authority},需要权限: ${deps.config.basic.advancedAuthority} 或以上`);
|
|
150
|
+
if (!authorityCheck.success)
|
|
151
|
+
return authorityCheck.message;
|
|
152
|
+
if (followers.includes('all'))
|
|
153
|
+
return '已经设置全员提醒';
|
|
154
|
+
followers.push('all');
|
|
155
|
+
await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
|
|
156
|
+
return '✅ 已设置全员提醒';
|
|
157
|
+
}
|
|
158
|
+
if (followers.includes(authorId))
|
|
159
|
+
return '已经关注过了';
|
|
160
|
+
followers.push(authorId);
|
|
161
|
+
await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
|
|
162
|
+
return '✅ 关注成功';
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function getGuildSubscriptions(ctx, platform, guildId) {
|
|
166
|
+
return ctx.database.get('rssOwl', { platform, guildId });
|
|
167
|
+
}
|
|
168
|
+
function getSubscriptionByIndex(rssList, id) {
|
|
169
|
+
const listIndex = id - 1;
|
|
170
|
+
if (listIndex < 0 || listIndex >= rssList.length)
|
|
171
|
+
return null;
|
|
172
|
+
return rssList[listIndex];
|
|
173
|
+
}
|
|
174
|
+
function getSubscriptionNotFoundMessage(id, total) {
|
|
175
|
+
return `❌ 序号 ${id} 不存在\n当前共有 ${total} 个订阅\n\n使用 rsso.list 查看完整列表`;
|
|
176
|
+
}
|
package/lib/commands/utils.d.ts
CHANGED
|
@@ -21,15 +21,23 @@ export interface SessionInfo {
|
|
|
21
21
|
authorId: string;
|
|
22
22
|
authority: number;
|
|
23
23
|
}
|
|
24
|
+
export interface ParseTargetsResult {
|
|
25
|
+
targets: string[];
|
|
26
|
+
invalidTarget?: string;
|
|
27
|
+
}
|
|
24
28
|
/**
|
|
25
29
|
* 从会话中提取信息
|
|
26
30
|
*/
|
|
27
31
|
export declare function extractSessionInfo(session: Session): SessionInfo;
|
|
32
|
+
/**
|
|
33
|
+
* 构建命令日志上下文
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildCommandLogContext(session: Session, command?: string, operation?: string): Record<string, any>;
|
|
28
36
|
/**
|
|
29
37
|
* 命令错误处理包装器
|
|
30
38
|
* 统一处理命令执行中的错误
|
|
31
39
|
*/
|
|
32
|
-
export declare function withCommandErrorHandling(config: Config, operation: string, handler: () => Promise<string>): Promise<string>;
|
|
40
|
+
export declare function withCommandErrorHandling(config: Config, operation: string, handler: () => Promise<string>, context?: Record<string, any>): Promise<string>;
|
|
33
41
|
/**
|
|
34
42
|
* 权限检查辅助函数
|
|
35
43
|
*/
|
|
@@ -44,6 +52,10 @@ export declare function parseTarget(target: string): {
|
|
|
44
52
|
platform: string;
|
|
45
53
|
guildId: string;
|
|
46
54
|
} | null;
|
|
55
|
+
/**
|
|
56
|
+
* 解析多个推送目标
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseTargets(targetInput?: string): ParseTargetsResult;
|
|
47
59
|
/**
|
|
48
60
|
* 验证 URL 格式
|
|
49
61
|
*/
|
package/lib/commands/utils.js
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.extractSessionInfo = extractSessionInfo;
|
|
8
|
+
exports.buildCommandLogContext = buildCommandLogContext;
|
|
8
9
|
exports.withCommandErrorHandling = withCommandErrorHandling;
|
|
9
10
|
exports.checkAuthority = checkAuthority;
|
|
10
11
|
exports.parseTarget = parseTarget;
|
|
12
|
+
exports.parseTargets = parseTargets;
|
|
11
13
|
exports.isValidUrl = isValidUrl;
|
|
12
14
|
const error_handler_1 = require("../utils/error-handler");
|
|
15
|
+
const error_tracker_1 = require("../utils/error-tracker");
|
|
13
16
|
const logger_1 = require("../utils/logger");
|
|
14
17
|
/**
|
|
15
18
|
* 从会话中提取信息
|
|
@@ -21,13 +24,30 @@ function extractSessionInfo(session) {
|
|
|
21
24
|
const { authority } = session.user;
|
|
22
25
|
return { guildId, platform, authorId, authority };
|
|
23
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* 构建命令日志上下文
|
|
29
|
+
*/
|
|
30
|
+
function buildCommandLogContext(session, command, operation) {
|
|
31
|
+
const sessionInfo = extractSessionInfo(session);
|
|
32
|
+
const context = {
|
|
33
|
+
...sessionInfo,
|
|
34
|
+
userId: sessionInfo.authorId,
|
|
35
|
+
};
|
|
36
|
+
if (command)
|
|
37
|
+
context.command = command;
|
|
38
|
+
if (operation)
|
|
39
|
+
context.operation = operation;
|
|
40
|
+
return context;
|
|
41
|
+
}
|
|
24
42
|
/**
|
|
25
43
|
* 命令错误处理包装器
|
|
26
44
|
* 统一处理命令执行中的错误
|
|
27
45
|
*/
|
|
28
|
-
function withCommandErrorHandling(config, operation, handler) {
|
|
46
|
+
function withCommandErrorHandling(config, operation, handler, context) {
|
|
29
47
|
return handler().catch((error) => {
|
|
30
|
-
(0,
|
|
48
|
+
const normalizedError = (0, error_handler_1.normalizeError)(error);
|
|
49
|
+
(0, logger_1.debug)(config, normalizedError, `${operation} error`, 'error', context);
|
|
50
|
+
(0, error_tracker_1.trackError)(normalizedError, context);
|
|
31
51
|
return Promise.resolve(`${operation}失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, operation)}`);
|
|
32
52
|
});
|
|
33
53
|
}
|
|
@@ -56,6 +76,27 @@ function parseTarget(target) {
|
|
|
56
76
|
guildId: parts[1]
|
|
57
77
|
};
|
|
58
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 解析多个推送目标
|
|
81
|
+
*/
|
|
82
|
+
function parseTargets(targetInput) {
|
|
83
|
+
if (!targetInput) {
|
|
84
|
+
return { targets: [] };
|
|
85
|
+
}
|
|
86
|
+
const targets = targetInput
|
|
87
|
+
.split(/[;,,;]/)
|
|
88
|
+
.map(target => target.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
for (const target of targets) {
|
|
91
|
+
if (!parseTarget(target)) {
|
|
92
|
+
return {
|
|
93
|
+
targets: [],
|
|
94
|
+
invalidTarget: target,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { targets };
|
|
99
|
+
}
|
|
59
100
|
/**
|
|
60
101
|
* 验证 URL 格式
|
|
61
102
|
*/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import type { Config, rssArg } from '../types';
|
|
3
|
+
type DebugType = 'disable' | 'error' | 'info' | 'details';
|
|
4
|
+
export interface WebMonitorCommandDeps {
|
|
5
|
+
ctx: Context;
|
|
6
|
+
config: Config;
|
|
7
|
+
debug: (message: any, name?: string, type?: DebugType, context?: Record<string, any>) => void;
|
|
8
|
+
mixinArg: (arg: Record<string, any>) => rssArg;
|
|
9
|
+
getRssData: (url: string, arg: Record<string, any>) => Promise<any[]>;
|
|
10
|
+
parseRssItem: (item: any, arg: Record<string, any>, authorId: string | number) => Promise<string>;
|
|
11
|
+
generateSelectorByAI: (url: string, instruction: string, html: string) => Promise<string>;
|
|
12
|
+
fetchUrl: (url: string, arg?: Record<string, any>) => Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerWebMonitorCommands(deps: WebMonitorCommandDeps): void;
|
|
15
|
+
export {};
|