@zhin.js/plugin-rss 1.0.1 → 1.1.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.
Files changed (51) hide show
  1. package/agent/tools/rss_list_subscriptions.ts +38 -0
  2. package/agent/tools/rss_preview_feed.ts +22 -0
  3. package/commands/rss-add/[url].js +59 -0
  4. package/commands/rss-add/[url].ts +61 -0
  5. package/commands/rss-check/[[url]].js +42 -0
  6. package/commands/rss-check/[[url]].ts +41 -0
  7. package/commands/rss-list.js +28 -0
  8. package/commands/rss-list.ts +31 -0
  9. package/commands/rss-preview/[url].js +22 -0
  10. package/commands/rss-preview/[url].ts +19 -0
  11. package/commands/rss-remove/[url].js +34 -0
  12. package/commands/rss-remove/[url].ts +36 -0
  13. package/lib/channel.d.ts +16 -0
  14. package/lib/channel.d.ts.map +1 -0
  15. package/lib/channel.js +42 -0
  16. package/lib/channel.js.map +1 -0
  17. package/lib/db-store.d.ts +16 -0
  18. package/lib/db-store.d.ts.map +1 -0
  19. package/lib/db-store.js +40 -0
  20. package/lib/db-store.js.map +1 -0
  21. package/lib/feed.d.ts +29 -0
  22. package/lib/feed.d.ts.map +1 -0
  23. package/lib/feed.js +89 -0
  24. package/lib/feed.js.map +1 -0
  25. package/lib/index.d.ts +6 -1
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +4 -492
  28. package/lib/index.js.map +1 -1
  29. package/lib/memory-store.d.ts +30 -0
  30. package/lib/memory-store.d.ts.map +1 -0
  31. package/lib/memory-store.js +61 -0
  32. package/lib/memory-store.js.map +1 -0
  33. package/lib/poll.d.ts +37 -0
  34. package/lib/poll.d.ts.map +1 -0
  35. package/lib/poll.js +154 -0
  36. package/lib/poll.js.map +1 -0
  37. package/lib/rss-agent-deps.d.ts +15 -0
  38. package/lib/rss-agent-deps.d.ts.map +1 -0
  39. package/lib/rss-agent-deps.js +10 -0
  40. package/lib/rss-agent-deps.js.map +1 -0
  41. package/package.json +46 -13
  42. package/plugin.js +93 -0
  43. package/schema.json +30 -0
  44. package/src/channel.ts +69 -0
  45. package/src/db-store.ts +59 -0
  46. package/src/feed.ts +106 -0
  47. package/src/index.ts +20 -556
  48. package/src/memory-store.ts +88 -0
  49. package/src/poll.ts +171 -0
  50. package/src/rss-agent-deps.ts +15 -0
  51. /package/{skills/rss/SKILL.md → agent/skills/rss.md} +0 -0
