@xmanrui/dsh-im 0.17.1 → 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.
Files changed (56) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/lib/client.js +1297 -600
  4. package/lib/index.js +190 -145
  5. package/package.json +1 -1
  6. package/plugin-src/client/agent-preset.js +110 -0
  7. package/plugin-src/client/channels/dingtalk/api.js +5 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +48 -2
  9. package/plugin-src/client/channels/feishu/api.js +28 -5
  10. package/plugin-src/client/channels/feishu/index.js +330 -41
  11. package/plugin-src/client/channels/feishu/styles.js +46 -0
  12. package/plugin-src/client/channels/qq/api.js +5 -0
  13. package/plugin-src/client/channels/qq/index.js +37 -6
  14. package/plugin-src/client/channels/shared/token-api.js +5 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +34 -6
  16. package/plugin-src/client/channels/wecom/api.js +5 -0
  17. package/plugin-src/client/channels/wecom/index.js +37 -6
  18. package/plugin-src/client/channels/weixin/api.js +5 -0
  19. package/plugin-src/client/channels/weixin/index.js +50 -4
  20. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +30 -4
  22. package/plugin-src/client/i18n.js +59 -0
  23. package/plugin-src/client/styles.js +8 -0
  24. package/plugin-src/host/channels/dingtalk/production.mjs +10 -4
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +12 -0
  26. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +1 -1
  27. package/plugin-src/host/channels/feishu/production.mjs +8 -3
  28. package/plugin-src/host/channels/feishu/rpc.mjs +78 -7
  29. package/plugin-src/host/channels/qq/production.mjs +10 -4
  30. package/plugin-src/host/channels/qq/rpc.mjs +12 -0
  31. package/plugin-src/host/channels/shared/agent-preset-rpc.mjs +25 -0
  32. package/plugin-src/host/channels/shared/production.mjs +10 -4
  33. package/plugin-src/host/channels/shared/rpc.mjs +12 -0
  34. package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
  35. package/plugin-src/host/channels/slack/production.mjs +10 -4
  36. package/plugin-src/host/channels/slack/rpc.mjs +13 -0
  37. package/plugin-src/host/channels/wecom/production.mjs +10 -4
  38. package/plugin-src/host/channels/wecom/rpc.mjs +12 -0
  39. package/plugin-src/host/channels/weixin/production.mjs +10 -4
  40. package/plugin-src/host/channels/weixin/rpc.mjs +15 -0
  41. package/plugin-src/host/channels/whatsapp/production.mjs +10 -4
  42. package/plugin-src/host/channels/whatsapp/rpc.mjs +12 -0
  43. package/src/channels/discord/discord-api.mjs +1 -1
  44. package/src/channels/feishu/bridge.mjs +29 -4
  45. package/src/channels/feishu/feishu-runtime.mjs +14 -0
  46. package/src/channels/feishu/group-message-permission-manager.mjs +71 -0
  47. package/src/channels/feishu/group-response-mode.mjs +15 -0
  48. package/src/channels/feishu/multi-bot-controller.mjs +258 -9
  49. package/src/channels/feishu/plugin-config-store.mjs +3 -0
  50. package/src/channels/feishu/repair-manager.mjs +17 -3
  51. package/src/channels/shared/agent-preset.mjs +74 -0
  52. package/src/channels/shared/bot-workspace-store.mjs +141 -14
  53. package/src/channels/shared/harness-client.mjs +6 -4
  54. package/src/channels/telegram/telegram-api.mjs +31 -0
  55. package/src/channels/telegram/telegram-runtime.mjs +25 -1
  56. package/src/channels/weixin/weixin-api.mjs +1 -1
@@ -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,
@@ -16,6 +17,11 @@ import {
16
17
  } from "./api.js";
17
18
  import { useAnimationFrameScheduler } from "../../lifecycle.js";
18
19
  import { WorkspaceEditor } from "../../workspace-editor.js";
20
+ import {
21
+ AgentPresetCatalogContext,
22
+ AgentPresetEditor,
23
+ EMPTY_AGENT_PRESET_CATALOG,
24
+ } from "../../agent-preset.js";
19
25
  import { useWorkspaceSnapshotFence } from "../../workspace-snapshot-fence.js";
20
26
  import { installFeishuStyles } from "./styles.js";
21
27
 
@@ -23,11 +29,20 @@ export const name = "feishu-settings";
23
29
  export const inject = ["slots", "connection"];
24
30
 
25
31
  const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
