@anyul/koishi-plugin-rss 5.2.48 → 5.3.1

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.
@@ -1,14 +1,27 @@
1
1
  import { Context } from 'koishi';
2
2
  import type { Config } from '../types';
3
+ export interface QuickListItem {
4
+ prefix: string;
5
+ name: string;
6
+ detail: string;
7
+ example: string;
8
+ replace: string;
9
+ }
10
+ /**
11
+ * 按稳定键解析快速订阅项(审查 #12)。
12
+ * 优先按 prefix(稳定标识)匹配,避免数组重排导致「编号 1 指向不同项」的漂移;
13
+ * prefix 未命中时再回退到数字序号(向后兼容老用法)。
14
+ * 返回 null 表示既非合法 prefix 也非合法序号。
15
+ */
16
+ export declare function resolveQuickItem(input: string, quickList: QuickListItem[]): {
17
+ item: QuickListItem;
18
+ matchedBy: 'prefix' | 'index';
19
+ } | null;
3
20
  export interface SubscriptionCreateCommandDeps {
4
21
  ctx: Context;
5
22
  config: Config;
6
23
  usage: string;
7
- quickList: Array<{
8
- name: string;
9
- detail: string;
10
- example: string;
11
- }>;
24
+ quickList: QuickListItem[];
12
25
  parseQuickUrl: (url: string) => string;
13
26
  parsePubDate: (pubDate: any) => Date;
14
27
  getRssData: (url: string, arg: any) => Promise<any[]>;
@@ -1,9 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveQuickItem = resolveQuickItem;
3
4
  exports.registerSubscriptionCreateCommand = registerSubscriptionCreateCommand;
4
5
  const common_1 = require("../utils/common");
5
6
  const error_handler_1 = require("../utils/error-handler");
6
7
  const utils_1 = require("./utils");
8
+ /**
9
+ * 按稳定键解析快速订阅项(审查 #12)。
10
+ * 优先按 prefix(稳定标识)匹配,避免数组重排导致「编号 1 指向不同项」的漂移;
11
+ * prefix 未命中时再回退到数字序号(向后兼容老用法)。
12
+ * 返回 null 表示既非合法 prefix 也非合法序号。
13
+ */
14
+ function resolveQuickItem(input, quickList) {
15
+ if (!input)
16
+ return null;
17
+ const trimmed = String(input).trim();
18
+ // 1) prefix 精确匹配(稳定键)
19
+ const byPrefix = quickList.find(q => q.prefix === trimmed);
20
+ if (byPrefix)
21
+ return { item: byPrefix, matchedBy: 'prefix' };
22
+ // 2) 数字序号回退(兼容历史用法,依赖数组顺序,不稳定)
23
+ const num = parseInt(trimmed, 10);
24
+ if (!Number.isNaN(num) && num >= 1 && num <= quickList.length) {
25
+ const item = quickList[num - 1];
26
+ return { item, matchedBy: 'index' };
27
+ }
28
+ return null;
29
+ }
7
30
  /**
8
31
  * 注册主订阅命令。
9
32
  */
@@ -12,16 +35,10 @@ function registerSubscriptionCreateCommand(deps) {
12
35
  .command('rssowl <url:text>', '订阅 RSS/源')
13
36
  .alias('rsso')
14
37
  .usage(deps.usage)
15
- .option('list', '-l [content] 查看订阅列表(详情) [已移至 rsso.list 子命令,使用列表序号]')
16
- .option('remove', '-r <序号> 删除订阅 [已移至 rsso.remove 子命令,使用列表序号]')
17
- .option('removeAll', '删除全部订阅 [已移至 rsso.remove --all 子命令]')
18
- .option('follow', '-f <序号> 关注订阅 [已移至 rsso.follow 子命令,使用列表序号]')
19
- .option('followAll', '<序号> 在该订阅更新时提醒所有人 [已移至 rsso.follow --all 子命令,使用列表序号]')
20
38
  .option('target', '--target <platform:guildId> 跨群订阅(高级权限)')
21
39
  .option('arg', '-a <content> 自定义配置')
22
40
  .option('template', '-i <content> 消息模板')
23
41
  .option('title', '-t <content> 自定义命名')
24
- .option('pull', '-p <序号> 拉取订阅最新更新 [已移至 rsso.pull 子命令,使用列表序号]')
25
42
  .option('force', '强行写入')
26
43
  .option('daily', '-d <content>')
27
44
  .option('test', '-T 测试')
@@ -34,34 +51,29 @@ function registerSubscriptionCreateCommand(deps) {
34
51
  const botSelfId = session.bot?.selfId;
35
52
  deps.debug(`${platform}:${userId}:${guildId}, bot:${botSelfId}`, '', 'info', logContext);
36
53
  if (options?.quick === '') {
37
- return '输入 rsso -q [id] 查询详情\n' + deps.quickList.map((v, i) => `${i + 1}.${v.name}`).join('\n');
54
+ // 同时显示序号与 prefix,引导用户使用稳定的 prefix
55
+ return '输入 rsso -q <prefix|序号> 查询详情\n' + deps.quickList.map((v, i) => `${i + 1}.${v.name} [${v.prefix}]`).join('\n');
38
56
  }
39
57
  if (options?.quick) {
40
- const currentQuickObj = deps.quickList[parseInt(options.quick) - 1];
41
- if (!currentQuickObj)
42
- return `快速订阅编号不存在: ${options.quick}`;
43
- return `${currentQuickObj.name}\n${currentQuickObj.detail}\n例:rsso -T ${currentQuickObj.example}\n(${deps.parseQuickUrl(currentQuickObj.example)})`;
58
+ const resolved = resolveQuickItem(options.quick, deps.quickList);
59
+ if (!resolved)
60
+ return `快速订阅不存在: ${options.quick}\n💡 使用 rsso -q 查看可用列表(支持 prefix 或序号)`;
61
+ const { item: currentQuickObj, matchedBy } = resolved;
62
+ const hint = matchedBy === 'index' ? '\n💡 提示:序号会随列表调整变化,推荐使用 prefix [' + currentQuickObj.prefix + '] 作为稳定键' : '';
63
+ return `${currentQuickObj.name}\n${currentQuickObj.detail}\n例:rsso -T ${currentQuickObj.example}\n(${deps.parseQuickUrl(currentQuickObj.example)})${hint}`;
44
64
  }
45
65
  if (platform.includes('sandbox') && !options.test && url) {
46
66
  session.send('沙盒中无法推送更新,但RSS依然会被订阅,建议使用 -T 选项进行测试');
47
67
  }
48
- const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
49
- if (options?.list === '' || options?.list)
50
- return '💡 提示:请使用子命令查看订阅\n\nrsso.list - 查看所有订阅(显示序号)\nrsso.list 1 - 查看订阅详情\n\n(旧选项 -l 仍可使用,但建议迁移到新命令)';
51
- if (options?.remove)
52
- return '💡 提示:请使用子命令删除订阅\n\nrsso.remove 1 - 删除订阅 #1(使用列表序号)\nrsso.remove --all - 删除全部订阅\n\n(旧选项 -r 仍可使用,但建议迁移到新命令)';
53
- if (options?.removeAll)
54
- return '💡 提示:请使用子命令删除订阅\n\nrsso.remove --all - 删除全部订阅\n\n(旧选项仍可使用,但建议迁移到新命令)';
55
- if (options?.follow)
56
- return '💡 提示:请使用子命令关注订阅\n\nrsso.follow 1 - 关注订阅 #1(使用列表序号)\n\n(旧选项 -f 仍可使用,但建议迁移到新命令)';
57
- if (options?.followAll)
58
- return '💡 提示:请使用子命令设置全员提醒\n\nrsso.follow 1 --all - 设置全员提醒(使用列表序号)\n\n(旧选项仍可使用,但建议迁移到新命令)';
59
- if (options?.pull)
60
- return '💡 提示:请使用子命令拉取订阅\n\nrsso.pull 1 - 拉取订阅 #1 的最新更新(使用列表序号)\n\n(旧选项 -p 仍可使用,但建议迁移到新命令)';
61
68
  if (!url)
62
69
  return deps.usage;
70
+ // 校验 URL 格式,给用户即时反馈,避免带着非法 URL 走到网络请求
71
+ if (!(0, utils_1.isValidUrl)((0, common_1.ensureUrlProtocol)(url))) {
72
+ return '❌ URL 格式不正确,请以 http:// 或 https:// 开头';
73
+ }
74
+ const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
63
75
  if (rssList.find(item => item.url === url))
64
- return '该订阅已存在';
76
+ return '该订阅已存在';
65
77
  const rawArg = deps.formatArg(options);
66
78
  const arg = deps.mixinArg(rawArg);
67
79
  let targetPlatform = platform;
@@ -78,7 +90,7 @@ function registerSubscriptionCreateCommand(deps) {
78
90
  targetGuildId = parsedTarget.guildId;
79
91
  if (options.test) {
80
92
  try {
81
- await deps.ctx.broadcast([`${targetPlatform}:${targetGuildId}`], '📤 跨群订阅测试消息');
93
+ await deps.ctx.broadcast([`${targetPlatform}:${targetGuildId}`], '跨群订阅测试消息');
82
94
  return `✅ 测试消息已发送到目标群组\n目标: ${targetPlatform}:${targetGuildId}\n\n说明:Bot 可以访问该群组,跨群订阅可以正常工作。\n去掉 --test 选项完成订阅。`;
83
95
  }
84
96
  catch (error) {
@@ -123,7 +135,7 @@ function registerSubscriptionCreateCommand(deps) {
123
135
  return forceCheck.message;
124
136
  }
125
137
  else if (deps.config.basic.urlDeduplication && rssList.find(item => item.rssId === rssItem.rssId)) {
126
- return `订阅已存在: ${rssItem.rssId}`;
138
+ return `❌ 订阅已存在: ${rssItem.rssId}`;
127
139
  }
128
140
  await deps.ctx.database.create('rssOwl', rssItem);
129
141
  if (deps.config.basic.firstLoad && arg.firstLoad !== false && rssItemList.length > 0) {
@@ -135,7 +147,7 @@ function registerSubscriptionCreateCommand(deps) {
135
147
  const messageList = await Promise.all(itemArray.filter((_, index) => index < maxItem).map(async (item) => deps.parseRssItem(item, { ...rssItem, ...mergedArg }, rssItem.author)));
136
148
  await deps.ctx.broadcast([`${targetPlatform}:${targetGuildId}`], messageList.join(''));
137
149
  }
138
- return `订阅成功: ${title}`;
150
+ return `✅ 订阅成功: ${title}`;
139
151
  }
140
152
  catch (error) {
141
153
  deps.debug(error, 'add error', 'error', logContext);
@@ -5,7 +5,7 @@ const utils_1 = require("./utils");
5
5
  function registerSubscriptionEditCommand(deps) {
6
6
  deps.ctx.guild()
7
7
  .command('rssowl.edit <id:number>', '修改订阅配置')
8
- .alias('rsso.edit')
8
+ .alias('rsso.edit', 'rsed')
9
9
  .usage(`修改订阅的配置项
10
10
 
11
11
  用法:
@@ -14,7 +14,7 @@ function registerSubscriptionEditCommand(deps) {
14
14
  rsso.edit 1 -u https://... - 修改URL
15
15
  rsso.edit 1 -s ".item" - 修改选择器(HTML监控)
16
16
  rsso.edit 1 --target onebot:123 - 修改推送目标(高级权限)
17
- rsso.edit 1 --target "onebot:123,telegram:456" - 多个推送目标(高级权限)⭐
17
+ rsso.edit 1 --target "onebot:123,telegram:456" - 多个推送目标(高级权限)
18
18
  rsso.edit 1 -t "新标题" --test - 测试修改(不保存)
19
19
 
20
20
  示例:
@@ -53,7 +53,7 @@ function registerSubscriptionEditCommand(deps) {
53
53
  }
54
54
  const hasChanges = options.title || options.url || options.template || options.selector || options.target;
55
55
  if (!hasChanges) {
56
- return '请指定要修改的内容\n可用选项: -t (标题), -u (URL), -i (模板), -s (选择器), --target (推送目标)\n使用 --help 查看详细帮助';
56
+ return '请指定要修改的内容\n可用选项: -t (标题), -u (URL), -i (模板), -s (选择器), --target (推送目标)\n💡 使用 --help 查看详细帮助';
57
57
  }
58
58
  const parseResult = (0, utils_1.parseTargets)(options.target);
59
59
  if (parseResult.invalidTarget) {
@@ -66,7 +66,7 @@ function registerSubscriptionEditCommand(deps) {
66
66
  });
67
67
  }
68
68
  function buildEditPreview(rssItem, id, options, parsedTargets) {
69
- let testOutput = `📝 修改预览 [序号:${id} | ID:${rssItem.id}]\n\n`;
69
+ let testOutput = `修改预览 [序号:${id} | ID:${rssItem.id}]\n\n`;
70
70
  testOutput += `当前标题: ${rssItem.title}\n`;
71
71
  testOutput += `当前URL: ${rssItem.url}\n`;
72
72
  testOutput += `当前推送目标: ${rssItem.platform}:${rssItem.guildId}\n`;
@@ -130,7 +130,7 @@ async function saveSubscriptionChanges(ctx, rssItem, id, options, parsedTargets)
130
130
  const originalTarget = `${rssItem.platform}:${rssItem.guildId}`;
131
131
  let result = `✅ 订阅已更新 [序号:${id} | ID:${rssItem.id}]\n\n`;
132
132
  result += `已创建 ${parsedTargets.length} 个推送目标:\n\n`;
133
- result += `1️⃣ 原订阅 (本群): ${originalTarget}\n`;
133
+ result += `1. 原订阅 (本群): ${originalTarget}\n`;
134
134
  await ctx.database.set('rssOwl', rssItem.id, updates);
135
135
  for (let i = 0; i < parsedTargets.length; i++) {
136
136
  const [newPlatform, newGuildId] = parsedTargets[i].split(/[::]/);
@@ -14,7 +14,7 @@ function registerSubscriptionManagementCommands(deps) {
14
14
  function registerListCommand(deps) {
15
15
  deps.ctx.guild()
16
16
  .command('rssowl.list [id:number]', '查看订阅列表')
17
- .alias('rsso.list')
17
+ .alias('rsso.list', 'rsls')
18
18
  .usage(`查看订阅列表
19
19
 
20
20
  用法:
@@ -40,9 +40,9 @@ function registerListCommand(deps) {
40
40
  const pushTarget = `${rssItem.platform}:${rssItem.guildId}`;
41
41
  const isCrossGroup = rssItem.platform !== platform || rssItem.guildId !== guildId;
42
42
  const targetInfo = isCrossGroup
43
- ? `📤 推送目标: ${pushTarget} (跨群订阅)`
44
- : `📤 推送目标: ${pushTarget} (本群)`;
45
- return `📰 订阅详情 [序号:${id} | ID:${rssItem.id}]
43
+ ? `推送目标: ${pushTarget} (跨群订阅)`
44
+ : `推送目标: ${pushTarget} (本群)`;
45
+ return `订阅详情 [序号:${id} | ID:${rssItem.id}]
46
46
  标题: ${rssItem.title}
47
47
  链接: ${rssItem.url}
48
48
  类型: ${rssItem.arg?.type || 'RSS'}
@@ -55,7 +55,7 @@ ${targetInfo}
55
55
  function registerRemoveCommand(deps) {
56
56
  deps.ctx.guild()
57
57
  .command('rssowl.remove <id:number>', '删除订阅')
58
- .alias('rsso.remove')
58
+ .alias('rsso.remove', 'rsrm')
59
59
  .usage(`删除订阅
60
60
 
61
61
  用法:
@@ -84,7 +84,7 @@ function registerRemoveCommand(deps) {
84
84
  function registerPullCommand(deps) {
85
85
  deps.ctx.guild()
86
86
  .command('rssowl.pull <id:number>', '拉取订阅最新内容')
87
- .alias('rsso.pull')
87
+ .alias('rsso.pull', 'rspl')
88
88
  .usage(`拉取订阅最新内容
89
89
 
90
90
  用法:
@@ -129,7 +129,7 @@ function registerPullCommand(deps) {
129
129
  function registerFollowCommand(deps) {
130
130
  deps.ctx.guild()
131
131
  .command('rssowl.follow <id:number>', '关注订阅')
132
- .alias('rsso.follow')
132
+ .alias('rsso.follow', 'rsfw')
133
133
  .usage(`关注订阅,在该订阅更新时提醒你
134
134
 
135
135
  用法:
@@ -150,13 +150,13 @@ function registerFollowCommand(deps) {
150
150
  if (!authorityCheck.success)
151
151
  return authorityCheck.message;
152
152
  if (followers.includes('all'))
153
- return '已经设置全员提醒';
153
+ return '💡 已经设置全员提醒';
154
154
  followers.push('all');
155
155
  await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
156
156
  return '✅ 已设置全员提醒';
157
157
  }
158
158
  if (followers.includes(authorId))
159
- return '已经关注过了';
159
+ return '💡 已经关注过了';
160
160
  followers.push(authorId);
161
161
  await deps.ctx.database.set('rssOwl', { id: rssItem.id }, { followers });
162
162
  return '✅ 关注成功';
@@ -1,6 +1,23 @@
1
1
  import { Context } from 'koishi';
2
- import type { Config, rssArg } from '../types';
2
+ import type { Config, TemplateType, rssArg } from '../types';
3
3
  type DebugType = 'disable' | 'error' | 'info' | 'details';
4
+ type HtmlMonitorArg = rssArg & {
5
+ title?: string;
6
+ };
7
+ interface HtmlMonitorOptions {
8
+ selector?: string;
9
+ title?: string;
10
+ template?: TemplateType;
11
+ text?: boolean;
12
+ puppeteer?: boolean;
13
+ wait?: string;
14
+ waitSelector?: string;
15
+ test?: boolean;
16
+ }
17
+ interface WatchCommandOptions {
18
+ puppeteer?: boolean;
19
+ test?: boolean;
20
+ }
4
21
  export interface WebMonitorCommandDeps {
5
22
  ctx: Context;
6
23
  config: Config;
@@ -12,4 +29,6 @@ export interface WebMonitorCommandDeps {
12
29
  fetchUrl: (url: string, arg?: Record<string, any>) => Promise<string>;
13
30
  }
14
31
  export declare function registerWebMonitorCommands(deps: WebMonitorCommandDeps): void;
32
+ export declare function buildHtmlMonitorArg(options: HtmlMonitorOptions): HtmlMonitorArg;
33
+ export declare function buildWatchArg(keyword: string | undefined, options: WatchCommandOptions): rssArg;
15
34
  export {};
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerWebMonitorCommands = registerWebMonitorCommands;
4
+ exports.buildHtmlMonitorArg = buildHtmlMonitorArg;
5
+ exports.buildWatchArg = buildWatchArg;
4
6
  const common_1 = require("../utils/common");
5
7
  const error_handler_1 = require("../utils/error-handler");
6
8
  const utils_1 = require("./utils");
@@ -12,7 +14,7 @@ function registerWebMonitorCommands(deps) {
12
14
  function registerHtmlMonitorCommand(deps) {
13
15
  deps.ctx.guild()
14
16
  .command('rssowl.html <url:string>', '监控网页变化 (CSS Selector)')
15
- .alias('rsso.html')
17
+ .alias('rsso.html', 'rshm')
16
18
  .usage(`
17
19
  HTML 网页监控功能,使用 CSS 选择器提取内容
18
20
  用法:
@@ -37,9 +39,9 @@ HTML 网页监控功能,使用 CSS 选择器提取内容
37
39
  .example('rsso.html https://news.ycombinator.com -s ".titleline > a"')
38
40
  .action(async ({ session, options }, url) => {
39
41
  if (!url)
40
- return '请输入 URL';
42
+ return '请输入 URL';
41
43
  if (!options.selector)
42
- return '请指定 CSS 选择器 (-s)';
44
+ return '请指定 CSS 选择器 (-s)';
43
45
  const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.html', options.test ? 'test' : 'create');
44
46
  const { guildId, platform } = (0, utils_1.extractSessionInfo)(session);
45
47
  const botSelfId = session.bot?.selfId;
@@ -50,15 +52,15 @@ HTML 网页监控功能,使用 CSS 选择器提取内容
50
52
  if (options.test) {
51
53
  const items = await deps.getRssData(normalizedUrl, arg);
52
54
  if (!items.length)
53
- return '未找到符合选择器的元素';
55
+ return '未找到符合选择器的元素';
54
56
  return buildItemsPreview(items, `找到 ${items.length} 个元素:`, true);
55
57
  }
56
58
  const rssList = await deps.ctx.database.get('rssOwl', { platform, guildId });
57
59
  if (rssList.find(item => item.url === normalizedUrl))
58
- return '该订阅已存在';
60
+ return '该订阅已存在';
59
61
  const htmlItems = await deps.getRssData(normalizedUrl, arg);
60
62
  if (!htmlItems.length)
61
- return '未找到符合选择器的元素,无法创建订阅';
63
+ return '未找到符合选择器的元素,无法创建订阅';
62
64
  const title = options.title || htmlItems[0]?.rss?.channel?.title || `HTML监控: ${normalizedUrl}`;
63
65
  const rssItem = {
64
66
  url: normalizedUrl,
@@ -73,24 +75,24 @@ HTML 网页监控功能,使用 CSS 选择器提取内容
73
75
  followers: [],
74
76
  };
75
77
  if (deps.config.basic.urlDeduplication && rssList.find(item => item.rssId === rssItem.rssId)) {
76
- return `订阅已存在: ${rssItem.rssId}`;
78
+ return `❌ 订阅已存在: ${rssItem.rssId}`;
77
79
  }
78
80
  await deps.ctx.database.create('rssOwl', rssItem);
79
81
  if (deps.config.basic.firstLoad && arg.firstLoad !== false && htmlItems.length > 0) {
80
82
  await broadcastInitialItems(deps, `${platform}:${guildId}`, htmlItems, rssItem);
81
83
  }
82
- return `订阅成功: ${title}\n提示: HTML监控基于内容变化检测,请确保选择器稳定`;
84
+ return `✅ 订阅成功: ${title}\n💡 提示: HTML监控基于内容变化检测,请确保选择器稳定`;
83
85
  }
84
86
  catch (error) {
85
87
  deps.debug(error, 'html error', 'error', logContext);
86
- return `抓取失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, 'HTML监控')}`;
88
+ return `❌ 抓取失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, 'HTML监控')}`;
87
89
  }
88
90
  });
89
91
  }
90
92
  function registerAskCommand(deps) {
91
93
  deps.ctx.guild()
92
94
  .command('rssowl.ask <url:string> <instruction:text>', 'AI 智能订阅网页')
93
- .alias('rsso.ask')
95
+ .alias('rsso.ask', 'rsak')
94
96
  .usage(`AI 智能订阅功能,自动生成 CSS 选择器
95
97
 
96
98
  前置要求:
@@ -108,9 +110,9 @@ function registerAskCommand(deps) {
108
110
  .example('rsso.ask https://news.ycombinator.com "监控首页的前5条新闻标题"')
109
111
  .action(async ({ session, options }, url, instruction) => {
110
112
  if (!url)
111
- return '请输入网址';
113
+ return '请输入网址';
112
114
  if (!instruction)
113
- return '请描述你的需求';
115
+ return '请描述你的需求';
114
116
  const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.ask', options.test ? 'test' : 'analyze');
115
117
  const normalizedUrl = (0, common_1.ensureUrlProtocol)(url);
116
118
  try {
@@ -123,21 +125,21 @@ function registerAskCommand(deps) {
123
125
  template: 'content',
124
126
  });
125
127
  if (!items.length)
126
- return `选择器未匹配到任何元素: ${selector}`;
128
+ return `❌ 选择器未匹配到任何元素: ${selector}`;
127
129
  return `AI 生成的选择器: ${selector}\n\n匹配到 ${items.length} 个元素:\n${items.slice(0, 2).map((item) => normalizePreviewText(item?.title) || '无标题').join('\n')}`;
128
130
  }
129
131
  return `AI 生成的选择器: ${selector}\n请使用 rsso.html ${normalizedUrl} -s "${selector}" 完成订阅`;
130
132
  }
131
133
  catch (error) {
132
134
  deps.debug(error, 'ask error', 'error', logContext);
133
- return `AI 分析失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, 'AI生成选择器')}`;
135
+ return `❌ AI 分析失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, 'AI生成选择器')}`;
134
136
  }
135
137
  });
136
138
  }
137
139
  function registerWatchCommand(deps) {
138
140
  deps.ctx.guild()
139
141
  .command('rssowl.watch <url:string> [keyword:text]', '简单网页监控')
140
- .alias('rsso.watch')
142
+ .alias('rsso.watch', 'rswt')
141
143
  .usage(`
142
144
  简单网页监控,支持关键词或整页监控。
143
145
  用法:
@@ -151,7 +153,7 @@ function registerWatchCommand(deps) {
151
153
  .example('rsso.watch https://example.com "缺货"')
152
154
  .action(async ({ session, options }, url, keyword) => {
153
155
  if (!url)
154
- return '请输入 URL';
156
+ return '请输入 URL';
155
157
  const logContext = (0, utils_1.buildCommandLogContext)(session, 'rsso.watch', options.test ? 'test' : 'preview');
156
158
  const normalizedUrl = (0, common_1.ensureUrlProtocol)(url);
157
159
  const arg = deps.mixinArg(buildWatchArg(keyword, options));
@@ -159,14 +161,14 @@ function registerWatchCommand(deps) {
159
161
  if (options.test) {
160
162
  const items = await deps.getRssData(normalizedUrl, arg);
161
163
  if (!items.length)
162
- return '未找到内容';
164
+ return '未找到内容';
163
165
  return buildItemsPreview(items, `找到 ${items.length} 条内容:`, false);
164
166
  }
165
- return '请使用 rsso 命令完成订阅,或使用 -T 测试';
167
+ return '💡 请使用 rsso 命令完成订阅,或使用 -T 测试';
166
168
  }
167
169
  catch (error) {
168
170
  deps.debug(error, 'watch error', 'error', logContext);
169
- return `监控失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '网页监控')}`;
171
+ return `❌ 监控失败: ${(0, error_handler_1.getFriendlyErrorMessage)(error, '网页监控')}`;
170
172
  }
171
173
  });
172
174
  }
@@ -1,4 +1,4 @@
1
- export declare const usage = "\n<details>\n<summary>RSS-OWL \u547D\u4EE4\u5BFC\u822A\uFF08\u53D1\u9001 rsso \u67E5\u770B\u672C\u5E2E\u52A9\uFF09</summary>\n\n## \u65B0\u5EFA / \u6D4B\u8BD5\u8BA2\u9605:\n rsso &lt;url&gt; - \u521B\u5EFA RSS / Atom / JSON Feed \u8BA2\u9605\n rsso -T &lt;url&gt; - \u6D4B\u8BD5\u6293\u53D6\uFF0C\u4E0D\u5199\u5165\u8BA2\u9605\n rsso &lt;url&gt; -t &lt;\u6807\u9898&gt; - \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n rsso &lt;url&gt; -i &lt;\u6A21\u677F&gt; - \u6307\u5B9A\u6D88\u606F\u6A21\u677F\n rsso &lt;url&gt; -a &lt;key:value,...&gt; - \u8986\u76D6\u8BA2\u9605\u53C2\u6570\n rsso &lt;url&gt; -d &lt;HH:mm[/\u6570\u91CF]&gt; - \u6BCF\u65E5\u5B9A\u65F6\u63A8\u9001\n rsso &lt;url&gt; --target &lt;\u5E73\u53F0:\u9891\u9053&gt; - \u8DE8\u7FA4 / \u8DE8\u9891\u9053\u8BA2\u9605\uFF08\u9AD8\u7EA7\u6743\u9650\uFF09\n rsso -q [\u7F16\u53F7] - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u5217\u8868 / \u8BE6\u60C5\n\n## \u7BA1\u7406\u8BA2\u9605\uFF08\u4F7F\u7528\u5217\u8868\u5E8F\u53F7\uFF09:\n rsso.list [id] - \u67E5\u770B\u8BA2\u9605\u5217\u8868 / \u8BE6\u60C5\n rsso.remove &lt;id&gt; [--all] - \u5220\u9664\u8BA2\u9605 / \u5220\u9664\u5168\u90E8\n rsso.pull &lt;id&gt; - \u62C9\u53D6\u8BA2\u9605\u6700\u65B0\u5185\u5BB9\n rsso.follow &lt;id&gt; [--all] - \u5173\u6CE8\u8BA2\u9605\u66F4\u65B0 / \u5168\u5458\u63D0\u9192\n rsso.edit &lt;id&gt; [\u9009\u9879] - \u4FEE\u6539\u6807\u9898\u3001URL\u3001\u6A21\u677F\u3001\u9009\u62E9\u5668\u3001\u76EE\u6807\n rsso.cache - \u6D88\u606F\u7F13\u5B58\u7BA1\u7406\n rsso.queue - \u53D1\u9001\u961F\u5217\u7BA1\u7406\n\n## \u7F51\u9875\u76D1\u63A7\u76F8\u5173:\n rsso.html &lt;url&gt; -s &lt;selector&gt; - \u4F7F\u7528 CSS \u9009\u62E9\u5668\u76D1\u63A7\u7F51\u9875\n rsso.ask &lt;url&gt; &lt;\u9700\u6C42&gt; - AI \u751F\u6210\u9009\u62E9\u5668\u540E\u521B\u5EFA\u7F51\u9875\u8BA2\u9605\n rsso.watch &lt;url&gt; [\u5173\u952E\u8BCD] - \u7B80\u5355\u7F51\u9875 / \u5173\u952E\u8BCD\u76D1\u63A7\n\n## \u5E38\u7528\u6A21\u677F:\n content - \u7EAF\u6587\u5B57\u6B63\u6587\n default - \u9ED8\u8BA4\u56FE\u7247\u6A21\u677F\n custom - \u81EA\u5B9A\u4E49\u6A21\u677F\n only text - \u4EC5\u6587\u672C\n only image - \u4EC5\u56FE\u7247\n only media - \u56FE\u7247 + \u89C6\u9891\n link - \u8DDF\u968F\u6B63\u6587\u4E2D\u7684\u7B2C\u4E00\u4E2A\u94FE\u63A5\n\n## \u5E38\u7528\u793A\u4F8B:\n rsso https://example.com/rss\n rsso -T tg:woshadiao\n rsso https://example.com/rss -i content -t \"\u793A\u4F8B\u8BA2\u9605\"\n rsso.list\n rsso.edit 1 -t \"\u65B0\u6807\u9898\"\n rsso.html https://example.com -s \".news-item\"\n\n## \u517C\u5BB9\u63D0\u793A:\n \u65E7\u9009\u9879 -l / -r / -f / -p \u4ECD\u4F1A\u8FD4\u56DE\u8FC1\u79FB\u63D0\u793A\uFF0C\n \u5EFA\u8BAE\u6539\u7528 rsso.list / rsso.remove / rsso.follow / rsso.pull\u3002\n\n</details>\n";
1
+ export declare const usage = "\n<details>\n<summary>RSS-OWL \u547D\u4EE4\u5BFC\u822A\uFF08\u53D1\u9001 rsso \u67E5\u770B\u672C\u5E2E\u52A9\uFF09</summary>\n\n## \u65B0\u5EFA / \u6D4B\u8BD5\u8BA2\u9605:\n rsso &lt;url&gt; - \u521B\u5EFA RSS / Atom / JSON Feed \u8BA2\u9605\n rsso -T &lt;url&gt; - \u6D4B\u8BD5\u6293\u53D6\uFF0C\u4E0D\u5199\u5165\u8BA2\u9605\n rsso &lt;url&gt; -t &lt;\u6807\u9898&gt; - \u81EA\u5B9A\u4E49\u8BA2\u9605\u6807\u9898\n rsso &lt;url&gt; -i &lt;\u6A21\u677F&gt; - \u6307\u5B9A\u6D88\u606F\u6A21\u677F\n rsso &lt;url&gt; -a &lt;key:value,...&gt; - \u8986\u76D6\u8BA2\u9605\u53C2\u6570\n rsso &lt;url&gt; -d &lt;HH:mm[/\u6570\u91CF]&gt; - \u6BCF\u65E5\u5B9A\u65F6\u63A8\u9001\n rsso &lt;url&gt; --target &lt;\u5E73\u53F0:\u9891\u9053&gt; - \u8DE8\u7FA4 / \u8DE8\u9891\u9053\u8BA2\u9605\uFF08\u9AD8\u7EA7\u6743\u9650\uFF09\n rsso -q [\u7F16\u53F7] - \u67E5\u770B\u5FEB\u901F\u8BA2\u9605\u5217\u8868 / \u8BE6\u60C5\n\n## \u7BA1\u7406\u8BA2\u9605\uFF08\u4F7F\u7528\u5217\u8868\u5E8F\u53F7\uFF09:\n rsso.list [id] - \u67E5\u770B\u8BA2\u9605\u5217\u8868 / \u8BE6\u60C5\n rsso.remove &lt;id&gt; [--all] - \u5220\u9664\u8BA2\u9605 / \u5220\u9664\u5168\u90E8\n rsso.pull &lt;id&gt; - \u62C9\u53D6\u8BA2\u9605\u6700\u65B0\u5185\u5BB9\n rsso.follow &lt;id&gt; [--all] - \u5173\u6CE8\u8BA2\u9605\u66F4\u65B0 / \u5168\u5458\u63D0\u9192\n rsso.edit &lt;id&gt; [\u9009\u9879] - \u4FEE\u6539\u6807\u9898\u3001URL\u3001\u6A21\u677F\u3001\u9009\u62E9\u5668\u3001\u76EE\u6807\n rsso.cache - \u6D88\u606F\u7F13\u5B58\u7BA1\u7406\uFF08list/search/stats/pull/clear/cleanup\uFF09\n rsso.queue - \u53D1\u9001\u961F\u5217\u7BA1\u7406\uFF08stats/retry/cleanup\uFF09\n\n## \u7F51\u9875\u76D1\u63A7\u76F8\u5173:\n rsso.html &lt;url&gt; -s &lt;selector&gt; - \u4F7F\u7528 CSS \u9009\u62E9\u5668\u76D1\u63A7\u7F51\u9875\n rsso.ask &lt;url&gt; &lt;\u9700\u6C42&gt; - AI \u751F\u6210\u9009\u62E9\u5668\u540E\u521B\u5EFA\u7F51\u9875\u8BA2\u9605\n rsso.watch &lt;url&gt; [\u5173\u952E\u8BCD] - \u7B80\u5355\u7F51\u9875 / \u5173\u952E\u8BCD\u76D1\u63A7\n\n## \u5E38\u7528\u6A21\u677F:\n content - \u7EAF\u6587\u5B57\u6B63\u6587\n default - \u9ED8\u8BA4\u56FE\u7247\u6A21\u677F\n custom - \u81EA\u5B9A\u4E49\u6A21\u677F\n only text - \u4EC5\u6587\u672C\n only image - \u4EC5\u56FE\u7247\n only media - \u56FE\u7247 + \u89C6\u9891\n link - \u8DDF\u968F\u6B63\u6587\u4E2D\u7684\u7B2C\u4E00\u4E2A\u94FE\u63A5\n\n## \u5E38\u7528\u793A\u4F8B:\n rsso https://example.com/rss\n rsso -T tg:woshadiao\n rsso https://example.com/rss -i content -t \"\u793A\u4F8B\u8BA2\u9605\"\n rsso.list\n rsso.edit 1 -t \"\u65B0\u6807\u9898\"\n rsso.html https://example.com -s \".news-item\"\n\n</details>\n";
2
2
  export declare const quickList: {
3
3
  prefix: string;
4
4
  name: string;
package/lib/constants.js CHANGED
@@ -21,8 +21,8 @@ exports.usage = `
21
21
  rsso.pull &lt;id&gt; - 拉取订阅最新内容
22
22
  rsso.follow &lt;id&gt; [--all] - 关注订阅更新 / 全员提醒
23
23
  rsso.edit &lt;id&gt; [选项] - 修改标题、URL、模板、选择器、目标
24
- rsso.cache - 消息缓存管理
25
- rsso.queue - 发送队列管理
24
+ rsso.cache - 消息缓存管理(list/search/stats/pull/clear/cleanup)
25
+ rsso.queue - 发送队列管理(stats/retry/cleanup)
26
26
 
27
27
  ## 网页监控相关:
28
28
  rsso.html &lt;url&gt; -s &lt;selector&gt; - 使用 CSS 选择器监控网页
@@ -46,10 +46,6 @@ exports.usage = `
46
46
  rsso.edit 1 -t "新标题"
47
47
  rsso.html https://example.com -s ".news-item"
48
48
 
49
- ## 兼容提示:
50
- 旧选项 -l / -r / -f / -p 仍会返回迁移提示,
51
- 建议改用 rsso.list / rsso.remove / rsso.follow / rsso.pull。
52
-
53
49
  </details>
54
50
  `;
55
51
  exports.quickList = [
@@ -33,6 +33,11 @@ export declare function renderLoadedHtml(deps: ItemProcessorRuntimeDeps, html: c
33
33
  export declare function renderTemplatedDescription(deps: ItemProcessorRuntimeDeps, item: any, arg: rssArg, description: string, options?: RenderDescriptionOptions): Promise<string>;
34
34
  /**
35
35
  * 提取 HTML 中的视频资源,并按配置补齐 poster 图。
36
+ *
37
+ * 视频的发送方式由 config.basic.videoMode 决定(assets/File/base64/href/filter)。
38
+ * 仅在 videoMode === 'base64' 时强制内联 base64;其它模式交给 getVideoUrl 内部
39
+ * 按 videoMode 走 file:// / assets / href / filter 分支。
40
+ * (历史代码曾对所有视频硬编码 useBase64Mode=true,导致 videoMode 配置失效。)
36
41
  */
37
42
  export declare function processVideos(deps: ItemProcessorRuntimeDeps, html: cheerio.CheerioAPI, arg: rssArg, videoList: Array<[string, string]>): Promise<void>;
38
43
  /**
@@ -154,11 +154,18 @@ async function renderTemplatedDescription(deps, item, arg, description, options)
154
154
  }
155
155
  /**
156
156
  * 提取 HTML 中的视频资源,并按配置补齐 poster 图。
157
+ *
158
+ * 视频的发送方式由 config.basic.videoMode 决定(assets/File/base64/href/filter)。
159
+ * 仅在 videoMode === 'base64' 时强制内联 base64;其它模式交给 getVideoUrl 内部
160
+ * 按 videoMode 走 file:// / assets / href / filter 分支。
161
+ * (历史代码曾对所有视频硬编码 useBase64Mode=true,导致 videoMode 配置失效。)
157
162
  */
158
163
  async function processVideos(deps, html, arg, videoList) {
164
+ const videoMode = deps.config.basic?.videoMode;
165
+ const useVideoBase64 = videoMode === 'base64';
159
166
  await Promise.all(html('video').map(async (_, element) => {
160
167
  videoList.push([
161
- await (0, media_1.getVideoUrl)(deps.ctx, deps.config, deps.$http, element.attribs.src, arg, true, element),
168
+ await (0, media_1.getVideoUrl)(deps.ctx, deps.config, deps.$http, element.attribs.src, arg, useVideoBase64, element),
162
169
  (element.attribs.poster && deps.config.basic?.usePoster)
163
170
  ? await (0, media_1.getImageUrl)(deps.ctx, deps.config, deps.$http, element.attribs.poster, arg, true)
164
171
  : '',