@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,30 @@
1
+ /**
2
+ * In-memory RSS models (slice-2) until Plugin Runtime DatabaseFeature Resource lands.
3
+ *
4
+ * 模型表面与 `@zhin.js/plugin-runtime` 的 DatabaseHostModel 结构兼容:
5
+ * select 的 where 返回 PromiseLike(host 返回链式 Selection,memory 返回 Promise),
6
+ * 这样 plugin.ts 可以把 PluginDatabaseHost 直接当 RssMemoryDb 用。
7
+ */
8
+ export type RssRow = Record<string, unknown>;
9
+ export interface RssModel {
10
+ select: (...fields: string[]) => {
11
+ where: (query: Record<string, unknown>) => PromiseLike<RssRow[]>;
12
+ then: <TResult1 = RssRow[], TResult2 = never>(onfulfilled?: ((value: RssRow[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null) => Promise<TResult1 | TResult2>;
13
+ };
14
+ insert: (row: Record<string, unknown>) => Promise<unknown>;
15
+ delete: () => {
16
+ where: (query: Record<string, unknown>) => Promise<unknown>;
17
+ };
18
+ update: (patch: Record<string, unknown>) => {
19
+ where: (query: Record<string, unknown>) => Promise<unknown>;
20
+ };
21
+ }
22
+ export interface RssMemoryDb {
23
+ models: {
24
+ get: (name: string) => RssModel | undefined;
25
+ };
26
+ }
27
+ export declare const RSS_SUBS_TABLE = "rss_subscriptions";
28
+ export declare const RSS_SEEN_TABLE = "rss_seen_items";
29
+ export declare function createInMemoryRssDb(): RssMemoryDb;
30
+ //# sourceMappingURL=memory-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7C,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,KAAK;QAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;QACjE,IAAI,EAAE,CAAC,QAAQ,GAAG,MAAM,EAAE,EAAE,QAAQ,GAAG,KAAK,EAC1C,WAAW,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,EAC5E,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,KACxE,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC;KACnC,CAAC;IACF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,MAAM,EAAE,MAAM;QAAE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IAC9E,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK;QAC1C,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;KAC7D,CAAC;CACH;AA6CD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QACN,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,QAAQ,GAAG,SAAS,CAAC;KAC7C,CAAC;CACH;AAED,eAAO,MAAM,cAAc,sBAAsB,CAAC;AAClD,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,wBAAgB,mBAAmB,IAAI,WAAW,CAUjD"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * In-memory RSS models (slice-2) until Plugin Runtime DatabaseFeature Resource lands.
3
+ *
4
+ * 模型表面与 `@zhin.js/plugin-runtime` 的 DatabaseHostModel 结构兼容:
5
+ * select 的 where 返回 PromiseLike(host 返回链式 Selection,memory 返回 Promise),
6
+ * 这样 plugin.ts 可以把 PluginDatabaseHost 直接当 RssMemoryDb 用。
7
+ */
8
+ function createMemoryModel() {
9
+ const rows = [];
10
+ let nextId = 1;
11
+ function matches(row, query) {
12
+ return Object.entries(query).every(([key, value]) => row[key] === value);
13
+ }
14
+ function all() {
15
+ return rows.map((row) => ({ ...row }));
16
+ }
17
+ return {
18
+ select: () => {
19
+ const promise = Promise.resolve(all());
20
+ return {
21
+ where: async (query) => rows.filter((row) => matches(row, query)).map((row) => ({ ...row })),
22
+ then: (onfulfilled, onrejected) => promise.then(onfulfilled, onrejected),
23
+ };
24
+ },
25
+ insert: async (row) => {
26
+ const withId = { id: String(nextId++), ...row };
27
+ rows.push(withId);
28
+ return { ...withId };
29
+ },
30
+ delete: () => ({
31
+ where: async (query) => {
32
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
33
+ if (matches(rows[i], query))
34
+ rows.splice(i, 1);
35
+ }
36
+ },
37
+ }),
38
+ update: (patch) => ({
39
+ where: async (query) => {
40
+ for (const row of rows) {
41
+ if (matches(row, query))
42
+ Object.assign(row, patch);
43
+ }
44
+ },
45
+ }),
46
+ };
47
+ }
48
+ export const RSS_SUBS_TABLE = 'rss_subscriptions';
49
+ export const RSS_SEEN_TABLE = 'rss_seen_items';
50
+ export function createInMemoryRssDb() {
51
+ const models = new Map([
52
+ [RSS_SUBS_TABLE, createMemoryModel()],
53
+ [RSS_SEEN_TABLE, createMemoryModel()],
54
+ ]);
55
+ return {
56
+ models: {
57
+ get: (name) => models.get(name),
58
+ },
59
+ };
60
+ }
61
+ //# sourceMappingURL=memory-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,SAAS,iBAAiB;IACxB,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,SAAS,OAAO,CAAC,GAA4B,EAAE,KAA8B;QAC3E,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC;IAC3E,CAAC;IAED,SAAS,GAAG;QACV,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,OAAO;QACL,MAAM,EAAE,GAAG,EAAE;YACX,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YACvC,OAAO;gBACL,KAAK,EAAE,KAAK,EAAE,KAA8B,EAAE,EAAE,CAC9C,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;gBACtE,IAAI,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC;aACzE,CAAC;QACJ,CAAC;QACD,MAAM,EAAE,KAAK,EAAE,GAA4B,EAAE,EAAE;YAC7C,MAAM,MAAM,GAAW,EAAE,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC;YACxD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAClB,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC;QACvB,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACb,KAAK,EAAE,KAAK,EAAE,KAA8B,EAAE,EAAE;gBAC9C,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC7C,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC;wBAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;SACF,CAAC;QACF,MAAM,EAAE,CAAC,KAA8B,EAAE,EAAE,CAAC,CAAC;YAC3C,KAAK,EAAE,KAAK,EAAE,KAA8B,EAAE,EAAE;gBAC9C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;oBACvB,IAAI,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC;wBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBACrD,CAAC;YACH,CAAC;SACF,CAAC;KACH,CAAC;AACJ,CAAC;AAQD,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAClD,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C,MAAM,UAAU,mBAAmB;IACjC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAmB;QACvC,CAAC,cAAc,EAAE,iBAAiB,EAAE,CAAC;QACrC,CAAC,cAAc,EAAE,iBAAiB,EAAE,CAAC;KACtC,CAAC,CAAC;IACH,OAAO;QACL,MAAM,EAAE;YACN,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;SACxC;KACF,CAAC;AACJ,CAAC"}
package/lib/poll.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Feed poll helpers: detect new items; optional OutboundHost push to subscribers.
3
+ */
4
+ import { type FeedItem, type RssConfig } from './feed.js';
5
+ export type RssOutboundPush = (input: {
6
+ readonly adapterName: string;
7
+ readonly endpointKey: string;
8
+ readonly channelType: string;
9
+ readonly channelId: string;
10
+ readonly content: string;
11
+ }) => Promise<void>;
12
+ export declare function setRssOutboundPush(push: RssOutboundPush | null): void;
13
+ export declare function getRssOutboundPush(): RssOutboundPush | null;
14
+ export declare function formatNewItems(feedTitle: string, items: FeedItem[], maxItems: number): string;
15
+ export declare function markItemsSeen(feedUrl: string, items: FeedItem[]): Promise<void>;
16
+ export interface CheckResult {
17
+ readonly text: string;
18
+ readonly totalNew: number;
19
+ readonly pushed: number;
20
+ }
21
+ /**
22
+ * Check subscribed feeds for new items and mark them seen.
23
+ * When OutboundHost is wired, pushes formatted text to each subscriber channel.
24
+ */
25
+ export declare function checkSubscriptions(options: {
26
+ urls: readonly string[];
27
+ config: RssConfig;
28
+ }): Promise<CheckResult>;
29
+ /**
30
+ * Periodically clean seen records older than 7 days.
31
+ */
32
+ export declare function cleanOldSeen(): Promise<void>;
33
+ /**
34
+ * Poll all unique feed URLs across subscriptions.
35
+ */
36
+ export declare function pollAllFeeds(config: RssConfig): Promise<CheckResult>;
37
+ //# sourceMappingURL=poll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAa,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAGrE,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAIpB,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAErE;AAED,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAE3D;AAMD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY7F;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AA4BD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,SAAS,CAAC;CACnB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuCvB;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBlD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAO1E"}
package/lib/poll.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Feed poll helpers: detect new items; optional OutboundHost push to subscribers.
3
+ */
4
+ import { fetchFeed } from './feed.js';
5
+ import { getRssSeen, getRssSubs } from './db-store.js';
6
+ let _outboundPush = null;
7
+ export function setRssOutboundPush(push) {
8
+ _outboundPush = push;
9
+ }
10
+ export function getRssOutboundPush() {
11
+ return _outboundPush;
12
+ }
13
+ function ts() {
14
+ return new Date().toISOString();
15
+ }
16
+ export function formatNewItems(feedTitle, items, maxItems) {
17
+ const lines = [`${feedTitle} 有 ${items.length} 条新内容:`];
18
+ for (const item of items.slice(0, maxItems)) {
19
+ lines.push('');
20
+ lines.push(`${item.title}`);
21
+ if (item.summary)
22
+ lines.push(item.summary);
23
+ if (item.link)
24
+ lines.push(item.link);
25
+ }
26
+ if (items.length > maxItems) {
27
+ lines.push(`\n...还有 ${items.length - maxItems} 条,请查看源站`);
28
+ }
29
+ return lines.join('\n');
30
+ }
31
+ export async function markItemsSeen(feedUrl, items) {
32
+ const Seen = getRssSeen();
33
+ if (!Seen)
34
+ return;
35
+ for (const item of items) {
36
+ const dup = await Seen.select().where({ feed_url: feedUrl, item_guid: item.guid });
37
+ if (dup.length === 0) {
38
+ await Seen.insert({
39
+ feed_url: feedUrl,
40
+ item_guid: item.guid,
41
+ item_title: item.title,
42
+ seen_at: ts(),
43
+ });
44
+ }
45
+ }
46
+ }
47
+ async function pushToSubscribers(feedUrl, content) {
48
+ const push = _outboundPush;
49
+ const Subs = getRssSubs();
50
+ if (!push || !Subs)
51
+ return 0;
52
+ const subscribers = await Subs.select().where({ url: feedUrl });
53
+ let pushed = 0;
54
+ for (const sub of subscribers) {
55
+ const adapterName = String(sub.adapter_name ?? '');
56
+ const channelId = String(sub.channel_id ?? '');
57
+ if (!adapterName || !channelId)
58
+ continue;
59
+ try {
60
+ await push({
61
+ adapterName,
62
+ endpointKey: String(sub.endpoint_id ?? adapterName),
63
+ channelType: String(sub.channel_type ?? 'private'),
64
+ channelId,
65
+ content,
66
+ });
67
+ pushed += 1;
68
+ }
69
+ catch {
70
+ // OutboundHost already logs; continue other subscribers.
71
+ }
72
+ }
73
+ return pushed;
74
+ }
75
+ /**
76
+ * Check subscribed feeds for new items and mark them seen.
77
+ * When OutboundHost is wired, pushes formatted text to each subscriber channel.
78
+ */
79
+ export async function checkSubscriptions(options) {
80
+ const Seen = getRssSeen();
81
+ const Subs = getRssSubs();
82
+ if (!Seen || !Subs) {
83
+ return { text: 'RSS 数据库尚未就绪', totalNew: 0, pushed: 0 };
84
+ }
85
+ const parts = [];
86
+ let totalNew = 0;
87
+ let pushed = 0;
88
+ for (const url of options.urls) {
89
+ try {
90
+ const { title, items } = await fetchFeed(url, options.config.timeout);
91
+ const seenRows = await Seen.select().where({ feed_url: url });
92
+ const seenGuids = new Set(seenRows.map((r) => String(r.item_guid ?? '')));
93
+ const newItems = items.filter((item) => item.guid && !seenGuids.has(item.guid));
94
+ if (newItems.length === 0) {
95
+ parts.push(`${title || url}: 无新内容`);
96
+ continue;
97
+ }
98
+ await markItemsSeen(url, newItems);
99
+ totalNew += newItems.length;
100
+ const body = formatNewItems(title || url, newItems, options.config.maxItems);
101
+ parts.push(body);
102
+ pushed += await pushToSubscribers(url, body);
103
+ }
104
+ catch (e) {
105
+ parts.push(`${url}: 检查失败 — ${e.message}`);
106
+ }
107
+ }
108
+ if (parts.length === 0)
109
+ return { text: '没有可检查的订阅', totalNew: 0, pushed: 0 };
110
+ const pushNote = _outboundPush
111
+ ? (pushed > 0 ? `,已推送 ${pushed} 个会话` : ',无出站推送')
112
+ : '(未装配 OutboundHost,仅本地标记)';
113
+ const header = totalNew > 0
114
+ ? `检查完成,共 ${totalNew} 条新内容${pushNote}`
115
+ : '检查完成,暂无新内容';
116
+ return { text: `${header}\n\n${parts.join('\n\n')}`, totalNew, pushed };
117
+ }
118
+ /**
119
+ * Periodically clean seen records older than 7 days.
120
+ */
121
+ export async function cleanOldSeen() {
122
+ const Seen = getRssSeen();
123
+ if (!Seen)
124
+ return;
125
+ try {
126
+ const cutoff = new Date();
127
+ cutoff.setDate(cutoff.getDate() - 7);
128
+ const cutoffStr = cutoff.toISOString();
129
+ const all = await Seen.select();
130
+ const expired = all.filter((r) => typeof r.seen_at === 'string' && r.seen_at < cutoffStr);
131
+ for (const row of expired) {
132
+ // 表结构没有 id 列,按业务键 (feed_url, item_guid) 删除。
133
+ await Seen.delete().where({ feed_url: row.feed_url, item_guid: row.item_guid });
134
+ }
135
+ }
136
+ catch (e) {
137
+ // Cleanup failure must not break the main flow, but it must be visible.
138
+ console.warn(`[rss] cleanOldSeen failed: ${e instanceof Error ? e.message : String(e)}`);
139
+ }
140
+ }
141
+ /**
142
+ * Poll all unique feed URLs across subscriptions.
143
+ */
144
+ export async function pollAllFeeds(config) {
145
+ const Subs = getRssSubs();
146
+ if (!Subs)
147
+ return { text: 'RSS 数据库尚未就绪', totalNew: 0, pushed: 0 };
148
+ const all = await Subs.select();
149
+ const urls = [...new Set(all.map((s) => String(s.url ?? '')).filter(Boolean))];
150
+ if (urls.length === 0)
151
+ return { text: '暂无任何 RSS 订阅', totalNew: 0, pushed: 0 };
152
+ return checkSubscriptions({ urls, config });
153
+ }
154
+ //# sourceMappingURL=poll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poll.js","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export interface RssAgentDeps {
2
+ getSubs: () => {
3
+ select: () => Promise<unknown[]>;
4
+ } | null;
5
+ fetchFeed: (url: string) => Promise<{
6
+ title: string;
7
+ items: Array<{
8
+ title: string;
9
+ link: string;
10
+ }>;
11
+ }>;
12
+ }
13
+ export declare function setRssAgentDeps(next: RssAgentDeps): void;
14
+ export declare function getRssAgentDeps(): RssAgentDeps;
15
+ //# sourceMappingURL=rss-agent-deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rss-agent-deps.d.ts","sourceRoot":"","sources":["../src/rss-agent-deps.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM;QAAE,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3D,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CACvG;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAExD;AAED,wBAAgB,eAAe,IAAI,YAAY,CAG9C"}
@@ -0,0 +1,10 @@
1
+ let deps = null;
2
+ export function setRssAgentDeps(next) {
3
+ deps = next;
4
+ }
5
+ export function getRssAgentDeps() {
6
+ if (!deps)
7
+ throw new Error('rss agent deps not initialized');
8
+ return deps;
9
+ }
10
+ //# sourceMappingURL=rss-agent-deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rss-agent-deps.js","sourceRoot":"","sources":["../src/rss-agent-deps.ts"],"names":[],"mappings":"AAKA,IAAI,IAAI,GAAwB,IAAI,CAAC;AAErC,MAAM,UAAU,eAAe,CAAC,IAAkB;IAChD,IAAI,GAAG,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-rss",
3
- "version": "1.0.1",
4
- "description": "RSS/Atom feed subscription & push plugin for Zhin.js — subscribe feeds, auto-poll, push new items to groups",
3
+ "version": "1.1.0",
4
+ "description": "RSS/Atom feed subscription plugin for Zhin.js (Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -14,11 +14,12 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
+ "plugin.js",
18
+ "schema.json",
19
+ "commands",
17
20
  "src",
18
21
  "lib",
19
- "client",
20
- "dist",
21
- "skills",
22
+ "agent",
22
23
  "README.md"
23
24
  ],
