@xmanrui/dsh-im 2.3.0 → 2.5.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 (57) hide show
  1. package/README.en.md +9 -4
  2. package/README.md +9 -4
  3. package/lib/client.js +411 -156
  4. package/lib/index.js +218 -210
  5. package/package.json +1 -1
  6. package/plugin-src/client/channel-card-meta.js +36 -1
  7. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +9 -1
  9. package/plugin-src/client/channels/feishu/api.js +3 -0
  10. package/plugin-src/client/channels/feishu/index.js +46 -28
  11. package/plugin-src/client/channels/feishu/styles.js +26 -1
  12. package/plugin-src/client/channels/qq/api.js +2 -0
  13. package/plugin-src/client/channels/qq/index.js +9 -1
  14. package/plugin-src/client/channels/shared/token-api.js +2 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +9 -1
  16. package/plugin-src/client/channels/wecom/api.js +2 -0
  17. package/plugin-src/client/channels/wecom/index.js +9 -1
  18. package/plugin-src/client/channels/weixin/api.js +2 -10
  19. package/plugin-src/client/channels/weixin/index.js +9 -5
  20. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  22. package/plugin-src/client/i18n.js +31 -25
  23. package/plugin-src/client/index.js +16 -2
  24. package/plugin-src/client/last-message-error.js +17 -0
  25. package/plugin-src/client/styles.js +5 -1
  26. package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
  27. package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
  28. package/src/channels/dingtalk/dingtalk-bridge.mjs +50 -16
  29. package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
  30. package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
  31. package/src/channels/feishu/bridge.mjs +148 -70
  32. package/src/channels/feishu/feishu-cards.mjs +10 -6
  33. package/src/channels/feishu/feishu-runtime.mjs +0 -1
  34. package/src/channels/feishu/message-utils.mjs +1 -1
  35. package/src/channels/feishu/multi-bot-controller.mjs +3 -17
  36. package/src/channels/feishu/repair-manager.mjs +7 -2
  37. package/src/channels/qq/qq-bridge.mjs +84 -28
  38. package/src/channels/qq/qq-controller.mjs +2 -0
  39. package/src/channels/shared/control-command.mjs +11 -1
  40. package/src/channels/shared/harness-client.mjs +40 -4
  41. package/src/channels/shared/i18n-en/feishu.mjs +27 -25
  42. package/src/channels/shared/i18n-en/shared-a.mjs +78 -0
  43. package/src/channels/shared/i18n-en/shared-b.mjs +1 -0
  44. package/src/channels/shared/i18n-en/telegram.mjs +1 -0
  45. package/src/channels/shared/message-failure.mjs +244 -0
  46. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  47. package/src/channels/shared/text-harness-bridge.mjs +66 -60
  48. package/src/channels/shared/token-bot-controller.mjs +2 -0
  49. package/src/channels/slack/slack-controller.mjs +2 -0
  50. package/src/channels/telegram/telegram-runtime.mjs +1 -0
  51. package/src/channels/wecom/wecom-bridge.mjs +50 -15
  52. package/src/channels/wecom/wecom-controller.mjs +2 -0
  53. package/src/channels/weixin/weixin-api.mjs +47 -0
  54. package/src/channels/weixin/weixin-bridge.mjs +262 -43
  55. package/src/channels/weixin/weixin-controller.mjs +2 -15
  56. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  57. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "2.3.0",
3
+ "version": "2.5.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",
@@ -1,6 +1,20 @@
1
1
  import * as React from 'react';
2
2
 