@@ -0,0 +1,59 @@
1
+ export {
2
+ createInMemoryRssDb,
3
+ RSS_SEEN_TABLE,
4
+ RSS_SUBS_TABLE,
5
+ type RssMemoryDb,
6
+ type RssModel,
7
+ } from './memory-store.js';
8
+
9
+ import { createGenerationStore, type Dispose, type GenerationStoreContext } from 'zhin.js';
10
+ import {
11
+ createInMemoryRssDb,
12
+ RSS_SEEN_TABLE,
13
+ RSS_SUBS_TABLE,
14
+ type RssMemoryDb,
15
+ type RssModel,
16
+ } from './memory-store.js';
17
+
18
+ const rssDbStore = createGenerationStore<RssMemoryDb>('rss/db');
19
+
20
+ let _db: RssMemoryDb | null = null;
21
+ let _memoryDb: RssMemoryDb | null = null;
22
+
23
+ /**
24
+ * Generation-owned database binding used by Plugin Runtime setup().
25
+ * Auto-unregisters when the generation lifecycle disposes, so no stale
26
+ * reference to the previous generation's host survives a reload.
27
+ */
28
+ export function provideRssDb(context: GenerationStoreContext, db: RssMemoryDb): Dispose {
29
+ return rssDbStore.provide(context, db);
30
+ }
31
+
32
+ export function ensureRssMemoryDb(): RssMemoryDb {
33
+ const provided = rssDbStore.tryUse() ?? _db;
34
+ if (provided) return provided;
35
+ if (!_memoryDb) _memoryDb = createInMemoryRssDb();
36
+ return _memoryDb;
37
+ }
38
+
39
+ export function getRssDb(): RssMemoryDb | null {
40
+ return ensureRssMemoryDb();
41
+ }
42
+
43
+ export function setRssDb(db: RssMemoryDb | null): void {
44
+ _db = db;
45
+ }
46
+
47
+ export function resetRssDb(): void {
48
+ rssDbStore.clear();
49
+ _db = null;
50
+ _memoryDb = null;
51
+ }
52
+
53
+ export function getRssSubs(): RssModel | null {
54
+ return ensureRssMemoryDb().models.get(RSS_SUBS_TABLE) ?? null;
55
+ }
56
+
57
+ export function getRssSeen(): RssModel | null {
58
+ return ensureRssMemoryDb().models.get(RSS_SEEN_TABLE) ?? null;
59
+ }
package/src/feed.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * RSS feed fetch/preview helpers (no Plugin host).
3
+ */
4
+ import Parser from 'rss-parser';
5
+
6
+ export interface FeedItem {
7
+ title: string;
8
+ link: string;
9
+ date: string;
10
+ summary: string;
11
+ guid: string;
12
+ }
13
+
14
+ export interface RssConfig {
15
+ readonly pollCron: string;
16
+ readonly maxPerGroup: number;
17
+ readonly maxItems: number;
18
+ readonly timeout: number;
19
+ }
20
+
21
+ export const DEFAULT_RSS_CONFIG: RssConfig = {
22
+ pollCron: '0 */5 * * * *',
23
+ maxPerGroup: 30,
24
+ maxItems: 5,
25
+ timeout: 15_000,
26
+ };
27
+
28
+ export function resolveRssConfig(raw: Partial<RssConfig> | undefined): RssConfig {
29
+ return { ...DEFAULT_RSS_CONFIG, ...raw };
30
+ }
31
+
32
+ /**
33
+ * 剔除 HTML 标签与 `&nbsp;` 实体并折叠空白。
34
+ * 线性单遍扫描实现(手写状态机),语义等价于原先的
35
+ * `/<[^>]*>/g` → `/&nbsp;/g` → `/\s+/g` 三步 replace,但避免:
36
+ * - js/polynomial-redos:大量 `<` 无闭合时正则逐位重扫的二次方回溯;
37
+ * - js/incomplete-multi-character-sanitization:多趟 replace 的残留问题。
38
+ */
39
+ export function stripHtml(html: string): string {
40
+ let out = '';
41
+ let pendingSpace = false;
42
+ let mayHaveTagEnd = html.includes('>');
43
+ let i = 0;
44
+ while (i < html.length) {
45
+ const ch = html[i]!;
46
+ if (ch === '<' && mayHaveTagEnd) {
47
+ const end = html.indexOf('>', i + 1);
48
+ if (end >= 0) {
49
+ i = end + 1;
50
+ continue;
51
+ }
52
+ // 后续不再有 `>`:剩余的 `<` 都是字面字符,不再重复扫描。
53
+ mayHaveTagEnd = false;
54
+ }
55
+ if (ch === '&' && html.startsWith('&nbsp;', i)) {
56
+ pendingSpace = true;
57
+ i += '&nbsp;'.length;
58
+ continue;
59
+ }
60
+ if (/\s/u.test(ch)) {
61
+ pendingSpace = true;
62
+ i += 1;
63
+ continue;
64
+ }
65
+ if (pendingSpace && out.length > 0) out += ' ';
66
+ pendingSpace = false;
67
+ out += ch;
68
+ i += 1;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ export async function fetchFeed(
74
+ url: string,
75
+ timeout = DEFAULT_RSS_CONFIG.timeout,
76
+ ): Promise<{ title: string; items: FeedItem[] }> {
77
+ const parser = new Parser({
78
+ timeout,
79
+ headers: {
80
+ 'User-Agent': 'ZhinBot-RSS/1.0',
81
+ Accept: 'application/rss+xml, application/atom+xml, application/xml, text/xml',
82
+ },
83
+ });
84
+ const feed = await parser.parseURL(url);
85
+ const items: FeedItem[] = (feed.items || []).slice(0, 20).map((item) => ({
86
+ title: (item.title || '').trim(),
87
+ link: (item.link || '').trim(),
88
+ date: item.isoDate || item.pubDate || '',
89
+ summary: stripHtml(item.contentSnippet || item.content || '').slice(0, 200),
90
+ guid: item.guid || item.link || item.title || '',
91
+ }));
92
+ return { title: (feed.title || url).trim(), items };
93
+ }
94
+
95
+ export function formatFeedPreview(title: string, items: FeedItem[]): string {
96
+ if (items.length === 0) return `${title}: 暂无内容`;
97
+ const lines = [`${title} (共 ${items.length} 条)`];
98
+ for (const item of items.slice(0, 5)) {
99
+ lines.push('');
100
+ lines.push(`${item.title}`);
101
+ if (item.date) lines.push(` ${new Date(item.date).toLocaleString('zh-CN')}`);
102
+ if (item.link) lines.push(` ${item.link}`);
103
+ }
104
+ if (items.length > 5) lines.push(`\n...还有 ${items.length - 5} 条`);
105
+ return lines.join('\n');
106
+ }