@zhin.js/plugin-rss 1.1.9 → 1.1.11

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 (61) hide show
  1. package/README.md +8 -0
  2. package/commands/rss-add/[url]/index.js +12 -10
  3. package/commands/rss-add/[url]/index.ts +12 -10
  4. package/commands/rss-check/[[url]]/index.js +8 -7
  5. package/commands/rss-check/[[url]]/index.ts +8 -7
  6. package/commands/rss-list/index.js +9 -7
  7. package/commands/rss-list/index.ts +9 -7
  8. package/commands/rss-preview/[url]/index.js +7 -6
  9. package/commands/rss-preview/[url]/index.ts +7 -6
  10. package/commands/rss-remove/[url]/index.js +8 -7
  11. package/commands/rss-remove/[url]/index.ts +8 -7
  12. package/lib/admin-i18n.d.ts +219 -0
  13. package/lib/admin-i18n.d.ts.map +1 -0
  14. package/lib/admin-i18n.js +216 -0
  15. package/lib/admin-i18n.js.map +1 -0
  16. package/lib/admin-tasks.d.ts +40 -2
  17. package/lib/admin-tasks.d.ts.map +1 -1
  18. package/lib/admin-tasks.js +99 -72
  19. package/lib/admin-tasks.js.map +1 -1
  20. package/lib/channel.d.ts +2 -1
  21. package/lib/channel.d.ts.map +1 -1
  22. package/lib/channel.js +3 -3
  23. package/lib/channel.js.map +1 -1
  24. package/lib/feed.d.ts +2 -1
  25. package/lib/feed.d.ts.map +1 -1
  26. package/lib/feed.js +6 -5
  27. package/lib/feed.js.map +1 -1
  28. package/lib/i18n.d.ts +2 -0
  29. package/lib/i18n.d.ts.map +1 -0
  30. package/lib/i18n.js +168 -0
  31. package/lib/i18n.js.map +1 -0
  32. package/lib/poll.d.ts +3 -1
  33. package/lib/poll.d.ts.map +1 -1
  34. package/lib/poll.js +23 -18
  35. package/lib/poll.js.map +1 -1
  36. package/lib/quiet-hours.d.ts +5 -0
  37. package/lib/quiet-hours.d.ts.map +1 -1
  38. package/lib/quiet-hours.js +9 -4
  39. package/lib/quiet-hours.js.map +1 -1
  40. package/lib/runtime.d.ts +3 -0
  41. package/lib/runtime.d.ts.map +1 -1
  42. package/lib/runtime.js.map +1 -1
  43. package/lib/storage.d.ts.map +1 -1
  44. package/lib/storage.js +6 -5
  45. package/lib/storage.js.map +1 -1
  46. package/package.json +7 -7
  47. package/plugin.js +6 -2
  48. package/schema.json +6 -1
  49. package/skills/rss/tools/rss_list_subscriptions/index.js +4 -3
  50. package/skills/rss/tools/rss_list_subscriptions/index.ts +4 -3
  51. package/skills/rss/tools/rss_preview_feed/index.js +4 -3
  52. package/skills/rss/tools/rss_preview_feed/index.ts +4 -3
  53. package/src/admin-i18n.ts +218 -0
  54. package/src/admin-tasks.ts +95 -72
  55. package/src/channel.ts +4 -2
  56. package/src/feed.ts +7 -5
  57. package/src/i18n.ts +168 -0
  58. package/src/poll.ts +24 -18
  59. package/src/quiet-hours.ts +11 -4
  60. package/src/runtime.ts +3 -0
  61. package/src/storage.ts +6 -5
package/src/poll.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { resolveLocale, type Locale } from 'zhin.js/i18n';
2
+ import { rssMessages as t } from './i18n.js';
1
3
  /**
2
4
  * Feed poll helpers: detect new items; optional OutboundHost push to subscribers.
3
5
  */
@@ -9,8 +11,8 @@ function ts(): string {
9
11
  return new Date().toISOString();
10
12
  }
11
13
 
