@xmanrui/dsh-im 0.18.0 → 0.19.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.
@@ -407,6 +407,50 @@ const CSS = String.raw`
407
407
  .bxf-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
408
408
  .bxf-metric dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; font-weight: 550; margin: 3px 0 0; }
409
409
 
410
+ .bxf-responseMode { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 5px; margin-top: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
411
+ .bxf-responseModeHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
412
+ .bxf-responseModeHeader > span:first-child { grid-column: 1; grid-row: 1; white-space: nowrap; }
413
+ .bxf-responseModeStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
414
+ .bxf-responseModeSelect { min-width: 0; width: 100%; grid-column: 1 / -1; grid-row: 2; height: 32px; padding: 0 30px 0 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background-color: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease, background-color .16s ease; }
415
+ .bxf-responseModeSelect:hover:not(:disabled) { border-color: color-mix(in srgb, var(--bxf-accent) 45%, var(--dsw-alias-border-l2, #dfe1e5)); }
416
+ .bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
417
+ .bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
418
+ .bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
419
+ .bxf-responseModePermissionAction { grid-column: 1 / -1; grid-row: 4; display: flex; justify-content: flex-start; margin-top: 2px; }
420
+ .bxf-responseModePermissionButton { min-height: 28px; padding: 3px 9px; color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 30%, var(--dsw-alias-border-l2, #dfe1e5)); background: var(--dsw-alias-bg-layer-1, #fff); }
421
+ .bxf-responseModePermissionButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
422
+ .bxf-responseModeError { grid-column: 1 / -1; grid-row: 5; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
423
+
424
+ .bxf-botProvision {
425
+ position: relative;
426
+ margin-top: 14px;
427
+ scroll-margin-block: 20px;
428
+ animation: bxf-revealProvision .2s var(--ds-ease-out, ease-out) both;
429
+ }
430
+ .bxf-botProvision:focus { outline: none; }
431
+ .bxf-botProvision:focus-visible {
432
+ outline: 2px solid color-mix(in srgb, var(--bxf-accent) 75%, transparent);
433
+ outline-offset: 3px;
434
+ border-radius: 12px;
435
+ }
436
+ .bxf-botProvision > .bxf-provisionCard {
437
+ border-radius: 11px;
438
+ box-shadow: none;
439
+ background: color-mix(in srgb, var(--bxf-accent) 2.5%, var(--dsw-alias-bg-layer-3, #fff));
440
+ }
441
+ .bxf-botProvision .bxf-cardBody { padding: 18px; }
442
+ .bxf-botProvision .bxf-qrLayout {
443
+ grid-template-columns: 184px minmax(0, 1fr);
444
+ align-items: start;
445
+ gap: 24px;
446
+ }
447
+ .bxf-botProvision .bxf-qrFrame { width: 176px; height: 176px; padding: 10px; border-radius: 11px; }
448
+ .bxf-botProvision .bxf-countdown { width: 176px; }
449
+ .bxf-botProvision .bxf-qrCopy h3 { font-size: 18px; line-height: 26px; }
450
+ .bxf-botProvision .bxf-steps { gap: 8px; margin-top: 14px; }
451
+ .bxf-botProvision .bxf-actions { margin-top: 16px; }
452
+ .bxf-botProvision .bxf-inlineError { min-height: 160px; padding: 22px; }
453
+
410
454
  .bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
411
455
  .bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
412
456
  .bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
@@ -459,6 +503,7 @@ const CSS = String.raw`
459
503
  @keyframes bxf-rotate { to { transform: rotate(360deg); } }
460
504
  @keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
461
505
  @keyframes bxf-shimmer { to { background-position: -220% 0; } }
506
+ @keyframes bxf-revealProvision { from { opacity: 0; transform: translateY(-5px); } }
462
507
 
