@zhin.js/plugin-bilibili 0.1.2 → 0.1.4

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @zhin.js/plugin-bilibili
2
2
 
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [eb8feeb]
8
+ - Updated dependencies [24a2b71]
9
+ - Updated dependencies [d0e1fac]
10
+ - @zhin.js/tool@1.1.11
11
+
12
+ ## 0.1.3
13
+
14
+ ### Patch Changes
15
+
16
+ - 53bc6a0: 官方工具插件与游戏支持中英文反馈、帮助和配置标签;提醒增加与中文对应的有限英文时间语法。聊天后台任务记录创建者当时语言,管理员任务记录项目语言,旧任务保持中文;多轮选择与游戏使用开始时语言,保留第三方内容和已生成报告。
17
+ - 66b6404: 让活动反馈、内容审核默认提示和后台任务通知保持发起操作或任务创建时的语言,并将语言沿现有 Host、精确投递目标与主动发送链传给适配器。
18
+
19
+ 显式配置的提示、用户及模型原文保持不变;审核默认提示仅在未配置模板时本地化,异步审核固定入队时语言,不修改审核规则、调度、权限或重试行为。
20
+
21
+ - Updated dependencies [53bc6a0]
22
+ - Updated dependencies [7595136]
23
+ - Updated dependencies [17d1301]
24
+ - @zhin.js/tool@1.1.10
25
+ - @zhin.js/skill@1.1.10
26
+
3
27
  ## 0.1.2
4
28
 
5
29
  ### Patch Changes
@@ -1,26 +1,26 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages, kindText, commandHint } from "../../../lib/i18n.js";
2
3
  import { defineCommand } from 'zhin.js/command';
3
4
  import { channelSourceError, extractChannelInfo, subscriptionScope } from "../../../lib/channel.js";
4
5
  import { bilibiliRuntimeToken } from "../../../lib/runtime.js";
5
6
  import { getBiliSubs } from "../../../lib/store.js";
6
- const KIND_TEXT = { all: '开播+动态', live: '开播', dynamic: '动态' };
7
7
  export default defineCommand({
8
- description: '查看当前会话的 B 站订阅',
9
- async execute({ input, use }) {
8
+ description: '查看当前会话的 B 站订阅', descriptionI18n: { en: "List this conversation's Bilibili subscriptions" },
9
+ async execute({ locale, formatCommand, input, use }) {
10
10
  const runtime = use(bilibiliRuntimeToken);
11
11
  const Subs = getBiliSubs(runtime.db);
12
12
  if (!Subs)
13
- return 'B 站订阅数据库尚未就绪,请稍后重试';
13
+ return messages(locale, 'db');
14
14
  const channel = extractChannelInfo(input);
15
15
  if (!channel)
16
- return channelSourceError(input);
16
+ return channelSourceError(input, locale);
17
17
  const rows = await Subs.select().where(subscriptionScope(channel));
18
18
  if (rows.length === 0)
19
- return '当前会话没有订阅任何 UP 主\n使用 bili sub <uid> 添加订阅';
20
- const lines = rows.map((row, index) => `${index + 1}. ${String(row.uname || '(未知)')}(UID ${String(row.uid)})· ${KIND_TEXT[String(row.kinds)] ?? '开播+动态'}`);
21
- const status = runtime.watcher.backoffStatus();
19
+ return messages(locale, 'empty') + commandHint(locale, formatCommand('bili sub'), '<uid>');
20
+ const lines = rows.map((row, index) => `${index + 1}. ${String(row.uname || messages(locale, 'unknown'))}(UID ${String(row.uid)})· ${kindText(row.kinds, locale)}`);
21
+ const status = runtime.watcher.backoffStatus(locale);
22
22
  if (status.length > 0)
23
- lines.push('', '暂停中的请求:', ...status);
24
- return `当前订阅 (${rows.length}/${runtime.config.maxPerChannel}):\n${lines.join('\n')}`;
23
+ lines.push('', messages(locale, 'paused'), ...status);
24
+ return messages(locale, 'list', { count: rows.length, limit: runtime.config.maxPerChannel, items: lines.join('\n') });
25
25
  },
26
26
  });
@@ -1,15 +1,16 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages, commandHint } from "../../../lib/i18n.js";
2
3
  import { defineCommand } from 'zhin.js/command';
3
4
  import { BiliApiError, parseRoomId } from "../../../lib/api.js";
4
5
  import { formatRoom } from "../../../lib/format.js";
5
6
  import { bilibiliRuntimeToken } from "../../../lib/runtime.js";
