@xmanrui/dsh-im 0.18.0 → 1.0.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.
Files changed (37) hide show
  1. package/README.en.md +12 -1
  2. package/README.md +12 -1
  3. package/lib/client.js +341 -37
  4. package/lib/index.js +162 -156
  5. package/package.json +1 -1
  6. package/plugin-src/client/channels/feishu/api.js +23 -5
  7. package/plugin-src/client/channels/feishu/index.js +291 -39
  8. package/plugin-src/client/channels/feishu/styles.js +46 -0
  9. package/plugin-src/client/i18n.js +50 -0
  10. package/plugin-src/host/channels/dingtalk/production.mjs +5 -2
  11. package/plugin-src/host/channels/feishu/production.mjs +7 -2
  12. package/plugin-src/host/channels/feishu/rpc.mjs +61 -7
  13. package/plugin-src/host/channels/qq/production.mjs +5 -2
  14. package/plugin-src/host/channels/shared/production.mjs +5 -2
  15. package/plugin-src/host/channels/slack/production.mjs +5 -2
  16. package/plugin-src/host/channels/wecom/production.mjs +5 -2
  17. package/plugin-src/host/channels/weixin/production.mjs +5 -2
  18. package/plugin-src/host/channels/whatsapp/production.mjs +5 -2
  19. package/src/channels/dingtalk/dingtalk-bridge.mjs +11 -1
  20. package/src/channels/discord/discord-api.mjs +1 -1
  21. package/src/channels/feishu/bridge.mjs +40 -5
  22. package/src/channels/feishu/feishu-cards.mjs +4 -0
  23. package/src/channels/feishu/feishu-runtime.mjs +14 -0
  24. package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
  25. package/src/channels/feishu/group-response-mode.mjs +15 -0
  26. package/src/channels/feishu/multi-bot-controller.mjs +258 -9
  27. package/src/channels/feishu/plugin-config-store.mjs +3 -0
  28. package/src/channels/feishu/repair-manager.mjs +17 -3
  29. package/src/channels/qq/qq-bridge.mjs +11 -1
  30. package/src/channels/shared/bot-workspace-store.mjs +75 -4
  31. package/src/channels/shared/preset-command.mjs +305 -0
  32. package/src/channels/shared/text-harness-bridge.mjs +11 -1
  33. package/src/channels/telegram/telegram-api.mjs +31 -0
  34. package/src/channels/telegram/telegram-runtime.mjs +27 -1
  35. package/src/channels/wecom/wecom-bridge.mjs +11 -1
  36. package/src/channels/weixin/weixin-api.mjs +1 -1
  37. package/src/channels/weixin/weixin-bridge.mjs +11 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.18.0",
3
+ "version": "1.0.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -14,6 +14,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
14
14
  status: "connection.status",
15
15
  beginProvisioning: "provision.begin",
16
16
  beginCallbackRepair: "bot.callback-repair.begin",
17
+ beginGroupMessagePermission: "bot.group-message-permission.begin",
17
18
  pollProvisioning: "provision.poll",
18
19
  cancelProvisioning: "provision.cancel",
19
20
  bindCredentials: "bot.bind-credentials",
@@ -22,6 +23,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
22
23
  deleteBot: "bot.delete",
23
24
  setWorkspace: "bot.workspace.set",
24
25
  setAgentPreset: "bot.preset.set",
26
+ setGroupResponseMode: "bot.group-response-mode.set",
25
27
  // Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
26
28
  testConnection: "connection.test",
27
29
  disconnect: "connection.disconnect",
@@ -30,6 +32,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
30
32
  export const FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
31
33
  PROVISION: "provision",
32
34
  CALLBACK_REPAIR: "callback_repair",
35
+ GROUP_MESSAGE_PERMISSION: "group_message_permission",
33
36
  });
34
37
 