12
- export function formatNewItems(feedTitle: string, items: FeedItem[], maxItems: number): string {
13
- const lines = [`${feedTitle} 有 ${items.length} 条新内容:`];
14
+ export function formatNewItems(feedTitle: string, items: FeedItem[], maxItems: number, locale: Locale = 'zh-CN'): string {
15
+ const lines = [t(locale, 'newItems', { title: feedTitle, count: items.length })];
14
16
  for (const item of items.slice(0, maxItems)) {
15
17
  lines.push('');
16
18
  lines.push(`${item.title}`);
@@ -18,7 +20,7 @@ export function formatNewItems(feedTitle: string, items: FeedItem[], maxItems: n
18
20
  if (item.link) lines.push(item.link);
19
21
  }
20
22
  if (items.length > maxItems) {
21
- lines.push(`\n...还有 ${items.length - maxItems} 条,请查看源站`);
23
+ lines.push(t(locale, 'moreSource', { count: items.length - maxItems }));
22
24
  }
23
25
  return lines.join('\n');
24
26
  }
@@ -45,7 +47,7 @@ export interface CheckResult {
45
47
  readonly pushed: number;
46
48
  }
47
49
 
48
- async function pushToSubscribers(runtime: RssRuntime, feedUrl: string, content: string): Promise<number> {
50
+ async function pushToSubscribers(runtime: RssRuntime, feedUrl: string, feedTitle: string, items: FeedItem[]): Promise<number> {
49
51
  const push = runtime.outbound;
50
52
  const Subs = getRssSubs(runtime.db);
51
53
  if (!push || !Subs) return 0;
@@ -66,7 +68,8 @@ async function pushToSubscribers(runtime: RssRuntime, feedUrl: string, content:
66
68
  endpointKey,
67
69
  channelType,
68
70
  channelId,
69
- content,
71
+ locale: resolveLocale(sub.locale),
72
+ content: formatNewItems(feedTitle, items, runtime.config.maxItems, resolveLocale(sub.locale)),
70
73
  });
71
74
  pushed += 1;
72
75
  } catch {
@@ -82,11 +85,13 @@ async function pushToSubscribers(runtime: RssRuntime, feedUrl: string, content:
82
85
  */
83
86
  export async function checkSubscriptions(runtime: RssRuntime, options: {
84
87
  urls: readonly string[];
88
+ locale?: Locale;
85
89
  }): Promise<CheckResult> {
90
+ const locale = resolveLocale(options.locale);
86
91
  const Seen = getRssSeen(runtime.db);
87
92
  const Subs = getRssSubs(runtime.db);
88
93
  if (!Seen || !Subs) {
89
- return { text: 'RSS 数据库尚未就绪', totalNew: 0, pushed: 0 };
94
+ return { text: t(locale, 'databaseUnavailable'), totalNew: 0, pushed: 0 };
90
95
  }
91
96
 
92
97
  const parts: string[] = [];
@@ -100,26 +105,26 @@ export async function checkSubscriptions(runtime: RssRuntime, options: {
100
105
  const seenGuids = new Set(seenRows.map((r) => String(r.item_guid ?? '')));
101
106
  const newItems = items.filter((item) => item.guid && !seenGuids.has(item.guid));
102
107
  if (newItems.length === 0) {
103
- parts.push(`${title || url}: 无新内容`);
108
+ parts.push(t(locale, 'noNew', { title: title || url }));
104
109
  continue;
105
110
  }
106
111
  await markItemsSeen(runtime, url, newItems);
107
112
  totalNew += newItems.length;
108
- const body = formatNewItems(title || url, newItems, runtime.config.maxItems);
113
+ const body = formatNewItems(title || url, newItems, runtime.config.maxItems, locale);
109
114
  parts.push(body);
110
- pushed += await pushToSubscribers(runtime, url, body);
115
+ pushed += await pushToSubscribers(runtime, url, title || url, newItems);
111
116
  } catch (e) {
112
- parts.push(`${url}: 检查失败 — ${(e as Error).message}`);
117
+ parts.push(t(locale, 'checkFailed', { url, error: (e as Error).message }));
113
118
  }
114
119
  }
115
120
 
116
- if (parts.length === 0) return { text: '没有可检查的订阅', totalNew: 0, pushed: 0 };
121
+ if (parts.length === 0) return { text: t(locale, 'nothingToCheck'), totalNew: 0, pushed: 0 };
117
122
  const pushNote = runtime.outbound
118
- ? (pushed > 0 ? `,已推送 ${pushed} 个会话` : ',无出站推送')
119
- : '(未装配 OutboundHost,仅本地标记)';
123
+ ? (pushed > 0 ? t(locale, 'pushed', { count: pushed }) : t(locale, 'notPushed'))
124
+ : t(locale, 'localOnly');
120
125
  const header = totalNew > 0
121
- ? `检查完成,共 ${totalNew} 条新内容${pushNote}`
122
- : '检查完成,暂无新内容';
126
+ ? t(locale, 'checked', { count: totalNew, pushNote })
127
+ : t(locale, 'checkedEmpty');
123
128
  return { text: `${header}\n\n${parts.join('\n\n')}`, totalNew, pushed };
124
129
  }
125
130
 
@@ -149,10 +154,11 @@ export async function cleanOldSeen(runtime: RssRuntime): Promise<void> {
149
154
  * Poll all unique feed URLs across subscriptions.
150
155
  */
151
156
  export async function pollAllFeeds(runtime: RssRuntime): Promise<CheckResult> {
157
+ const locale = resolveLocale(runtime.locale);
152
158
  const Subs = getRssSubs(runtime.db);
153
- if (!Subs) return { text: 'RSS 数据库尚未就绪', totalNew: 0, pushed: 0 };
159
+ if (!Subs) return { text: t(locale, 'databaseUnavailable'), totalNew: 0, pushed: 0 };
154
160
  const all = await Subs.select();
155
161
  const urls = [...new Set(all.map((s) => String(s.url ?? '')).filter(Boolean))];
156
- if (urls.length === 0) return { text: '暂无任何 RSS 订阅', totalNew: 0, pushed: 0 };
157
- return checkSubscriptions(runtime, { urls });
162
+ if (urls.length === 0) return { text: t(locale, 'noFeeds'), totalNew: 0, pushed: 0 };
163
+ return checkSubscriptions(runtime, { urls, locale });
158
164
  }
@@ -1,3 +1,10 @@
1
+ import { adminText, type AdminTextKey } from './admin-i18n.js';
2
+
3
+ export class LocalizedQuietHoursError extends TypeError {
4
+ readonly messageKey: AdminTextKey;
5
+ constructor(messageKey: AdminTextKey) { super(adminText('zh-CN', messageKey)); this.messageKey = messageKey; }
6
+ }
7
+
1
8
  export type RssQuietHours = {
2
9
  readonly timezone: string;
3
10
  readonly start: string;
@@ -8,13 +15,13 @@ export type RssQuietHours = {
8
15
  export function parseQuietHours(parameters: Readonly<Record<string, unknown>>): RssQuietHours | undefined {
9
16
  const values = [parameters.quietTimezone, parameters.quietStart, parameters.quietEnd];
10
17
  if (values.every(value => value === undefined || (typeof value === 'string' && !value.trim()))) return undefined;
11
- if (!values.every(value => typeof value === 'string' && value.trim())) throw new TypeError('静默时区、开始和结束时间须全部填写;三项全部留空关闭静默');
18
+ if (!values.every(value => typeof value === 'string' && value.trim())) throw new LocalizedQuietHoursError('quietFields');
12
19
  const [timezone, start, end] = values.map(value => (value as string).trim());
13
- if (timezone.length > 128 || !/^[A-Za-z][A-Za-z0-9_+/-]*$/u.test(timezone)) throw new TypeError('请填写有效 IANA 时区,如 Asia/Shanghai;不能使用 +08:00 等偏移');
20
+ if (timezone.length > 128 || !/^[A-Za-z][A-Za-z0-9_+/-]*$/u.test(timezone)) throw new LocalizedQuietHoursError('quietZoneOffset');
14
21
  try { new Intl.DateTimeFormat('en-GB', { timeZone: timezone }).format(0); }
15
- catch { throw new TypeError('请填写有效 IANA 时区,如 Asia/Shanghai'); }
22
+ catch { throw new LocalizedQuietHoursError('quietZone'); }
16
23
  if (!/^([01]\d|2[0-3]):[0-5]\d$/u.test(start) || !/^([01]\d|2[0-3]):[0-5]\d$/u.test(end) || start === end) {
17
- throw new TypeError('静默时间须为 HH:mm(24 小时制),开始和结束不能相同');
24
+ throw new LocalizedQuietHoursError('quietClock');
18
25
  }
19
26
  return Object.freeze({ timezone, start, end });
20
27
  }
package/src/runtime.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Locale } from 'zhin.js/i18n';
1
2
  import { createToken } from 'zhin.js';
2
3
  import type { RssConfig } from './feed.js';
3
4
  import type { RssMemoryDb } from './memory-store.js';
@@ -9,9 +10,11 @@ export type RssOutboundPush = (input: {
9
10
  readonly channelType: ChannelInfo['channelType'];
10
11
  readonly channelId: string;
11
12
  readonly content: string;
13
+ readonly locale?: Locale;
12
14
  }) => Promise<void>;
13
15
 
14
16
  export interface RssRuntime {
17
+ readonly locale?: Locale;
15
18
  readonly db: RssMemoryDb;
16
19
  readonly config: Readonly<RssConfig>;
17
20
  readonly outbound: RssOutboundPush | null;
package/src/storage.ts CHANGED
@@ -10,9 +10,10 @@ const child = { id: { type: 'text', length: 80, primary: true }, task_id: { type
10
10
  const codec: RelationalStateCodec = {
11
11
  key: ADMIN_RSS_STATE_KEY,
12
12
  header: { table: 'admin_rss_book', definition: { version: { type: 'integer', nullable: true }, revision: text } },
13
+ addColumns: { [tasks]: ['locale', 'detail_key'] },
13
14
  tables: {
14
15
  [tasks]: { id: { type: 'text', length: 36, primary: true }, position: { type: 'integer', nullable: false }, ...TASK_TARGET_COLUMNS,
15
- creation: text, version: text, url: text, state: text, created_at: time, claim_id: text, last_check_at: time, last_run_at: time, detail: text,
16
+ creation: text, version: text, locale: text, url: text, state: text, created_at: time, claim_id: text, last_check_at: time, last_run_at: time, detail: text, detail_key: text,
16
17
  seen_known: { type: 'boolean', nullable: false }, quiet_timezone: text, quiet_start: text, quiet_end: text,
17
18
  pending_kind: text, pending_from: time, pending_to: time, pending_count: { type: 'integer', nullable: true },
18
19
  pending_capped: { type: 'boolean', nullable: true } },
@@ -25,8 +26,8 @@ const codec: RelationalStateCodec = {
25
26
  if (![1, 2].includes(book.version) || !Array.isArray(book.tasks) || !Array.isArray(book.receipts)) throw new Error('Unsupported RSS book; original state was preserved');
26
27
  return { header: { version: book.version, revision: book.revision }, rows: {
27
28
  [tasks]: book.tasks.map((task, position) => ({ id: task.id, position, ...encodeTaskTarget(task.target),
28
- creation: task.creation, version: task.version, url: encodeStorageIdentity(task.url), state: task.state, created_at: task.createdAt,
29
- claim_id: task.claimId, last_check_at: task.lastCheckAt, last_run_at: task.lastRunAt, detail: encodeStorageIdentity(task.detail),
29
+ creation: task.creation, version: task.version, locale: task.locale ?? 'zh-CN', url: encodeStorageIdentity(task.url), state: task.state, created_at: task.createdAt,
30
+ claim_id: task.claimId, last_check_at: task.lastCheckAt, last_run_at: task.lastRunAt, detail: encodeStorageIdentity(task.detail), detail_key: task.detailKey ?? null,
30
31
  seen_known: task.seen !== null, quiet_timezone: task.quiet?.timezone ?? null, quiet_start: task.quiet?.start ?? null, quiet_end: task.quiet?.end ?? null,
31
32
  pending_kind: task.pending === undefined ? 'absent' : task.pending === null ? 'null' : 'value',
32
33
  pending_from: task.pending?.from ?? null, pending_to: task.pending?.to ?? null,
@@ -40,8 +41,8 @@ const codec: RelationalStateCodec = {
40
41
  decode(header, rows) {
41
42
  return { version: Number(header.version), revision: String(header.revision), receipts: decodeTaskReceipts(rows[receipts]!),
42
43
  tasks: rows[tasks]!.map(row => ({ id: String(row.id), target: decodeTaskTarget(row), creation: String(row.creation), version: String(row.version),
43
- url: decodeStorageIdentity(row.url), state: String(row.state), createdAt: Number(row.created_at), claimId: row.claim_id == null ? null : String(row.claim_id),
44
- lastCheckAt: row.last_check_at == null ? null : Number(row.last_check_at), lastRunAt: row.last_run_at == null ? null : Number(row.last_run_at), detail: decodeStorageIdentity(row.detail),
44
+ url: decodeStorageIdentity(row.url), locale: row.locale == null ? 'zh-CN' : String(row.locale), state: String(row.state), createdAt: Number(row.created_at), claimId: row.claim_id == null ? null : String(row.claim_id),
45
+ lastCheckAt: row.last_check_at == null ? null : Number(row.last_check_at), lastRunAt: row.last_run_at == null ? null : Number(row.last_run_at), detail: decodeStorageIdentity(row.detail), ...(row.detail_key == null ? {} : { detailKey: String(row.detail_key) }),
45
46
  seen: row.seen_known ? rows[seen]!.filter(item => item.task_id === row.id).map(item => decodeStorageIdentity(item.item_key)) : null,
46
47
  ...(row.quiet_timezone == null ? {} : { quiet: { timezone: String(row.quiet_timezone), start: String(row.quiet_start), end: String(row.quiet_end) } }),
47
48
  ...(row.pending_kind === 'absent' ? {} : { pending: row.pending_kind === 'null' ? null : {