@xmanrui/dsh-im 4.14.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -816,14 +816,21 @@ export class FeishuHarnessBridge {
816
816
  accept(event) {
817
817
  if (this.#signal?.aborted) return Promise.resolve();
818
818
  const messageId = nonEmptyString(event?.message?.message_id);
819
- if (!messageId || isBotSender(event)) return Promise.resolve();
819
+ if (!messageId) return Promise.resolve();
820
+ const addressed = this.#isAddressed(event);
821
+ // Bot messages require a known, explicit group mention even in all-message mode.
822
+ if (isBotSender(event) && (
823
+ event?.message?.chat_type !== 'group'
824
+ || !this.#botOpenId
825
+ || !addressed
826
+ || senderOpenId(event) === this.#botOpenId
827
+ )) return Promise.resolve();
820
828
  if (!this.#accessPolicy && !isAllowedSender(event, this.#allowedSenderOpenIds)) {
821
829
  this.#status.messagesRejected += 1;
822
830
  this.#status.lastRejectedAt = new Date().toISOString();
823
831
  this.#logger.warn?.('[dsh-feishu] ignored a message from a sender outside the legacy allowlist');
824
832
  return Promise.resolve();
825
833
  }
826
- const addressed = this.#isAddressed(event);
827
834
  if (event?.message?.chat_type !== 'p2p'
828
835
  && this.#groupResponseMode === FEISHU_GROUP_RESPONSE_MODES.MENTION
829
836
  && !addressed) {
@@ -1771,7 +1778,7 @@ export class FeishuHarnessBridge {
1771
1778
  : t('链接约 {minutes} 分钟后过期', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
1772
1779
  await this.#send(chatId, [
1773
1780
  restarted ? t('旧授权链接已作废,已生成新的修复链接。') : t('🔧 准备补全权限与回调。'),
1774
- t('本次会增量添加当前缺少项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);im:resource(用于上传机器人发送的图片或文件);以及原生命令面板所需的 application:app_slash_command:read / write。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。'),
1781
+ t('本次会增量添加当前缺少项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);im:resource(用于上传机器人发送的图片或文件);im:message.group_at_msg.include_bot:readonly(用于接收群内其他机器人 @ 当前机器人的消息);以及原生命令面板所需的 application:app_slash_command:read / write。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。'),
1775
1782
  '',
1776
1783
  t('当前设备直接打开:'),
1777
1784
  url,
@@ -6,6 +6,7 @@ export const FEISHU_SECRET_REF = 'DSH_FEISHU_APP_SECRET';
6
6
  export const REQUIRED_TENANT_SCOPES = Object.freeze([
7
7
  'im:message.p2p_msg:readonly',
8
8
  'im:message.group_at_msg:readonly',
9
+ 'im:message.group_at_msg.include_bot:readonly',
9
10
  'im:message:readonly',
10
11
  'im:message:send_as_bot',
11
12
  'im:message.reactions:write_only',
@@ -76,8 +76,8 @@ export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu')
76
76
  * shares RegistrationManager's polling/state implementation while fixing the
77
77
  * update manifest in one place so callers can add only the card callback, the
78
78
  * message-read scope needed to download user-sent media, the resource scope
79
- * needed to upload bot-sent images/files, and the Slash Command scopes needed
80
- * for the native command panel, without adding unrelated scopes, events,
79
+ * needed to upload bot-sent images/files, the group bot-mention scope, and the
80
+ * Slash Command scopes for the native command panel, without adding unrelated scopes, events,
81
81
  * presets, or createOnly.
82
82
  */
83
83
  export class CallbackRepairManager {
@@ -116,6 +116,7 @@ export class CallbackRepairManager {
116
116
  tenant: [
117
117
  FEISHU_MESSAGE_READ_SCOPE,
118
118
  FEISHU_RESOURCE_SCOPE,
119
+ 'im:message.group_at_msg.include_bot:readonly',
119
120
  ...SLASH_COMMAND_TENANT_SCOPES,
120
121
  ],
121
122
  },
@@ -72,8 +72,8 @@ export default {
72
72
  '旧授权链接已作废,已生成新的修复链接。':
73
73
  'The previous authorization link was invalidated and a new repair link was generated.',
74
74
  '🔧 准备补全权限与回调。': '🔧 Preparing to complete permissions and the callback.',
75
- '本次会增量添加当前缺少项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);im:resource(用于上传机器人发送的图片或文件);以及原生命令面板所需的 application:app_slash_command:read / write。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。':
76
- 'This incrementally adds the currently missing items: the card callback card.action.trigger; the tenant scope im:message:readonly, shown by Feishu as “Read direct and group messages” and used to read images or files in user messages; im:resource, used to upload images or files sent by the bot; and application:app_slash_command:read / write for the native command panel. The confirmation page shows only items the app is currently missing; cancel if anything outside this scope appears.',
75
+ '本次会增量添加当前缺少项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);im:resource(用于上传机器人发送的图片或文件);im:message.group_at_msg.include_bot:readonly(用于接收群内其他机器人 @ 当前机器人的消息);以及原生命令面板所需的 application:app_slash_command:read / write。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。':
76
+ 'This incrementally adds the currently missing items: the card callback card.action.trigger; the tenant scope im:message:readonly, shown by Feishu as “Read direct and group messages” and used to read images or files in user messages; im:resource, used to upload images or files sent by the bot; im:message.group_at_msg.include_bot:readonly, used to receive group messages from other bots that mention this bot; and application:app_slash_command:read / write for the native command panel. The confirmation page shows only items the app is currently missing; cancel if anything outside this scope appears.',
77
77
  '当前设备直接打开:': 'Open directly on this device:',
78
78
  '若要用另一台设备扫码,发送 /repair qr。{expiry}。':
79
79
  'To scan with another device, send /repair qr. {expiry}.',
@@ -0,0 +1,26 @@
1
+ export const SESSION_CHANNEL_LABELS = Object.freeze(Object.fromEntries(Object.entries({
2
+ weixin: ['微信', 'WeChat'],
3
+ feishu: ['飞书', 'Feishu'],
4
+ dingtalk: ['钉钉', 'DingTalk'],
5
+ wecom: ['企业微信', 'WeCom'],
6
+ qq: ['QQ', 'QQ'],
7
+ slack: ['Slack', 'Slack'],
8
+ telegram: ['Telegram', 'Telegram'],
9
+ discord: ['Discord', 'Discord'],
10
+ whatsapp: ['WhatsApp', 'WhatsApp'],
11
+ office: ['AI Office', 'AI Office'],
12
+ }).map(([channel, labels]) => [channel, Object.freeze(labels)])));
13
+
14
+ /** Read only the exact, leading title prefix emitted by dsh-im. */
15
+ export function parseSessionChannelTitle(title) {
16
+ if (typeof title !== 'string') return null;
17
+ for (const [channel, labels] of Object.entries(SESSION_CHANNEL_LABELS)) {
18
+ for (const label of labels) {
19
+ const prefix = `${label} · `;
20
+ if (title.startsWith(prefix) && title.slice(prefix.length).trim()) {
21
+ return { channel, title: title.slice(prefix.length) };
22
+ }
23
+ }
24
+ }
25
+ return null;
26
+ }