3
- import { h } from './i18n.js';
3
+ import { h, isEnglish } from './i18n.js';
4
+
5
+ function messageErrorTime(value) {
6
+ try {
7
+ return new Intl.DateTimeFormat(isEnglish() ? 'en-US' : 'zh-CN', {
8
+ year: 'numeric',
9
+ month: '2-digit',
10
+ day: '2-digit',
11
+ hour: '2-digit',
12
+ minute: '2-digit',
13
+ }).format(new Date(value));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
4
18
 
5
19
  export function ChannelListHeading({ className = '', id, title, connectionLabel }) {
6
20
  const helpId = React.useId();
@@ -46,3 +60,24 @@ export function BotStatusMeta({
46
60
  h('span', null, '最近检查'),
47
61
  h('span', null, formatCheckedTime(lastCheckedAt))));
48
62
  }
63
+
64
+ export function LastMessageErrorSummary({ className = '', error }) {
65
+ if (!error) return null;
66
+ const occurredAt = messageErrorTime(error.at);
67
+ return h('div', {
68
+ className: `${className} dim-cardSummary`.trim(),
69
+ role: 'status',
70
+ },
71
+ h('strong', null, '最近一条消息处理失败'),
72
+ ':',
73
+ h('span', null, error.message),
74
+ '(',
75
+ h('span', null, '错误码'),
76
+ ` ${error.code} · `,
77
+ h('span', null, '参考号'),
78
+ ` ${error.referenceId}`,
79
+ occurredAt ? h(React.Fragment, null,
80
+ ' · ',
81
+ h('time', { dateTime: new Date(error.at).toISOString() }, occurredAt)) : null,
82
+ ')');
83
+ }
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const DINGTALK_RPC_CHANNEL = '/dingtalk';
4
5
 
@@ -178,6 +179,7 @@ function normalizeBot(value) {
178
179
  messagesReceived: nonNegativeInteger(stats.messagesReceived),
179
180
  messagesReplied: nonNegativeInteger(stats.messagesReplied),
180
181
  },
182
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
181
183
  error: normalizeError(value.error, 'DINGTALK_ACCOUNT_ERROR', '钉钉连接尚未就绪') ?? null,
182
184
  };
183
185
  }
@@ -9,7 +9,11 @@ import {
9
9
  EMPTY_AGENT_PRESET_CATALOG,
10
10
  } from '../../agent-preset.js';
11
11
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
12
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
12
+ import {
13
+ BotStatusMeta,
14
+ ChannelListHeading,
15
+ LastMessageErrorSummary,
16
+ } from '../../channel-card-meta.js';
13
17
  import {
14
18
  DINGTALK_ENDPOINTS,
15
19
  DINGTALK_RPC_CHANNEL,
@@ -262,6 +266,10 @@ export function AccountCard({
262
266
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) },
263
267
  '移除接入')),
264
268
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
269
+ account.lastMessageError ? h(LastMessageErrorSummary, {
270
+ className: 'ddt-summary',
271
+ error: account.lastMessageError,
272
+ }) : null,
265
273
  feedback ? h('div', {
266
274
  className: 'ddt-summary dim-cardFeedback',
267
275
  role: 'status',
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
10
+ import { normalizeLastMessageError } from "../../last-message-error.js";
10
11
 
11
12
  export const FEISHU_RPC_CHANNEL = "/feishu";
12
13
 
@@ -206,6 +207,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
206
207
  groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
207
208
  bot: normalizeBot(value.bot),
208
209
  health: normalizeHealth(value.health, connected),
210
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
209
211
  error: normalizeError(value.error),
210
212
  };
211
213
  }
@@ -226,6 +228,7 @@ export function normalizeBotsSnapshot(value) {
226
228
  configured: true,
227
229
  bot: value.bot,
228
230
  health: value.health,
231
+ lastMessageError: value.lastMessageError,
229
232
  error: value.error,
230
233
  }];
231
234
  }
@@ -23,7 +23,11 @@ import {
23
23
  EMPTY_AGENT_PRESET_CATALOG,
24
24
  } from "../../agent-preset.js";
25
25
  import { useWorkspaceSnapshotFence } from "../../workspace-snapshot-fence.js";
26
- import { BotStatusMeta, ChannelListHeading } from "../../channel-card-meta.js";
26
+ import {
27
+ BotStatusMeta,
28
+ ChannelListHeading,
29
+ LastMessageErrorSummary,
30
+ } from "../../channel-card-meta.js";
27
31
  import { installFeishuStyles } from "./styles.js";
28
32
 
29
33
  export const name = "feishu-settings";