463
508
  @container (max-width: 620px) {
464
509
  .bxf-headingTools { gap: 6px; }
@@ -470,6 +515,7 @@ const CSS = String.raw`
470
515
  .bxf-intro { grid-template-columns: minmax(0, 1fr); }
471
516
  .bxf-markStage { display: none; }
472
517
  .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
518
+ .bxf-botProvision .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); }
473
519
  .bxf-qrCopy { width: 100%; }
474
520
  .bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
475
521
  .bxf-connectedTop { align-items: flex-start; flex-direction: column; }
@@ -68,6 +68,24 @@ const EN = Object.freeze({
68
68
  'WhatsApp账号': 'WhatsApp account',
69
69
  '微信设置': 'WeChat settings',
70
70
  '飞书机器人设置': 'Feishu bot settings',
71
+ '群聊响应方式': 'Group response mode',
72
+ '仅在 @机器人时响应(推荐)': 'Only respond when @mentioned (recommended)',
73
+ '响应所有群消息': 'Respond to all group messages',
74
+ '响应所有群消息(需飞书敏感权限)': 'Respond to all group messages (requires a sensitive Feishu scope)',
75
+ '重新授权': 'Reauthorize',
76
+ '去授权': 'Authorize',
77
+ '重新授权群消息权限': 'Reauthorize group-message permission',
78
+ '授权群消息权限': 'Authorize group-message permission',
79
+ '正在准备授权…': 'Preparing authorization…',
80
+ '正在准备…': 'Preparing…',
81
+ '私聊始终响应;群聊仅处理明确 @当前机器人的消息。群消息权限已开通,再次切换无需授权。': 'Direct messages always work; group chats require an explicit @mention of this bot. The group-message permission is already granted, so switching again needs no authorization.',
82
+ '私聊始终响应;群聊仅处理明确 @当前机器人的消息。选择全部消息后会打开飞书官方授权流程。': 'Direct messages always work; group chats require an explicit @mention of this bot. Selecting all messages opens the official Feishu authorization flow.',
83
+ '已开通“获取群组中所有消息”权限(im:message.group_msg);机器人会处理群聊中的所有可见消息。': 'The “Read all messages in associated group chat” scope (im:message.group_msg) is granted; the bot processes every visible group message.',
84
+ '尚未确认“获取群组中所有消息”权限,请完成飞书授权。': 'The “Read all messages in associated group chat” scope has not been confirmed. Complete Feishu authorization.',
85
+ '私聊始终响应;群聊仅处理明确 @当前机器人的消息。': 'Direct messages always work; group chats require an explicit @mention of this bot.',
86
+ '需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。': 'Grant this bot the “Read all messages in associated group chat” Feishu scope (im:message.group_msg); once granted, it will process every visible group message.',
87
+ '群聊响应方式修改失败,请重试。': 'Could not update the group response mode. Try again.',
88
+ '群消息权限授权失败,请重试。': 'Could not authorize group-message permission. Try again.',
71
89
  '钉钉设置': 'DingTalk settings',
72
90
  '企业微信设置': 'WeCom settings',
73
91
  '扫码接入机器人': 'Scan QR code',
@@ -190,6 +208,7 @@ const EN = Object.freeze({
190
208
  '生成 QQ 二维码': 'Generate QQ QR code',
191
209
  '正在生成二维码…': 'Generating QR code…',
192
210
  '正在准备授权二维码': 'Preparing authorization QR code',
211
+ '正在准备权限授权二维码': 'Preparing permission authorization QR code',
193
212
  '正在准备微信二维码': 'Preparing WeChat QR code',
194
213
  '正在添加新机器人': 'Adding a new bot',
195
214
  '正在申请钉钉授权二维码…': 'Requesting DingTalk authorization QR code…',
@@ -207,6 +226,16 @@ const EN = Object.freeze({
207
226
  '在飞书中打开': 'Open in Feishu',
208
227
  '取消添加': 'Cancel',
209
228
  '使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
229
+ '使用飞书确认群消息权限': 'Confirm group-message permission with Feishu',
230
+ '扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the “Read all messages in associated group chat” scope. It does not create a new app. After confirmation, “Respond to all group messages” is enabled automatically; other bots are unaffected.',
231
+ '核对现有应用,并确认“获取群组中所有消息”权限': 'Review the existing app and confirm the “Read all messages in associated group chat” permission',
232
+ '保持本页打开,等待权限生效并自动切换响应方式': 'Keep this page open while the permission takes effect and the response mode switches automatically',
233
+ '取消授权': 'Cancel authorization',
234
+ '已确认,正在启用全部消息模式': 'Confirmed. Enabling all-message mode',
235
+ '权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Saving the setting and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
236
+ '权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
237
+ '正在为现有飞书应用申请群消息权限二维码,请稍候。': 'Requesting a group-message permission QR code for the existing Feishu app…',
238
+ '群消息权限没有开通完成': 'Group-message permission was not granted',
210
239
  '扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the card-button callback. It does not create a new app. This bot reconnects briefly after confirmation; other bots are not affected.',
211
240
  '核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
212
241
  '保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
@@ -219,14 +248,25 @@ const EN = Object.freeze({
219
248
  '修复卡片按钮': 'Repair card buttons',
220
249
  '等待扫码…': 'Waiting for scan…',
221
250
  '飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
251
+ '飞书服务返回了不匹配的群消息权限二维码': 'Feishu returned a group-message permission QR code for a different bot',
222
252
  '飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
253
+ '飞书服务返回的应用更新信息缺少 botId': 'Feishu app-update status is missing the bot ID',
223
254
  '飞书服务返回了不匹配的注册进度': 'Feishu returned registration progress for a different operation',
224
255
  '此机器人': 'this bot',
256
+ '用于为${botName}开通群消息权限的一次性授权二维码': 'One-time QR code for granting group-message permission to ${botName}',
257
+ '正在为「${botName}」开通群消息权限': 'Granting group-message permission to “${botName}”',
225
258
  '${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
259
+ '${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。': 'Group-message permission QR code generated for ${botName ?? "bot"}. Confirm it with Feishu.',
260
+ '${targetBotName}已开通群消息权限,并启用“响应所有群消息”。': '${targetBotName} now has group-message permission and “Respond to all group messages” is enabled.',
261
+ '${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。': '${targetBot.bot.name} now has group-message permission and “Respond to all group messages” is enabled.',
226
262
  '${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
227
263
  '已取消卡片按钮修复。': 'Card-button repair was cancelled.',
264
+ '已取消群消息权限授权。': 'Group-message permission authorization was cancelled.',
228
265
  '卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
266
+ '群消息权限已更新,但暂时无法确认机器人连接状态': 'The group-message permission was updated, but the bot connection could not be confirmed yet',
229
267
  '飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
268
+ '飞书群消息权限开通失败': 'Could not grant the Feishu group-message permission',
269
+ '请先完成当前飞书授权操作,再开通群消息权限。': 'Finish the current Feishu authorization before granting group-message permission.',
230
270
  '已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
231
271
  '正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
232
272
  '正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
@@ -525,16 +565,26 @@ function translateDynamic(text) {
525
565
  if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
526
566
  match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
527
567
  if (match) return `Remove ${match[1]} from DeepSeek Harness`;
568
+ match = /^(.+)的飞书授权流程$/.exec(text);
569
+ if (match) return `Feishu authorization flow for ${match[1]}`;
528
570
  match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
529
571
  if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
572
+ match = /^用于为(.+)开通群消息权限的一次性授权二维码$/.exec(text);
573
+ if (match) return `One-time QR code for granting group-message permission to ${match[1]}`;
530
574
  match = /^正在修复「(.+)」$/.exec(text);
531
575
  if (match) return `Repairing “${match[1]}”`;
576
+ match = /^正在为「(.+)」开通群消息权限$/.exec(text);
577
+ if (match) return `Granting group-message permission to “${match[1]}”`;
532
578
  match = /^修复(.+)的卡片按钮$/.exec(text);
533
579
  if (match) return `Repair card buttons for ${match[1]}`;
534
580
  match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
535
581
  if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
582
+ match = /^(.+)的群消息权限二维码已生成,请使用飞书确认。$/.exec(text);
583
+ if (match) return `Group-message permission QR code generated for ${match[1]}. Confirm it with Feishu.`;
536
584
  match = /^(.+)的卡片按钮已修复。$/.exec(text);
537
585
  if (match) return `Card buttons repaired for ${match[1]}.`;
586
+ match = /^(.+)已开通群消息权限,并启用“响应所有群消息”。$/.exec(text);
587
+ if (match) return `${match[1]} now has group-message permission and “Respond to all group messages” is enabled.`;
538
588
  match = /^(检查连接|重试连接)(.+)$/.exec(text);
539
589
  if (match) return `${localizeText(match[1])} ${match[2]}`;
540
590
  match = /^移除(.+)$/.exec(text);
@@ -144,6 +144,8 @@ export async function createProductionController(ctx, config = {}, internals = {
144
144
  appId: botConfig.appId,
145
145
  appSecret,
146
146
  domain: botConfig.domain,
147
+ botOpenId: botConfig.botOpenId,
148
+ groupResponseMode: botConfig.groupResponseMode,
147
149
  ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
148
150
  harness: workspaceScope.harness,
149
151
  state: workspaceScope.state,
@@ -7,6 +7,10 @@ import { publicConnectionTestResult } from '../../../../src/channels/shared/conn
7
7
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
8
8
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
9
9
  import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
10
+ import {
11
+ isFeishuGroupResponseMode,
12
+ normalizeFeishuGroupResponseMode,
13
+ } from '../../../../src/channels/feishu/group-response-mode.mjs';
10
14
  import {
11
15
  FEISHU_ENDPOINTS,
12
16
  FEISHU_RPC_CHANNEL,
@@ -26,8 +30,17 @@ const REGISTRATION_STATES = new Set([
26
30
  'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
27
31
  'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
28
32
  ]);
29
- const REGISTRATION_OPERATIONS = new Set(['provision', 'callback_repair']);
33
+ const REGISTRATION_OPERATIONS = new Set([
34
+ 'provision',
35
+ 'callback_repair',
36
+ 'group_message_permission',
37
+ ]);
30
38
  const CALLBACK_REPAIR_OPERATION = 'callback_repair';
39
+ const GROUP_MESSAGE_PERMISSION_OPERATION = 'group_message_permission';
40
+ const TARGETED_APP_UPDATE_OPERATIONS = new Set([
41
+ CALLBACK_REPAIR_OPERATION,
42
+ GROUP_MESSAGE_PERMISSION_OPERATION,
43
+ ]);
31
44
  const OFFICIAL_REGISTRATION_HOSTS = new Set([
32
45
  'accounts.feishu.cn',
33
46
  'accounts.larksuite.com',
@@ -59,6 +72,9 @@ const PUBLIC_ERROR_MESSAGES = Object.freeze({
59
72
  credential_update_failed: 'Unable to store the repaired Feishu credentials.',
60
73
  credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
61
74
  repair_connection_failed: 'The callback update was accepted, but the selected bot could not reconnect.',
75
+ group_message_permission_required: 'Authorize access to all group messages before enabling this mode.',
76
+ group_message_permission_save_failed: 'Feishu granted the permission, but all-message mode could not be saved.',
77
+ group_message_permission_connection_failed: 'Feishu granted the permission, but the selected bot could not reconnect.',
62
78
  card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
63
79
  card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
64
80
  card_action_probe_timeout: 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.',
@@ -138,7 +154,7 @@ function safeRegistrationUrl(value, operation, expectedHost) {
138
154
  || url.port
139
155
  || url.username
140
156
  || url.password) return undefined;
141
- if (operation === CALLBACK_REPAIR_OPERATION) {
157
+ if (TARGETED_APP_UPDATE_OPERATIONS.has(operation)) {
142
158
  const clientIds = url.searchParams.getAll('clientID');
143
159
  const transportKinds = url.searchParams.getAll('tp');
144
160
  const addons = url.searchParams.getAll('addons');
@@ -240,7 +256,7 @@ async function publicProvisioning(registration, encodeQr, expectedHost) {
240
256
  // remote update is committed. Keep only the opaque attempt identity so a
241
257
  // browser reload can resume polling the non-cancellable verification phase.
242
258
  if (registration.state === 'saving'
243
- && registration.operation === CALLBACK_REPAIR_OPERATION
259
+ && TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
244
260
  && registration.botId) {
245
261
  return { ...projection, submitted: true };
246
262
  }
@@ -259,6 +275,8 @@ function publicBotEntry(entry) {
259
275
  connected,
260
276
  configured: source.configured === true,
261
277
  agentPreset: normalizeAgentPresetId(source.agentPreset),
278
+ groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
279
+ groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
262
280
  bot: publicBot(source.bot),
263
281
  health: publicHealth(source, connected),
264
282
  };
@@ -274,12 +292,12 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
274
292
  const registration = publicRegistration(source.registration);
275
293
  const facts = connectionFacts(source.connection);
276
294
  const connected = source.connected === true || facts.connected;
277
- const repairTarget = registration.operation === CALLBACK_REPAIR_OPERATION
295
+ const appUpdateTarget = TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
278
296
  && registration.botId
279
297
  && Array.isArray(source.bots)
280
298
  ? source.bots.find((entry) => entry?.botId === registration.botId)
281
299
  : undefined;
282
- const expectedRegistrationHost = repairTarget?.bot?.domain === 'lark'
300
+ const expectedRegistrationHost = appUpdateTarget?.bot?.domain === 'lark'
283
301
  ? 'open.larksuite.com'
284
302
  : 'open.feishu.cn';
285
303
  const provisioning = await publicProvisioning(
@@ -345,6 +363,11 @@ function validPayload(endpoint, payload) {
345
363
  ? null
346
364
  : 'Callback repair requires a single valid botId.';
347
365
  }
366
+ if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
367
+ return hasOnlyKeys(payload, new Set(['botId'])) && safeOpaqueId(payload.botId)
368
+ ? null
369
+ : 'Group message permission update requires a single valid botId.';
370
+ }
348
371
  if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
349
372
  return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
350
373
  && validCredential(payload.appId, 256)
@@ -389,6 +412,13 @@ function validPayload(endpoint, payload) {
389
412
  return validAgentPresetPayload(payload)
390
413
  ? null : '请选择 Agent Preset。';
391
414
  }
415
+ if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
416
+ return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
417
+ && safeOpaqueId(payload.botId)
418
+ && isFeishuGroupResponseMode(payload.groupResponseMode)
419
+ ? null
420
+ : '请选择群聊响应方式。';
421
+ }
392
422
  return 'Unknown Feishu endpoint.';
393
423
  }
394
424
 
@@ -508,6 +538,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
508
538
  throw new Error('Callback repair did not produce a safe QR code.');
509
539
  }
510
540
  attemptQr.set(attemptId, value.verificationUrl);
541
+ } else if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
542
+ if (typeof controller.startGroupMessagePermission !== 'function') {
543
+ throw new Error('Feishu group message permission update is unavailable.');
544
+ }
545
+ const started = await controller.startGroupMessagePermission(payload.botId);
546
+ const attemptId = String(publicRegistration(started?.registration).attempt);
547
+ const ready = await waitForQr(controller, started, attemptId, signal);
548
+ value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
549
+ if (!value
550
+ || value.operation !== GROUP_MESSAGE_PERMISSION_OPERATION
551
+ || value.botId !== payload.botId) {
552
+ throw new Error('Group message permission update did not produce a safe QR code.');
553
+ }
554
+ attemptQr.set(attemptId, value.verificationUrl);
511
555
  } else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
512
556
  const current = await statusForRegistration(controller, payload.attemptId);
513
557
  if (!current || !sameAttempt(current, payload.attemptId)) {
@@ -542,14 +586,16 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
542
586
  after = await controller.cancelRegistration(payload.attemptId);
543
587
  }
544
588
  const afterRegistration = publicRegistration(after?.registration);
545
- if (registration.operation === CALLBACK_REPAIR_OPERATION
589
+ if (TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
546
590
  && registration.state === 'saving'
547
591
  && ['saving', 'succeeded'].includes(afterRegistration.state)) {
548
592
  value = {
549
593
  status: pollStatus(after),
550
594
  operation: afterRegistration.operation,
551
595
  ...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
552
- message: 'Callback repair was already submitted and is still being verified.',
596
+ message: registration.operation === GROUP_MESSAGE_PERMISSION_OPERATION
597
+ ? 'The permission update was already submitted and is still being applied.'
598
+ : 'Callback repair was already submitted and is still being verified.',
553
599
  };
554
600
  }
555
601
  if (value?.status !== 'connecting') {
@@ -623,6 +669,14 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
623
669
  await controller.updateAgentPreset(payload.botId, payload.agentPreset),
624
670
  { encodeQr: cachedEncodeQr },
625
671
  );
672
+ } else if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
673
+ if (typeof controller.updateGroupResponseMode !== 'function') {
674
+ throw new Error('Group response mode update is unavailable');
675
+ }
676
+ value = await toPublicFeishuStatus(
677
+ await controller.updateGroupResponseMode(payload.botId, payload.groupResponseMode),
678
+ { encodeQr: cachedEncodeQr },
679
+ );
626
680
  } else {
627
681
  if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
628
682
  value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
@@ -108,7 +108,7 @@ export class DiscordApi {
108
108
  headers: {
109
109
  authorization: `Bot ${this.#token}`,
110
110
  'content-type': 'application/json',
111
- 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.18.0)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.19.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),
@@ -40,6 +40,10 @@ import {
40
40
  workspaceListCard,
41
41
  } from './feishu-cards.mjs';
