@zhin.js/plugin-rss 3.0.5 → 3.0.7

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.
@@ -0,0 +1,58 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from '@zhin.js/command';
3
+ import { extractChannelInfo } from "../../lib/channel.js";
4
+ import { getRssSeen, getRssSubs } from "../../lib/db-store.js";
5
+ import { fetchFeed, resolveRssConfig } from "../../lib/feed.js";
6
+ import { markItemsSeen } from "../../lib/poll.js";
7
+ export default defineCommand({
8
+ description: '订阅一个 RSS/Atom 源',
9
+ async execute({ params, config, input }) {
10
+ const Subs = getRssSubs();
11
+ if (!Subs)
12
+ return 'RSS 数据库尚未就绪,请稍后重试';
13
+ const url = String(params.url ?? '').trim();
14
+ if (!url)
15
+ return '请提供 RSS 源地址,格式:rss-add <URL>';
16
+ if (!/^https?:\/\//i.test(url))
17
+ return '请提供有效的 HTTP/HTTPS 地址';
18
+ const channel = extractChannelInfo(input);
19
+ const cfg = resolveRssConfig(config);
20
+ const existing = await Subs.select().where({
21
+ url,
22
+ adapter_name: channel.adapterName,
23
+ channel_type: channel.channelType,
24
+ channel_id: channel.channelId,
25
+ });
26
+ if (existing.length > 0)
27
+ return '当前会话已订阅该源,无需重复添加';
28
+ const currentSubs = await Subs.select().where({
29
+ adapter_name: channel.adapterName,
30
+ channel_type: channel.channelType,
31
+ channel_id: channel.channelId,
32
+ });
33
+ if (currentSubs.length >= cfg.maxPerGroup) {
34
+ return `当前会话订阅数已达上限 (${cfg.maxPerGroup}),请先取消一些`;
35
+ }
36
+ let feedTitle = '';
37
+ try {
38
+ const { title, items } = await fetchFeed(url, cfg.timeout);
39
+ feedTitle = title;
40
+ await markItemsSeen(url, items);
41
+ }
42
+ catch (e) {
43
+ return `无法解析该地址: ${e.message}\n请确认是有效的 RSS/Atom 源`;
44
+ }
45
+ await Subs.insert({
46
+ url,
47
+ feed_title: feedTitle,
48
+ adapter_name: channel.adapterName,
49
+ endpoint_id: channel.endpointId,
50
+ channel_type: channel.channelType,
51
+ channel_id: channel.channelId,
52
+ creator_id: channel.senderId,
53
+ creator_name: channel.senderName,
54
+ created_at: new Date().toISOString(),
55
+ });
56
+ return `订阅成功!\n源: ${feedTitle || url}\n新内容将在轮询时推送到本会话(需 OutboundHost)`;
57
+ },
58
+ });
@@ -0,0 +1,41 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from '@zhin.js/command';
3
+ import { extractChannelInfo } from "../../lib/channel.js";
4
+ import { getRssSubs } from "../../lib/db-store.js";
5
+ import { resolveRssConfig } from "../../lib/feed.js";
6
+ import { checkSubscriptions } from "../../lib/poll.js";
7
+ export default defineCommand({
8
+ description: '手动触发检查 RSS 更新',
9
+ async execute({ params, config, input }) {
10
+ const Subs = getRssSubs();
11
+ if (!Subs)
12
+ return 'RSS 数据库尚未就绪';
13
+ const channel = extractChannelInfo(input);
14
+ const cfg = resolveRssConfig(config);
15
+ const targetUrl = String(params.url ?? '').trim();
16
+ let urls;
17
+ if (targetUrl) {
18
+ const rows = await Subs.select().where({
19
+ url: targetUrl,
20
+ adapter_name: channel.adapterName,
21
+ channel_type: channel.channelType,
22
+ channel_id: channel.channelId,
23
+ });
24
+ if (rows.length === 0)
25
+ return '当前会话未订阅该源';
26
+ urls = [targetUrl];
27
+ }
28
+ else {
29
+ const rows = await Subs.select().where({
30
+ adapter_name: channel.adapterName,
31
+ channel_type: channel.channelType,
32
+ channel_id: channel.channelId,
33
+ });
34
+ if (rows.length === 0)
35
+ return '当前会话没有任何订阅';
36
+ urls = [...new Set(rows.map((r) => String(r.url ?? '')).filter(Boolean))];
37
+ }
38
+ const result = await checkSubscriptions({ urls, config: cfg });
39
+ return result.text;
40
+ },
41
+ });
@@ -0,0 +1,28 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from '@zhin.js/command';
3
+ import { extractChannelInfo } from "../lib/channel.js";
4
+ import { getRssSubs } from "../lib/db-store.js";
5
+ import { resolveRssConfig } from "../lib/feed.js";
6
+ export default defineCommand({
7
+ description: '查看当前会话的所有 RSS 订阅',
8
+ async execute({ config, input }) {
9
+ const Subs = getRssSubs();
10
+ if (!Subs)
11
+ return 'RSS 数据库尚未就绪';
12
+ const channel = extractChannelInfo(input);
13
+ const cfg = resolveRssConfig(config);
14
+ const rows = await Subs.select().where({
15
+ adapter_name: channel.adapterName,
16
+ channel_type: channel.channelType,
17
+ channel_id: channel.channelId,
18
+ });
19
+ if (rows.length === 0) {
20
+ return '当前会话没有订阅任何 RSS 源\n使用 rss-add <URL> 添加订阅';
21
+ }
22
+ const lines = rows.map((r, i) => {
23
+ const title = String(r.feed_title || '(未知标题)');
24
+ return `${i + 1}. ${title}\n ${String(r.url ?? '')}`;
25
+ });
26
+ return `当前订阅 (${rows.length}/${cfg.maxPerGroup}):\n${lines.join('\n')}`;
27
+ },
28
+ });
@@ -0,0 +1,21 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from '@zhin.js/command';
3
+ import { fetchFeed, formatFeedPreview, resolveRssConfig } from "../../lib/feed.js";
4
+ export default defineCommand({
5
+ description: '预览一个 RSS 源的最新内容(不订阅)',
6
+ async execute({ params, config }) {
7
+ const url = String(params.url ?? '').trim();
8
+ if (!url)
9
+ return '请提供 RSS 源地址';
10
+ if (!/^https?:\/\//i.test(url))
11
+ return '请提供有效的 HTTP/HTTPS 地址';
12
+ const cfg = resolveRssConfig(config);
13
+ try {
14
+ const { title, items } = await fetchFeed(url, cfg.timeout);
15
+ return formatFeedPreview(title, items);
16
+ }
17
+ catch (e) {
18
+ return `解析失败: ${e.message}\n请确认是有效的 RSS/Atom 地址`;
19
+ }
20
+ },
21
+ });
@@ -0,0 +1,33 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineCommand } from '@zhin.js/command';
3
+ import { extractChannelInfo } from "../../lib/channel.js";
4
+ import { getRssSubs } from "../../lib/db-store.js";
5
+ export default defineCommand({
6
+ description: '取消一个 RSS 订阅',
7
+ async execute({ params, input }) {
8
+ const Subs = getRssSubs();
9
+ if (!Subs)
10
+ return 'RSS 数据库尚未就绪';
11
+ const url = String(params.url ?? '').trim();
12
+ if (!url)
13
+ return '请提供要取消的 RSS 源地址';
14
+ const channel = extractChannelInfo(input);
15
+ const rows = await Subs.select().where({
16
+ url,
17
+ adapter_name: channel.adapterName,
18
+ channel_type: channel.channelType,
19
+ channel_id: channel.channelId,
20
+ });
21
+ if (rows.length === 0)
22
+ return '未找到该订阅';
23
+ const row = rows[0];
24
+ // 表结构没有 id 列,删除必须按业务键匹配(与上面的 select 条件一致)。
25
+ await Subs.delete().where({
26
+ url,
27
+ adapter_name: channel.adapterName,
28
+ channel_type: channel.channelType,
29
+ channel_id: channel.channelId,
30
+ });
31
+ return `已取消订阅: ${String(row.feed_title || url)}`;
32
+ },
33
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-rss",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "RSS/Atom feed subscription plugin for Zhin.js (Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -14,7 +14,7 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "plugin.ts",
17
+ "plugin.js",
18
18
  "schema.json",
19
19
  "commands",
20
20
  "src",
@@ -41,18 +41,18 @@
41
41
  "license": "MIT",
42
42
  "dependencies": {
43
43
  "rss-parser": "^3.13.0",
44
- "@zhin.js/command": "1.0.3",
45
- "@zhin.js/plugin-runtime": "1.1.1"
44
+ "@zhin.js/plugin-runtime": "1.1.1",
45
+ "@zhin.js/command": "1.0.4"
46
46
  },
47
47
  "devDependencies": {
48
48
  "typescript": "^6.0.3",
49
49
  "vitest": "^4.1.10",
50
50
  "zod": "^4.4.3",
51
- "@zhin.js/agent": "1.0.6"
51
+ "@zhin.js/agent": "1.0.8"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "zod": "^4.0.0",
55
- "@zhin.js/agent": "1.0.6"
55
+ "@zhin.js/agent": "1.0.8"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@zhin.js/agent": {
@@ -77,7 +77,7 @@
77
77
  "zhin": {
78
78
  "protocol": 1,
79
79
  "type": "plugin",
80
- "entry": "./plugin.ts",
80
+ "entry": "./plugin.js",
81
81
  "engine": "^1.0.0",
82
82
  "runtime": "trusted",
83
83
  "features": [
package/plugin.js ADDED
@@ -0,0 +1,91 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePlugin, databaseHostToken, outboundHostToken, scheduleHostToken, } from '@zhin.js/plugin-runtime';
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
+ endpointId: input.endpointId,
55
+ channelType: input.channelType,
56
+ channelId: input.channelId,
57
+ content: input.content,
58
+ });
59
+ });
60
+ context.lifecycle.add(() => setRssOutboundPush(null));
61
+ }
62
+ setRssAgentDeps({
63
+ getSubs: () => getRssSubs(),
64
+ fetchFeed: (url) => fetchFeed(url, config.timeout).then((f) => ({
65
+ title: f.title,
66
+ items: f.items.map((i) => ({ title: i.title, link: i.link })),
67
+ })),
68
+ });
69
+ if (!context.resources.has(scheduleHostToken))
70
+ return;
71
+ const schedule = context.resources.use(scheduleHostToken);
72
+ const dispose = schedule.register({
73
+ id: 'rss/poll_feeds',
74
+ cron: config.pollCron,
75
+ description: 'Poll RSS subscriptions',
76
+ async execute() {
77
+ await pollAllFeeds(config);
78
+ },
79
+ });
80
+ const disposeClean = schedule.register({
81
+ id: 'rss/clean_old_seen',
82
+ cron: '0 0 4 * * *',
83
+ description: 'Clean seen records older than 7 days',
84
+ async execute() {
85
+ await cleanOldSeen();
86
+ },
87
+ });
88
+ context.lifecycle.add(dispose);
89
+ context.lifecycle.add(disposeClean);
90
+ },
91
+ });
package/plugin.ts DELETED
@@ -1,104 +0,0 @@
1
- import {
2
- definePlugin,
3
- databaseHostToken,
4
- outboundHostToken,
5
- scheduleHostToken,
6
- } from '@zhin.js/plugin-runtime';
7
- import { fetchFeed, resolveRssConfig, type RssConfig } from './src/feed.js';
8
- import {
9
- ensureRssMemoryDb,
10
- getRssSubs,
11
- provideRssDb,
12
- RSS_SEEN_TABLE,
13
- RSS_SUBS_TABLE,
14
- } from './src/db-store.js';
15
- import { setRssAgentDeps } from './src/rss-agent-deps.js';
16
- import { cleanOldSeen, pollAllFeeds, setRssOutboundPush } from './src/poll.js';
17
-
18
- function defineRssTables(db: { define: (name: string, schema: Record<string, unknown>) => void }): void {
19
- db.define(RSS_SUBS_TABLE, {
20
- url: { type: 'text', nullable: false },
21
- feed_title: { type: 'text', default: '' },
22
- adapter_name: { type: 'text', nullable: false },
23
- endpoint_id: { type: 'text', default: '' },
24
- channel_type: { type: 'text', default: 'private' },
25
- channel_id: { type: 'text', nullable: false },
26
- creator_id: { type: 'text', default: '' },
27
- creator_name: { type: 'text', default: '' },
28
- created_at: { type: 'text', default: '' },
29
- });
30
- db.define(RSS_SEEN_TABLE, {
31
- feed_url: { type: 'text', nullable: false },
32
- item_guid: { type: 'text', nullable: false },
33
- item_title: { type: 'text', default: '' },
34
- seen_at: { type: 'text', default: '' },
35
- });
36
- }
37
-
38
- /**
39
- * Plugin Runtime:
40
- * - Commands: add/list/remove/check.
41
- * - DB: prefer `databaseHostToken`; else in-memory.
42
- * - Outbound: `outboundHostToken` pushes new items to subscriber channels.
43
- * - Cron: `scheduleHostToken` poll job when available.
44
- */
45
- export default definePlugin<RssConfig>({
46
- name: 'rss',
47
- metadata: {
48
- displayName: 'RSS',
49
- },
50
- setup(context) {
51
- const config = resolveRssConfig(context.config.get());
52
- if (context.resources.has(databaseHostToken)) {
53
- const host = context.resources.use(databaseHostToken);
54
- defineRssTables(host);
55
- // provide 自动挂 lifecycle:代际结束反注册,避免悬挂到上一代的 host
56
- provideRssDb(context, host);
57
- } else {
58
- ensureRssMemoryDb();
59
- }
60
-
61
- if (context.resources.has(outboundHostToken)) {
62
- const outbound = context.resources.use(outboundHostToken);
63
- setRssOutboundPush(async (input) => {
64
- await outbound.send({
65
- adapter: input.adapterName,
66
- endpointId: input.endpointId,
67
- channelType: input.channelType,
68
- channelId: input.channelId,
69
- content: input.content,
70
- });
71
- });
72
- context.lifecycle.add(() => setRssOutboundPush(null));
73
- }
74
-
75
- setRssAgentDeps({
76
- getSubs: () => getRssSubs() as { select: () => Promise<unknown[]> } | null,
77
- fetchFeed: (url) => fetchFeed(url, config.timeout).then((f) => ({
78
- title: f.title,
79
- items: f.items.map((i) => ({ title: i.title, link: i.link })),
80
- })),
81
- });
82
-
83
- if (!context.resources.has(scheduleHostToken)) return;
84
- const schedule = context.resources.use(scheduleHostToken);
85
- const dispose = schedule.register({
86
- id: 'rss/poll_feeds',
87
- cron: config.pollCron,
88
- description: 'Poll RSS subscriptions',
89
- async execute() {
90
- await pollAllFeeds(config);
91
- },
92
- });
93
- const disposeClean = schedule.register({
94
- id: 'rss/clean_old_seen',
95
- cron: '0 0 4 * * *',
96
- description: 'Clean seen records older than 7 days',
97
- async execute() {
98
- await cleanOldSeen();
99
- },
100
- });
101
- context.lifecycle.add(dispose);
102
- context.lifecycle.add(disposeClean);
103
- },
104
- });