32
+ const GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
26
33
 
27
34
  function isCallbackRepair(value) {
28
35
  return value?.operation === CALLBACK_REPAIR_OPERATION;
29
36
  }
30
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
+
31
46
  function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
32
47
  return h("svg", {
33
48
  width: size,
@@ -236,6 +251,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
236
251
  const expired = provision.expired === true || remaining === 0;
237
252
  const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
238
253
  const repairing = isCallbackRepair(provision);
254
+ const grantingGroupMessages = isGroupMessagePermission(provision);
239
255
  const botName = provision.botName ?? "此机器人";
240
256
 
241
257
  React.useEffect(() => setImageFailed(false), [qrSource]);
@@ -249,7 +265,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
249
265
  src: qrSource,
250
266
  alt: repairing
251
267
  ? `用于修复${botName}卡片按钮的一次性授权二维码`
252
- : "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
268
+ : grantingGroupMessages
269
+ ? `用于为${botName}开通群消息权限的一次性授权二维码`
270
+ : "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
253
271
  onError: () => setImageFailed(true),
254
272
  })
255
273
  : h("div", { className: "bxf-qrFallback dim-qrFallback" },
@@ -273,21 +291,35 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
273
291
  h("div", { className: "bxf-qrCopy dim-qrCopy" },
274
292
  h("div", { className: "bxf-stateLabel dim-stateLabel" },
275
293
  h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
276
- h("span", null, repairing ? `正在修复「${botName}」` : "正在添加新机器人")),
294
+ h("span", null, repairing
295
+ ? `正在修复「${botName}」`
296
+ : grantingGroupMessages
297
+ ? `正在为「${botName}」开通群消息权限`
298
+ : "正在添加新机器人")),
277
299
  h("h3", null, expired
278
300
  ? "刷新二维码后继续"
279
- : repairing ? "使用飞书扫码修复卡片按钮" : "使用飞书扫码创建机器人"),
301
+ : repairing
302
+ ? "使用飞书扫码修复卡片按钮"
303
+ : grantingGroupMessages
304
+ ? "使用飞书确认群消息权限"
305
+ : "使用飞书扫码创建机器人"),
280
306
  h("p", null, repairing
281
307
  ? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
282
- : "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
308
+ : grantingGroupMessages
309
+ ? "扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。"
310
+ : "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
283
311
  h("ol", { className: "bxf-steps dim-steps" },
284
312
  h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
285
313
  h("li", null, repairing
286
314
  ? "核对现有应用名称,并确认只新增卡片回调"
287
- : "核对应用名称与权限范围,并确认创建"),
315
+ : grantingGroupMessages
316
+ ? "核对现有应用,并确认“获取群组中所有消息”权限"
317
+ : "核对应用名称与权限范围,并确认创建"),
288
318
  h("li", null, repairing
289
319
  ? "保持本页打开,等待卡片按钮修复完成"
290
- : "保持本页打开,等待新机器人的长连接就绪")),
320
+ : grantingGroupMessages
321
+ ? "保持本页打开,等待权限生效并自动切换响应方式"
322
+ : "保持本页打开,等待新机器人的长连接就绪")),
291
323
  h("div", { className: "bxf-actions dim-viewActions" },
292
324
  expired
293
325
  ? h(Button, {
@@ -302,7 +334,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
302
334
  !expired
303
335
  ? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
304
336
  : null,
305
- h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加")),
337
+ h(Button, { onClick: onCancel, disabled: busy }, repairing
338
+ ? "取消修复"
339
+ : grantingGroupMessages ? "取消授权" : "取消添加")),
306
340
  ),
307
341
  ),
308
342
  );