@@ -265,7 +269,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
265
269
  ? h("img", {
266
270
  src: qrSource,
267
271
  alt: repairing
268
- ? `用于修复${botName}卡片按钮的一次性授权二维码`
272
+ ? `用于为${botName}补全权限与回调的一次性授权二维码`
269
273
  : grantingGroupMessages
270
274
  ? `用于为${botName}开通群消息权限的一次性授权二维码`
271
275
  : "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
@@ -293,31 +297,31 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
293
297
  h("div", { className: "bxf-stateLabel dim-stateLabel" },
294
298
  h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
295
299
  h("span", null, repairing
296
- ? `正在修复「${botName}」`
300
+ ? `正在为「${botName}」补全权限与回调`
297
301
  : grantingGroupMessages
298
302
  ? `正在为「${botName}」开通群消息权限`
299
303
  : "正在添加新机器人")),
300
304
  h("h3", null, expired
301
305
  ? "刷新二维码后继续"
302
306
  : repairing
303
- ? "使用飞书扫码修复卡片按钮"
307
+ ? "使用飞书扫码补全权限"
304
308
  : grantingGroupMessages
305
309
  ? "使用飞书确认群消息权限"
306
310
  : "使用飞书扫码创建机器人"),
307
311
  h("p", null, repairing
308
- ? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
312
+ ? "扫码会更新现有飞书应用,最多增量补充卡片按钮回调、读取用户消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;不会创建新应用。确认页只显示当前缺少项,完成后此机器人会短暂重连,其他机器人不受影响。"
309
313
  : grantingGroupMessages
310
314
  ? "扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。"
311
315
  : "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
312
316
  h("ol", { className: "bxf-steps dim-steps" },
313
317
  h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
314
318
  h("li", null, repairing
315
- ? "核对现有应用名称,并确认只新增卡片回调"
319
+ ? "核对现有应用名称,并确认只新增当前缺少的上述配置"
316
320
  : grantingGroupMessages
317
321
  ? "核对现有应用,并确认“获取群组中所有消息”权限"
318
322
  : "核对应用名称与权限范围,并确认创建"),
319
323
  h("li", null, repairing
320
- ? "保持本页打开,等待卡片按钮修复完成"
324
+ ? "保持本页打开,等待权限与回调补全完成"
321
325
  : grantingGroupMessages
322
326
  ? "保持本页打开,等待权限生效并自动切换响应方式"
323
327
  : "保持本页打开,等待新机器人的长连接就绪")),
@@ -336,7 +340,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
336
340
  ? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
337
341
  : null,
338
342
  h(Button, { onClick: onCancel, disabled: busy }, repairing
339
- ? "取消修复"
343
+ ? "取消补全"
340
344
  : grantingGroupMessages ? "取消授权" : "取消添加")),
341
345
  ),
342
346
  ),