24
25
  "keywords": [
@@ -30,10 +31,7 @@
30
31
  "atom",
31
32
  "feed",
32
33
  "subscription",
33
- "push",
34
- "notify",
35
- "chatbot",
36
- "im"
34
+ "push"
37
35
  ],
38
36
  "author": {
39
37
  "name": "lc-cn",
@@ -46,10 +44,27 @@
46
44
  },
47
45
  "devDependencies": {
48
46
  "typescript": "^6.0.3",
49
- "zhin.js": "2.0.1"
47
+ "vitest": "^4.1.10",
48
+ "zod": "^4.4.3",
49
+ "@zhin.js/agent": "1.1.23",
50
+ "zhin.js": "1.1.0"
50
51
  },
51
52
  "peerDependencies": {
52
- "zhin.js": "2.0.1"
53
+ "zod": "^4.0.0",
54
+ "@zhin.js/agent": "^1.1.23",
55
+ "@zhin.js/command": "^1.1.0",
56
+ "zhin.js": "^1.1.0"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@zhin.js/agent": {
60
+ "optional": true
61
+ },
62
+ "@zhin.js/command": {
63
+ "optional": true
64
+ },
65
+ "zod": {
66
+ "optional": true
67
+ }
53
68
  },
54
69
  "repository": {
55
70
  "type": "git",
@@ -60,8 +75,26 @@
60
75
  "access": "public",
61
76
  "registry": "https://registry.npmjs.org"
62
77
  },