@@ -311,34 +345,50 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
311
345
  function ProvisionProgress({ phase, provision, onCancel, busy }) {
312
346
  const connecting = phase === "connecting";
313
347
  const repairing = isCallbackRepair(provision);
348
+ const grantingGroupMessages = isGroupMessagePermission(provision);
314
349
  return h("div", {
315
350
  className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
316
351
  "aria-busy": "true",
317
352
  },
318
353
  h("div", { className: "dim-spinner", "aria-hidden": "true" }),
319
354
  h("h3", null, connecting
320
- ? repairing ? "已确认,正在完成卡片按钮修复" : "已确认,正在连接新机器人"
321
- : repairing ? "正在准备修复二维码" : "正在准备授权二维码"),
355
+ ? repairing
356
+ ? "已确认,正在完成卡片按钮修复"
357
+ : grantingGroupMessages
358
+ ? "已确认,正在启用全部消息模式"
359
+ : "已确认,正在连接新机器人"
360
+ : repairing
361
+ ? "正在准备修复二维码"
362
+ : grantingGroupMessages ? "正在准备权限授权二维码" : "正在准备授权二维码"),
322
363
  h("p", null, connecting
323
364
  ? repairing
324
365
  ? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
325
- : "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
366
+ : grantingGroupMessages
367
+ ? "权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。"
368
+ : "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
326
369
  : repairing
327
370
  ? "正在为现有飞书应用申请一次性更新二维码,请稍候。"
328
- : "正在向飞书申请一次性授权二维码,请稍候。"),
371
+ : grantingGroupMessages
372
+ ? "正在为现有飞书应用申请群消息权限二维码,请稍候。"
373
+ : "正在向飞书申请一次性授权二维码,请稍候。"),
329
374
  connecting && onCancel
330
375
  ? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
331
- h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加"))
376
+ h(Button, { onClick: onCancel, disabled: busy }, repairing
377
+ ? "取消修复"
378
+ : grantingGroupMessages ? "取消授权" : "取消添加"))
332
379
  : null,
333
380
  );
334
381
  }
335
382
 
336
383
  function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
337
384
  const repairing = isCallbackRepair(provision);
385
+ const grantingGroupMessages = isGroupMessagePermission(provision);
338
386
  return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
339
387
  h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