@@ -354,16 +358,16 @@ function ProvisionProgress({ phase, provision, onCancel, busy }) {
354
358
  h("div", { className: "dim-spinner", "aria-hidden": "true" }),
355
359
  h("h3", null, connecting
356
360
  ? repairing
357
- ? "已确认,正在完成卡片按钮修复"
361
+ ? "已确认,正在完成权限与回调配置"
358
362
  : grantingGroupMessages
359
363
  ? "已确认,正在启用全部消息模式"
360
364
  : "已确认,正在连接新机器人"
361
365
  : repairing
362
- ? "正在准备修复二维码"
366
+ ? "正在准备权限补全二维码"
363
367
  : grantingGroupMessages ? "正在准备权限授权二维码" : "正在准备授权二维码"),
364
368
  h("p", null, connecting
365
369
  ? repairing
366
- ? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
370
+ ? "配置已提交,正在保存权限、验证卡片回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
367
371
  : grantingGroupMessages
368
372
  ? "权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。"
369
373
  : "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
@@ -375,7 +379,7 @@ function ProvisionProgress({ phase, provision, onCancel, busy }) {
375
379
  connecting && onCancel
376
380
  ? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
377
381
  h(Button, { onClick: onCancel, disabled: busy }, repairing
378
- ? "取消修复"
382
+ ? "取消补全"
379
383
  : grantingGroupMessages ? "取消授权" : "取消添加"))
380
384
  : null,
381
385
  );
@@ -388,7 +392,7 @@ function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
388
392
  h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
389
393
  h("div", null,
390
394
  h("h3", null, repairing
391
- ? "卡片按钮没有修复完成"
395
+ ? "权限与回调没有补全完成"
392
396
  : grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
393
397
  h("p", null, error.message),
394
398
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
@@ -564,6 +568,7 @@ export function BotCard({
564
568
  removeButtonRef,
565
569
  }) {
566
570
  const { bot, health, state, connected } = connection;
571
+ const repairTooltipId = React.useId();
567
572
  const stateForDisplay = busy === "reconnect"
568
573
  ? "connecting"
569
574
  : state;
@@ -637,13 +642,22 @@ export function BotCard({
637
642
  disabled: Boolean(busy), "aria-busy": busy === "reconnect" ? "true" : undefined,
638
643
  "aria-label": `${connected ? "检查连接" : "重试连接"}${bot.name}`,
639
644
  }, busy === "reconnect" ? (connected ? "检查中…" : "正在连接…") : connected ? "检查连接" : "重试连接"),
640
- h(Button, {
641
- className: "bxf-repairButton dim-cardAction",
642
- onClick: onRepairCallback,
643
- disabled: Boolean(busy) || repairDisabled,
644
- "aria-busy": busy === "callback-repair" ? "true" : undefined,
645
- "aria-label": `修复${bot.name}的卡片按钮`,
646
- }, busy === "callback-repair" ? "等待扫码…" : "修复卡片按钮"),
645
+ h("span", { className: "bxf-repairAction" },
646
+ h(Button, {
647
+ className: "bxf-repairButton dim-cardAction",
648
+ onClick: onRepairCallback,
649
+ disabled: Boolean(busy) || repairDisabled,
650
+ "aria-busy": busy === "callback-repair" ? "true" : undefined,
651
+ "aria-label": `为${bot.name}补全权限与回调`,
652
+ "aria-describedby": repairTooltipId,
653
+ }, busy === "callback-repair" ? "等待扫码…" : "补全权限"),
654
+ h("span", {
655
+ id: repairTooltipId,
656
+ className: "bxf-repairTooltip",
657
+ role: "tooltip",
658
+ },
659
+ h("strong", null, "补全范围"),
660
+ h("span", null, "最多增量添加卡片回调 card.action.trigger、读取消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;确认页只显示当前缺少项,不会创建新应用。"))),
647
661
  h(Button, {
648
662
  className: "dim-cardAction", kind: "danger", onClick: onRequestRemove,
649
663
  disabled: Boolean(busy), ref: removeButtonRef,
@@ -651,6 +665,10 @@ export function BotCard({
651
665
  }, "移除接入")),
652
666
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
653
667
  summary) : null,
668
+ connection.lastMessageError ? h(LastMessageErrorSummary, {
669
+ className: "bxf-healthSummary",
670
+ error: connection.lastMessageError,
671
+ }) : null,
654
672
  testNotice ? h("div", {
655
673
  className: "bxf-healthSummary dim-cardFeedback",
656
674
  role: "status",
@@ -933,7 +951,7 @@ export function FeishuSettingsTab({ rpcCall }) {
933
951
  && (provision.operation !== operation || provision.botId !== botId)) {
934
952
  throw new Error(grantingGroupMessages
935
953
  ? "飞书服务返回了不匹配的群消息权限二维码"
936
- : "飞书服务返回了不匹配的卡片修复二维码");
954
+ : "飞书服务返回了不匹配的权限补全二维码");
937
955
  }
938
956
  const timestamp = Date.now();
939
957
  setNow(timestamp);
@@ -948,7 +966,7 @@ export function FeishuSettingsTab({ rpcCall }) {
948
966
  },
949
967
  }));
950
968
  announce(repairing
951
- ? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
969
+ ? `${botName ?? "机器人"}的权限补全二维码已生成,请使用飞书扫码。`
952
970
  : grantingGroupMessages
953
971
  ? `${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。`
954
972
  : "授权二维码已生成,请使用飞书扫码。");
@@ -1032,7 +1050,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1032
1050
  : current);
1033
1051
  announce(grantingGroupMessages
1034
1052
  ? "权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。"
1035
- : "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
1053
+ : "配置已提交,正在保存权限、验证卡片回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
1036
1054
  return;
1037
1055
  }
1038
1056
  if (result.status === "connected") {
@@ -1040,7 +1058,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1040
1058
  setModel((current) => ({ ...current, provisioning: null }));
1041
1059
  announce(grantingGroupMessages
1042
1060
  ? `${targetBotName}已开通群消息权限,并启用“响应所有群消息”。`
1043
- : `${targetBotName}的卡片按钮已修复。`);
1061
+ : `${targetBotName}的权限与回调已补全。`);
1044
1062
  if (activeProvision.botId) setFocusBotId(activeProvision.botId);
1045
1063
  await loadStatus({ silent: true, restoreProvisioning: false });
1046
1064
  return;
@@ -1048,7 +1066,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1048
1066
  }