35
38
  const CONNECTION_STATES = new Set([
@@ -70,6 +73,10 @@ function optionalTimestamp(value) {
70
73
  return undefined;
71
74
  }
72
75
 
76
+ export function normalizeGroupResponseMode(value) {
77
+ return value === "all" ? "all" : "mention";
78
+ }
79
+
73
80
  function clamp(value, min, max, fallback) {
74
81
  return typeof value === "number" && Number.isFinite(value)
75
82
  ? Math.min(max, Math.max(min, value))
@@ -77,9 +84,18 @@ function clamp(value, min, max, fallback) {
77
84
  }
78
85
 
79
86
  function normalizeRegistrationOperation(value) {
80
- return value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
81
- ? FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
82
- : FEISHU_REGISTRATION_OPERATIONS.PROVISION;
87
+ if (value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR) {
88
+ return FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
89
+ }
90
+ if (value === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION) {
91
+ return FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
92
+ }
93
+ return FEISHU_REGISTRATION_OPERATIONS.PROVISION;
94
+ }
95
+
96
+ function isTargetedAppUpdate(operation) {
97
+ return operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
98
+ || operation === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
83
99
  }
84
100
 
85
101
  export function unwrapRpcResult(result) {
@@ -112,8 +128,8 @@ export function normalizeProvisioning(value, now = Date.now()) {
112
128
  const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60);
113
129
  const operation = normalizeRegistrationOperation(source.operation);
114
130
  const botId = optionalString(source.botId);
115
- if (operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR && !botId) {
116
- throw new Error("飞书服务返回的修复信息缺少 botId");
131
+ if (isTargetedAppUpdate(operation) && !botId) {
132
+ throw new Error("飞书服务返回的应用更新信息缺少 botId");
117
133
  }
118
134
  return {
119
135
  attemptId,
@@ -186,6 +202,8 @@ export function normalizeBotConnection(value, fallbackBotId) {
186
202
  configured: value.configured !== false,
187
203
  workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
188
204
  agentPreset: normalizeAgentPresetId(value.agentPreset),
205
+ groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
206
+ groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
189
207
  bot: normalizeBot(value.bot),
190
208
  health: normalizeHealth(value.health, connected),
191
209
  error: normalizeError(value.error),
@@ -9,6 +9,7 @@ import {
9
9
  FEISHU_RPC_CHANNEL,
10
10
  formatRemaining,
11
11
  normalizeBotsSnapshot,
12
+ normalizeGroupResponseMode,
12
13
  normalizePollResult,
13
14
  normalizeProvisioning,
14
15
  presentError,
@@ -28,11 +29,20 @@ export const name = "feishu-settings";
28
29
  export const inject = ["slots", "connection"];
29
30
 
30
31
  const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
32
+ const GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
31
33
 
32
34
  function isCallbackRepair(value) {
33
35
  return value?.operation === CALLBACK_REPAIR_OPERATION;
34
36
  }
35
37
 
38
+ function isGroupMessagePermission(value) {
39
+ return value?.operation === GROUP_MESSAGE_PERMISSION_OPERATION;
40
+ }
41
+
42
+ function isTargetedAppUpdate(value) {
43
+ return isCallbackRepair(value) || isGroupMessagePermission(value);
44
+ }
45
+
36
46
  function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
37
47
  return h("svg", {
38
48
  width: size,
@@ -241,6 +251,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
241
251
  const expired = provision.expired === true || remaining === 0;
242
252
  const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
243
253
  const repairing = isCallbackRepair(provision);
254
+ const grantingGroupMessages = isGroupMessagePermission(provision);
244
255
  const botName = provision.botName ?? "此机器人";
245
256
 
246
257
  React.useEffect(() => setImageFailed(false), [qrSource]);
@@ -254,7 +265,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
254
265
  src: qrSource,
255
266
  alt: repairing
256
267
  ? `用于修复${botName}卡片按钮的一次性授权二维码`
257
- : "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
268
+ : grantingGroupMessages
269
+ ? `用于为${botName}开通群消息权限的一次性授权二维码`
270
+ : "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
258
271
  onError: () => setImageFailed(true),
259
272
  })
260
273
  : h("div", { className: "bxf-qrFallback dim-qrFallback" },
@@ -278,21 +291,35 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
278
291
  h("div", { className: "bxf-qrCopy dim-qrCopy" },
279
292
  h("div", { className: "bxf-stateLabel dim-stateLabel" },
280
293
  h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
281
- h("span", null, repairing ? `正在修复「${botName}」` : "正在添加新机器人")),
294
+ h("span", null, repairing
295
+ ? `正在修复「${botName}」`
296
+ : grantingGroupMessages
297
+ ? `正在为「${botName}」开通群消息权限`
298
+ : "正在添加新机器人")),
282
299
  h("h3", null, expired
283
300
  ? "刷新二维码后继续"
284
- : repairing ? "使用飞书扫码修复卡片按钮" : "使用飞书扫码创建机器人"),
301
+ : repairing
302
+ ? "使用飞书扫码修复卡片按钮"
303
+ : grantingGroupMessages
304
+ ? "使用飞书确认群消息权限"
305
+ : "使用飞书扫码创建机器人"),
285
306
  h("p", null, repairing
286
307
  ? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
287
- : "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
308
+ : grantingGroupMessages
309
+ ? "扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。"
310
+ : "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
288
311
  h("ol", { className: "bxf-steps dim-steps" },
289
312
  h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
290
313
  h("li", null, repairing
291
314
  ? "核对现有应用名称,并确认只新增卡片回调"
292
- : "核对应用名称与权限范围,并确认创建"),
315
+ : grantingGroupMessages
316
+ ? "核对现有应用,并确认“获取群组中所有消息”权限"
317
+ : "核对应用名称与权限范围,并确认创建"),
293
318
  h("li", null, repairing
294
319
  ? "保持本页打开,等待卡片按钮修复完成"
295
- : "保持本页打开,等待新机器人的长连接就绪")),
320
+ : grantingGroupMessages
321
+ ? "保持本页打开,等待权限生效并自动切换响应方式"
322
+ : "保持本页打开,等待新机器人的长连接就绪")),
296
323
  h("div", { className: "bxf-actions dim-viewActions" },
297
324
  expired
298
325
  ? h(Button, {
@@ -307,7 +334,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
307
334
  !expired
308
335
  ? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
309
336
  : null,
310
- h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加")),
337
+ h(Button, { onClick: onCancel, disabled: busy }, repairing
338
+ ? "取消修复"
339
+ : grantingGroupMessages ? "取消授权" : "取消添加")),
311
340
  ),
312
341
  ),
313
342
  );
@@ -316,34 +345,50 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
316
345
  function ProvisionProgress({ phase, provision, onCancel, busy }) {
317
346
  const connecting = phase === "connecting";
318
347
  const repairing = isCallbackRepair(provision);
348
+ const grantingGroupMessages = isGroupMessagePermission(provision);
319
349
  return h("div", {
320
350
  className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
321
351
  "aria-busy": "true",
322
352
  },
323
353
  h("div", { className: "dim-spinner", "aria-hidden": "true" }),
324
354
  h("h3", null, connecting
325
- ? repairing ? "已确认,正在完成卡片按钮修复" : "已确认,正在连接新机器人"
326
- : repairing ? "正在准备修复二维码" : "正在准备授权二维码"),
355
+ ? repairing
356
+ ? "已确认,正在完成卡片按钮修复"
357
+ : grantingGroupMessages
358
+ ? "已确认,正在启用全部消息模式"
359
+ : "已确认,正在连接新机器人"
360
+ : repairing
361
+ ? "正在准备修复二维码"
362
+ : grantingGroupMessages ? "正在准备权限授权二维码" : "正在准备授权二维码"),
327
363
  h("p", null, connecting
328
364
  ? repairing
329
365
  ? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
330
- : "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
366
+ : grantingGroupMessages
367
+ ? "权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。"
368
+ : "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
331
369
  : repairing
332
370
  ? "正在为现有飞书应用申请一次性更新二维码,请稍候。"
333
- : "正在向飞书申请一次性授权二维码,请稍候。"),
371
+ : grantingGroupMessages
372
+ ? "正在为现有飞书应用申请群消息权限二维码,请稍候。"
373
+ : "正在向飞书申请一次性授权二维码,请稍候。"),
334
374
  connecting && onCancel
335
375
  ? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
336
- h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加"))
376
+ h(Button, { onClick: onCancel, disabled: busy }, repairing
377
+ ? "取消修复"
378
+ : grantingGroupMessages ? "取消授权" : "取消添加"))
337
379
  : null,
338
380
  );
339
381
  }
340
382
 
341
383
  function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
342
384
  const repairing = isCallbackRepair(provision);
385
+ const grantingGroupMessages = isGroupMessagePermission(provision);
343
386
  return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
344
387
  h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
345
388
  h("div", null,
346
- h("h3", null, repairing ? "卡片按钮没有修复完成" : "新机器人没有添加完成"),
389
+ h("h3", null, repairing
390
+ ? "卡片按钮没有修复完成"
391
+ : grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
347
392
  h("p", null, error.message),
348
393
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
349
394
  h("div", { className: "bxf-actions dim-viewActions" },
@@ -413,10 +458,95 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
413
458
  );
414
459
  }
415
460
 
461
+ function GroupResponseModeEditor({
462
+ value,
463
+ permissionGranted = false,
464
+ disabled = false,
465
+ authorizationDisabled = false,
466
+ onSave,
467
+ onAuthorize,
468
+ }) {
469
+ const current = normalizeGroupResponseMode(value);
470
+ const [saving, setSaving] = React.useState(false);
471
+ const [authorizing, setAuthorizing] = React.useState(false);
472
+ const [error, setError] = React.useState(null);
473
+
474
+ const change = async (event) => {
475
+ const next = normalizeGroupResponseMode(event.target.value);
476
+ if (next === current || saving || disabled) return;
477
+ setSaving(true);
478
+ setError(null);
479
+ try {
480
+ await onSave?.(next);
481
+ } catch (cause) {
482
+ setError(cause?.message ?? "群聊响应方式修改失败,请重试。");
483
+ } finally {
484
+ setSaving(false);
485
+ }
486
+ };
487
+
488
+ const authorize = async () => {
489
+ if (current !== "all" || saving || authorizing || disabled || authorizationDisabled) return;
490
+ setAuthorizing(true);
491
+ setError(null);
492
+ try {
493
+ await onAuthorize?.();
494
+ } catch (cause) {
495
+ setError(cause?.message ?? "群消息权限授权失败,请重试。");
496
+ } finally {
497
+ setAuthorizing(false);
498
+ }
499
+ };
500
+
501
+ return h("div", { className: "bxf-responseMode dim-responseMode" },
502
+ h("div", { className: "bxf-responseModeHeader dim-responseModeHeader" },
503
+ h("span", null, "群聊响应方式"),
504
+ saving || authorizing
505
+ ? h("span", { className: "bxf-responseModeStatus dim-responseModeStatus" },
506
+ saving ? "保存中…" : "正在准备授权…")
507
+ : null),
508
+ h("select", {
509
+ className: "bxf-responseModeSelect dim-responseModeSelect",
510
+ value: current,
511
+ disabled: disabled || saving,
512
+ "aria-label": "群聊响应方式",
513
+ onChange: (event) => { void change(event); },
514
+ },
515
+ h("option", { value: "mention" }, "仅在 @机器人时响应(推荐)"),
516
+ h("option", { value: "all" }, "响应所有群消息"),
517
+ ),
518
+ h("small", { className: "bxf-responseModeHelp dim-responseModeHelp" },
519
+ current === "mention"
520
+ ? permissionGranted
521
+ ? "私聊始终响应;群聊仅处理明确 @当前机器人的消息。群消息权限已开通,再次切换无需授权。"
522
+ : "私聊始终响应;群聊仅处理明确 @当前机器人的消息。选择全部消息后会打开飞书官方授权流程。"
523
+ : permissionGranted
524
+ ? "已开通“获取群组中所有消息”权限(im:message.group_msg);机器人会处理群聊中的所有可见消息。"
525
+ : "尚未确认“获取群组中所有消息”权限,请完成飞书授权。"),
526
+ current === "all"
527
+ ? h("div", { className: "bxf-responseModePermissionAction dim-responseModePermissionAction" },
528
+ h(Button, {
529
+ className: "bxf-responseModePermissionButton",
530
+ size: "small",
531
+ disabled: disabled || authorizationDisabled || saving || authorizing,
532
+ "aria-busy": authorizing ? "true" : undefined,
533
+ "aria-label": permissionGranted ? "重新授权群消息权限" : "授权群消息权限",
534
+ onClick: () => { void authorize(); },
535
+ }, authorizing ? "正在准备…" : permissionGranted ? "重新授权" : "去授权"))
536
+ : null,
537
+ error ? h("p", {
538
+ className: "bxf-responseModeError dim-responseModeError",
539
+ role: "alert",
540
+ }, error) : null,
541
+ );
542
+ }
543
+
416
544
  export function BotCard({
417
545
  connection,
418
546
  busy,
419
547
  repairDisabled,
548
+ provisionContent,
549
+ provisionRef,
420
550
  actionError,
421
551
  testNotice,
422
552
  removing,
@@ -424,6 +554,8 @@ export function BotCard({
424
554
  onRepairCallback,
425
555
  onWorkspaceSave,
426
556
  onAgentPresetSave,
557
+ onGroupResponseModeSave,
558
+ onGroupMessagePermissionAuthorize,
427
559
  onRequestRemove,
428
560
  onConfirmRemove,
429
561
  onCancelRemove,
@@ -479,6 +611,23 @@ export function BotCard({
479
611
  disabled: Boolean(busy),
480
612
  onSave: onAgentPresetSave,
481
613
  }),
614
+ h(GroupResponseModeEditor, {
615
+ value: connection.groupResponseMode,
616
+ permissionGranted: connection.groupMessagePermissionGranted,
617
+ disabled: Boolean(busy),
618
+ authorizationDisabled: repairDisabled,
619
+ onSave: onGroupResponseModeSave,
620
+ onAuthorize: onGroupMessagePermissionAuthorize,
621
+ }),
622
+ provisionContent
623
+ ? h("section", {
624
+ className: "bxf-botProvision dim-botProvision",
625
+ "aria-label": `${bot.name}的飞书授权流程`,
626
+ "data-provision-for": connection.botId,
627
+ ref: provisionRef,
628
+ tabIndex: -1,
629
+ }, provisionContent)
630
+ : null,
482
631
  h("div", { className: "bxf-connectedFooter dim-cardFooter" },
483
632
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
484
633
  summary) : null,
@@ -526,9 +675,14 @@ function BotList(props) {
526
675
  h(BotCard, {
527
676
  connection: bot,
528
677
  busy: props.busyByBot[bot.botId]
529
- ?? (isCallbackRepair(props.provisioning)
530
- && props.provisioning.botId === bot.botId ? "callback-repair" : undefined),
678
+ ?? (isTargetedAppUpdate(props.provisioning)
679
+ && props.provisioning.botId === bot.botId ? props.provisioning.operation : undefined),
531
680
  repairDisabled: Boolean(props.provisioning),
681
+ provisionContent: isTargetedAppUpdate(props.provisioning)
682
+ && props.provisioning.botId === bot.botId
683
+ ? props.provisionContent
684
+ : null,
685
+ provisionRef: props.provisionRef,
532
686
  actionError: props.errorsByBot[bot.botId],
533
687
  testNotice: props.testNoticesByBot[bot.botId],
534
688
  removing: props.removeTargetId === bot.botId,
@@ -536,6 +690,8 @@ function BotList(props) {
536
690
  onRepairCallback: () => props.onRepairCallback(bot),
537
691
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
538
692
  onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
693
+ onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
694
+ onGroupMessagePermissionAuthorize: () => props.onGroupMessagePermissionAuthorize(bot),
539
695
  onRequestRemove: () => props.onRequestRemove(bot),
540
696
  onConfirmRemove: () => props.onConfirmRemove(bot),
541
697
  onCancelRemove: props.onCancelRemove,
@@ -616,6 +772,7 @@ export function FeishuSettingsTab({ rpcCall }) {
616
772
  const [focusBotId, setFocusBotId] = React.useState(null);
617
773
  const cardRefs = React.useRef(new Map());
618
774
  const removeButtonRefs = React.useRef(new Map());
775
+ const targetedProvisionRef = React.useRef(null);
619
776
  const addButtonRef = React.useRef(null);
620
777
  const mountedRef = React.useRef(true);
621
778
  const workspaceFence = useWorkspaceSnapshotFence();
@@ -706,15 +863,30 @@ export function FeishuSettingsTab({ rpcCall }) {
706
863
  setFocusBotId(null);
707
864
  }, [focusBotId, model.bots]);
708
865
 
866
+ const targetedProvisionFocusKey = isTargetedAppUpdate(model.provisioning)
867
+ ? `${model.provisioning.botId}:${model.provisioning.attemptId ?? "preparing"}:${model.provisioning.phase}`
868
+ : null;
869
+ React.useEffect(() => {
870
+ if (!targetedProvisionFocusKey) return;
871
+ scheduleAnimationFrame(() => {
872
+ const node = targetedProvisionRef.current;
873
+ if (!node) return;
874
+ node.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
875
+ node.focus?.({ preventScroll: true });
876
+ }, "targeted-provision-focus");
877
+ }, [scheduleAnimationFrame, targetedProvisionFocusKey]);
878
+
709
879
  const startProvisioning = React.useCallback(async ({
710
880
  replace = false,
711
881
  operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
712
882
  bot,
713
883
  } = {}) => {
714
884
  const repairing = operation === CALLBACK_REPAIR_OPERATION;
715
- const botId = repairing ? bot?.botId ?? model.provisioning?.botId : undefined;
716
- const botName = repairing ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
717
- if (repairing && !botId) return;
885
+ const grantingGroupMessages = operation === GROUP_MESSAGE_PERMISSION_OPERATION;
886
+ const targetedUpdate = repairing || grantingGroupMessages;
887
+ const botId = targetedUpdate ? bot?.botId ?? model.provisioning?.botId : undefined;
888
+ const botName = targetedUpdate ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
889
+ if (targetedUpdate && !botId) return;
718
890
  setCredentialOpen(false);
719
891
  setCredentialError(null);
720
892
  setProvisionBusy(true);
@@ -742,13 +914,20 @@ export function FeishuSettingsTab({ rpcCall }) {
742
914
  // Continue with begin. It is the source of truth for the new attempt.
743
915
  }
744
916
  }
917
+ const endpoint = repairing
918
+ ? FEISHU_ENDPOINTS.beginCallbackRepair
919
+ : grantingGroupMessages
920
+ ? FEISHU_ENDPOINTS.beginGroupMessagePermission
921
+ : FEISHU_ENDPOINTS.beginProvisioning;
745
922
  const provision = normalizeProvisioning(await invoke(
746
- repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : FEISHU_ENDPOINTS.beginProvisioning,
747
- repairing ? { botId } : { locale: "zh-CN" },
923
+ endpoint,
924
+ targetedUpdate ? { botId } : { locale: "zh-CN" },
748
925
  ));
749
- if (repairing
750
- && (provision.operation !== CALLBACK_REPAIR_OPERATION || provision.botId !== botId)) {
751
- throw new Error("飞书服务返回了不匹配的卡片修复二维码");
926
+ if (targetedUpdate
927
+ && (provision.operation !== operation || provision.botId !== botId)) {
928
+ throw new Error(grantingGroupMessages
929
+ ? "飞书服务返回了不匹配的群消息权限二维码"
930
+ : "飞书服务返回了不匹配的卡片修复二维码");
752
931
  }
753
932
  const timestamp = Date.now();
754
933
  setNow(timestamp);
@@ -764,7 +943,9 @@ export function FeishuSettingsTab({ rpcCall }) {
764
943
  }));
765
944
  announce(repairing
766
945
  ? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
767
- : "授权二维码已生成,请使用飞书扫码。");
946
+ : grantingGroupMessages
947
+ ? `${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。`
948
+ : "授权二维码已生成,请使用飞书扫码。");
768
949
  } catch (error) {
769
950
  setModel((current) => ({
770
951
  ...current,
@@ -815,7 +996,9 @@ export function FeishuSettingsTab({ rpcCall }) {
815
996
  const activeProvision = model.provisioning;
816
997
  const attemptId = activeProvision?.attemptId;
817
998
  const repairing = isCallbackRepair(activeProvision);
818
- const targetBot = repairing
999
+ const grantingGroupMessages = isGroupMessagePermission(activeProvision);
1000
+ const targetedUpdate = isTargetedAppUpdate(activeProvision);
1001
+ const targetBot = targetedUpdate
819
1002
  ? model.bots.find((bot) => bot.botId === activeProvision?.botId)
820
1003
  : undefined;
821
1004
  setProvisionBusy(true);
@@ -823,8 +1006,8 @@ export function FeishuSettingsTab({ rpcCall }) {
823
1006
  const result = attemptId
824
1007
  ? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId }))
825
1008
  : null;
826
- if (repairing && result) {
827
- if (result.operation !== CALLBACK_REPAIR_OPERATION
1009
+ if (targetedUpdate && result) {
1010
+ if (result.operation !== activeProvision.operation
828
1011
  || result.botId !== activeProvision.botId) {
829
1012
  throw new Error("飞书服务返回了不匹配的注册进度");
830
1013
  }
@@ -841,23 +1024,29 @@ export function FeishuSettingsTab({ rpcCall }) {
841
1024
  },
842
1025
  }
843
1026
  : current);
844
- announce("配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
1027
+ announce(grantingGroupMessages
1028
+ ? "权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。"
1029
+ : "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
845
1030
  return;
846
1031
  }
847
1032
  if (result.status === "connected") {
848
1033
  const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "机器人";
849
1034
  setModel((current) => ({ ...current, provisioning: null }));
850
- announce(`${targetBotName}的卡片按钮已修复。`);
1035
+ announce(grantingGroupMessages
1036
+ ? `${targetBotName}已开通群消息权限,并启用“响应所有群消息”。`
1037
+ : `${targetBotName}的卡片按钮已修复。`);
851
1038
  if (activeProvision.botId) setFocusBotId(activeProvision.botId);
852
1039
  await loadStatus({ silent: true, restoreProvisioning: false });
853
1040
  return;
854
1041
  }
855
1042
  }
856
1043
  setModel((current) => ({ ...current, provisioning: null }));
857
- announce(repairing ? "已取消卡片按钮修复。" : "已取消添加机器人。");
1044
+ announce(repairing
1045
+ ? "已取消卡片按钮修复。"
1046
+ : grantingGroupMessages ? "已取消群消息权限授权。" : "已取消添加机器人。");
858
1047
  await loadStatus({ silent: true, restoreProvisioning: false });
859
1048
  scheduleAnimationFrame(() => {
860
- if (repairing && activeProvision.botId) {
1049
+ if (targetedUpdate && activeProvision.botId) {
861
1050
  cardRefs.current.get(activeProvision.botId)?.focus();
862
1051
  } else {
863
1052
  addButtonRef.current?.focus();
@@ -914,7 +1103,7 @@ export function FeishuSettingsTab({ rpcCall }) {
914
1103
  controller.signal,
915
1104
  ));
916
1105
  if (result.operation !== provision.operation
917
- || (isCallbackRepair(provision) && result.botId !== provision.botId)) {
1106
+ || (isTargetedAppUpdate(provision) && result.botId !== provision.botId)) {
918
1107
  throw new Error("飞书服务返回了不匹配的注册进度");
919
1108
  }
920
1109
  if (result.status === "connected") {
@@ -923,7 +1112,9 @@ export function FeishuSettingsTab({ rpcCall }) {
923
1112
  if (!snapshot) {
924
1113
  throw new Error(isCallbackRepair(provision)
925
1114
  ? "卡片按钮已更新,但暂时无法确认机器人连接状态"
926
- : "机器人已经创建,但暂时无法确认连接状态");
1115
+ : isGroupMessagePermission(provision)
1116
+ ? "群消息权限已更新,但暂时无法确认机器人连接状态"
1117
+ : "机器人已经创建,但暂时无法确认连接状态");
927
1118
  }
928
1119
  if (!targetBot?.connected) {
929
1120
  setModel((current) => current.provisioning?.attemptId === provision.attemptId
@@ -934,15 +1125,21 @@ export function FeishuSettingsTab({ rpcCall }) {
934
1125
  setModel((current) => ({ ...current, provisioning: null }));
935
1126
  announce(isCallbackRepair(provision)
936
1127
  ? `${targetBot.bot.name}的卡片按钮已修复。`
937
- : targetBot
938
- ? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
939
- : "新飞书机器人已连接,可以开始聊天。");
1128
+ : isGroupMessagePermission(provision)
1129
+ ? `${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。`
1130
+ : targetBot
1131
+ ? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
1132
+ : "新飞书机器人已连接,可以开始聊天。");
940
1133
  if (result.botId) setFocusBotId(result.botId);
941
1134
  return;
942
1135
  }
943
1136
  if (result.status === "failed") {
944
1137
  const error = new Error(result.message
945
- ?? (isCallbackRepair(provision) ? "飞书卡片按钮修复失败" : "飞书应用创建失败"));
1138
+ ?? (isCallbackRepair(provision)
1139
+ ? "飞书卡片按钮修复失败"
1140
+ : isGroupMessagePermission(provision)
1141
+ ? "飞书群消息权限开通失败"
1142
+ : "飞书应用创建失败"));
946
1143
  error.code = "FEISHU_PROVISION_FAILED";
947
1144
  throw error;
948
1145
  }
@@ -1101,6 +1298,56 @@ export function FeishuSettingsTab({ rpcCall }) {
1101
1298
  }
1102
1299
  }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
1103
1300
 
1301
+ const authorizeGroupMessages = React.useCallback(async (connection) => {
1302
+ const { botId } = connection;
1303
+ if (model.provisioning) {
1304
+ throw new Error("请先完成当前飞书授权操作,再开通群消息权限。");
1305
+ }
1306
+ setRemoveTargetId(null);
1307
+ setBotError(botId, null);
1308
+ setTestNoticesByBot((current) => {
1309
+ const next = { ...current };
1310
+ delete next[botId];
1311
+ return next;
1312
+ });
1313
+ await startProvisioning({
1314
+ operation: GROUP_MESSAGE_PERMISSION_OPERATION,
1315
+ bot: connection,
1316
+ });
1317
+ }, [model.provisioning, setBotError, startProvisioning]);
1318
+
1319
+ const saveGroupResponseMode = React.useCallback(async (connection, groupResponseMode) => {
1320
+ const { botId } = connection;
1321
+ if (groupResponseMode === "all" && connection.groupMessagePermissionGranted !== true) {
1322
+ await authorizeGroupMessages(connection);
1323
+ return;
1324
+ }
1325
+ const snapshotVersion = workspaceFence.beginMutation();
1326
+ setBotBusy(botId, "group-response-mode");
1327
+ setBotError(botId, null);
1328
+ try {
1329
+ const snapshot = normalizeBotsSnapshot(await invoke(
1330
+ FEISHU_ENDPOINTS.setGroupResponseMode,
1331
+ { botId, groupResponseMode },
1332
+ ));
1333
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
1334
+ mergeSnapshot(snapshot);
1335
+ }
1336
+ } finally {
1337
+ const shouldRefresh = workspaceFence.endMutation();
1338
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
1339
+ if (mountedRef.current) setBotBusy(botId, null);
1340
+ }
1341
+ }, [
1342
+ invoke,
1343
+ authorizeGroupMessages,
1344
+ loadStatus,
1345
+ mergeSnapshot,
1346
+ setBotBusy,
1347
+ setBotError,
1348
+ workspaceFence,
1349
+ ]);
1350
+
1104
1351
  const requestRemove = React.useCallback((connection) => {
1105
1352
  setRemoveTargetId(connection.botId);
1106
1353
  }, []);
@@ -1138,6 +1385,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1138
1385
  }, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
1139
1386
 
1140
1387
  const provision = model.provisioning;
1388
+ const targetedProvisioning = isTargetedAppUpdate(provision);
1141
1389
  const provisionBot = provision?.botId
1142
1390
  ? model.bots.find((bot) => bot.botId === provision.botId)
1143
1391
  ?? { botId: provision.botId, bot: { name: provision.botName ?? "此机器人" } }
@@ -1163,7 +1411,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1163
1411
  provisionContent = h(ProvisionProgress, {
1164
1412
  phase: "connecting",
1165
1413
  provision,
1166
- onCancel: isCallbackRepair(provision) ? undefined : () => void cancelProvisioning(),
1414
+ onCancel: isTargetedAppUpdate(provision) ? undefined : () => void cancelProvisioning(),
1167
1415
  busy: provisionBusy,
1168
1416
  });
1169
1417
  } else if (provision?.phase === "error") {
@@ -1238,7 +1486,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1238
1486
  })
1239
1487
  : h(React.Fragment, null,
1240
1488
  credentialContent,
1241
- provisionContent,
1489
+ targetedProvisioning ? null : provisionContent,
1242
1490
  model.bots.length === 0 && !provision && !credentialOpen
1243
1491
  ? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy })
1244
1492
  : null,
@@ -1250,10 +1498,14 @@ export function FeishuSettingsTab({ rpcCall }) {
1250
1498
  testNoticesByBot,
1251
1499
  removeTargetId,
1252
1500
  provisioning: provision,
1501
+ provisionContent,
1502
+ provisionRef: targetedProvisionRef,
1253
1503
  onReconnect: (bot) => void reconnectOneBot(bot),
1254
1504
  onRepairCallback: repairCallback,
1255
1505
  onWorkspaceSave: saveWorkspace,
1256
1506
  onAgentPresetSave: saveAgentPreset,
1507
+ onGroupResponseModeSave: saveGroupResponseMode,
1508
+ onGroupMessagePermissionAuthorize: authorizeGroupMessages,
1257
1509
  onRequestRemove: requestRemove,
1258
1510
  onConfirmRemove: (bot) => void confirmRemove(bot),
1259
1511
  onCancelRemove: cancelRemove,