42
42
  import { MAX_WATCHES_PER_KEY } from './state-store.mjs';
43
+ import {
44
+ FEISHU_GROUP_RESPONSE_MODES,
45
+ normalizeFeishuGroupResponseMode,
46
+ } from './group-response-mode.mjs';
43
47
 
44
48
  const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
45
49
  const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
@@ -249,6 +253,8 @@ export class FeishuHarnessBridge {
249
253
  #signal;
250
254
  #botId;
251
255
  #appId;
256
+ #botOpenId;
257
+ #groupResponseMode;
252
258
  #repair;
253
259
  #repairOwnerOpenIds;
254
260
  #repairAttempt = null;
@@ -275,6 +281,8 @@ export class FeishuHarnessBridge {
275
281
  allowedSenderOpenIds = new Set(),
276
282
  botId,
277
283
  appId,
284
+ botOpenId,
285
+ groupResponseMode = FEISHU_GROUP_RESPONSE_MODES.ALL,
278
286
  repair,
279
287
  repairOwnerOpenIds,
280
288
  repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
@@ -308,6 +316,8 @@ export class FeishuHarnessBridge {
308
316
  this.#allowedSenderOpenIds = allowedSenderOpenIds;
309
317
  this.#botId = nonEmptyString(botId);
310
318
  this.#appId = nonEmptyString(appId);
319
+ this.#botOpenId = nonEmptyString(botOpenId);
320
+ this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
311
321
  this.#repair = repair ?? null;
312
322
  const repairOwners = repairOwnerOpenIds ?? allowedSenderOpenIds;
313
323
  this.#repairOwnerOpenIds = new Set(
@@ -327,6 +337,18 @@ export class FeishuHarnessBridge {
327
337
  }
328
338
  }
329
339
 
340
+ setGroupResponseMode(value) {
341
+ this.#groupResponseMode = normalizeFeishuGroupResponseMode(value);
342
+ }
343
+
344
+ #isAddressed(event) {
345
+ if (event?.message?.chat_type === 'p2p') return true;
346
+ const mentions = Array.isArray(event?.message?.mentions) ? event.message.mentions : [];
347
+ if (!this.#botOpenId) return mentions.length > 0;
348
+ return mentions.some((mention) => mention?.id?.open_id === this.#botOpenId
349
+ || mention?.open_id === this.#botOpenId);
350
+ }
351
+
330
352
  accept(event) {
331
353
  if (this.#signal?.aborted) return Promise.resolve();
332
354
  const messageId = nonEmptyString(event?.message?.message_id);
@@ -337,6 +359,12 @@ export class FeishuHarnessBridge {
337
359
  this.#logger.warn?.('[dsh-feishu] ignored a message from a sender outside the allowlist');
338
360
  return Promise.resolve();
339
361
  }
362
+ const addressed = this.#isAddressed(event);
363
+ if (event?.message?.chat_type !== 'p2p'
364
+ && this.#groupResponseMode === FEISHU_GROUP_RESPONSE_MODES.MENTION
365
+ && !addressed) {
366
+ return Promise.resolve();
367
+ }
340
368
  if (this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
341
369
  return Promise.resolve();
342
370
  }
@@ -362,8 +390,6 @@ export class FeishuHarnessBridge {
362
390
  const commandRunner = isControlCommand(commandText)
363
391
  ? runControlCommand
364
392
  : (isModelCommand(commandText) ? runModelCommand : null);
365
- const addressed = event?.message?.chat_type === 'p2p'
366
- || (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0);
367
393
  if (commandRunner && addressed) {
368
394
  const processing = this.#processFastCommand(
369
395
  event,
@@ -407,8 +433,7 @@ export class FeishuHarnessBridge {
407
433
  actor: senderOpenId(event),
408
434
  messageId,
409
435
  text: extractText(event) ?? '',
410
- addressed: event?.message?.chat_type === 'p2p'
411
- || (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0),
436
+ addressed,
412
437
  hasPendingQuestion: Boolean(pending),
413
438
  questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
414
439
  ? pending.queue
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { FeishuHarnessBridge } from './bridge.mjs';
3
3
  import { cardActionProbeCard } from './feishu-cards.mjs';
4
4
  import { VerifiedFeishuChannel } from './feishu-channel.mjs';
5
+ import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
5
6
  import {
6
7
  connectionTestTargetUnavailable,
7
8
  sendRememberedConnectionTest,
@@ -87,6 +88,8 @@ export class FeishuRuntime {
87
88
  #appId;
88
89
  #appSecret;
89
90
  #domain;
91
+ #botOpenId;
92
+ #groupResponseMode;
90
93
  #ownerOpenIds;
91
94
  #harness;
92
95
  #state;
@@ -109,6 +112,8 @@ export class FeishuRuntime {
109
112
  appId,
110
113
  appSecret,
111
114
  domain = 'feishu',
115
+ botOpenId,
116
+ groupResponseMode,
112
117
  ownerOpenId,
113
118
  ownerOpenIds,
114
119
  harness,
@@ -138,6 +143,8 @@ export class FeishuRuntime {
138
143
  this.#appId = appId;
139
144
  this.#appSecret = appSecret;
140
145
  this.#domain = domain;
146
+ this.#botOpenId = nonEmptyString(botOpenId);
147
+ this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
141
148
  this.#ownerOpenIds = normalizedOwners;
142
149
  this.#harness = harness;
143
150
  this.#state = state;
@@ -153,6 +160,11 @@ export class FeishuRuntime {
153
160
  return structuredClone(this.#status);
154
161
  }
155
162
 
163
+ setGroupResponseMode(value) {
164
+ this.#groupResponseMode = normalizeFeishuGroupResponseMode(value);
165
+ this.#bridge?.setGroupResponseMode(this.#groupResponseMode);
166
+ }
167
+
156
168
  async start() {
157
169
  if (this.#wsClient && this.#status.ready) return this.status;
158
170
  if (this.#starting) return this.#starting;
@@ -202,6 +214,8 @@ export class FeishuRuntime {
202
214
  allowedSenderOpenIds: new Set(this.#ownerOpenIds),
203
215
  botId: this.#botId,
204
216
  appId: this.#appId,
217
+ botOpenId: this.#botOpenId,
218
+ groupResponseMode: this.#groupResponseMode,
205
219
  repair: this.#repair,
206
220
  repairOwnerOpenIds: new Set(this.#ownerOpenIds.filter((value) => value !== '*')),
207
221
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -0,0 +1,71 @@
1
+ import { RegistrationManager } from './registration-manager.mjs';
2
+ import { assertTargetedAppUpdateUrl } from './repair-manager.mjs';
3
+
4
+ export const FEISHU_GROUP_MESSAGE_SCOPE = 'im:message.group_msg';
5
+ export const GROUP_MESSAGE_PERMISSION_OPERATION = 'group_message_permission';
6
+
7
+ function accountsDomain(domain) {
8
+ return domain === 'lark' ? 'accounts.larksuite.com' : 'accounts.feishu.cn';
9
+ }
10
+
11
+ export function assertGroupMessagePermissionUrl(value, expectedAppId, domain = 'feishu') {
12
+ return assertTargetedAppUpdateUrl(
13
+ value,
14
+ expectedAppId,
15
+ domain,
16
+ 'Feishu group message permission update',
17
+ );
18
+ }
19
+
20
+ /**
21
+ * Incrementally grants the one sensitive tenant scope needed by all-message
22
+ * mode to an existing app. The fixed manifest prevents this UI action from
23
+ * creating another app or silently adding unrelated capabilities.
24
+ */
25
+ export class GroupMessagePermissionManager {
26
+ #manager;
27
+ #appId;
28
+ #domain;
29
+
30
+ constructor({ registerApp, onCredentials, appId, domain = 'feishu' } = {}) {
31
+ if (typeof registerApp !== 'function') throw new TypeError('registerApp is required');
32
+ if (typeof onCredentials !== 'function') throw new TypeError('onCredentials is required');
33
+ if (typeof appId !== 'string' || !appId.trim()) throw new TypeError('appId is required');
34
+ if (domain !== 'feishu' && domain !== 'lark') throw new TypeError('domain is invalid');
35
+
36
+ this.#appId = appId.trim();
37
+ this.#domain = domain;
38
+ this.#manager = new RegistrationManager({
39
+ registerApp: (options) => registerApp({
40
+ ...options,
41
+ onQRCodeReady: (info) => {
42
+ assertGroupMessagePermissionUrl(info?.url, this.#appId, this.#domain);
43
+ options.onQRCodeReady(info);
44
+ },
45
+ }),
46
+ onCredentials,
47
+ });
48
+ }
49
+
50
+ start() {
51
+ return this.#manager.start({
52
+ source: 'deepseek-harness-group-message-permission',
53
+ domain: accountsDomain(this.#domain),
54
+ appId: this.#appId,
55
+ addons: {
56
+ preset: false,
57
+ scopes: { tenant: [FEISHU_GROUP_MESSAGE_SCOPE] },
58
+ },
59
+ });
60
+ }
61
+
62
+ status() {
63
+ return this.#manager.status();
64
+ }
65
+
66
+ cancel() {
67
+ return this.#manager.cancel();
68
+ }
69
+ }
70
+
71
+ export default GroupMessagePermissionManager;
@@ -0,0 +1,15 @@
1
+ export const FEISHU_GROUP_RESPONSE_MODES = Object.freeze({
2
+ MENTION: 'mention',
3
+ ALL: 'all',
4
+ });
5
+
6
+ export function normalizeFeishuGroupResponseMode(value) {
7
+ return value === FEISHU_GROUP_RESPONSE_MODES.ALL
8
+ ? FEISHU_GROUP_RESPONSE_MODES.ALL
9
+ : FEISHU_GROUP_RESPONSE_MODES.MENTION;
10
+ }
11
+
12
+ export function isFeishuGroupResponseMode(value) {
13
+ return value === FEISHU_GROUP_RESPONSE_MODES.MENTION
14
+ || value === FEISHU_GROUP_RESPONSE_MODES.ALL;
15
+ }