1049
1067
  setModel((current) => ({ ...current, provisioning: null }));
1050
1068
  announce(repairing
1051
- ? "已取消卡片按钮修复。"
1069
+ ? "已取消补全权限与回调。"
1052
1070
  : grantingGroupMessages ? "已取消群消息权限授权。" : "已取消添加机器人。");
1053
1071
  await loadStatus({ silent: true, restoreProvisioning: false });
1054
1072
  scheduleAnimationFrame(() => {
@@ -1117,7 +1135,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1117
1135
  const targetBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
1118
1136
  if (!snapshot) {
1119
1137
  throw new Error(isCallbackRepair(provision)
1120
- ? "卡片按钮已更新,但暂时无法确认机器人连接状态"
1138
+ ? "权限与回调已更新,但暂时无法确认机器人连接状态"
1121
1139
  : isGroupMessagePermission(provision)
1122
1140
  ? "群消息权限已更新,但暂时无法确认机器人连接状态"
1123
1141
  : "机器人已经创建,但暂时无法确认连接状态");
@@ -1130,7 +1148,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1130
1148
  }
1131
1149
  setModel((current) => ({ ...current, provisioning: null }));
1132
1150
  announce(isCallbackRepair(provision)
1133
- ? `${targetBot.bot.name}的卡片按钮已修复。`
1151
+ ? `${targetBot.bot.name}的权限与回调已补全。`
1134
1152
  : isGroupMessagePermission(provision)
1135
1153
  ? `${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。`
1136
1154
  : targetBot
@@ -1142,7 +1160,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1142
1160
  if (result.status === "failed") {
1143
1161
  const error = new Error(result.message
1144
1162
  ?? (isCallbackRepair(provision)
1145
- ? "飞书卡片按钮修复失败"
1163
+ ? "飞书权限与回调补全失败"
1146
1164
  : isGroupMessagePermission(provision)
1147
1165
  ? "飞书群消息权限开通失败"
1148
1166
  : "飞书应用创建失败"));
@@ -450,10 +450,35 @@ const CSS = String.raw`
450
450
  .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); }
451
451
  .bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
452
452
  .bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
453
- .bxf-botActions { flex: none; width: 100%; flex-wrap: wrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
453
+ .bxf-botActions { position: relative; flex: none; width: 100%; flex-wrap: wrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
454
454
  .bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
455
455
  .bxf-botActions .bxf-repairButton { color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 35%, var(--dsw-alias-border-l2, #dee0e3)); }
456
456
  .bxf-botActions .bxf-repairButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
457
+ .bxf-repairAction { display: inline-flex; }
458
+ .bxf-repairTooltip {
459
+ position: absolute;
460
+ right: 0;
461
+ bottom: calc(100% + 8px);
462
+ z-index: 40;
463
+ width: min(330px, 100%);
464
+ display: grid;
465
+ gap: 3px;
466
+ padding: 9px 10px;
467
+ border: 1px solid var(--dsw-alias-border-l2, #dfe1e5);
468
+ border-radius: 8px;
469
+ color: var(--dsw-alias-label-primary, #1f2329);
470
+ background: var(--dsw-alias-bg-layer-3, #fff);
471
+ box-shadow: 0 10px 28px rgb(31 35 41 / 16%);
472
+ opacity: 0;
473
+ visibility: hidden;
474
+ transform: translateY(3px);
475
+ pointer-events: none;
476
+ transition: opacity .15s ease, transform .15s ease, visibility .15s ease;
477
+ }
478
+ .bxf-repairTooltip strong { font-size: 12px; line-height: 17px; font-weight: 650; }
479
+ .bxf-repairTooltip > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 17px; font-weight: 400; overflow-wrap: anywhere; }
480
+ .bxf-repairAction:hover .bxf-repairTooltip,
481
+ .bxf-repairAction:focus-within .bxf-repairTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
457
482
 
458
483
  .bxf-confirm {
459
484
  border-top: 1px solid var(--dsw-alias-border-l2, #dee0e3);
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const QQ_RPC_CHANNEL = '/qq';
4
5
 
@@ -94,6 +95,7 @@ function normalizeBot(value) {
94
95
  summary: text(value.health?.summary, connected ? 'QQ WebSocket 长连接运行正常' : 'QQ 连接尚未就绪'),
95
96
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
96
97
  },
98
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
97
99
  error: isRecord(value.error) ? {
98
100
  code: text(value.error.code, 'QQ_ACCOUNT_ERROR', 80),
99
101
  message: text(value.error.message, 'QQ 连接尚未就绪'),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
15
19
  import {
16
20
  QQ_ENDPOINTS,
@@ -201,6 +205,10 @@ export function AccountCard({
201
205
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
202
206
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
203
207
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
208
+ account.lastMessageError ? h(LastMessageErrorSummary, {
209
+ className: 'ddt-summary',
210
+ error: account.lastMessageError,
211
+ }) : null,
204
212
  feedback ? h('div', {
205
213
  className: 'ddt-summary dim-cardFeedback',
206
214
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
4
5
 
@@ -68,6 +69,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
68
69
  ),
69
70
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
70
71
  },
72
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
71
73
  error: isRecord(value.error) ? {
72
74
  code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
73
75
  message: text(value.error.message, `${channel}连接尚未就绪`),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
 
15
19
  const Button = React.forwardRef(function Button(
16
20
  { children, kind = 'secondary', className = '', ...props },
@@ -121,6 +125,10 @@ export function createTokenChannelSettings(definition) {
121
125
  disabled: Boolean(busy),
122
126
  }, '移除接入')),
123
127
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
128
+ account.lastMessageError ? h(LastMessageErrorSummary, {
129
+ className: 'ddt-summary',
130
+ error: account.lastMessageError,
131
+ }) : null,
124
132
  testNotice ? h('div', {
125
133
  className: 'ddt-summary dim-cardFeedback',
126
134
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const WECOM_RPC_CHANNEL = '/wecom';
4
5
 
@@ -103,6 +104,7 @@ function normalizeBot(value) {
103
104
  summary: text(value.health?.summary, connected ? '企业微信 WebSocket 长连接运行正常' : '企业微信连接尚未就绪'),
104
105
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
105
106
  },
107
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
106
108
  error: isRecord(value.error) ? {
107
109
  code: text(value.error.code, 'WECOM_ACCOUNT_ERROR', 80),
108
110
  message: text(value.error.message, '企业微信连接尚未就绪'),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
15
19
  import {
16
20
  WECOM_ENDPOINTS,
@@ -200,6 +204,10 @@ export function AccountCard({
200
204
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
201
205
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
202
206
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
207
+ account.lastMessageError ? h(LastMessageErrorSummary, {
208
+ className: 'ddt-summary',
209
+ error: account.lastMessageError,
210
+ }) : null,
203
211
  feedback ? h('div', {
204
212
  className: 'ddt-summary dim-cardFeedback',
205
213
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const WEIXIN_RPC_CHANNEL = '/weixin';
4
5
  export const WEIXIN_ENDPOINTS = Object.freeze({
@@ -48,15 +49,6 @@ function normalizeTestMessage(value) {
48
49
  return { sent: false, code };
49
50
  }
50
51
 
51
- function normalizeMessageError(value) {
52
- if (!isRecord(value)) return null;
53
- const code = string(value.code).slice(0, 64);
54
- const reason = string(value.reason).slice(0, 128);
55
- const message = string(value.message).slice(0, 500);
56
- const at = timestamp(value.at);
57
- return code && reason && message && at !== null ? { code, reason, message, at } : null;
58
- }
59
-
60
52
  export function unwrapRpcResult(result) {
61
53
  if (!isRecord(result) || typeof result.ok !== 'boolean') {
62
54
  throw new Error('微信服务返回了无法识别的响应');
@@ -141,7 +133,7 @@ function normalizeBot(value) {
141
133
  messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
142
134
  messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
143
135
  },
144
- lastMessageError: normalizeMessageError(value.lastMessageError),
136
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
145
137
  error: isRecord(value.error)
146
138
  ? {
147
139
  code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
@@ -22,7 +22,11 @@ import {
22
22
  EMPTY_AGENT_PRESET_CATALOG,
23
23
  } from '../../agent-preset.js';
24
24
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
25
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
25
+ import {
26
+ BotStatusMeta,
27
+ ChannelListHeading,
28
+ LastMessageErrorSummary,
29
+ } from '../../channel-card-meta.js';
26
30
  import { installWeixinStyles } from './styles.js';
27
31
 
28
32
  export const name = 'weixin-settings';
@@ -247,10 +251,10 @@ export function AccountCard({
247
251
  busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
248
252
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
249
253
  summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
250
- account.lastMessageError ? h('div', {
251
- className: 'dxw-summary dim-cardSummary',
252
- role: 'status',
253
- }, `最近一条消息处理失败:${account.lastMessageError.message}`) : null,
254
+ account.lastMessageError ? h(LastMessageErrorSummary, {
255
+ className: 'dxw-summary',
256
+ error: account.lastMessageError,
257
+ }) : null,
254
258
  feedback ? h('div', {
255
259
  className: 'dxw-summary dim-cardFeedback',
256
260
  role: 'status',
@@ -1,4 +1,5 @@
1
1
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
2
+ import { normalizeLastMessageError } from '../../last-message-error.js';
2
3
 
3
4
  export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
4
5
 
@@ -106,6 +107,7 @@ function normalizeBot(value) {
106
107
  ? 'WhatsApp Web 关联设备运行正常' : 'WhatsApp 连接尚未就绪'),
107
108
  lastCheckedAt: timestamp(value.health?.lastCheckedAt),
108
109
  },
110
+ lastMessageError: normalizeLastMessageError(value.lastMessageError),
109
111
  error: isRecord(value.error) ? {
110
112
  code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
111
113
  message: text(value.error.message, 'WhatsApp 连接尚未就绪'),
@@ -10,7 +10,11 @@ import {
10
10
  EMPTY_AGENT_PRESET_CATALOG,
11
11
  } from '../../agent-preset.js';
12
12
  import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
13
- import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
13
+ import {
14
+ BotStatusMeta,
15
+ ChannelListHeading,
16
+ LastMessageErrorSummary,
17
+ } from '../../channel-card-meta.js';
14
18
  import { installDingtalkStyles } from '../dingtalk/styles.js';
15
19
  import {
16
20
  WHATSAPP_ENDPOINTS,
@@ -345,6 +349,10 @@ export function WhatsappAccountCard({
345
349
  className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
346
350
  }, '移除接入')),
347
351
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
352
+ account.lastMessageError ? h(LastMessageErrorSummary, {
353
+ className: 'ddt-summary',
354
+ error: account.lastMessageError,
355
+ }) : null,
348
356
  testNotice ? h('div', {
349
357
  className: 'ddt-summary dim-cardFeedback',
350
358
  role: 'status',