78
+ "engines": {
79
+ "node": "^20.19.0 || >=22.12.0"
80
+ },
81
+ "zhin": {
82
+ "protocol": 1,
83
+ "type": "plugin",
84
+ "entry": "./plugin.js",
85
+ "engine": "^1.0.0",
86
+ "runtime": "trusted",
87
+ "features": [
88
+ {
89
+ "package": "@zhin.js/command",
90
+ "api": "^1.0.0"
91
+ }
92
+ ],
93
+ "plugins": []
94
+ },
63
95
  "scripts": {
64
- "build": "tsc --build",
65
- "clean": "rimraf lib"
96
+ "build": "tsc",
97
+ "clean": "rimraf lib",
98
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/utils/rss/tests"
66
99
  }
67
100
  }
package/plugin.js ADDED
@@ -0,0 +1,93 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePlugin, databaseHostToken, outboundHostToken, scheduleHostToken, } from 'zhin.js';
3
+ import { fetchFeed, resolveRssConfig } from "./lib/feed.js";
4
+ import { ensureRssMemoryDb, getRssSubs, provideRssDb, RSS_SEEN_TABLE, RSS_SUBS_TABLE, } from "./lib/db-store.js";
5
+ import { setRssAgentDeps } from "./lib/rss-agent-deps.js";
6
+ import { cleanOldSeen, pollAllFeeds, setRssOutboundPush } from "./lib/poll.js";
7
+ function defineRssTables(db) {
8
+ db.define(RSS_SUBS_TABLE, {
9
+ url: { type: 'text', nullable: false },
10
+ feed_title: { type: 'text', default: '' },
11
+ adapter_name: { type: 'text', nullable: false },
12
+ endpoint_id: { type: 'text', default: '' },
13
+ channel_type: { type: 'text', default: 'private' },
14
+ channel_id: { type: 'text', nullable: false },
15
+ creator_id: { type: 'text', default: '' },
16
+ creator_name: { type: 'text', default: '' },
17
+ created_at: { type: 'text', default: '' },
18
+ });
19
+ db.define(RSS_SEEN_TABLE, {
20
+ feed_url: { type: 'text', nullable: false },
21
+ item_guid: { type: 'text', nullable: false },
22
+ item_title: { type: 'text', default: '' },
23
+ seen_at: { type: 'text', default: '' },
24
+ });
25
+ }
26
+ /**
27
+ * Plugin Runtime:
28
+ * - Commands: add/list/remove/check.
29
+ * - DB: prefer `databaseHostToken`; else in-memory.
30
+ * - Outbound: `outboundHostToken` pushes new items to subscriber channels.
31
+ * - Cron: `scheduleHostToken` poll job when available.
32
+ */
33
+ export default definePlugin({
34
+ name: 'rss',
35
+ metadata: {
36
+ displayName: 'RSS',
37
+ },
38
+ setup(context) {
39
+ const config = resolveRssConfig(context.config.get());
40
+ if (context.resources.has(databaseHostToken)) {
41
+ const host = context.resources.use(databaseHostToken);
42
+ defineRssTables(host);
43
+ // provide 自动挂 lifecycle:代际结束反注册,避免悬挂到上一代的 host
44
+ provideRssDb(context, host);
45
+ }
46
+ else {
47
+ ensureRssMemoryDb();
48
+ }
49
+ if (context.resources.has(outboundHostToken)) {
50
+ const outbound = context.resources.use(outboundHostToken);
51
+ setRssOutboundPush(async (input) => {
52
+ await outbound.send({
53
+ adapter: input.adapterName,
54
+ endpointKey: input.endpointKey,
55
+ conversation: {
56
+ kind: input.channelType,
57
+ id: input.channelId,
58
+ },
59
+ content: input.content,
60
+ });
61
+ });
62
+ context.lifecycle.add(() => setRssOutboundPush(null));
63
+ }
64
+ setRssAgentDeps({
65
+ getSubs: () => getRssSubs(),
66
+ fetchFeed: (url) => fetchFeed(url, config.timeout).then((f) => ({
67
+ title: f.title,
68
+ items: f.items.map((i) => ({ title: i.title, link: i.link })),
69
+ })),
70
+ });
71
+ if (!context.resources.has(scheduleHostToken))
72
+ return;
73
+ const schedule = context.resources.use(scheduleHostToken);
74
+ const dispose = schedule.register({
75
+ id: 'rss/poll_feeds',
76
+ cron: config.pollCron,
77
+ description: 'Poll RSS subscriptions',
78
+ async execute() {
79
+ await pollAllFeeds(config);
80
+ },
81
+ });
82
+ const disposeClean = schedule.register({
83
+ id: 'rss/clean_old_seen',
84
+ cron: '0 0 4 * * *',
85
+ description: 'Clean seen records older than 7 days',
86
+ async execute() {
87
+ await cleanOldSeen();
88
+ },
89
+ });
90
+ context.lifecycle.add(dispose);
91
+ context.lifecycle.add(disposeClean);
92
+ },
93
+ });
package/schema.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "pollCron": {
7
+ "type": "string",
8
+ "default": "0 */5 * * * *",
9
+ "description": "轮询频率 (6 段 Cron 表达式)"
10
+ },
11
+ "maxPerGroup": {
12
+ "type": "number",
13
+ "default": 30,
14
+ "minimum": 1,
15
+ "maximum": 200
16
+ },
17
+ "maxItems": {
18
+ "type": "number",
19
+ "default": 5,
20
+ "minimum": 1,
21
+ "maximum": 20
22
+ },
23
+ "timeout": {
24
+ "type": "number",
25
+ "default": 15000,
26
+ "minimum": 1000,
27
+ "maximum": 60000
28
+ }
29
+ }
30
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Extract subscription channel keys from Runtime Message (`input`).
3
+ * Gaps vs legacy: no `$channel` / `$group` — use `conversation` / `metadata`.
4
+ */
5
+
6
+ export interface ChannelInfo {
7
+ adapterName: string;
8
+ endpointKey: string;
9
+ channelType: string;
10
+ channelId: string;
11
+ senderId: string;
12
+ senderName: string;
13
+ }
14
+
15
+ /** Smoke / agent / execute()-without-dispatch fallback. */
16
+ export const SMOKE_CHANNEL: ChannelInfo = {
17
+ adapterName: 'smoke',
18
+ endpointKey: 'local',
19
+ channelType: 'private',
20
+ channelId: 'global',
21
+ senderId: '',
22
+ senderName: '',
23
+ };
24
+
25
+ export function extractChannelInfo(input: unknown): ChannelInfo {
26
+ if (!input || typeof input !== 'object') return { ...SMOKE_CHANNEL };
27
+
28
+ const msg = input as {
29
+ conversation?: {
30
+ endpoint?: { id?: string; adapter?: string };
31
+ kind?: string;
32
+ id?: string;
33
+ };
34
+ sender?: string;
35
+ metadata?: Record<string, unknown>;
36
+ };
37
+ const meta = msg.metadata ?? {};
38
+ const conversation = msg.conversation;
39
+ const channelType = String(
40
+ meta.channelType ?? meta.type ?? meta.channel_type ?? conversation?.kind ?? 'private',
41
+ );
42
+ const channelId = String(
43
+ meta.channelId ?? meta.channel_id ?? conversation?.id ?? SMOKE_CHANNEL.channelId,
44
+ );
45
+ const endpointCapabilityId = typeof conversation?.endpoint?.id === 'string'
46
+ ? conversation.endpoint.id
47
+ : undefined;
48
+ const adapterName = String(
49
+ (endpointCapabilityId?.split('\0').pop() ?? endpointCapabilityId)?.split('~')[0]
50
+ ?? meta.adapter
51
+ ?? SMOKE_CHANNEL.adapterName,
52
+ );
53
+ const endpointKey = String(
54
+ meta.endpoint ?? meta.endpointKey ?? meta.endpoint_id ?? adapterName,
55
+ );
56
+ const senderId = String(msg.sender ?? meta.senderId ?? '');
57
+ const senderName = String(meta.senderName ?? meta.name ?? '');
58
+
59
+ if (!channelId) return { ...SMOKE_CHANNEL, adapterName, endpointKey, senderId, senderName };
60
+
61
+ return {
62
+ adapterName,
63
+ endpointKey,
64
+ channelType,
65
+ channelId,
66
+ senderId,
67
+ senderName,
68
+ };
69
+ }