6
7
  export default defineCommand({
7
- description: '查询 B 站直播间状态',
8
- params: { room: { type: 'string', description: '直播间号或直播间链接' } },
9
- async execute({ params, use }) {
8
+ description: '查询 B 站直播间状态', descriptionI18n: { en: "Look up a Bilibili live room" },
9
+ params: { room: { type: 'string', description: '直播间号或直播间链接', descriptionI18n: { en: "Live room ID or URL" } } },
10
+ async execute({ locale, formatCommand, params, use }) {
10
11
  const roomId = parseRoomId(String(params.room ?? ''));
11
12
  if (!roomId)
12
- return '请提供直播间号或链接,格式:bili live <房间号>';
13
+ return messages(locale, 'room') + commandHint(locale, formatCommand('bili live'), '<room>');
13
14
  const { client } = use(bilibiliRuntimeToken);
14
15
  try {
15
16
  const room = await client.getRoomInfo(roomId);
@@ -20,12 +21,12 @@ export default defineCommand({
20
21
  uname = info?.uname ?? '';
21
22
  liveTime = info?.liveTime ?? 0;
22
23
  }
23
- return formatRoom(room, uname, liveTime);
24
+ return formatRoom(room, uname, liveTime, Date.now(), locale);
24
25
  }
25
26
  catch (error) {
26
27
  if (error instanceof BiliApiError && error.kind === 'not_found')
27
- return `直播间 ${roomId} 不存在`;
28
- return `查询失败:${error.message}`;
28
+ return messages(locale, 'missingRoom', { room: roomId });
29
+ return messages(locale, 'failed', { error: error.message });
29
30
  }
30
31
  },
31
32
  });
@@ -1,44 +1,43 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages, kindText, commandHint } from "../../../lib/i18n.js";
2
3
  import { canManageConversation, defineCommand } from 'zhin.js/command';
3
4
  import { BiliApiError, parseUid } from "../../../lib/api.js";
4
5
  import { channelSourceError, extractChannelInfo, subscriptionScope } from "../../../lib/channel.js";
5
6
  import { bilibiliRuntimeToken } from "../../../lib/runtime.js";
6
- import { SUBSCRIPTION_PERMISSION_DENIED_MESSAGE } from "../../../lib/permission.js";
7
7
  import { getBiliSubs, parseKinds } from "../../../lib/store.js";
8
- const KIND_TEXT = { all: '开播 + 动态', live: '开播', dynamic: '动态' };
9
8
  export default defineCommand({
10
- description: '订阅 B 站 UP 主的开播和动态推送(群聊限群主、管理员或机器人主人)',
9
+ description: '订阅 B 站 UP 主的开播和动态推送(群聊限群主、管理员或机器人主人)', descriptionI18n: { en: "Subscribe to a Bilibili creator's live streams and posts (group owner, administrator or bot master required in groups)" },
11
10
  params: {
12
- uid: { type: 'string', description: 'UP 主 UID 或空间链接' },
13
- kind: { type: 'string', default: 'all', description: 'all(默认)/ live / dynamic' },
11
+ uid: { type: 'string', description: 'UP 主 UID 或空间链接', descriptionI18n: { en: "Creator UID or profile URL" } },
12
+ kind: { type: 'string', default: 'all', description: 'all(默认)/ live / dynamic', descriptionI18n: { en: "all (default), live, or dynamic" } },
14
13
  },
15
- async execute({ params, input, use, adapter, scene, sender }) {
14
+ async execute({ locale, formatCommand, params, input, use, adapter, scene, sender }) {
16
15
  const runtime = use(bilibiliRuntimeToken);
17
16
  const Subs = getBiliSubs(runtime.db);
18
17
  if (!Subs)
19
- return 'B 站订阅数据库尚未就绪,请稍后重试';
18
+ return messages(locale, 'db');
20
19
  const uid = parseUid(String(params.uid ?? ''));
21
20
  if (!uid)
22
- return '请提供 UP 主 UID 或空间链接,格式:bili sub <uid> [all|live|dynamic]';
21
+ return messages(locale, 'uid') + commandHint(locale, formatCommand('bili sub'), '<uid> [all|live|dynamic]');
23
22
  const kinds = parseKinds(String(params.kind ?? 'all'));
24
23
  if (!kinds)
25
- return '订阅类型只能是 all、live 或 dynamic';
24
+ return messages(locale, 'kind');
26
25
  const channel = extractChannelInfo(input);
27
26
  if (!channel)
28
- return channelSourceError(input);
27
+ return channelSourceError(input, locale);
29
28
  if (!canManageConversation({ adapter, scene, sender }))
30
- return SUBSCRIPTION_PERMISSION_DENIED_MESSAGE;
29
+ return messages(locale, 'denied');
31
30
  const scope = subscriptionScope(channel);
32
31
  const existing = await Subs.select().where({ uid, ...scope });
33
32
  if (existing.length > 0) {
34
33
  if (existing[0].kinds === kinds)
35
- return '当前会话已订阅该 UP 主,无需重复添加';
34
+ return messages(locale, 'exists');
36
35
  await Subs.update({ kinds }).where({ uid, ...scope });
37
- return `已将 ${String(existing[0].uname || uid)} 的订阅改为:${KIND_TEXT[kinds]}`;
36
+ return messages(locale, 'changed', { name: String(existing[0].uname || uid), kind: kindText(kinds, locale) });
38
37
  }
39
38
  const current = await Subs.select().where(scope);
40
39
  if (current.length >= runtime.config.maxPerChannel) {
41
- return `当前会话订阅数已达上限 (${runtime.config.maxPerChannel}),请先取消一些`;
40
+ return messages(locale, 'limit', { limit: runtime.config.maxPerChannel });
42
41
  }
43
42
  let uname = '';
44
43
  try {
@@ -46,7 +45,7 @@ export default defineCommand({
46
45
  }
47
46
  catch (error) {
48
47
  if (error instanceof BiliApiError && error.kind === 'not_found')
49
- return `找不到 UID 为 ${uid} 的用户`;
48
+ return messages(locale, 'missingUser', { uid });
50
49
  // Lookup is cosmetic; subscribe anyway and let the watcher fill gaps.
51
50
  }
52
51
  await Subs.insert({
@@ -55,14 +54,15 @@ export default defineCommand({
55
54
  kinds,
56
55
  ...scope,
57
56
  creator_id: channel.senderId,
57
+ locale,
58
58
  created_at: new Date().toISOString(),
59
59
  });
60
60
  await runtime.watcher.baseline(uid).catch(() => undefined);
61
61
  const notes = [];
62
62
  if (!runtime.outbound)
63
- notes.push('⚠ 未接入 OutboundHost,暂时无法主动推送');
63
+ notes.push(messages(locale, 'noOutbound'));
64
64
  if (!runtime.persistent)
65
- notes.push('⚠ 未配置数据库,订阅在重启后会丢失');
66
- return [`订阅成功:${uname || uid}(UID ${uid}),类型:${KIND_TEXT[kinds]}`, ...notes].join('\n');
65
+ notes.push(messages(locale, 'volatile'));
66
+ return [messages(locale, 'subscribed', { name: uname || uid, uid, kind: kindText(kinds, locale) }), ...notes].join('\n');
67
67
  },
68
68
  });
@@ -1,30 +1,30 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages, commandHint } from "../../../lib/i18n.js";
2
3
  import { canManageConversation, defineCommand } from 'zhin.js/command';
3
4
  import { parseUid } from "../../../lib/api.js";
4
5
  import { channelSourceError, extractChannelInfo, subscriptionScope } from "../../../lib/channel.js";
5
6
  import { bilibiliRuntimeToken } from "../../../lib/runtime.js";
6
- import { SUBSCRIPTION_PERMISSION_DENIED_MESSAGE } from "../../../lib/permission.js";
7
7
  import { getBiliState, getBiliSubs } from "../../../lib/store.js";
8
8
  export default defineCommand({
9
- description: '取消当前会话对某个 UP 主的订阅(群聊限群主、管理员或机器人主人)',
10
- params: { uid: { type: 'string', description: 'UP 主 UID 或空间链接' } },
11
- async execute({ params, input, use, adapter, scene, sender }) {
9
+ description: '取消当前会话对某个 UP 主的订阅(群聊限群主、管理员或机器人主人)', descriptionI18n: { en: "Unsubscribe this conversation from a Bilibili creator (group owner, administrator or bot master required in groups)" },
10
+ params: { uid: { type: 'string', description: 'UP 主 UID 或空间链接', descriptionI18n: { en: "Creator UID or profile URL" } } },
11
+ async execute({ locale, formatCommand, params, input, use, adapter, scene, sender }) {
12
12
  const runtime = use(bilibiliRuntimeToken);
13
13
  const Subs = getBiliSubs(runtime.db);
14
14
  if (!Subs)
15
- return 'B 站订阅数据库尚未就绪,请稍后重试';
15
+ return messages(locale, 'db');
16
16
  const uid = parseUid(String(params.uid ?? ''));
17
17
  if (!uid)
18
- return '请提供 UP 主 UID,格式:bili unsub <uid>';
18
+ return messages(locale, 'uid') + commandHint(locale, formatCommand('bili unsub'), '<uid>');
19
19
  const channel = extractChannelInfo(input);
20
20
  if (!channel)
21
- return channelSourceError(input);
21
+ return channelSourceError(input, locale);
22
22
  if (!canManageConversation({ adapter, scene, sender }))
23
- return SUBSCRIPTION_PERMISSION_DENIED_MESSAGE;
23
+ return messages(locale, 'denied');
24
24
  const scope = subscriptionScope(channel);
25
25
  const existing = await Subs.select().where({ uid, ...scope });
26
26
  if (existing.length === 0)
27
- return '当前会话没有订阅该 UP 主';
27
+ return messages(locale, 'notSubscribed');
28
28
  await Subs.delete().where({ uid, ...scope });
29
29
  // Drop the watcher cursor and backoff once nobody watches this UP any more.
30
30
  const remaining = await Subs.select().where({ uid });
@@ -32,6 +32,6 @@ export default defineCommand({
32
32
  await getBiliState(runtime.db)?.delete().where({ uid });
33
33
  runtime.watcher.forget(uid);
34
34
  }
35
- return `已取消订阅:${String(existing[0].uname || uid)}`;
35
+ return messages(locale, 'removed', { name: String(existing[0].uname || uid) });
36
36
  },
37
37
  });
package/lib/channel.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Locale } from 'zhin.js/i18n';
1
2
  /** A subscription destination uses public Message fields, never CapabilityId internals. */
2
3
  export interface ChannelInfo {
3
4
  adapterName: string;
@@ -8,7 +9,7 @@ export interface ChannelInfo {
8
9
  }
9
10
  export declare const CHANNEL_REQUIRED_MESSAGE = "\u8BF7\u5728\u673A\u5668\u4EBA\u79C1\u804A\u3001\u7FA4\u804A\u6216\u9891\u9053\u5185\u6267\u884C\u6B64\u547D\u4EE4\uFF1B\u5F53\u524D\u8C03\u7528\u6CA1\u6709\u6709\u6548\u7684\u8BA2\u9605\u4F1A\u8BDD";
10
11
  export declare const UNSUPPORTED_ADDRESS_MESSAGE = "\u6682\u4E0D\u652F\u6301\u7EBF\u7A0B\uFF0F\u7236\u9891\u9053\u5BFB\u5740\u7684\u8BA2\u9605\uFF0C\u8BF7\u5728\u65E0\u9700\u7EBF\u7A0B\u6216\u7236\u9891\u9053\u4FE1\u606F\u7684\u4F1A\u8BDD\u5185\u6267\u884C";
11
- export declare function channelSourceError(input: unknown): string;
12
+ export declare function channelSourceError(input: unknown, locale?: Locale): string;
12
13
  /** No IM source means no destination; callers must not invent a global subscription. */
13
14
  export declare function extractChannelInfo(input: unknown): ChannelInfo | null;
14
15
  /** Full subscription scope, including the bot endpoint for multi-account adapters. */
package/lib/channel.js CHANGED
@@ -1,4 +1,4 @@
1
- /** A subscription destination uses public Message fields, never CapabilityId internals. */
1
+ import { messages } from './i18n.js';
2
2
  export const CHANNEL_REQUIRED_MESSAGE = '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话';
3
3
  export const UNSUPPORTED_ADDRESS_MESSAGE = '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行';
4
4
  function isRecord(value) {
@@ -11,8 +11,8 @@ function hasUnsupportedAddress(input) {
11
11
  return isRecord(input) && isRecord(input.conversation)
12
12
  && (input.conversation.parent !== undefined || input.conversation.threadId !== undefined);
13
13
  }
14
- export function channelSourceError(input) {
15
- return hasUnsupportedAddress(input) ? UNSUPPORTED_ADDRESS_MESSAGE : CHANNEL_REQUIRED_MESSAGE;
14
+ export function channelSourceError(input, locale = 'zh-CN') {
15
+ return messages(locale, hasUnsupportedAddress(input) ? 'address' : 'source');
16
16
  }
17
17
  /** No IM source means no destination; callers must not invent a global subscription. */
18
18
  export function extractChannelInfo(input) {
@@ -1 +1 @@
1
- {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAU3F,MAAM,CAAC,MAAM,wBAAwB,GAAG,mCAAmC,CAAC;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAAG,oCAAoC,CAAC;AAEhF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;WACjD,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,YAAY,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC;AAC9F,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,wBAAwB,CAAC;AAC/F,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACxC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9E,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACzH,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,OAAO;QACL,WAAW,EAAE,KAAK,CAAC,aAAa;QAChC,WAAW,EAAE,KAAK,CAAC,UAAU;QAC7B,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,YAAY,CAAC,EAAE;QAC1B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;KACvE,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,OAAO;QACL,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,UAAU,EAAE,OAAO,CAAC,SAAS;KAC9B,CAAC;AACJ,CAAC","sourcesContent":["/** A subscription destination uses public Message fields, never CapabilityId internals. */\n\nexport interface ChannelInfo {\n adapterName: string;\n endpointKey: string;\n channelType: 'private' | 'group' | 'channel';\n channelId: string;\n senderId: string;\n}\n\nexport const CHANNEL_REQUIRED_MESSAGE = '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话';\nexport const UNSUPPORTED_ADDRESS_MESSAGE = '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isIdentifier(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction hasUnsupportedAddress(input: unknown): boolean {\n return isRecord(input) && isRecord(input.conversation)\n && (input.conversation.parent !== undefined || input.conversation.threadId !== undefined);\n}\n\nexport function channelSourceError(input: unknown): string {\n return hasUnsupportedAddress(input) ? UNSUPPORTED_ADDRESS_MESSAGE : CHANNEL_REQUIRED_MESSAGE;\n}\n\n/** No IM source means no destination; callers must not invent a global subscription. */\nexport function extractChannelInfo(input: unknown): ChannelInfo | null {\n if (!isRecord(input) || !isRecord(input.conversation)) return null;\n if (hasUnsupportedAddress(input)) return null;\n const conversation = input.conversation;\n const kind = conversation.kind;\n if (kind !== 'private' && kind !== 'group' && kind !== 'channel') return null;\n if (!isIdentifier(input.clientAdapter) || !isIdentifier(input.endpointId) || !isIdentifier(conversation.id)) return null;\n const sender = input.sender;\n return {\n adapterName: input.clientAdapter,\n endpointKey: input.endpointId,\n channelType: kind,\n channelId: conversation.id,\n senderId: isRecord(sender) && isIdentifier(sender.id) ? sender.id : '',\n };\n}\n\n/** Full subscription scope, including the bot endpoint for multi-account adapters. */\nexport function subscriptionScope(channel: ChannelInfo): Record<string, string> {\n return {\n adapter_name: channel.adapterName,\n endpoint_id: channel.endpointKey,\n channel_type: channel.channelType,\n channel_id: channel.channelId,\n };\n}\n"]}
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAWrC,MAAM,CAAC,MAAM,wBAAwB,GAAG,mCAAmC,CAAC;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAAG,oCAAoC,CAAC;AAEhF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAc;IAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;WACjD,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,YAAY,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC;AAC9F,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc,EAAE,SAAiB,OAAO;IACzE,OAAO,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC/E,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACxC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC9E,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACzH,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,OAAO;QACL,WAAW,EAAE,KAAK,CAAC,aAAa;QAChC,WAAW,EAAE,KAAK,CAAC,UAAU;QAC7B,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,YAAY,CAAC,EAAE;QAC1B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;KACvE,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,OAAO;QACL,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,YAAY,EAAE,OAAO,CAAC,WAAW;QACjC,UAAU,EAAE,OAAO,CAAC,SAAS;KAC9B,CAAC;AACJ,CAAC","sourcesContent":["import type { Locale } from 'zhin.js/i18n';\nimport { messages } from './i18n.js';\n/** A subscription destination uses public Message fields, never CapabilityId internals. */\n\nexport interface ChannelInfo {\n adapterName: string;\n endpointKey: string;\n channelType: 'private' | 'group' | 'channel';\n channelId: string;\n senderId: string;\n}\n\nexport const CHANNEL_REQUIRED_MESSAGE = '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话';\nexport const UNSUPPORTED_ADDRESS_MESSAGE = '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isIdentifier(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction hasUnsupportedAddress(input: unknown): boolean {\n return isRecord(input) && isRecord(input.conversation)\n && (input.conversation.parent !== undefined || input.conversation.threadId !== undefined);\n}\n\nexport function channelSourceError(input: unknown, locale: Locale = 'zh-CN'): string {\n return messages(locale, hasUnsupportedAddress(input) ? 'address' : 'source');\n}\n\n/** No IM source means no destination; callers must not invent a global subscription. */\nexport function extractChannelInfo(input: unknown): ChannelInfo | null {\n if (!isRecord(input) || !isRecord(input.conversation)) return null;\n if (hasUnsupportedAddress(input)) return null;\n const conversation = input.conversation;\n const kind = conversation.kind;\n if (kind !== 'private' && kind !== 'group' && kind !== 'channel') return null;\n if (!isIdentifier(input.clientAdapter) || !isIdentifier(input.endpointId) || !isIdentifier(conversation.id)) return null;\n const sender = input.sender;\n return {\n adapterName: input.clientAdapter,\n endpointKey: input.endpointId,\n channelType: kind,\n channelId: conversation.id,\n senderId: isRecord(sender) && isIdentifier(sender.id) ? sender.id : '',\n };\n}\n\n/** Full subscription scope, including the bot endpoint for multi-account adapters. */\nexport function subscriptionScope(channel: ChannelInfo): Record<string, string> {\n return {\n adapter_name: channel.adapterName,\n endpoint_id: channel.endpointKey,\n channel_type: channel.channelType,\n channel_id: channel.channelId,\n };\n}\n"]}
package/lib/format.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import type { Locale } from 'zhin.js/i18n';
1
2
  import type { DynamicItem, LiveInfo, RoomInfo } from './api.js';
2
- export declare function liveStatusText(status: number): string;
3
- export declare function formatLiveStart(info: LiveInfo): string;
4
- export declare function formatDynamic(uname: string, item: DynamicItem): string;
5
- export declare function formatRoom(room: RoomInfo, uname: string, liveTime: number, nowMs?: number): string;
3
+ export declare function liveStatusText(status: number, locale?: Locale): string;
4
+ export declare function formatLiveStart(info: LiveInfo, locale?: Locale): string;
5
+ export declare function formatDynamic(uname: string, item: DynamicItem, locale?: Locale): string;
6
+ export declare function formatRoom(room: RoomInfo, uname: string, liveTime: number, nowMs?: number, locale?: Locale): string;
package/lib/format.js CHANGED
@@ -1,40 +1,40 @@
1
- const LIVE_STATUS_TEXT = { 0: '未开播', 1: '直播中', 2: '轮播中' };
2
- export function liveStatusText(status) {
3
- return LIVE_STATUS_TEXT[status] ?? `未知状态 (${status})`;
1
+ import { messages as t } from './i18n.js';
2
+ export function liveStatusText(status, locale = 'zh-CN') {
3
+ return status === 0 ? t(locale, 'offline') : status === 1 ? t(locale, 'broadcasting')
4
+ : status === 2 ? t(locale, 'replay') : t(locale, 'statusUnknown', { status });
4
5
  }
5
- function since(liveTime, nowMs) {
6
+ function since(liveTime, nowMs, locale) {
6
7
  if (liveTime <= 0)
7
8
  return '';
8
9
  const minutes = Math.max(0, Math.floor((nowMs / 1000 - liveTime) / 60));
9
- if (minutes < 60)
10
- return `已开播 ${minutes} 分钟`;
11
- return `已开播 ${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分钟`;
10
+ return minutes < 60 ? t(locale, 'minutes', { minutes }) : t(locale, 'hours', { hours: Math.floor(minutes / 60), minutes: minutes % 60 });
12
11
  }
13
- export function formatLiveStart(info) {
14
- const lines = [`【开播】${info.uname || info.uid} 开播啦!`];
12
+ export function formatLiveStart(info, locale = 'zh-CN') {
13
+ const lines = [t(locale, 'started', { name: info.uname || info.uid })];
15
14
  if (info.title)
16
- lines.push(`标题:${info.title}`);
15
+ lines.push(t(locale, 'title', { title: info.title }));
17
16
  if (info.area)
18
- lines.push(`分区:${info.area}`);
17
+ lines.push(t(locale, 'area', { area: info.area }));
19
18
  if (info.url)
20
19
  lines.push(info.url);
21
20
  return lines.join('\n');
22
21
  }
23
- export function formatDynamic(uname, item) {
24
- return `【动态】${item.author || uname} ${item.summary}\n${item.url}`;
22
+ export function formatDynamic(uname, item, locale = 'zh-CN') {
23
+ return t(locale, 'post', { name: item.author || uname, summary: item.summary, url: item.url });
25
24
  }
26
- export function formatRoom(room, uname, liveTime, nowMs = Date.now()) {
27
- const lines = [`${uname ? `${uname} 的直播间` : '直播间'} ${room.roomId}:${liveStatusText(room.liveStatus)}`];
25
+ export function formatRoom(room, uname, liveTime, nowMs = Date.now(), locale = 'zh-CN') {
26
+ const name = uname ? t(locale, 'namedRoom', { name: uname }) : t(locale, 'liveRoom');
27
+ const lines = [`${name} ${room.roomId}${locale === 'en' ? ': ' : ':'}${liveStatusText(room.liveStatus, locale)}`];
28
28
  if (room.title)
29
- lines.push(`标题:${room.title}`);
29
+ lines.push(t(locale, 'title', { title: room.title }));
30
30
  if (room.area)
31
- lines.push(`分区:${room.area}`);
31
+ lines.push(t(locale, 'area', { area: room.area }));
32
32
  if (room.liveStatus === 1) {
33
- const elapsed = since(liveTime, nowMs);
33
+ const elapsed = since(liveTime, nowMs, locale);
34
34
  if (elapsed)
35
35
  lines.push(elapsed);
36
36
  if (room.online > 0)
37
- lines.push(`人气:${room.online}`);
37
+ lines.push(t(locale, 'popularity', { count: room.online }));
38
38
  }
39
39
  lines.push(room.url);
40
40
  return lines.join('\n');
package/lib/format.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAA2B,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AAElF,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,gBAAgB,CAAC,MAAM,CAAC,IAAI,SAAS,MAAM,GAAG,CAAC;AACxD,CAAC;AAED,SAAS,KAAK,CAAC,QAAgB,EAAE,KAAa;IAC5C,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACxE,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,OAAO,OAAO,KAAK,CAAC;IAC7C,OAAO,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,OAAO,OAAO,GAAG,EAAE,KAAK,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAc;IAC5C,MAAM,KAAK,GAAG,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,IAAiB;IAC5D,OAAO,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAc,EAAE,KAAa,EAAE,QAAgB,EAAE,QAAgB,IAAI,CAAC,GAAG,EAAE;IACpG,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IACvG,IAAI,IAAI,CAAC,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACvC,IAAI,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["import type { DynamicItem, LiveInfo, RoomInfo } from './api.js';\n\nconst LIVE_STATUS_TEXT: Record<number, string> = { 0: '未开播', 1: '直播中', 2: '轮播中' };\n\nexport function liveStatusText(status: number): string {\n return LIVE_STATUS_TEXT[status] ?? `未知状态 (${status})`;\n}\n\nfunction since(liveTime: number, nowMs: number): string {\n if (liveTime <= 0) return '';\n const minutes = Math.max(0, Math.floor((nowMs / 1000 - liveTime) / 60));\n if (minutes < 60) return `已开播 ${minutes} 分钟`;\n return `已开播 ${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分钟`;\n}\n\nexport function formatLiveStart(info: LiveInfo): string {\n const lines = [`【开播】${info.uname || info.uid} 开播啦!`];\n if (info.title) lines.push(`标题:${info.title}`);\n if (info.area) lines.push(`分区:${info.area}`);\n if (info.url) lines.push(info.url);\n return lines.join('\\n');\n}\n\nexport function formatDynamic(uname: string, item: DynamicItem): string {\n return `【动态】${item.author || uname} ${item.summary}\\n${item.url}`;\n}\n\nexport function formatRoom(room: RoomInfo, uname: string, liveTime: number, nowMs: number = Date.now()): string {\n const lines = [`${uname ? `${uname} 的直播间` : '直播间'} ${room.roomId}:${liveStatusText(room.liveStatus)}`];\n if (room.title) lines.push(`标题:${room.title}`);\n if (room.area) lines.push(`分区:${room.area}`);\n if (room.liveStatus === 1) {\n const elapsed = since(liveTime, nowMs);\n if (elapsed) lines.push(elapsed);\n if (room.online > 0) lines.push(`人气:${room.online}`);\n }\n lines.push(room.url);\n return lines.join('\\n');\n}\n"]}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Locale } from 'zhin.js/i18n';\nimport type { DynamicItem, LiveInfo, RoomInfo } from './api.js';\nimport { messages as t } from './i18n.js';\n\nexport function liveStatusText(status: number, locale: Locale = 'zh-CN'): string {\n return status === 0 ? t(locale, 'offline') : status === 1 ? t(locale, 'broadcasting')\n : status === 2 ? t(locale, 'replay') : t(locale, 'statusUnknown', { status });\n}\n\nfunction since(liveTime: number, nowMs: number, locale: Locale): string {\n if (liveTime <= 0) return '';\n const minutes = Math.max(0, Math.floor((nowMs / 1000 - liveTime) / 60));\n return minutes < 60 ? t(locale, 'minutes', { minutes }) : t(locale, 'hours', { hours: Math.floor(minutes / 60), minutes: minutes % 60 });\n}\n\nexport function formatLiveStart(info: LiveInfo, locale: Locale = 'zh-CN'): string {\n const lines = [t(locale, 'started', { name: info.uname || info.uid })];\n if (info.title) lines.push(t(locale, 'title', { title: info.title }));\n if (info.area) lines.push(t(locale, 'area', { area: info.area }));\n if (info.url) lines.push(info.url);\n return lines.join('\\n');\n}\n\nexport function formatDynamic(uname: string, item: DynamicItem, locale: Locale = 'zh-CN'): string {\n return t(locale, 'post', { name: item.author || uname, summary: item.summary, url: item.url });\n}\n\nexport function formatRoom(room: RoomInfo, uname: string, liveTime: number, nowMs: number = Date.now(), locale: Locale = 'zh-CN'): string {\n const name = uname ? t(locale, 'namedRoom', { name: uname }) : t(locale, 'liveRoom');\n const lines = [`${name} ${room.roomId}${locale === 'en' ? ': ' : ':'}${liveStatusText(room.liveStatus, locale)}`];\n if (room.title) lines.push(t(locale, 'title', { title: room.title }));\n if (room.area) lines.push(t(locale, 'area', { area: room.area }));\n if (room.liveStatus === 1) {\n const elapsed = since(liveTime, nowMs, locale);\n if (elapsed) lines.push(elapsed);\n if (room.online > 0) lines.push(t(locale, 'popularity', { count: room.online }));\n }\n lines.push(room.url);\n return lines.join('\\n');\n}\n"]}
package/lib/i18n.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type Locale } from 'zhin.js/i18n';
2
+ export declare const messages: (locale: Locale | undefined, key: "uid" | "title" | "area" | "db" | "room" | "usage" | "kind" | "exists" | "changed" | "limit" | "missingUser" | "noOutbound" | "volatile" | "subscribed" | "empty" | "unknown" | "paused" | "list" | "missingRoom" | "failed" | "notSubscribed" | "removed" | "denied" | "source" | "address" | "all" | "live" | "dynamic" | "offline" | "broadcasting" | "replay" | "statusUnknown" | "minutes" | "hours" | "started" | "post" | "liveRoom" | "namedRoom" | "popularity" | "backoff" | "toolInput" | "noRoom" | "allEmpty" | "overview", values?: import("zhin.js").MessageValues) => string;
3
+ export declare function kindText(kind: unknown, locale?: Locale): string;
4
+ export declare function commandHint(locale: Locale, invocation: string | undefined, args: string): string;
package/lib/i18n.js ADDED
@@ -0,0 +1,54 @@
1
+ import { createMessages } from 'zhin.js/i18n';
2
+ export const messages = createMessages({
3
+ db: { 'zh-CN': 'B 站订阅数据库尚未就绪,请稍后重试', en: 'The Bilibili subscription database is not ready. Try again later.' },
4
+ uid: { 'zh-CN': '请提供 UP 主 UID 或空间链接', en: 'Provide a creator UID or profile URL.' },
5
+ room: { 'zh-CN': '请提供直播间号或链接', en: 'Provide a live room ID or URL.' },
6
+ usage: { 'zh-CN': '用法:{command}', en: 'Usage: {command}' },
7
+ kind: { 'zh-CN': '订阅类型只能是 all、live 或 dynamic', en: 'Subscription type must be all, live, or dynamic.' },
8
+ exists: { 'zh-CN': '当前会话已订阅该 UP 主,无需重复添加', en: 'This conversation already subscribes to this creator.' },
9
+ changed: { 'zh-CN': '已将 {name} 的订阅改为:{kind}', en: 'Updated {name} subscription to: {kind}' },
10
+ limit: { 'zh-CN': '当前会话订阅数已达上限 ({limit}),请先取消一些', en: 'This conversation has reached its subscription limit ({limit}). Remove a subscription first.' },
11
+ missingUser: { 'zh-CN': '找不到 UID 为 {uid} 的用户', en: 'No user found for UID {uid}.' },
12
+ noOutbound: { 'zh-CN': '⚠ 未接入 OutboundHost,暂时无法主动推送', en: '⚠ No OutboundHost is configured; proactive delivery is unavailable.' },
13
+ volatile: { 'zh-CN': '⚠ 未配置数据库,订阅在重启后会丢失', en: '⚠ No database is configured; subscriptions will be lost on restart.' },
14
+ subscribed: { 'zh-CN': '订阅成功:{name}(UID {uid}),类型:{kind}', en: 'Subscribed to {name} (UID {uid}): {kind}' },
15
+ empty: { 'zh-CN': '当前会话没有订阅任何 UP 主', en: 'This conversation has no creator subscriptions.' },
16
+ unknown: { 'zh-CN': '(未知)', en: '(unknown)' },
17
+ paused: { 'zh-CN': '暂停中的请求:', en: 'Requests in backoff:' },
18
+ list: { 'zh-CN': '当前订阅 ({count}/{limit}):\n{items}', en: 'Subscriptions ({count}/{limit}):\n{items}' },
19
+ missingRoom: { 'zh-CN': '直播间 {room} 不存在', en: 'Live room {room} does not exist.' },
20
+ failed: { 'zh-CN': '查询失败:{error}', en: 'Lookup failed: {error}' },
21
+ notSubscribed: { 'zh-CN': '当前会话没有订阅该 UP 主', en: 'This conversation does not subscribe to this creator.' },
22
+ removed: { 'zh-CN': '已取消订阅:{name}', en: 'Unsubscribed from {name}.' },
23
+ denied: { 'zh-CN': '只有群主、群管理员或机器人主人可以在群聊中修改 B 站订阅;私聊中可以直接订阅本会话。', en: 'Only a group owner, administrator, or bot master may change group subscriptions. In private chat you can subscribe the current conversation.' },
24
+ source: { 'zh-CN': '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话', en: 'Run this command in a private chat, group, or channel with the bot. This invocation has no valid subscription destination.' },
25
+ address: { 'zh-CN': '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行', en: 'Subscriptions with a thread or parent-channel address are not supported. Use a conversation that requires neither.' },
26
+ all: { 'zh-CN': '开播 + 动态', en: 'Live streams + posts' },
27
+ live: { 'zh-CN': '开播', en: 'Live streams' },
28
+ dynamic: { 'zh-CN': '动态', en: 'Posts' },
29
+ offline: { 'zh-CN': '未开播', en: 'Offline' },
30
+ broadcasting: { 'zh-CN': '直播中', en: 'Live' },
31
+ replay: { 'zh-CN': '轮播中', en: 'Replaying' },
32
+ statusUnknown: { 'zh-CN': '未知状态 ({status})', en: 'Unknown status ({status})' },
33
+ minutes: { 'zh-CN': '已开播 {minutes} 分钟', en: 'Live for {minutes} min' },
34
+ hours: { 'zh-CN': '已开播 {hours} 小时 {minutes} 分钟', en: 'Live for {hours} h {minutes} min' },
35
+ started: { 'zh-CN': '【开播】{name} 开播啦!', en: '[Live] {name} is streaming!' },
36
+ title: { 'zh-CN': '标题:{title}', en: 'Title: {title}' },
37
+ area: { 'zh-CN': '分区:{area}', en: 'Category: {area}' },
38
+ post: { 'zh-CN': '【动态】{name} {summary}\n{url}', en: '[Post] {name} {summary}\n{url}' },
39
+ liveRoom: { 'zh-CN': '直播间', en: 'Live room' },
40
+ namedRoom: { 'zh-CN': '{name} 的直播间', en: '{name} live room' },
41
+ popularity: { 'zh-CN': '人气:{count}', en: 'Popularity: {count}' },
42
+ backoff: { 'zh-CN': '{key}:{minutes} 分钟后重试({reason})', en: '{key}: retry in {minutes} min ({reason})' },
43
+ toolInput: { 'zh-CN': '请提供直播间号(room)或 UP 主 UID(uid)', en: 'Provide a live room ID (room) or creator UID (uid).' },
44
+ noRoom: { 'zh-CN': 'UID {uid} 没有开通直播间', en: 'UID {uid} has no live room.' },
45
+ allEmpty: { 'zh-CN': '暂无任何 B 站订阅', en: 'There are no Bilibili subscriptions.' },
46
+ overview: { 'zh-CN': 'B 站订阅总览 ({count} 条):\n{items}', en: 'Bilibili subscriptions ({count}):\n{items}' },
47
+ });
48
+ export function kindText(kind, locale = 'zh-CN') {
49
+ return messages(locale, kind === 'live' || kind === 'dynamic' ? kind : 'all');
50
+ }
51
+ export function commandHint(locale, invocation, args) {
52
+ return invocation === undefined ? '' : '\n' + messages(locale, 'usage', { command: `${invocation} ${args}` });
53
+ }
54
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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","sourcesContent":["import { createMessages, type Locale } from 'zhin.js/i18n';\nexport const messages = createMessages({\n db: { 'zh-CN': 'B 站订阅数据库尚未就绪,请稍后重试', en: 'The Bilibili subscription database is not ready. Try again later.' },\n uid: { 'zh-CN': '请提供 UP 主 UID 或空间链接', en: 'Provide a creator UID or profile URL.' },\n room: { 'zh-CN': '请提供直播间号或链接', en: 'Provide a live room ID or URL.' },\n usage: { 'zh-CN': '用法:{command}', en: 'Usage: {command}' },\n kind: { 'zh-CN': '订阅类型只能是 all、live 或 dynamic', en: 'Subscription type must be all, live, or dynamic.' },\n exists: { 'zh-CN': '当前会话已订阅该 UP 主,无需重复添加', en: 'This conversation already subscribes to this creator.' },\n changed: { 'zh-CN': '已将 {name} 的订阅改为:{kind}', en: 'Updated {name} subscription to: {kind}' },\n limit: { 'zh-CN': '当前会话订阅数已达上限 ({limit}),请先取消一些', en: 'This conversation has reached its subscription limit ({limit}). Remove a subscription first.' },\n missingUser: { 'zh-CN': '找不到 UID 为 {uid} 的用户', en: 'No user found for UID {uid}.' },\n noOutbound: { 'zh-CN': '⚠ 未接入 OutboundHost,暂时无法主动推送', en: '⚠ No OutboundHost is configured; proactive delivery is unavailable.' },\n volatile: { 'zh-CN': '⚠ 未配置数据库,订阅在重启后会丢失', en: '⚠ No database is configured; subscriptions will be lost on restart.' },\n subscribed: { 'zh-CN': '订阅成功:{name}(UID {uid}),类型:{kind}', en: 'Subscribed to {name} (UID {uid}): {kind}' },\n empty: { 'zh-CN': '当前会话没有订阅任何 UP 主', en: 'This conversation has no creator subscriptions.' },\n unknown: { 'zh-CN': '(未知)', en: '(unknown)' },\n paused: { 'zh-CN': '暂停中的请求:', en: 'Requests in backoff:' },\n list: { 'zh-CN': '当前订阅 ({count}/{limit}):\\n{items}', en: 'Subscriptions ({count}/{limit}):\\n{items}' },\n missingRoom: { 'zh-CN': '直播间 {room} 不存在', en: 'Live room {room} does not exist.' },\n failed: { 'zh-CN': '查询失败:{error}', en: 'Lookup failed: {error}' },\n notSubscribed: { 'zh-CN': '当前会话没有订阅该 UP 主', en: 'This conversation does not subscribe to this creator.' },\n removed: { 'zh-CN': '已取消订阅:{name}', en: 'Unsubscribed from {name}.' },\n denied: { 'zh-CN': '只有群主、群管理员或机器人主人可以在群聊中修改 B 站订阅;私聊中可以直接订阅本会话。', en: 'Only a group owner, administrator, or bot master may change group subscriptions. In private chat you can subscribe the current conversation.' },\n source: { 'zh-CN': '请在机器人私聊、群聊或频道内执行此命令;当前调用没有有效的订阅会话', en: 'Run this command in a private chat, group, or channel with the bot. This invocation has no valid subscription destination.' },\n address: { 'zh-CN': '暂不支持线程/父频道寻址的订阅,请在无需线程或父频道信息的会话内执行', en: 'Subscriptions with a thread or parent-channel address are not supported. Use a conversation that requires neither.' },\n all: { 'zh-CN': '开播 + 动态', en: 'Live streams + posts' },\n live: { 'zh-CN': '开播', en: 'Live streams' },\n dynamic: { 'zh-CN': '动态', en: 'Posts' },\n offline: { 'zh-CN': '未开播', en: 'Offline' },\n broadcasting: { 'zh-CN': '直播中', en: 'Live' },\n replay: { 'zh-CN': '轮播中', en: 'Replaying' },\n statusUnknown: { 'zh-CN': '未知状态 ({status})', en: 'Unknown status ({status})' },\n minutes: { 'zh-CN': '已开播 {minutes} 分钟', en: 'Live for {minutes} min' },\n hours: { 'zh-CN': '已开播 {hours} 小时 {minutes} 分钟', en: 'Live for {hours} h {minutes} min' },\n started: { 'zh-CN': '【开播】{name} 开播啦!', en: '[Live] {name} is streaming!' },\n title: { 'zh-CN': '标题:{title}', en: 'Title: {title}' },\n area: { 'zh-CN': '分区:{area}', en: 'Category: {area}' },\n post: { 'zh-CN': '【动态】{name} {summary}\\n{url}', en: '[Post] {name} {summary}\\n{url}' },\n liveRoom: { 'zh-CN': '直播间', en: 'Live room' },\n namedRoom: { 'zh-CN': '{name} 的直播间', en: '{name} live room' },\n popularity: { 'zh-CN': '人气:{count}', en: 'Popularity: {count}' },\n backoff: { 'zh-CN': '{key}:{minutes} 分钟后重试({reason})', en: '{key}: retry in {minutes} min ({reason})' },\n toolInput: { 'zh-CN': '请提供直播间号(room)或 UP 主 UID(uid)', en: 'Provide a live room ID (room) or creator UID (uid).' },\n noRoom: { 'zh-CN': 'UID {uid} 没有开通直播间', en: 'UID {uid} has no live room.' },\n allEmpty: { 'zh-CN': '暂无任何 B 站订阅', en: 'There are no Bilibili subscriptions.' },\n overview: { 'zh-CN': 'B 站订阅总览 ({count} 条):\\n{items}', en: 'Bilibili subscriptions ({count}):\\n{items}' },\n});\nexport function kindText(kind: unknown, locale: Locale = 'zh-CN'): string {\n return messages(locale, kind === 'live' || kind === 'dynamic' ? kind : 'all');\n}\nexport function commandHint(locale: Locale, invocation: string | undefined, args: string): string {\n return invocation === undefined ? '' : '\\n' + messages(locale, 'usage', { command: `${invocation} ${args}` });\n}\n"]}
package/lib/poll.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Locale } from 'zhin.js/i18n';
1
2
  import { type BiliClient, type DynamicItem, type LiveInfo } from './api.js';
2
3
  import type { ResolvedBilibiliConfig } from './config.js';
3
4
  import type { BiliOutboundPush } from './runtime.js';
@@ -35,7 +36,7 @@ export declare class BilibiliWatcher {
35
36
  /** Drop per-UP state once nobody subscribes to that UP any more. */
36
37
  forget(uid: string): void;
37
38
  /** Human readable list of sources currently backing off. */
38
- backoffStatus(): string[];
39
+ backoffStatus(locale?: Locale): string[];
39
40
  /** Record the current live / dynamic position of one UP without pushing. */
40
41
  baseline(uid: string): Promise<void>;
41
42
  tick(): Promise<TickResult>;
package/lib/poll.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { resolveLocale } from 'zhin.js/i18n';
2
+ import { messages } from './i18n.js';
1
3
  /**
2
4
  * Conservative live / dynamic watcher.
3
5
  *
@@ -68,11 +70,11 @@ export class BilibiliWatcher {
68
70
  this.#backoff.delete(`dynamic:${uid}`);
69
71
  }
70
72
  /** Human readable list of sources currently backing off. */
71
- backoffStatus() {
73
+ backoffStatus(locale = 'zh-CN') {
72
74
  const now = this.#now;
73
75
  return [...this.#backoff.entries()]
74
76
  .filter(([, entry]) => entry.until > now)
75
- .map(([key, entry]) => `${key}:${Math.ceil((entry.until - now) / 60_000)} 分钟后重试(${entry.reason})`);
77
+ .map(([key, entry]) => messages(locale, 'backoff', { key, minutes: Math.ceil((entry.until - now) / 60_000), reason: entry.reason }));
76
78
  }
77
79
  async #push(rows, content) {
78
80
  const push = this.#options.outbound;
@@ -86,7 +88,7 @@ export class BilibiliWatcher {
86
88
  || (channelType !== 'private' && channelType !== 'group' && channelType !== 'channel'))
87
89
  continue;
88
90
  try {
89
- await push({ adapterName, endpointKey, channelType, channelId, content });
91
+ await push({ adapterName, endpointKey, channelType, channelId, locale: resolveLocale(row.locale), content: content(resolveLocale(row.locale)) });
90
92
  pushed += 1;
91
93
  }
92
94
  catch {
@@ -171,7 +173,7 @@ export class BilibiliWatcher {
171
173
  liveChecked += 1;
172
174
  const state = (await loadState(State, uid)) ?? emptyState(uid);
173
175
  if (shouldPushLive(state, info)) {
174
- pushed += await this.#push(byUid.get(uid).filter((s) => wantsLive(s.kinds)), formatLiveStart(info));
176
+ pushed += await this.#push(byUid.get(uid).filter((s) => wantsLive(s.kinds)), locale => formatLiveStart(info, locale));
175
177
  }
176
178
  if (state.liveStatus !== info.liveStatus || state.liveTime !== info.liveTime) {
177
179
  state.liveStatus = info.liveStatus;
@@ -214,7 +216,7 @@ export class BilibiliWatcher {
214
216
  const uname = typeof subs[0]?.uname === 'string' ? subs[0].uname : uid;
215
217
  const recent = fresh.filter((item) => item.pubTs >= Math.floor(this.#now / 1000) - MAX_DYNAMIC_AGE_SECONDS);
216
218
  for (const item of recent.slice(-MAX_DYNAMICS_PER_TICK)) {
217
- pushed += await this.#push(targets, formatDynamic(uname, item));
219
+ pushed += await this.#push(targets, locale => formatDynamic(uname, item, locale));
218
220
  }
219
221
  for (const item of fresh) {
220
222
  state.seenDynamics.push(item.id);
package/lib/poll.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"poll.js","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Conservative live / dynamic watcher.\n *\n * - One poll per schedule tick (default every 60s) after a random jitter.\n * - Live status is fetched in one batched request for all watched UPs.\n * - Dynamics are fetched per UP with a gap between requests.\n * - Failures back off exponentially per source (live batch or one UP);\n * risk-control responses back off harder. Nothing throws to the scheduler.\n * - The first observation of an UP only records a baseline, so restarts and\n * new subscriptions never flood a conversation with old items.\n */\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { BiliApiError, type BiliClient, type DynamicItem, type LiveInfo } from './api.js';\nimport type { ResolvedBilibiliConfig } from './config.js';\nimport { formatDynamic, formatLiveStart } from './format.js';\nimport type { BiliOutboundPush } from './runtime.js';\nimport {\n emptyState,\n getBiliState,\n getBiliSubs,\n loadState,\n saveState,\n wantsDynamic,\n wantsLive,\n type BiliDb,\n type BiliRow,\n type WatchState,\n} from './store.js';\n\n/** At most this many new dynamics per UP are pushed in one tick. */\nexport const MAX_DYNAMICS_PER_TICK = 3;\n/** Dynamics older than this are recorded as seen but never pushed. */\nexport const MAX_DYNAMIC_AGE_SECONDS = 6 * 60 * 60;\n\nexport interface WatcherOptions {\n readonly db: BiliDb;\n readonly config: Readonly<ResolvedBilibiliConfig>;\n readonly client: BiliClient;\n readonly outbound: BiliOutboundPush | null;\n readonly signal?: AbortSignal;\n readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly random?: () => number;\n readonly now?: () => number;\n readonly warn?: (message: string) => void;\n}\n\nexport interface TickResult {\n readonly skipped?: 'busy' | 'aborted';\n readonly liveChecked: number;\n readonly dynamicsChecked: number;\n readonly pushed: number;\n readonly errors: readonly string[];\n}\n\ninterface Backoff {\n failures: number;\n until: number;\n reason: string;\n}\n\nasync function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return;\n try {\n await delay(ms, undefined, signal ? { signal } : undefined);\n } catch {\n // Aborted: the caller checks the signal.\n }\n}\n\nexport class BilibiliWatcher {\n readonly #options: WatcherOptions;\n readonly #backoff = new Map<string, Backoff>();\n #running = false;\n lastError: { readonly at: number; readonly message: string } | undefined;\n lastSuccessAt: number | undefined;\n\n constructor(options: WatcherOptions) {\n this.#options = options;\n }\n\n get #now(): number {\n return (this.#options.now ?? Date.now)();\n }\n\n #sleep(ms: number): Promise<void> {\n return (this.#options.sleep ?? defaultSleep)(ms, this.#options.signal);\n }\n\n #aborted(): boolean {\n return this.#options.signal?.aborted === true;\n }\n\n #blocked(key: string): boolean {\n const entry = this.#backoff.get(key);\n return entry !== undefined && entry.until > this.#now;\n }\n\n #fail(key: string, error: unknown): string {\n const previous = this.#backoff.get(key);\n const risk = error instanceof BiliApiError && error.kind === 'risk';\n const failures = (previous?.failures ?? 0) + (risk ? 2 : 1);\n const minutes = Math.min(this.#options.config.maxBackoffMinutes, 2 ** Math.min(failures - 1, 10));\n const message = error instanceof Error ? error.message : String(error);\n this.#backoff.set(key, { failures, until: this.#now + minutes * 60_000, reason: message });\n this.lastError = { at: this.#now, message: `${key}: ${message}` };\n (this.#options.warn ?? console.warn)(`[bilibili] ${key} 请求失败,${minutes} 分钟后重试:${message}`);\n return `${key}: ${message}`;\n }\n\n #ok(key: string): void {\n this.#backoff.delete(key);\n }\n\n /** Drop per-UP state once nobody subscribes to that UP any more. */\n forget(uid: string): void {\n this.#backoff.delete(`dynamic:${uid}`);\n }\n\n /** Human readable list of sources currently backing off. */\n backoffStatus(): string[] {\n const now = this.#now;\n return [...this.#backoff.entries()]\n .filter(([, entry]) => entry.until > now)\n .map(([key, entry]) => `${key}:${Math.ceil((entry.until - now) / 60_000)} 分钟后重试(${entry.reason})`);\n }\n\n async #push(rows: readonly BiliRow[], content: string): Promise<number> {\n const push = this.#options.outbound;\n if (!push) return 0;\n let pushed = 0;\n for (const row of rows) {\n const { adapter_name: adapterName, endpoint_id: endpointKey, channel_type: channelType, channel_id: channelId } = row;\n if (typeof adapterName !== 'string' || !adapterName || typeof endpointKey !== 'string' || !endpointKey\n || typeof channelId !== 'string' || !channelId\n || (channelType !== 'private' && channelType !== 'group' && channelType !== 'channel')) continue;\n try {\n await push({ adapterName, endpointKey, channelType, channelId, content });\n pushed += 1;\n } catch {\n // OutboundHost already logs; keep delivering to other subscribers.\n }\n }\n return pushed;\n }\n\n /** Record the current live / dynamic position of one UP without pushing. */\n async baseline(uid: string): Promise<void> {\n const State = getBiliState(this.#options.db);\n if (!State) return;\n const state = (await loadState(State, uid)) ?? emptyState(uid);\n const { config, client } = this.#options;\n if (config.live) {\n try {\n const info = (await client.getLiveStatus([uid])).get(uid);\n if (info) {\n state.liveStatus = info.liveStatus;\n state.liveTime = info.liveTime;\n }\n } catch { /* the next tick records the baseline */ }\n }\n if (config.dynamic && !state.dynamicReady) {\n try {\n const items = await client.getDynamics(uid);\n if (items.length > 0) markBaseline(state, items);\n } catch { /* the next tick records the baseline */ }\n }\n await saveState(State, state);\n }\n\n async tick(): Promise<TickResult> {\n if (this.#running) return { skipped: 'busy', liveChecked: 0, dynamicsChecked: 0, pushed: 0, errors: [] };\n this.#running = true;\n try {\n return await this.#tick();\n } finally {\n this.#running = false;\n }\n }\n\n async #tick(): Promise<TickResult> {\n const { db, config, client } = this.#options;\n const Subs = getBiliSubs(db);\n const State = getBiliState(db);\n const errors: string[] = [];\n let pushed = 0;\n let liveChecked = 0;\n let dynamicsChecked = 0;\n if (!Subs || !State) return { liveChecked, dynamicsChecked, pushed, errors: ['数据库尚未就绪'] };\n\n const rows = await Subs.select();\n if (rows.length === 0) return { liveChecked, dynamicsChecked, pushed, errors };\n const byUid = new Map<string, BiliRow[]>();\n for (const row of rows) {\n if (typeof row.uid !== 'string' || !row.uid) continue;\n const list = byUid.get(row.uid) ?? [];\n list.push(row);\n byUid.set(row.uid, list);\n }\n\n if (config.jitterMs > 0) await this.#sleep(Math.floor((this.#options.random ?? Math.random)() * config.jitterMs));\n if (this.#aborted()) return { skipped: 'aborted', liveChecked, dynamicsChecked, pushed, errors };\n\n if (config.live) {\n const liveUids = [...byUid.entries()].filter(([, subs]) => subs.some((s) => wantsLive(s.kinds))).map(([uid]) => uid);\n if (liveUids.length > 0 && !this.#blocked('live')) {\n try {\n const statuses = await client.getLiveStatus(liveUids);\n this.#ok('live');\n for (const uid of liveUids) {\n const info = statuses.get(uid);\n if (!info) continue;\n liveChecked += 1;\n const state = (await loadState(State, uid)) ?? emptyState(uid);\n if (shouldPushLive(state, info)) {\n pushed += await this.#push(byUid.get(uid)!.filter((s) => wantsLive(s.kinds)), formatLiveStart(info));\n }\n if (state.liveStatus !== info.liveStatus || state.liveTime !== info.liveTime) {\n state.liveStatus = info.liveStatus;\n state.liveTime = info.liveTime;\n await saveState(State, state);\n }\n }\n } catch (error) {\n errors.push(this.#fail('live', error));\n }\n }\n }\n\n if (config.dynamic) {\n let first = true;\n for (const [uid, subs] of byUid) {\n const targets = subs.filter((s) => wantsDynamic(s.kinds));\n const key = `dynamic:${uid}`;\n if (targets.length === 0 || this.#blocked(key)) continue;\n if (!first && config.requestGapMs > 0) await this.#sleep(config.requestGapMs);\n first = false;\n if (this.#aborted()) return { skipped: 'aborted', liveChecked, dynamicsChecked, pushed, errors };\n try {\n const items = await client.getDynamics(uid);\n this.#ok(key);\n dynamicsChecked += 1;\n const state = (await loadState(State, uid)) ?? emptyState(uid);\n // An empty feed is also what a soft risk-control block looks like:\n // never treat it as a baseline, or the next real answer would flood.\n if (items.length === 0) continue;\n if (!state.dynamicReady) {\n markBaseline(state, items);\n } else {\n const fresh = selectFreshDynamics(state, items);\n const uname = typeof subs[0]?.uname === 'string' ? subs[0].uname : uid;\n const recent = fresh.filter((item) => item.pubTs >= Math.floor(this.#now / 1000) - MAX_DYNAMIC_AGE_SECONDS);\n for (const item of recent.slice(-MAX_DYNAMICS_PER_TICK)) {\n pushed += await this.#push(targets, formatDynamic(uname, item));\n }\n for (const item of fresh) {\n state.seenDynamics.push(item.id);\n state.lastDynamicTs = Math.max(state.lastDynamicTs, item.pubTs);\n }\n if (fresh.length === 0) continue;\n }\n await saveState(State, state);\n } catch (error) {\n errors.push(this.#fail(key, error));\n // A risk-control answer applies to the whole client; stop this round.\n if (error instanceof BiliApiError && error.kind === 'risk') break;\n }\n }\n }\n if (errors.length === 0) this.lastSuccessAt = this.#now;\n return { liveChecked, dynamicsChecked, pushed, errors };\n }\n}\n\n/** Live start = known previous state, now live, and either was offline or a new session began. */\nexport function shouldPushLive(state: WatchState, info: LiveInfo): boolean {\n if (info.liveStatus !== 1 || state.liveStatus === -1) return false;\n if (state.liveStatus !== 1) return true;\n return info.liveTime > 0 && state.liveTime > 0 && info.liveTime !== state.liveTime;\n}\n\nexport function markBaseline(state: WatchState, items: readonly DynamicItem[]): void {\n for (const item of items) {\n if (!state.seenDynamics.includes(item.id)) state.seenDynamics.push(item.id);\n state.lastDynamicTs = Math.max(state.lastDynamicTs, item.pubTs);\n }\n state.dynamicReady = true;\n}\n\n/** New, unseen, timestamped items newer than the cursor, oldest first. */\nexport function selectFreshDynamics(state: WatchState, items: readonly DynamicItem[]): DynamicItem[] {\n const seen = new Set(state.seenDynamics);\n return items\n .filter((item) => item.pubTs > 0 && item.pubTs > state.lastDynamicTs && !seen.has(item.id))\n .sort((a, b) => a.pubTs - b.pubTs);\n}\n"]}
1
+ {"version":3,"file":"poll.js","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveLocale, type Locale } from 'zhin.js/i18n';\nimport { messages } from './i18n.js';\n/**\n * Conservative live / dynamic watcher.\n *\n * - One poll per schedule tick (default every 60s) after a random jitter.\n * - Live status is fetched in one batched request for all watched UPs.\n * - Dynamics are fetched per UP with a gap between requests.\n * - Failures back off exponentially per source (live batch or one UP);\n * risk-control responses back off harder. Nothing throws to the scheduler.\n * - The first observation of an UP only records a baseline, so restarts and\n * new subscriptions never flood a conversation with old items.\n */\nimport { setTimeout as delay } from 'node:timers/promises';\nimport { BiliApiError, type BiliClient, type DynamicItem, type LiveInfo } from './api.js';\nimport type { ResolvedBilibiliConfig } from './config.js';\nimport { formatDynamic, formatLiveStart } from './format.js';\nimport type { BiliOutboundPush } from './runtime.js';\nimport {\n emptyState,\n getBiliState,\n getBiliSubs,\n loadState,\n saveState,\n wantsDynamic,\n wantsLive,\n type BiliDb,\n type BiliRow,\n type WatchState,\n} from './store.js';\n\n/** At most this many new dynamics per UP are pushed in one tick. */\nexport const MAX_DYNAMICS_PER_TICK = 3;\n/** Dynamics older than this are recorded as seen but never pushed. */\nexport const MAX_DYNAMIC_AGE_SECONDS = 6 * 60 * 60;\n\nexport interface WatcherOptions {\n readonly db: BiliDb;\n readonly config: Readonly<ResolvedBilibiliConfig>;\n readonly client: BiliClient;\n readonly outbound: BiliOutboundPush | null;\n readonly signal?: AbortSignal;\n readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly random?: () => number;\n readonly now?: () => number;\n readonly warn?: (message: string) => void;\n}\n\nexport interface TickResult {\n readonly skipped?: 'busy' | 'aborted';\n readonly liveChecked: number;\n readonly dynamicsChecked: number;\n readonly pushed: number;\n readonly errors: readonly string[];\n}\n\ninterface Backoff {\n failures: number;\n until: number;\n reason: string;\n}\n\nasync function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0) return;\n try {\n await delay(ms, undefined, signal ? { signal } : undefined);\n } catch {\n // Aborted: the caller checks the signal.\n }\n}\n\nexport class BilibiliWatcher {\n readonly #options: WatcherOptions;\n readonly #backoff = new Map<string, Backoff>();\n #running = false;\n lastError: { readonly at: number; readonly message: string } | undefined;\n lastSuccessAt: number | undefined;\n\n constructor(options: WatcherOptions) {\n this.#options = options;\n }\n\n get #now(): number {\n return (this.#options.now ?? Date.now)();\n }\n\n #sleep(ms: number): Promise<void> {\n return (this.#options.sleep ?? defaultSleep)(ms, this.#options.signal);\n }\n\n #aborted(): boolean {\n return this.#options.signal?.aborted === true;\n }\n\n #blocked(key: string): boolean {\n const entry = this.#backoff.get(key);\n return entry !== undefined && entry.until > this.#now;\n }\n\n #fail(key: string, error: unknown): string {\n const previous = this.#backoff.get(key);\n const risk = error instanceof BiliApiError && error.kind === 'risk';\n const failures = (previous?.failures ?? 0) + (risk ? 2 : 1);\n const minutes = Math.min(this.#options.config.maxBackoffMinutes, 2 ** Math.min(failures - 1, 10));\n const message = error instanceof Error ? error.message : String(error);\n this.#backoff.set(key, { failures, until: this.#now + minutes * 60_000, reason: message });\n this.lastError = { at: this.#now, message: `${key}: ${message}` };\n (this.#options.warn ?? console.warn)(`[bilibili] ${key} 请求失败,${minutes} 分钟后重试:${message}`);\n return `${key}: ${message}`;\n }\n\n #ok(key: string): void {\n this.#backoff.delete(key);\n }\n\n /** Drop per-UP state once nobody subscribes to that UP any more. */\n forget(uid: string): void {\n this.#backoff.delete(`dynamic:${uid}`);\n }\n\n /** Human readable list of sources currently backing off. */\n backoffStatus(locale: Locale = 'zh-CN'): string[] {\n const now = this.#now;\n return [...this.#backoff.entries()]\n .filter(([, entry]) => entry.until > now)\n .map(([key, entry]) => messages(locale, 'backoff', {key, minutes: Math.ceil((entry.until - now) / 60_000), reason: entry.reason}));\n }\n\n async #push(rows: readonly BiliRow[], content: (locale: Locale) => string): Promise<number> {\n const push = this.#options.outbound;\n if (!push) return 0;\n let pushed = 0;\n for (const row of rows) {\n const { adapter_name: adapterName, endpoint_id: endpointKey, channel_type: channelType, channel_id: channelId } = row;\n if (typeof adapterName !== 'string' || !adapterName || typeof endpointKey !== 'string' || !endpointKey\n || typeof channelId !== 'string' || !channelId\n || (channelType !== 'private' && channelType !== 'group' && channelType !== 'channel')) continue;\n try {\n await push({ adapterName, endpointKey, channelType, channelId, locale: resolveLocale(row.locale), content: content(resolveLocale(row.locale)) });\n pushed += 1;\n } catch {\n // OutboundHost already logs; keep delivering to other subscribers.\n }\n }\n return pushed;\n }\n\n /** Record the current live / dynamic position of one UP without pushing. */\n async baseline(uid: string): Promise<void> {\n const State = getBiliState(this.#options.db);\n if (!State) return;\n const state = (await loadState(State, uid)) ?? emptyState(uid);\n const { config, client } = this.#options;\n if (config.live) {\n try {\n const info = (await client.getLiveStatus([uid])).get(uid);\n if (info) {\n state.liveStatus = info.liveStatus;\n state.liveTime = info.liveTime;\n }\n } catch { /* the next tick records the baseline */ }\n }\n if (config.dynamic && !state.dynamicReady) {\n try {\n const items = await client.getDynamics(uid);\n if (items.length > 0) markBaseline(state, items);\n } catch { /* the next tick records the baseline */ }\n }\n await saveState(State, state);\n }\n\n async tick(): Promise<TickResult> {\n if (this.#running) return { skipped: 'busy', liveChecked: 0, dynamicsChecked: 0, pushed: 0, errors: [] };\n this.#running = true;\n try {\n return await this.#tick();\n } finally {\n this.#running = false;\n }\n }\n\n async #tick(): Promise<TickResult> {\n const { db, config, client } = this.#options;\n const Subs = getBiliSubs(db);\n const State = getBiliState(db);\n const errors: string[] = [];\n let pushed = 0;\n let liveChecked = 0;\n let dynamicsChecked = 0;\n if (!Subs || !State) return { liveChecked, dynamicsChecked, pushed, errors: ['数据库尚未就绪'] };\n\n const rows = await Subs.select();\n if (rows.length === 0) return { liveChecked, dynamicsChecked, pushed, errors };\n const byUid = new Map<string, BiliRow[]>();\n for (const row of rows) {\n if (typeof row.uid !== 'string' || !row.uid) continue;\n const list = byUid.get(row.uid) ?? [];\n list.push(row);\n byUid.set(row.uid, list);\n }\n\n if (config.jitterMs > 0) await this.#sleep(Math.floor((this.#options.random ?? Math.random)() * config.jitterMs));\n if (this.#aborted()) return { skipped: 'aborted', liveChecked, dynamicsChecked, pushed, errors };\n\n if (config.live) {\n const liveUids = [...byUid.entries()].filter(([, subs]) => subs.some((s) => wantsLive(s.kinds))).map(([uid]) => uid);\n if (liveUids.length > 0 && !this.#blocked('live')) {\n try {\n const statuses = await client.getLiveStatus(liveUids);\n this.#ok('live');\n for (const uid of liveUids) {\n const info = statuses.get(uid);\n if (!info) continue;\n liveChecked += 1;\n const state = (await loadState(State, uid)) ?? emptyState(uid);\n if (shouldPushLive(state, info)) {\n pushed += await this.#push(byUid.get(uid)!.filter((s) => wantsLive(s.kinds)), locale => formatLiveStart(info, locale));\n }\n if (state.liveStatus !== info.liveStatus || state.liveTime !== info.liveTime) {\n state.liveStatus = info.liveStatus;\n state.liveTime = info.liveTime;\n await saveState(State, state);\n }\n }\n } catch (error) {\n errors.push(this.#fail('live', error));\n }\n }\n }\n\n if (config.dynamic) {\n let first = true;\n for (const [uid, subs] of byUid) {\n const targets = subs.filter((s) => wantsDynamic(s.kinds));\n const key = `dynamic:${uid}`;\n if (targets.length === 0 || this.#blocked(key)) continue;\n if (!first && config.requestGapMs > 0) await this.#sleep(config.requestGapMs);\n first = false;\n if (this.#aborted()) return { skipped: 'aborted', liveChecked, dynamicsChecked, pushed, errors };\n try {\n const items = await client.getDynamics(uid);\n this.#ok(key);\n dynamicsChecked += 1;\n const state = (await loadState(State, uid)) ?? emptyState(uid);\n // An empty feed is also what a soft risk-control block looks like:\n // never treat it as a baseline, or the next real answer would flood.\n if (items.length === 0) continue;\n if (!state.dynamicReady) {\n markBaseline(state, items);\n } else {\n const fresh = selectFreshDynamics(state, items);\n const uname = typeof subs[0]?.uname === 'string' ? subs[0].uname : uid;\n const recent = fresh.filter((item) => item.pubTs >= Math.floor(this.#now / 1000) - MAX_DYNAMIC_AGE_SECONDS);\n for (const item of recent.slice(-MAX_DYNAMICS_PER_TICK)) {\n pushed += await this.#push(targets, locale => formatDynamic(uname, item, locale));\n }\n for (const item of fresh) {\n state.seenDynamics.push(item.id);\n state.lastDynamicTs = Math.max(state.lastDynamicTs, item.pubTs);\n }\n if (fresh.length === 0) continue;\n }\n await saveState(State, state);\n } catch (error) {\n errors.push(this.#fail(key, error));\n // A risk-control answer applies to the whole client; stop this round.\n if (error instanceof BiliApiError && error.kind === 'risk') break;\n }\n }\n }\n if (errors.length === 0) this.lastSuccessAt = this.#now;\n return { liveChecked, dynamicsChecked, pushed, errors };\n }\n}\n\n/** Live start = known previous state, now live, and either was offline or a new session began. */\nexport function shouldPushLive(state: WatchState, info: LiveInfo): boolean {\n if (info.liveStatus !== 1 || state.liveStatus === -1) return false;\n if (state.liveStatus !== 1) return true;\n return info.liveTime > 0 && state.liveTime > 0 && info.liveTime !== state.liveTime;\n}\n\nexport function markBaseline(state: WatchState, items: readonly DynamicItem[]): void {\n for (const item of items) {\n if (!state.seenDynamics.includes(item.id)) state.seenDynamics.push(item.id);\n state.lastDynamicTs = Math.max(state.lastDynamicTs, item.pubTs);\n }\n state.dynamicReady = true;\n}\n\n/** New, unseen, timestamped items newer than the cursor, oldest first. */\nexport function selectFreshDynamics(state: WatchState, items: readonly DynamicItem[]): DynamicItem[] {\n const seen = new Set(state.seenDynamics);\n return items\n .filter((item) => item.pubTs > 0 && item.pubTs > state.lastDynamicTs && !seen.has(item.id))\n .sort((a, b) => a.pubTs - b.pubTs);\n}\n"]}
package/lib/runtime.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Locale } from 'zhin.js/i18n';
1
2
  import type { BiliClient } from './api.js';
2
3
  import type { ChannelInfo } from './channel.js';
3
4
  import type { ResolvedBilibiliConfig } from './config.js';
@@ -9,6 +10,7 @@ export type BiliOutboundPush = (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
  export interface BilibiliRuntime {
14
16
  readonly db: BiliDb;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAyBtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAC7C,uBAAuB,EACvB,4CAA4C,CAC7C,CAAC","sourcesContent":["import { createToken } from 'zhin.js';\nimport type { BiliClient } from './api.js';\nimport type { ChannelInfo } from './channel.js';\nimport type { ResolvedBilibiliConfig } from './config.js';\nimport type { BiliDb } from './store.js';\nimport type { BilibiliWatcher } from './poll.js';\n\nexport type BiliOutboundPush = (input: {\n readonly adapterName: string;\n readonly endpointKey: string;\n readonly channelType: ChannelInfo['channelType'];\n readonly channelId: string;\n readonly content: string;\n}) => Promise<void>;\n\nexport interface BilibiliRuntime {\n readonly db: BiliDb;\n readonly config: Readonly<ResolvedBilibiliConfig>;\n readonly client: BiliClient;\n readonly outbound: BiliOutboundPush | null;\n readonly watcher: BilibiliWatcher;\n /** False when no database is configured (subscriptions live in memory). */\n readonly persistent: boolean;\n}\n\nexport const bilibiliRuntimeToken = createToken<BilibiliRuntime>(\n 'zhin.bilibili.runtime',\n 'Owner-scoped Bilibili subscription runtime',\n);\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AA0BtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAC7C,uBAAuB,EACvB,4CAA4C,CAC7C,CAAC","sourcesContent":["import type { Locale } from 'zhin.js/i18n';\nimport { createToken } from 'zhin.js';\nimport type { BiliClient } from './api.js';\nimport type { ChannelInfo } from './channel.js';\nimport type { ResolvedBilibiliConfig } from './config.js';\nimport type { BiliDb } from './store.js';\nimport type { BilibiliWatcher } from './poll.js';\n\nexport type BiliOutboundPush = (input: {\n readonly adapterName: string;\n readonly endpointKey: string;\n readonly channelType: ChannelInfo['channelType'];\n readonly channelId: string;\n readonly content: string;\n readonly locale?: Locale;\n}) => Promise<void>;\n\nexport interface BilibiliRuntime {\n readonly db: BiliDb;\n readonly config: Readonly<ResolvedBilibiliConfig>;\n readonly client: BiliClient;\n readonly outbound: BiliOutboundPush | null;\n readonly watcher: BilibiliWatcher;\n /** False when no database is configured (subscriptions live in memory). */\n readonly persistent: boolean;\n}\n\nexport const bilibiliRuntimeToken = createToken<BilibiliRuntime>(\n 'zhin.bilibili.runtime',\n 'Owner-scoped Bilibili subscription runtime',\n);\n"]}
package/lib/store.d.ts CHANGED
@@ -29,7 +29,9 @@ export declare const BILI_SUBS_TABLE = "bilibili_subscriptions";
29
29
  export declare const BILI_STATE_TABLE = "bilibili_watch_state";
30
30
  export declare const BILI_TABLE_DEFINITIONS: Readonly<Record<string, Record<string, unknown>>>;
31
31
  export declare function defineBiliTables(db: {
32
- define: (name: string, schema: Record<string, unknown>) => void;
32
+ define: (name: string, schema: Record<string, unknown>, options?: {
33
+ addColumns: readonly string[];
34
+ }) => void;
33
35
  }): void;
34
36
  export declare function createInMemoryBiliDb(): BiliDb;
35
37
  export declare function getBiliSubs(db: BiliDb): BiliModel | null;
package/lib/store.js CHANGED
@@ -19,6 +19,7 @@ export const BILI_TABLE_DEFINITIONS = Object.freeze({
19
19
  channel_type: { type: 'text', default: 'private' },
20
20
  channel_id: { type: 'text', nullable: false },
21
21
  creator_id: { type: 'text', default: '' },
22
+ locale: { type: 'text', length: 16, nullable: true },
22
23
  created_at: { type: 'text', default: '' },
23
24
  },
24
25
  [BILI_STATE_TABLE]: {
@@ -34,7 +35,7 @@ export const BILI_TABLE_DEFINITIONS = Object.freeze({
34
35
  });
35
36
  export function defineBiliTables(db) {
36
37
  for (const [name, definition] of Object.entries(BILI_TABLE_DEFINITIONS))
37
- db.define(name, definition);
38
+ db.define(name, definition, name === BILI_SUBS_TABLE ? { addColumns: ['locale'] } : undefined);
38
39
  }
39
40
  function createMemoryModel() {
40
41
  const rows = [];
package/lib/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Subscription / watcher-state tables.\n *\n * The model surface matches the Plugin Runtime `DatabaseHostModel`, so the\n * scoped `databaseHostToken` facade is used directly when a database is\n * configured; otherwise an in-memory store keeps chat-only bots working\n * (subscriptions are then lost on restart).\n */\n\nexport type BiliRow = Record<string, unknown>;\n\nexport interface BiliModel {\n select: (...fields: string[]) => {\n where: (query: Record<string, unknown>) => PromiseLike<BiliRow[]>;\n then: <TResult1 = BiliRow[], TResult2 = never>(\n onfulfilled?: ((value: BiliRow[]) => TResult1 | PromiseLike<TResult1>) | null,\n onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,\n ) => Promise<TResult1 | TResult2>;\n };\n insert: (row: Record<string, unknown>) => Promise<unknown>;\n delete: () => { where: (query: Record<string, unknown>) => Promise<unknown> };\n update: (patch: Record<string, unknown>) => {\n where: (query: Record<string, unknown>) => Promise<unknown>;\n };\n}\n\nexport interface BiliDb {\n models: { get: (name: string) => BiliModel | undefined };\n}\n\nexport const BILI_SUBS_TABLE = 'bilibili_subscriptions';\nexport const BILI_STATE_TABLE = 'bilibili_watch_state';\n\nexport const BILI_TABLE_DEFINITIONS: Readonly<Record<string, Record<string, unknown>>> = Object.freeze({\n [BILI_SUBS_TABLE]: {\n uid: { type: 'text', nullable: false },\n uname: { type: 'text', default: '' },\n /** `all` | `live` | `dynamic` */\n kinds: { type: 'text', default: 'all' },\n adapter_name: { type: 'text', nullable: false },\n endpoint_id: { type: 'text', default: '' },\n channel_type: { type: 'text', default: 'private' },\n channel_id: { type: 'text', nullable: false },\n creator_id: { type: 'text', default: '' },\n created_at: { type: 'text', default: '' },\n },\n [BILI_STATE_TABLE]: {\n uid: { type: 'text', nullable: false },\n live_status: { type: 'integer', default: -1 },\n live_time: { type: 'integer', default: 0 },\n last_dynamic_ts: { type: 'integer', default: 0 },\n /** JSON array of recently pushed dynamic ids (dedupe across restarts). */\n seen_dynamics: { type: 'text', default: '[]' },\n dynamic_ready: { type: 'integer', default: 0 },\n updated_at: { type: 'text', default: '' },\n },\n});\n\nexport function defineBiliTables(db: { define: (name: string, schema: Record<string, unknown>) => void }): void {\n for (const [name, definition] of Object.entries(BILI_TABLE_DEFINITIONS)) db.define(name, definition);\n}\n\nfunction createMemoryModel(): BiliModel {\n const rows: BiliRow[] = [];\n let nextId = 1;\n const matches = (row: BiliRow, query: Record<string, unknown>): boolean =>\n Object.entries(query).every(([key, value]) => row[key] === value);\n return {\n select: () => {\n const promise = Promise.resolve().then(() => rows.map((row) => ({ ...row })));\n return {\n where: async (query: Record<string, unknown>) => rows.filter((row) => matches(row, query)).map((row) => ({ ...row })),\n then: (onfulfilled, onrejected) => promise.then(onfulfilled, onrejected),\n };\n },\n insert: async (row: Record<string, unknown>) => {\n const withId: BiliRow = { id: String(nextId++), ...row };\n rows.push(withId);\n return { ...withId };\n },\n delete: () => ({\n where: async (query: Record<string, unknown>) => {\n for (let index = rows.length - 1; index >= 0; index -= 1) if (matches(rows[index]!, query)) rows.splice(index, 1);\n },\n }),\n update: (patch: Record<string, unknown>) => ({\n where: async (query: Record<string, unknown>) => {\n for (const row of rows) if (matches(row, query)) Object.assign(row, patch);\n },\n }),\n };\n}\n\nexport function createInMemoryBiliDb(): BiliDb {\n const models = new Map<string, BiliModel>(Object.keys(BILI_TABLE_DEFINITIONS).map((name) => [name, createMemoryModel()]));\n return { models: { get: (name: string) => models.get(name) } };\n}\n\nexport function getBiliSubs(db: BiliDb): BiliModel | null {\n return db.models.get(BILI_SUBS_TABLE) ?? null;\n}\n\nexport function getBiliState(db: BiliDb): BiliModel | null {\n return db.models.get(BILI_STATE_TABLE) ?? null;\n}\n\nexport type SubscriptionKinds = 'all' | 'live' | 'dynamic';\n\nexport function parseKinds(input: string): SubscriptionKinds | undefined {\n const text = input.trim().toLowerCase();\n if (!text || text === 'all' || text === '全部') return 'all';\n if (text === 'live' || text === '直播' || text === '开播') return 'live';\n if (text === 'dynamic' || text === 'dyn' || text === '动态') return 'dynamic';\n return undefined;\n}\n\nexport function wantsLive(kinds: unknown): boolean {\n return kinds === 'all' || kinds === 'live' || kinds === undefined || kinds === '';\n}\n\nexport function wantsDynamic(kinds: unknown): boolean {\n return kinds === 'all' || kinds === 'dynamic' || kinds === undefined || kinds === '';\n}\n\nexport interface WatchState {\n uid: string;\n liveStatus: number;\n liveTime: number;\n lastDynamicTs: number;\n seenDynamics: string[];\n dynamicReady: boolean;\n}\n\nfunction intOf(value: unknown, fallback: number): number {\n const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;\n return Number.isFinite(parsed) ? parsed : fallback;\n}\n\nexport async function loadState(model: BiliModel, uid: string): Promise<WatchState | undefined> {\n const [row] = await model.select().where({ uid });\n if (!row) return undefined;\n let seen: string[] = [];\n try {\n const parsed: unknown = JSON.parse(typeof row.seen_dynamics === 'string' ? row.seen_dynamics : '[]');\n if (Array.isArray(parsed)) seen = parsed.filter((id): id is string => typeof id === 'string');\n } catch { /* corrupted cache: start over */ }\n return {\n uid,\n liveStatus: intOf(row.live_status, -1),\n liveTime: intOf(row.live_time, 0),\n lastDynamicTs: intOf(row.last_dynamic_ts, 0),\n seenDynamics: seen,\n dynamicReady: intOf(row.dynamic_ready, 0) === 1,\n };\n}\n\nexport const SEEN_DYNAMICS_LIMIT = 40;\n\nexport async function saveState(model: BiliModel, state: WatchState): Promise<void> {\n const patch = {\n live_status: state.liveStatus,\n live_time: state.liveTime,\n last_dynamic_ts: state.lastDynamicTs,\n seen_dynamics: JSON.stringify(state.seenDynamics.slice(-SEEN_DYNAMICS_LIMIT)),\n dynamic_ready: state.dynamicReady ? 1 : 0,\n updated_at: new Date().toISOString(),\n };\n const existing = await model.select().where({ uid: state.uid });\n if (existing.length > 0) await model.update(patch).where({ uid: state.uid });\n else await model.insert({ uid: state.uid, ...patch });\n}\n\nexport function emptyState(uid: string): WatchState {\n return { uid, liveStatus: -1, liveTime: 0, lastDynamicTs: 0, seenDynamics: [], dynamicReady: false };\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Subscription / watcher-state tables.\n *\n * The model surface matches the Plugin Runtime `DatabaseHostModel`, so the\n * scoped `databaseHostToken` facade is used directly when a database is\n * configured; otherwise an in-memory store keeps chat-only bots working\n * (subscriptions are then lost on restart).\n */\n\nexport type BiliRow = Record<string, unknown>;\n\nexport interface BiliModel {\n select: (...fields: string[]) => {\n where: (query: Record<string, unknown>) => PromiseLike<BiliRow[]>;\n then: <TResult1 = BiliRow[], TResult2 = never>(\n onfulfilled?: ((value: BiliRow[]) => TResult1 | PromiseLike<TResult1>) | null,\n onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,\n ) => Promise<TResult1 | TResult2>;\n };\n insert: (row: Record<string, unknown>) => Promise<unknown>;\n delete: () => { where: (query: Record<string, unknown>) => Promise<unknown> };\n update: (patch: Record<string, unknown>) => {\n where: (query: Record<string, unknown>) => Promise<unknown>;\n };\n}\n\nexport interface BiliDb {\n models: { get: (name: string) => BiliModel | undefined };\n}\n\nexport const BILI_SUBS_TABLE = 'bilibili_subscriptions';\nexport const BILI_STATE_TABLE = 'bilibili_watch_state';\n\nexport const BILI_TABLE_DEFINITIONS: Readonly<Record<string, Record<string, unknown>>> = Object.freeze({\n [BILI_SUBS_TABLE]: {\n uid: { type: 'text', nullable: false },\n uname: { type: 'text', default: '' },\n /** `all` | `live` | `dynamic` */\n kinds: { type: 'text', default: 'all' },\n adapter_name: { type: 'text', nullable: false },\n endpoint_id: { type: 'text', default: '' },\n channel_type: { type: 'text', default: 'private' },\n channel_id: { type: 'text', nullable: false },\n creator_id: { type: 'text', default: '' },\n locale: { type: 'text', length: 16, nullable: true },\n created_at: { type: 'text', default: '' },\n },\n [BILI_STATE_TABLE]: {\n uid: { type: 'text', nullable: false },\n live_status: { type: 'integer', default: -1 },\n live_time: { type: 'integer', default: 0 },\n last_dynamic_ts: { type: 'integer', default: 0 },\n /** JSON array of recently pushed dynamic ids (dedupe across restarts). */\n seen_dynamics: { type: 'text', default: '[]' },\n dynamic_ready: { type: 'integer', default: 0 },\n updated_at: { type: 'text', default: '' },\n },\n});\n\nexport function defineBiliTables(db: { define: (name: string, schema: Record<string, unknown>, options?: {addColumns: readonly string[]}) => void }): void {\n for (const [name, definition] of Object.entries(BILI_TABLE_DEFINITIONS)) db.define(name, definition, name === BILI_SUBS_TABLE ? { addColumns: ['locale'] } : undefined);\n}\n\nfunction createMemoryModel(): BiliModel {\n const rows: BiliRow[] = [];\n let nextId = 1;\n const matches = (row: BiliRow, query: Record<string, unknown>): boolean =>\n Object.entries(query).every(([key, value]) => row[key] === value);\n return {\n select: () => {\n const promise = Promise.resolve().then(() => rows.map((row) => ({ ...row })));\n return {\n where: async (query: Record<string, unknown>) => rows.filter((row) => matches(row, query)).map((row) => ({ ...row })),\n then: (onfulfilled, onrejected) => promise.then(onfulfilled, onrejected),\n };\n },\n insert: async (row: Record<string, unknown>) => {\n const withId: BiliRow = { id: String(nextId++), ...row };\n rows.push(withId);\n return { ...withId };\n },\n delete: () => ({\n where: async (query: Record<string, unknown>) => {\n for (let index = rows.length - 1; index >= 0; index -= 1) if (matches(rows[index]!, query)) rows.splice(index, 1);\n },\n }),\n update: (patch: Record<string, unknown>) => ({\n where: async (query: Record<string, unknown>) => {\n for (const row of rows) if (matches(row, query)) Object.assign(row, patch);\n },\n }),\n };\n}\n\nexport function createInMemoryBiliDb(): BiliDb {\n const models = new Map<string, BiliModel>(Object.keys(BILI_TABLE_DEFINITIONS).map((name) => [name, createMemoryModel()]));\n return { models: { get: (name: string) => models.get(name) } };\n}\n\nexport function getBiliSubs(db: BiliDb): BiliModel | null {\n return db.models.get(BILI_SUBS_TABLE) ?? null;\n}\n\nexport function getBiliState(db: BiliDb): BiliModel | null {\n return db.models.get(BILI_STATE_TABLE) ?? null;\n}\n\nexport type SubscriptionKinds = 'all' | 'live' | 'dynamic';\n\nexport function parseKinds(input: string): SubscriptionKinds | undefined {\n const text = input.trim().toLowerCase();\n if (!text || text === 'all' || text === '全部') return 'all';\n if (text === 'live' || text === '直播' || text === '开播') return 'live';\n if (text === 'dynamic' || text === 'dyn' || text === '动态') return 'dynamic';\n return undefined;\n}\n\nexport function wantsLive(kinds: unknown): boolean {\n return kinds === 'all' || kinds === 'live' || kinds === undefined || kinds === '';\n}\n\nexport function wantsDynamic(kinds: unknown): boolean {\n return kinds === 'all' || kinds === 'dynamic' || kinds === undefined || kinds === '';\n}\n\nexport interface WatchState {\n uid: string;\n liveStatus: number;\n liveTime: number;\n lastDynamicTs: number;\n seenDynamics: string[];\n dynamicReady: boolean;\n}\n\nfunction intOf(value: unknown, fallback: number): number {\n const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;\n return Number.isFinite(parsed) ? parsed : fallback;\n}\n\nexport async function loadState(model: BiliModel, uid: string): Promise<WatchState | undefined> {\n const [row] = await model.select().where({ uid });\n if (!row) return undefined;\n let seen: string[] = [];\n try {\n const parsed: unknown = JSON.parse(typeof row.seen_dynamics === 'string' ? row.seen_dynamics : '[]');\n if (Array.isArray(parsed)) seen = parsed.filter((id): id is string => typeof id === 'string');\n } catch { /* corrupted cache: start over */ }\n return {\n uid,\n liveStatus: intOf(row.live_status, -1),\n liveTime: intOf(row.live_time, 0),\n lastDynamicTs: intOf(row.last_dynamic_ts, 0),\n seenDynamics: seen,\n dynamicReady: intOf(row.dynamic_ready, 0) === 1,\n };\n}\n\nexport const SEEN_DYNAMICS_LIMIT = 40;\n\nexport async function saveState(model: BiliModel, state: WatchState): Promise<void> {\n const patch = {\n live_status: state.liveStatus,\n live_time: state.liveTime,\n last_dynamic_ts: state.lastDynamicTs,\n seen_dynamics: JSON.stringify(state.seenDynamics.slice(-SEEN_DYNAMICS_LIMIT)),\n dynamic_ready: state.dynamicReady ? 1 : 0,\n updated_at: new Date().toISOString(),\n };\n const existing = await model.select().where({ uid: state.uid });\n if (existing.length > 0) await model.update(patch).where({ uid: state.uid });\n else await model.insert({ uid: state.uid, ...patch });\n}\n\nexport function emptyState(uid: string): WatchState {\n return { uid, liveStatus: -1, liveTime: 0, lastDynamicTs: 0, seenDynamics: [], dynamicReady: false };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-bilibili",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Bilibili live-start and UP dynamic subscriptions for Zhin.js (Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -41,20 +41,20 @@
41
41
  },
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
- "@zhin.js/skill": "1.1.9",
45
- "@zhin.js/tool": "1.1.9"
44
+ "@zhin.js/skill": "1.1.10",
45
+ "@zhin.js/tool": "1.1.11"
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/command": "1.1.10",
52
- "zhin.js": "1.1.15"
51
+ "@zhin.js/command": "1.1.11",
52
+ "zhin.js": "1.1.17"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "zod": "^4.0.0",
56
- "@zhin.js/command": "^1.1.10",
57
- "zhin.js": "^1.1.15"
56
+ "@zhin.js/command": "^1.1.11",
57
+ "zhin.js": "^1.1.17"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "@zhin.js/command": {
package/plugin.js CHANGED
@@ -36,6 +36,7 @@ export default definePlugin({
36
36
  endpointKey: input.endpointKey,
37
37
  conversation: { kind: input.channelType, id: input.channelId },
38
38
  content: input.content,
39
+ locale: input.locale,
39
40
  });
40
41
  }
41
42
  : null;
package/schema.json CHANGED
@@ -6,57 +6,102 @@
6
6
  "pollCron": {
7
7
  "type": "string",
8
8
  "default": "0 * * * * *",
9
- "description": "轮询频率(6 段 Cron,默认每 60 秒) / Poll schedule (6-field cron, default every 60s)"
9
+ "description": "轮询频率(6 段 Cron,默认每 60 秒) / Poll schedule (6-field cron, default every 60s)",
10
+ "x-i18n": {
11
+ "en": {
12
+ "description": "Poll schedule (6-field cron, every 60 seconds by default)."
13
+ }
14
+ }
10
15
  },
11
16
  "jitterMs": {
12
17
  "type": "number",
13
18
  "default": 15000,
14
19
  "minimum": 0,
15
20
  "maximum": 120000,
16
- "description": "每轮轮询前的随机延迟上限(毫秒) / Max random delay before each poll (ms)"
21
+ "description": "每轮轮询前的随机延迟上限(毫秒) / Max random delay before each poll (ms)",
22
+ "x-i18n": {
23
+ "en": {
24
+ "description": "Maximum random delay before each poll, in milliseconds."
25
+ }
26
+ }
17
27
  },
18
28
  "requestGapMs": {
19
29
  "type": "number",
20
30
  "default": 1500,
21
31
  "minimum": 0,
22
32
  "maximum": 30000,
23
- "description": "同一轮内相邻动态请求的间隔(毫秒) / Gap between dynamic requests within one poll (ms)"
33
+ "description": "同一轮内相邻动态请求的间隔(毫秒) / Gap between dynamic requests within one poll (ms)",
34
+ "x-i18n": {
35
+ "en": {
36
+ "description": "Gap between dynamic requests within one poll, in milliseconds."
37
+ }
38
+ }
24
39
  },
25
40
  "maxBackoffMinutes": {
26
41
  "type": "number",
27
42
  "default": 30,
28
43
  "minimum": 1,
29
44
  "maximum": 720,
30
- "description": "接口失败或风控时指数退避的上限(分钟) / Upper bound of exponential backoff (minutes)"
45
+ "description": "接口失败或风控时指数退避的上限(分钟) / Upper bound of exponential backoff (minutes)",
46
+ "x-i18n": {
47
+ "en": {
48
+ "description": "Upper bound of exponential backoff after API failures or rate limiting, in minutes."
49
+ }
50
+ }
31
51
  },
32
52
  "maxPerChannel": {
33
53
  "type": "number",
34
54
  "default": 20,
35
55
  "minimum": 1,
36
56
  "maximum": 200,
37
- "description": "每个会话最多订阅的 UP 主数量 / Max UP subscriptions per conversation"
57
+ "description": "每个会话最多订阅的 UP 主数量 / Max UP subscriptions per conversation",
58
+ "x-i18n": {
59
+ "en": {
60
+ "description": "Maximum UP subscriptions per conversation."
61
+ }
62
+ }
38
63
  },
39
64
  "timeout": {
40
65
  "type": "number",
41
66
  "default": 10000,
42
67
  "minimum": 1000,
43
68
  "maximum": 60000,
44
- "description": "单次 HTTP 请求超时(毫秒) / HTTP timeout (ms)"
69
+ "description": "单次 HTTP 请求超时(毫秒) / HTTP timeout (ms)",
70
+ "x-i18n": {
71
+ "en": {
72
+ "description": "HTTP request timeout in milliseconds."
73
+ }
74
+ }
45
75
  },
46
76
  "live": {
47
77
  "type": "boolean",
48
78
  "default": true,
49
- "description": "是否轮询开播状态 / Poll live status"
79
+ "description": "是否轮询开播状态 / Poll live status",
80
+ "x-i18n": {
81
+ "en": {
82
+ "description": "Poll live-stream status."
83
+ }
84
+ }
50
85
  },
51
86
  "dynamic": {
52
87
  "type": "boolean",
53
88
  "default": true,
54
- "description": "是否轮询 UP 主动态 / Poll UP dynamics"
89
+ "description": "是否轮询 UP 主动态 / Poll UP dynamics",
90
+ "x-i18n": {
91
+ "en": {
92
+ "description": "Poll UP activity."
93
+ }
94
+ }
55
95
  },
56
96
  "cookie": {
57
97
  "type": "string",
58
98
  "default": "",
59
- "description": "可选的 B 站 Cookie(如 SESSDATA=...),风控严格时使用,请写成 ${BILIBILI_COOKIE} / Optional cookie, reference it as ${BILIBILI_COOKIE}"
99
+ "description": "可选的 B 站 Cookie(如 SESSDATA=...),风控严格时使用,请写成 ${BILIBILI_COOKIE} / Optional cookie, reference it as ${BILIBILI_COOKIE}",
100
+ "x-i18n": {
101
+ "en": {
102
+ "description": "Optional Bilibili cookie for stricter rate limits. Reference ${BILIBILI_COOKIE}; do not put credentials directly in configuration."
103
+ }
104
+ }
60
105
  }
61
106
  }
62
107
  }
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t, kindText } from "../../../../lib/i18n.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { bilibiliRuntimeToken } from "../../../../lib/runtime.js";
@@ -12,15 +13,15 @@ export default defineAgentTool({
12
13
  async execute(_input, context) {
13
14
  const Subs = getBiliSubs(context.use(bilibiliRuntimeToken).db);
14
15
  if (!Subs)
15
- return 'B 站订阅数据库尚未就绪';
16
+ return t(context.locale, 'db');
16
17
  const rows = await Subs.select();
17
18
  if (rows.length === 0)
18
- return '暂无任何 B 站订阅';
19
+ return t(context.locale, 'allEmpty');
19
20
  const grouped = new Map();
20
21
  for (const row of rows) {
21
22
  const key = `${String(row.channel_type)}:${String(row.channel_id)}`;
22
23
  const list = grouped.get(key) ?? [];
23
- list.push(`${String(row.uname || row.uid)}(UID ${String(row.uid)},${String(row.kinds || 'all')})`);
24
+ list.push(`${String(row.uname || row.uid)}(UID ${String(row.uid)},${kindText(row.kinds, context.locale)})`);
24
25
  grouped.set(key, list);
25
26
  }
26
27
  const lines = [];
@@ -29,6 +30,6 @@ export default defineAgentTool({
29
30
  for (const sub of subs)
30
31
  lines.push(` - ${sub}`);
31
32
  }
32
- return `B 站订阅总览 (${rows.length} 条):\n${lines.join('\n')}`;
33
+ return t(context.locale, 'overview', { count: rows.length, items: lines.join('\n') });
33
34
  },
34
35
  });
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t } from "../../../../lib/i18n.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  import { parseRoomId, parseUid } from "../../../../lib/api.js";
@@ -20,20 +21,20 @@ export default defineAgentTool({
20
21
  if (roomId) {
21
22
  const info = await client.getRoomInfo(roomId);
22
23
  const live = info.uid ? (await client.getLiveStatus([info.uid])).get(info.uid) : undefined;
23
- return formatRoom(info, live?.uname ?? '', live?.liveTime ?? 0);
24
+ return formatRoom(info, live?.uname ?? '', live?.liveTime ?? 0, Date.now(), context.locale);
24
25
  }
25
26
  const upUid = uid ? parseUid(uid) : undefined;
26
27
  if (!upUid)
27
- return '请提供直播间号(room)或 UP 主 UID(uid)';
28
+ return t(context.locale, 'toolInput');
28
29
  const live = (await client.getLiveStatus([upUid])).get(upUid);
29
30
  if (!live)
30
- return `UID ${upUid} 没有开通直播间`;
31
- return [`${live.uname || upUid}:${liveStatusText(live.liveStatus)}`, live.title && `标题:${live.title}`, live.url]
31
+ return t(context.locale, 'noRoom', { uid: upUid });
32
+ return [`${live.uname || upUid}:${liveStatusText(live.liveStatus, context.locale)}`, live.title && t(context.locale, 'title', { title: live.title }), live.url]
32
33
  .filter(Boolean)
33
34
  .join('\n');
34
35
  }
35
36
  catch (error) {
36
- return `查询失败:${error.message}`;
37
+ return t(context.locale, 'failed', { error: error.message });
37
38
  }
38
39
  },
39
40
  });