340
388
  h("div", null,
341
- h("h3", null, repairing ? "卡片按钮没有修复完成" : "新机器人没有添加完成"),
389
+ h("h3", null, repairing
390
+ ? "卡片按钮没有修复完成"
391
+ : grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
342
392
  h("p", null, error.message),
343
393
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
344
394
  h("div", { className: "bxf-actions dim-viewActions" },
@@ -408,16 +458,104 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
408
458
  );
409
459
  }
410
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
+
411
544
  export function BotCard({
412
545
  connection,
413
546
  busy,
414
547
  repairDisabled,
548
+ provisionContent,
549
+ provisionRef,
415
550
  actionError,
416
551
  testNotice,
417
552
  removing,
418
553
  onReconnect,
419
554
  onRepairCallback,
420
555
  onWorkspaceSave,
556
+ onAgentPresetSave,
557
+ onGroupResponseModeSave,
558
+ onGroupMessagePermissionAuthorize,
421
559
  onRequestRemove,
422
560
  onConfirmRemove,
423
561
  onCancelRemove,
@@ -468,6 +606,28 @@ export function BotCard({
468
606
  disabled: Boolean(busy),
469
607
  onSave: onWorkspaceSave,
470
608
  }),
609
+ h(AgentPresetEditor, {
610
+ agentPreset: connection.agentPreset,
611
+ disabled: Boolean(busy),
612
+ onSave: onAgentPresetSave,
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,
471
631
  h("div", { className: "bxf-connectedFooter dim-cardFooter" },
472
632
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
473
633
  summary) : null,
@@ -515,15 +675,23 @@ function BotList(props) {
515
675
  h(BotCard, {
516
676
  connection: bot,
517
677
  busy: props.busyByBot[bot.botId]
518
- ?? (isCallbackRepair(props.provisioning)
519
- && props.provisioning.botId === bot.botId ? "callback-repair" : undefined),
678
+ ?? (isTargetedAppUpdate(props.provisioning)
679
+ && props.provisioning.botId === bot.botId ? props.provisioning.operation : undefined),
520
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,
521
686
  actionError: props.errorsByBot[bot.botId],
522
687
  testNotice: props.testNoticesByBot[bot.botId],
523
688
  removing: props.removeTargetId === bot.botId,
524
689
  onReconnect: () => props.onReconnect(bot),
525
690
  onRepairCallback: () => props.onRepairCallback(bot),
526
691
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
692
+ onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
693
+ onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
694
+ onGroupMessagePermissionAuthorize: () => props.onGroupMessagePermissionAuthorize(bot),
527
695
  onRequestRemove: () => props.onRequestRemove(bot),
528
696
  onConfirmRemove: () => props.onConfirmRemove(bot),
529
697
  onCancelRemove: props.onCancelRemove,
@@ -575,6 +743,7 @@ export function mergeFeishuSnapshotState(
575
743
  provisioning,
576
744
  pageError: null,
577
745
  statusError: null,
746
+ agentPresetCatalog: snapshot.agentPresetCatalog ?? current.agentPresetCatalog,
578
747
  };
579
748
  }
580
749
 
@@ -587,6 +756,7 @@ export function FeishuSettingsTab({ rpcCall }) {
587
756
  provisioning: null,
588
757
  pageError: null,
589
758
  statusError: null,
759
+ agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
590
760
  });
591
761
  const [pageBusy, setPageBusy] = React.useState(false);
592
762
  const [provisionBusy, setProvisionBusy] = React.useState(false);
@@ -602,6 +772,7 @@ export function FeishuSettingsTab({ rpcCall }) {
602
772
  const [focusBotId, setFocusBotId] = React.useState(null);
603
773
  const cardRefs = React.useRef(new Map());
604
774
  const removeButtonRefs = React.useRef(new Map());
775
+ const targetedProvisionRef = React.useRef(null);
605
776
  const addButtonRef = React.useRef(null);
606
777
  const mountedRef = React.useRef(true);
607
778
  const workspaceFence = useWorkspaceSnapshotFence();
@@ -692,15 +863,30 @@ export function FeishuSettingsTab({ rpcCall }) {
692
863
  setFocusBotId(null);
693
864
  }, [focusBotId, model.bots]);
694
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
+
695
879
  const startProvisioning = React.useCallback(async ({
696
880
  replace = false,
697
881
  operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
698
882
  bot,
699
883
  } = {}) => {
700
884
  const repairing = operation === CALLBACK_REPAIR_OPERATION;
701
- const botId = repairing ? bot?.botId ?? model.provisioning?.botId : undefined;
702
- const botName = repairing ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
703
- 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;
704
890
  setCredentialOpen(false);
705
891
  setCredentialError(null);
706
892
  setProvisionBusy(true);
@@ -728,13 +914,20 @@ export function FeishuSettingsTab({ rpcCall }) {
728
914
  // Continue with begin. It is the source of truth for the new attempt.
729
915
  }
730
916
  }
917
+ const endpoint = repairing
918
+ ? FEISHU_ENDPOINTS.beginCallbackRepair
919
+ : grantingGroupMessages
920
+ ? FEISHU_ENDPOINTS.beginGroupMessagePermission
921
+ : FEISHU_ENDPOINTS.beginProvisioning;
731
922
  const provision = normalizeProvisioning(await invoke(
732
- repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : FEISHU_ENDPOINTS.beginProvisioning,
733
- repairing ? { botId } : { locale: "zh-CN" },
923
+ endpoint,
924
+ targetedUpdate ? { botId } : { locale: "zh-CN" },
734
925
  ));
735
- if (repairing
736
- && (provision.operation !== CALLBACK_REPAIR_OPERATION || provision.botId !== botId)) {
737
- throw new Error("飞书服务返回了不匹配的卡片修复二维码");
926
+ if (targetedUpdate
927
+ && (provision.operation !== operation || provision.botId !== botId)) {
928
+ throw new Error(grantingGroupMessages
929
+ ? "飞书服务返回了不匹配的群消息权限二维码"
930
+ : "飞书服务返回了不匹配的卡片修复二维码");
738
931
  }
739
932
  const timestamp = Date.now();
740
933
  setNow(timestamp);
@@ -750,7 +943,9 @@ export function FeishuSettingsTab({ rpcCall }) {
750
943
  }));
751
944
  announce(repairing
752
945
  ? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
753
- : "授权二维码已生成,请使用飞书扫码。");
946
+ : grantingGroupMessages
947
+ ? `${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。`
948
+ : "授权二维码已生成,请使用飞书扫码。");
754
949
  } catch (error) {
755
950
  setModel((current) => ({
756
951
  ...current,
@@ -801,7 +996,9 @@ export function FeishuSettingsTab({ rpcCall }) {
801
996
  const activeProvision = model.provisioning;
802
997
  const attemptId = activeProvision?.attemptId;
803
998
  const repairing = isCallbackRepair(activeProvision);
804
- const targetBot = repairing
999
+ const grantingGroupMessages = isGroupMessagePermission(activeProvision);
1000
+ const targetedUpdate = isTargetedAppUpdate(activeProvision);
1001
+ const targetBot = targetedUpdate
805
1002
  ? model.bots.find((bot) => bot.botId === activeProvision?.botId)
806
1003
  : undefined;
807
1004
  setProvisionBusy(true);
@@ -809,8 +1006,8 @@ export function FeishuSettingsTab({ rpcCall }) {
809
1006
  const result = attemptId
810
1007
  ? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId }))
811
1008
  : null;
812
- if (repairing && result) {
813
- if (result.operation !== CALLBACK_REPAIR_OPERATION
1009
+ if (targetedUpdate && result) {
1010
+ if (result.operation !== activeProvision.operation
814
1011
  || result.botId !== activeProvision.botId) {
815
1012
  throw new Error("飞书服务返回了不匹配的注册进度");
816
1013
  }
@@ -827,23 +1024,29 @@ export function FeishuSettingsTab({ rpcCall }) {
827
1024
  },
828
1025
  }
829
1026
  : current);
830
- announce("配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
1027
+ announce(grantingGroupMessages
1028
+ ? "权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。"
1029
+ : "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
831
1030
  return;
832
1031
  }
833
1032
  if (result.status === "connected") {
834
1033
  const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "机器人";
835
1034
  setModel((current) => ({ ...current, provisioning: null }));
836
- announce(`${targetBotName}的卡片按钮已修复。`);
1035
+ announce(grantingGroupMessages
1036
+ ? `${targetBotName}已开通群消息权限,并启用“响应所有群消息”。`
1037
+ : `${targetBotName}的卡片按钮已修复。`);
837
1038
  if (activeProvision.botId) setFocusBotId(activeProvision.botId);
838
1039
  await loadStatus({ silent: true, restoreProvisioning: false });
839
1040
  return;
840
1041
  }
841
1042
  }
842
1043
  setModel((current) => ({ ...current, provisioning: null }));
843
- announce(repairing ? "已取消卡片按钮修复。" : "已取消添加机器人。");
1044
+ announce(repairing
1045
+ ? "已取消卡片按钮修复。"
1046
+ : grantingGroupMessages ? "已取消群消息权限授权。" : "已取消添加机器人。");
844
1047
  await loadStatus({ silent: true, restoreProvisioning: false });
845
1048
  scheduleAnimationFrame(() => {
846
- if (repairing && activeProvision.botId) {
1049
+ if (targetedUpdate && activeProvision.botId) {
847
1050
  cardRefs.current.get(activeProvision.botId)?.focus();
848
1051
  } else {
849
1052
  addButtonRef.current?.focus();
@@ -900,7 +1103,7 @@ export function FeishuSettingsTab({ rpcCall }) {
900
1103
  controller.signal,
901
1104
  ));
902
1105
  if (result.operation !== provision.operation
903
- || (isCallbackRepair(provision) && result.botId !== provision.botId)) {
1106
+ || (isTargetedAppUpdate(provision) && result.botId !== provision.botId)) {
904
1107
  throw new Error("飞书服务返回了不匹配的注册进度");
905
1108
  }
906
1109
  if (result.status === "connected") {
@@ -909,7 +1112,9 @@ export function FeishuSettingsTab({ rpcCall }) {
909
1112
  if (!snapshot) {
910
1113
  throw new Error(isCallbackRepair(provision)
911
1114
  ? "卡片按钮已更新,但暂时无法确认机器人连接状态"
912
- : "机器人已经创建,但暂时无法确认连接状态");
1115
+ : isGroupMessagePermission(provision)
1116
+ ? "群消息权限已更新,但暂时无法确认机器人连接状态"
1117
+ : "机器人已经创建,但暂时无法确认连接状态");
913
1118
  }
914
1119
  if (!targetBot?.connected) {
915
1120
  setModel((current) => current.provisioning?.attemptId === provision.attemptId
@@ -920,15 +1125,21 @@ export function FeishuSettingsTab({ rpcCall }) {
920
1125
  setModel((current) => ({ ...current, provisioning: null }));
921
1126
  announce(isCallbackRepair(provision)
922
1127
  ? `${targetBot.bot.name}的卡片按钮已修复。`
923
- : targetBot
924
- ? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
925
- : "新飞书机器人已连接,可以开始聊天。");
1128
+ : isGroupMessagePermission(provision)
1129
+ ? `${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。`
1130
+ : targetBot
1131
+ ? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
1132
+ : "新飞书机器人已连接,可以开始聊天。");
926
1133
  if (result.botId) setFocusBotId(result.botId);
927
1134
  return;
928
1135
  }
929
1136
  if (result.status === "failed") {
930
1137
  const error = new Error(result.message
931
- ?? (isCallbackRepair(provision) ? "飞书卡片按钮修复失败" : "飞书应用创建失败"));
1138
+ ?? (isCallbackRepair(provision)
1139
+ ? "飞书卡片按钮修复失败"
1140
+ : isGroupMessagePermission(provision)
1141
+ ? "飞书群消息权限开通失败"
1142
+ : "飞书应用创建失败"));
932
1143
  error.code = "FEISHU_PROVISION_FAILED";
933
1144
  throw error;
934
1145
  }
@@ -1067,6 +1278,76 @@ export function FeishuSettingsTab({ rpcCall }) {
1067
1278
  }
1068
1279
  }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
1069
1280
 
1281
+ const saveAgentPreset = React.useCallback(async (connection, agentPreset) => {
1282
+ const { botId } = connection;
1283
+ const snapshotVersion = workspaceFence.beginMutation();
1284
+ setBotBusy(botId, "preset");
1285
+ setBotError(botId, null);
1286
+ try {
1287
+ const snapshot = normalizeBotsSnapshot(await invoke(
1288
+ FEISHU_ENDPOINTS.setAgentPreset,
1289
+ { botId, agentPreset },
1290
+ ));
1291
+ if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
1292
+ mergeSnapshot(snapshot);
1293
+ }
1294
+ } finally {
1295
+ const shouldRefresh = workspaceFence.endMutation();
1296
+ if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
1297
+ if (mountedRef.current) setBotBusy(botId, null);
1298
+ }
1299
+ }, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
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
+
1070
1351
  const requestRemove = React.useCallback((connection) => {
1071
1352
  setRemoveTargetId(connection.botId);
1072
1353
  }, []);
@@ -1104,6 +1385,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1104
1385
  }, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
1105
1386
 
1106
1387
  const provision = model.provisioning;
1388
+ const targetedProvisioning = isTargetedAppUpdate(provision);
1107
1389
  const provisionBot = provision?.botId
1108
1390
  ? model.bots.find((bot) => bot.botId === provision.botId)
1109
1391
  ?? { botId: provision.botId, bot: { name: provision.botName ?? "此机器人" } }
@@ -1129,7 +1411,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1129
1411
  provisionContent = h(ProvisionProgress, {
1130
1412
  phase: "connecting",
1131
1413
  provision,
1132
- onCancel: isCallbackRepair(provision) ? undefined : () => void cancelProvisioning(),
1414
+ onCancel: isTargetedAppUpdate(provision) ? undefined : () => void cancelProvisioning(),
1133
1415
  busy: provisionBusy,
1134
1416
  });
1135
1417
  } else if (provision?.phase === "error") {
@@ -1173,7 +1455,9 @@ export function FeishuSettingsTab({ rpcCall }) {
1173
1455
  else removeButtonRefs.current.delete(botId);
1174
1456
  }, []);
1175
1457
 
1176
- return h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
1458
+ return h(AgentPresetCatalogContext.Provider, {
1459
+ value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
1460
+ }, h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
1177
1461
  h(Heading, {
1178
1462
  totals: model.totals,
1179
1463
  onAdd: () => void startProvisioning(),
@@ -1202,7 +1486,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1202
1486
  })
1203
1487
  : h(React.Fragment, null,
1204
1488
  credentialContent,
1205
- provisionContent,
1489
+ targetedProvisioning ? null : provisionContent,
1206
1490
  model.bots.length === 0 && !provision && !credentialOpen
1207
1491
  ? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy })
1208
1492
  : null,
@@ -1214,9 +1498,14 @@ export function FeishuSettingsTab({ rpcCall }) {
1214
1498
  testNoticesByBot,
1215
1499
  removeTargetId,
1216
1500
  provisioning: provision,
1501
+ provisionContent,
1502
+ provisionRef: targetedProvisionRef,
1217
1503
  onReconnect: (bot) => void reconnectOneBot(bot),
1218
1504
  onRepairCallback: repairCallback,
1219
1505
  onWorkspaceSave: saveWorkspace,
1506
+ onAgentPresetSave: saveAgentPreset,
1507
+ onGroupResponseModeSave: saveGroupResponseMode,
1508
+ onGroupMessagePermissionAuthorize: authorizeGroupMessages,
1220
1509
  onRequestRemove: requestRemove,
1221
1510
  onConfirmRemove: (bot) => void confirmRemove(bot),
1222
1511
  onCancelRemove: cancelRemove,
@@ -1225,7 +1514,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1225
1514
  })
1226
1515
  : null,
1227
1516
  ),
1228
- );
1517
+ ));
1229
1518
  }
1230
1519
 
1231
1520
  export function apply(ctx) {