@xmanrui/dsh-im 4.22.0 → 4.23.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 (137) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +3 -3
  3. package/lib/client.js +2505 -1152
  4. package/lib/index.js +290 -299
  5. package/package.json +8 -1
  6. package/plugin-src/client/channel-card-meta.js +2 -0
  7. package/plugin-src/client/channel-logos.js +11 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  9. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  10. package/plugin-src/client/channels/email/api.js +52 -0
  11. package/plugin-src/client/channels/email/index.js +728 -0
  12. package/plugin-src/client/channels/email/styles.js +51 -0
  13. package/plugin-src/client/channels/feishu/api.js +4 -2
  14. package/plugin-src/client/channels/feishu/index.js +16 -3
  15. package/plugin-src/client/channels/imessage/index.js +2 -1
  16. package/plugin-src/client/channels/office/api.js +2 -0
  17. package/plugin-src/client/channels/office/index.js +6 -5
  18. package/plugin-src/client/channels/qq/api.js +7 -0
  19. package/plugin-src/client/channels/qq/index.js +16 -3
  20. package/plugin-src/client/channels/shared/token-api.js +5 -1
  21. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  22. package/plugin-src/client/channels/slack/index.js +2 -1
  23. package/plugin-src/client/channels/wecom/api.js +7 -1
  24. package/plugin-src/client/channels/wecom/index.js +16 -3
  25. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  26. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  27. package/plugin-src/client/channels/weixin/api.js +2 -1
  28. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  29. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  30. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  31. package/plugin-src/client/connection-error.js +87 -0
  32. package/plugin-src/client/global-settings.js +81 -1
  33. package/plugin-src/client/i18n.js +61 -0
  34. package/plugin-src/client/index.js +30 -2
  35. package/plugin-src/client/last-message-error.js +2 -1
  36. package/plugin-src/client/styles.js +6 -0
  37. package/plugin-src/host/build.mjs +4 -0
  38. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  39. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  40. package/plugin-src/host/channels/email/availability.mjs +60 -0
  41. package/plugin-src/host/channels/email/index.mjs +36 -0
  42. package/plugin-src/host/channels/email/production.mjs +58 -0
  43. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  44. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  45. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  46. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  47. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  48. package/plugin-src/host/channels/qq/production.mjs +2 -0
  49. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  50. package/plugin-src/host/channels/shared/production.mjs +52 -5
  51. package/plugin-src/host/channels/shared/rpc.mjs +5 -3
  52. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  53. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  54. package/plugin-src/host/channels/slack/production.mjs +2 -0
  55. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  56. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  57. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  58. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  59. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  61. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  62. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  63. package/plugin-src/host/delivery-adapter.mjs +11 -0
  64. package/plugin-src/host/image-input-rpc.mjs +24 -0
  65. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  66. package/plugin-src/host/index.mjs +3 -0
  67. package/plugin-src/management-rpc.mjs +12 -2
  68. package/scripts/verify-package.mjs +3 -1
  69. package/src/channels/dingtalk/connection-error.mjs +5 -12
  70. package/src/channels/dingtalk/device-auth.mjs +4 -1
  71. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  72. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  73. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  74. package/src/channels/discord/discord-api.mjs +3 -3
  75. package/src/channels/discord/discord-runtime.mjs +18 -12
  76. package/src/channels/email/config-store.mjs +194 -0
  77. package/src/channels/email/email-api.mjs +20 -0
  78. package/src/channels/email/email-bridge.mjs +17 -0
  79. package/src/channels/email/email-controller.mjs +824 -0
  80. package/src/channels/email/email-runtime.mjs +692 -0
  81. package/src/channels/email/harness-client.mjs +7 -0
  82. package/src/channels/email/mail-format.mjs +91 -0
  83. package/src/channels/email/state-store.mjs +176 -0
  84. package/src/channels/email/transport.mjs +64 -0
  85. package/src/channels/email/transports/agent-mail.mjs +593 -0
  86. package/src/channels/email/transports/agently-cli.mjs +288 -0
  87. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  88. package/src/channels/feishu/bridge.mjs +8 -7
  89. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  90. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  91. package/src/channels/feishu/message-utils.mjs +3 -2
  92. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  93. package/src/channels/feishu/registration-manager.mjs +8 -4
  94. package/src/channels/feishu/repair-manager.mjs +2 -1
  95. package/src/channels/imessage/runtime.mjs +9 -5
  96. package/src/channels/office/office-controller.mjs +23 -12
  97. package/src/channels/office/office-runtime.mjs +6 -3
  98. package/src/channels/office/office-transport.mjs +1 -0
  99. package/src/channels/qq/qq-bridge.mjs +7 -5
  100. package/src/channels/qq/qq-controller.mjs +56 -42
  101. package/src/channels/qq/qq-runtime.mjs +16 -11
  102. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  103. package/src/channels/shared/connection-error.mjs +191 -0
  104. package/src/channels/shared/connection-test.mjs +3 -1
  105. package/src/channels/shared/conversation-state-store.mjs +46 -1
  106. package/src/channels/shared/diagnostic-details.mjs +95 -0
  107. package/src/channels/shared/harness-client.mjs +63 -18
  108. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  109. package/src/channels/shared/i18n-en/email.mjs +25 -0
  110. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  111. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  112. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  113. package/src/channels/shared/i18n-en.mjs +6 -0
  114. package/src/channels/shared/image-input-policy.mjs +35 -0
  115. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  116. package/src/channels/shared/image-input.mjs +124 -0
  117. package/src/channels/shared/image-prompt.mjs +15 -7
  118. package/src/channels/shared/message-failure.mjs +12 -0
  119. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  120. package/src/channels/shared/text-harness-bridge.mjs +90 -11
  121. package/src/channels/shared/token-bot-controller.mjs +52 -47
  122. package/src/channels/slack/slack-api.mjs +3 -3
  123. package/src/channels/slack/slack-controller.mjs +59 -51
  124. package/src/channels/slack/slack-runtime.mjs +18 -12
  125. package/src/channels/telegram/telegram-api.mjs +2 -2
  126. package/src/channels/telegram/telegram-runtime.mjs +17 -13
  127. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  128. package/src/channels/wecom/wecom-controller.mjs +51 -37
  129. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  130. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  131. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  132. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  133. package/src/channels/weixin/connection-error.mjs +26 -92
  134. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  135. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  136. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  137. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -0,0 +1,51 @@
1
+ export const EMAIL_STYLE_ID = 'xmanrui-dsh-im-email-settings';
2
+ const CSS = String.raw`
3
+ .dim-pageEmail { --ddt-accent: #2f6fd0; --ddt-accent-deep: #24559f; --ddt-accent-wash: #eef4fd; }
4
+ .dim-avatarEmail { color: #fff; background: linear-gradient(180deg, #6ea8f5 0%, #3d7fe0 50%, #24559f 100%); box-shadow: inset 0 1px 1px rgb(255 255 255 / 45%), 0 1px 3px rgb(31 35 41 / 12%); }
5
+ .dim-avatarEmail svg { display: block; }
6
+ .dim-emailPanel { padding: 20px; }
7
+ .dim-emailPanel p { color: var(--dsw-alias-label-secondary, #646a73); }
8
+ .dim-emailFields { display: grid; gap: 12px; margin: 16px 0; }
9
+ .dim-emailField { display: grid; gap: 6px; }
10
+ .dim-emailField > span { font-size: 13px; color: var(--dsw-alias-label-secondary, #646a73); }
11
+ .dim-emailField input, .dim-emailField select, .dim-emailField textarea {
12
+ padding: 8px 10px; border: 1px solid var(--dsw-alias-border, #dee0e3); border-radius: 6px;
13
+ background: var(--dsw-alias-bg-base, #fff); color: inherit; font: inherit;
14
+ }
15
+ .dim-emailField textarea { min-height: 76px; resize: vertical; }
16
+ .dim-emailHint { font-size: 12px; color: var(--dsw-alias-label-tertiary, #8f959e); }
17
+ .dim-emailGrid { display: grid; grid-template-columns: 1fr 120px; gap: 12px; }
18
+ .dim-emailBinding { margin-top: 20px; border-top: 1px solid var(--dsw-alias-border, #dee0e3); padding-top: 16px; }
19
+ .dim-emailBinding h4 { margin: 0 0 4px; font-size: 14px; }
20
+ .dim-emailBindingHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
21
+ /* The sender address wraps above its picker instead of squeezing the select
22
+ into a narrow column, so a long session title stays fully readable. */
23
+ .dim-emailBindingRow {
24
+ display: grid;
25
+ grid-template-columns: minmax(0, 1fr);
26
+ gap: 4px;
27
+ align-items: center;
28
+ padding: 8px 0;
29
+ border-top: 1px dashed var(--dsw-alias-border, #eef0f2);
30
+ }
31
+ .dim-emailBindingRow:first-of-type { border-top: 0; }
32
+ .dim-emailBindingSender { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
33
+ .dim-emailBindingRow select { width: 100%; }
34
+ /* Selects clip their own text; a minimum width keeps the chosen title legible. */
35
+ .dim-emailBinding select { min-width: 0; }
36
+ /* QR authorization block for the Agent mailbox. */
37
+ .dim-emailAuth { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--dsw-alias-border, #dee0e3); border-radius: 8px; }
38
+ .dim-emailAuthPanel { display: grid; gap: 6px; }
39
+ .dim-emailAuthRow { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 8px; align-items: center; font-size: 13px; }
40
+ .dim-emailAuthLink { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
41
+ /* The full title of the current choice, wrapping instead of clipping. */
42
+ .dim-emailBindingSelected { display: block; white-space: normal; overflow-wrap: anywhere; }
43
+ `;
44
+ export function installEmailStyles() {
45
+ if (typeof document === 'undefined') return () => {};
46
+ const existing = document.querySelector(`style[data-plugin-css="${EMAIL_STYLE_ID}"]`);
47
+ if (existing) return () => {};
48
+ const style = document.createElement('style'); style.dataset.plugin = '@xmanrui/dsh-im';
49
+ style.dataset.pluginCss = EMAIL_STYLE_ID; style.textContent = CSS; document.head.appendChild(style);
50
+ return () => style.remove();
51
+ }
@@ -1,3 +1,4 @@
1
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
1
2
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
3
  /**
3
4
  * Browser-safe contract for the Feishu Host plugin.
@@ -119,6 +120,7 @@ export function unwrapRpcResult(result) {
119
120
  const message = optionalString(result.error?.message) ?? "飞书服务请求失败";
120
121
  const error = new Error(message);
121
122
  error.code = optionalString(result.error?.code) ?? "FEISHU_RPC_ERROR";
123
+ Object.assign(error, diagnosticFields(result.error));
122
124
  throw error;
123
125
  }
124
126
  return result.value;
@@ -190,7 +192,7 @@ function normalizeError(value) {
190
192
  if (!isRecord(value)) return undefined;
191
193
  const message = optionalString(value.message);
192
194
  if (!message) return undefined;
193
- return { message, code: optionalString(value.code) };
195
+ return { ...diagnosticFields(value), message, code: optionalString(value.code) };
194
196
  }
195
197
 
196
198
  function authoritativeState(value, connected) {
@@ -386,7 +388,7 @@ export function presentError(error) {
386
388
  const message = raw
387
389
  .replace(/(client[_-]?secret|app[_-]?secret|secret|token)\s*[:=]\s*[^\s,;]+/gi, "$1=••••••")
388
390
  .slice(0, 240);
389
- return { message, code: optionalString(error?.code) };
391
+ return { message, code: optionalString(error?.code), ...diagnosticFields(error) };
390
392
  }
391
393
 
392
394
  export function formatRemaining(milliseconds) {
@@ -1,3 +1,4 @@
1
+ import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
1
2
  import { BotName } from '../../bot-alias.js';
2
3
  import * as React from "react";
3
4
 
@@ -396,7 +397,7 @@ function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
396
397
  h("h3", null, repairing
397
398
  ? "权限与回调没有补全完成"
398
399
  : grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
399
- h("p", null, error.message),
400
+ h(ConnectionError, { error: error }),
400
401
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
401
402
  h("div", { className: "bxf-actions dim-viewActions" },
402
403
  h(Button, { kind: "primary", onClick: onRetry, disabled: busy },
@@ -700,6 +701,7 @@ export function BotCard({
700
701
  }, "移除接入")),
701
702
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
702
703
  summary) : null,
704
+ (actionError || connection.error) ? h(ConnectionError, { error: actionError || connection.error, showMessage: false }) : null,
703
705
  connection.lastMessageError ? h(LastMessageErrorSummary, {
704
706
  className: "bxf-healthSummary",
705
707
  error: connection.lastMessageError,
@@ -770,7 +772,7 @@ function PageError({ error, onRetry, busy }) {
770
772
  h("div", { className: "bxf-error dim-inlineError", role: "alert" },
771
773
  h("div", null,
772
774
  h("h3", null, "无法读取飞书机器人"),
773
- h("p", null, error.message),
775
+ h(ConnectionError, { error: error }),
774
776
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
775
777
  h("div", { className: "bxf-actions dim-viewActions" },
776
778
  h(Button, { kind: "primary", onClick: onRetry, disabled: busy },
@@ -812,6 +814,7 @@ export function mergeFeishuSnapshotState(
812
814
  }
813
815
 
814
816
  export function FeishuSettingsTab({ rpcCall }) {
817
+ const [operationError, setOperationError] = React.useState(null);
815
818
  const [model, setModel] = React.useState({
816
819
  phase: "loading",
817
820
  revision: 0,
@@ -857,7 +860,16 @@ export function FeishuSettingsTab({ rpcCall }) {
857
860
  }, [scheduleAnimationFrame]);
858
861
 
859
862
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
860
- return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
863
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
864
+ if (operation && mountedRef.current) setOperationError(null);
865
+ try {
866
+ const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
867
+ if (operation && mountedRef.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
868
+ return value;
869
+ } catch (error) {
870
+ if (endpoint === 'bot.delete' && mountedRef.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
871
+ throw error;
872
+ }
861
873
  }, [rpcCall]);
862
874
 
863
875
  const mergeSnapshot = React.useCallback((snapshot, { restoreProvisioning = false } = {}) => {
@@ -1490,6 +1502,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1490
1502
  }, h(AgentPresetCatalogContext.Provider, {
1491
1503
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
1492
1504
  }, h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
1505
+ operationError ? h(ConnectionError, { error: operationError }) : null,
1493
1506
  h(Heading, {
1494
1507
  totals: model.totals,
1495
1508
  onAdd: () => void startProvisioning(),
@@ -1,3 +1,4 @@
1
+ import { ConnectionError } from '../../connection-error.js';
1
2
  import * as React from 'react';
2
3
  import { IMessageLogoGlyph } from '../../channel-logos.js';
3
4
  import { createTokenChannelSettings } from '../shared/token-channel.js';
@@ -21,7 +22,7 @@ function PermissionPanel({ permissions, busy, error, onSubmit, onCancel }) {
21
22
  h('strong', null, automationReady ? '✓ Messages 自动化权限已授予' : '2. 允许自动化控制 Messages'),
22
23
  automationReady ? null : h('p', null, '在“系统设置 → 隐私与安全性 → 自动化”中,允许运行 DeepSeek Harness 的应用控制 Messages。'),
23
24
  automationReady ? null : h('a', { href: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Automation' }, '打开自动化设置'))),
24
- error ? h('p', { className: 'dim-inlineError', role: 'alert' }, error.message ?? String(error)) : null,
25
+ error ? h(ConnectionError, { error: error }) : null,
25
26
  h('div', { className: 'ddt-actions dim-viewActions' },
26
27
  h('button', { type: 'button', className: 'ddt-button', onClick: onCancel, disabled: busy }, '取消'),
27
28
  h('button', { type: 'button', className: 'ddt-button', 'data-kind': 'primary', onClick: () => onSubmit({}), disabled: busy },
@@ -1,3 +1,4 @@
1
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
1
2
  import {
2
3
  OFFICE_PROTOCOL_VERSION,
3
4
  OFFICE_RPC_CHANNEL,
@@ -12,6 +13,7 @@ export function unwrapOfficeRpc(result) {
12
13
  if (!result.ok) {
13
14
  const error = new Error(typeof result.error?.message === 'string' ? result.error.message : 'AI Office 操作失败');
14
15
  error.code = typeof result.error?.code === 'string' ? result.error.code : 'office-rpc-error';
16
+ Object.assign(error, diagnosticFields(result.error));
15
17
  throw error;
16
18
  }
17
19
  return result.value;
@@ -1,3 +1,4 @@
1
+ import { ConnectionError } from '../../connection-error.js';
1
2
  import * as React from 'react';
2
3
 
3
4
  import { h } from '../../i18n.js';
@@ -76,15 +77,15 @@ export function OfficeSettingsTab({ rpcCall, initialStatus }) {
76
77
 
77
78
  const load = React.useCallback(async () => {
78
79
  try { adopt(await invoke(OFFICE_RPC_ENDPOINTS.status)); setPhase('ready'); setError(''); }
79
- catch (caught) { setPhase('error'); setError(caught.message); }
80
+ catch (caught) { setPhase('error'); setError(caught); }
80
81
  }, [adopt, invoke]);
81
82
 
82
83
  React.useEffect(() => { void load(); }, [load]);
83
84
 
84
85
  const run = async (name, operation) => {
85
86
  setBusy(name); setError(''); setNotice('');
86
- try { const value = await operation(); adopt(value); setNotice(name === 'test' ? '连接测试通过。' : '配置已保存。'); }
87
- catch (caught) { setError(caught.message); }
87
+ try { const value = await operation(); adopt(value); if (value?.warnings?.length) setError(value.warnings[0]); setNotice(name === 'test' ? '连接测试通过。' : '配置已保存。'); }
88
+ catch (caught) { setError(caught); }
88
89
  finally { setBusy(''); }
89
90
  };
90
91
 
@@ -128,9 +129,9 @@ export function OfficeSettingsTab({ rpcCall, initialStatus }) {
128
129
  h('label', { className: 'dof-field', 'data-wide': 'true' }, 'Instruction Preset 映射',
129
130
  h('textarea', { value: form.instructionPresets, placeholder: 'action-items=转换为负责人、截止和验收明确的工单', onChange: (event) => setForm({ ...form, instructionPresets: event.target.value }) }),
130
131
  h('small', null, '每行 alias=指令;新增 preset 不需要改 Office 代码。'))),
131
- error ? h('p', { className: 'dof-error', role: 'alert' }, error) : null,
132
+ error ? h(ConnectionError, { error }) : null,
132
133
  notice ? h('p', { className: 'dof-notice', role: 'status' }, notice) : null,
133
- health.error?.message ? h('p', { className: 'dof-error' }, health.error.message) : null,
134
+ health.error?.message ? h(ConnectionError, { error: health.error }) : null,
134
135
  h('div', { className: 'dof-actions' },
135
136
  h(Button, { kind: 'primary', disabled: Boolean(busy), onClick: () => void run('save', () => invoke(OFFICE_RPC_ENDPOINTS.configure, {
136
137
  baseUrl: form.baseUrl,
@@ -1,3 +1,5 @@
1
+ import { normalizeConnectionError as normalizeTestError } from '../../connection-error.js';
2
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
1
3
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
4
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
3
5
  import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
@@ -52,6 +54,7 @@ export function unwrapRpcResult(result) {
52
54
  if (!result.ok) {
53
55
  const error = new Error(text(result.error?.message, 'QQ 操作失败'));
54
56
  error.code = text(result.error?.code, 'QQ_RPC_ERROR', 80);
57
+ Object.assign(error, diagnosticFields(result.error));
55
58
  throw error;
56
59
  }
57
60
  return result.value;
@@ -79,6 +82,7 @@ export function normalizeProvisioning(value, now = Date.now()) {
79
82
  if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
80
83
  if (id(source.botId)) result.botId = id(source.botId);
81
84
  if (isRecord(source.error)) result.error = {
85
+ ...diagnosticFields(source.error),
82
86
  code: text(source.error.code, 'QQ_PROVISION_FAILED', 80),
83
87
  message: text(source.error.message, 'QQ 机器人没有接入完成'),
84
88
  };
@@ -111,6 +115,7 @@ function normalizeBot(value) {
111
115
  },
112
116
  lastMessageError: normalizeLastMessageError(value.lastMessageError),
113
117
  error: isRecord(value.error) ? {
118
+ ...diagnosticFields(value.error),
114
119
  code: text(value.error.code, 'QQ_ACCOUNT_ERROR', 80),
115
120
  message: text(value.error.message, 'QQ 连接尚未就绪'),
116
121
  } : null,
@@ -123,6 +128,7 @@ function normalizeTestMessage(value) {
123
128
  const code = text(value.code, 'test-message-failed', 80);
124
129
  return {
125
130
  sent: false,
131
+ ...(value.error ? { error: normalizeTestError(value.error) } : {}),
126
132
  code: TEST_MESSAGE_CODES.has(code) ? code : 'test-message-failed',
127
133
  };
128
134
  }
@@ -153,6 +159,7 @@ export function connectionTestFeedback(result) {
153
159
 
154
160
  export function presentError(error) {
155
161
  return {
162
+ ...diagnosticFields(error),
156
163
  code: text(error?.code, 'QQ_ERROR', 80),
157
164
  message: text(error?.message, 'QQ 操作失败,请稍后重试'),
158
165
  };
@@ -1,3 +1,4 @@
1
+ import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
1
2
  import { BotName } from '../../bot-alias.js';
2
3
  import * as React from 'react';
3
4
 
@@ -151,7 +152,7 @@ function ProvisionView({ provision, busy, onRetry, onClose }) {
151
152
  return h('div', { className: 'ddt-card dim-surfaceCard' },
152
153
  h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
153
154
  h('h3', null, 'QQ 机器人没有绑定完成'),
154
- h('p', null, error.message),
155
+ h(ConnectionError, { error: error }),
155
156
  h('span', { className: 'ddt-errorCode' }, error.code),
156
157
  h('div', { className: 'ddt-actions dim-viewActions' },
157
158
  h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
@@ -242,6 +243,7 @@ export function AccountCard({
242
243
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
243
244
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
244
245
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
246
+ account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
245
247
  account.lastMessageError ? h(LastMessageErrorSummary, {
246
248
  className: 'ddt-summary',
247
249
  error: account.lastMessageError,
@@ -259,6 +261,7 @@ export function AccountCard({
259
261
  }
260
262
 
261
263
  export function QqSettingsTab({ rpcCall }) {
264
+ const [operationError, setOperationError] = React.useState(null);
262
265
  const [model, setModel] = React.useState({
263
266
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
264
267
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
@@ -289,7 +292,16 @@ export function QqSettingsTab({ rpcCall }) {
289
292
 
290
293
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
291
294
  if (typeof rpcCall !== 'function') throw new TypeError('QQ 设置页缺少 RPC 连接');
292
- return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
295
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
296
+ if (operation && mounted.current) setOperationError(null);
297
+ try {
298
+ const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
299
+ if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
300
+ return value;
301
+ } catch (error) {
302
+ if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
303
+ throw error;
304
+ }
293
305
  }, [rpcCall]);
294
306
 
295
307
  const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
@@ -558,6 +570,7 @@ export function QqSettingsTab({ rpcCall }) {
558
570
  }, h(AgentPresetCatalogContext.Provider, {
559
571
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
560
572
  }, h('section', { className: 'ddt-page dqq-page dim-channelPage', 'aria-label': 'QQ 设置' },
573
+ operationError ? h(ConnectionError, { error: operationError }) : null,
561
574
  h(Heading, {
562
575
  totals: model.totals,
563
576
  adding: Boolean(provision),
@@ -569,7 +582,7 @@ export function QqSettingsTab({ rpcCall }) {
569
582
  }),
570
583
  model.phase === 'loading' ? h(LoadingView)
571
584
  : model.phase === 'error'
572
- ? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取 QQ 机器人状态'), h('p', null, model.error?.message), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
585
+ ? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取 QQ 机器人状态'), h(ConnectionError, { error: model.error }), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
573
586
  : h(React.Fragment, null,
574
587
  credentialView,
575
588
  provisionView,
@@ -1,3 +1,4 @@
1
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
1
2
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
3
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
3
4
  import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
@@ -49,7 +50,8 @@ export function createTokenChannelApi(channel, connectionSummary, {
49
50
  if (!result.ok) {
50
51
  const error = new Error(text(result.error?.message, `${channel} 操作失败`));
51
52
  error.code = text(result.error?.code, `${channel.toUpperCase()}_RPC_ERROR`, 80);
52
- throw error;
53
+ Object.assign(error, diagnosticFields(result.error));
54
+ throw error;
53
55
  }
54
56
  return result.value;
55
57
  };
@@ -85,6 +87,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
85
87
  },
86
88
  lastMessageError: normalizeLastMessageError(value.lastMessageError),
87
89
  error: isRecord(value.error) ? {
90
+ ...diagnosticFields(value.error),
88
91
  code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
89
92
  message: text(value.error.message, `${channel}连接尚未就绪`),
90
93
  } : null,
@@ -109,6 +112,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
109
112
  };
110
113
 
111
114
  const presentError = (error) => ({
115
+ ...diagnosticFields(error),
112
116
  code: text(error?.code, `${channel.toUpperCase()}_ERROR`, 80),
113
117
  message: text(error?.message, `${channel}操作失败,请稍后重试`),
114
118
  });
@@ -1,3 +1,4 @@
1
+ import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
1
2
  import { BotName } from '../../bot-alias.js';
2
3
  import * as React from 'react';
3
4
 
@@ -82,7 +83,7 @@ export function createTokenChannelSettings(definition) {
82
83
  accountSettingsEndpoint = null,
83
84
  } = definition;
84
85
 
85
- function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAliasSave, onModelSave, onAgentPresetSave, onContextEnhancementSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
86
+ function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAliasSave, onModelSave, onAgentPresetSave, onContextEnhancementSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove, rpcCall, reload }) {
86
87
  const state = busy === 'reconnect' ? 'connecting' : account.state;
87
88
  const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
88
89
  const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
@@ -144,6 +145,11 @@ export function createTokenChannelSettings(definition) {
144
145
  account,
145
146
  busy: Boolean(busy),
146
147
  onSave: onAccountSettingsSave,
148
+ // Channels with extra settings panels (email session binding) call
149
+ // their own endpoints through the same RPC bridge.
150
+ rpcCall,
151
+ endpoints,
152
+ onChanged: reload,
147
153
  }) : null,
148
154
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
149
155
  h('div', { className: 'dim-cardFooterLayout' },
@@ -160,6 +166,7 @@ export function createTokenChannelSettings(definition) {
160
166
  disabled: Boolean(busy),
161
167
  }, '移除接入')),
162
168
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
169
+ account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
163
170
  account.lastMessageError ? h(LastMessageErrorSummary, {
164
171
  className: 'ddt-summary',
165
172
  error: account.lastMessageError,
@@ -180,6 +187,7 @@ export function createTokenChannelSettings(definition) {
180
187
  }
181
188
 
182
189
  function SettingsTab({ rpcCall }) {
190
+ const [operationError, setOperationError] = React.useState(null);
183
191
  const [model, setModel] = React.useState({
184
192
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
185
193
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
@@ -207,7 +215,16 @@ export function createTokenChannelSettings(definition) {
207
215
 
208
216
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
209
217
  if (typeof rpcCall !== 'function') throw new TypeError(`${channel} 设置页缺少 RPC 连接`);
210
- return api.unwrapRpcResult(await rpcCall(endpoint, payload, signal));
218
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
219
+ if (operation && mounted.current) setOperationError(null);
220
+ try {
221
+ const value = api.unwrapRpcResult(await rpcCall(endpoint, payload, signal));
222
+ if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
223
+ return value;
224
+ } catch (error) {
225
+ if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
226
+ throw error;
227
+ }
211
228
  }, [rpcCall]);
212
229
 
213
230
  const loadStatus = React.useCallback(async ({ signal, silent = false } = {}) => {
@@ -331,6 +348,8 @@ export function createTokenChannelSettings(definition) {
331
348
  h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
332
349
  h('li', { key: account.botId }, h(AccountCard, {
333
350
  account,
351
+ rpcCall,
352
+ reload: loadStatus,
334
353
  busy: busyByBot[account.botId],
335
354
  testNotice: testNoticeByBot[account.botId],
336
355
  removing: removeTarget === account.botId,
@@ -398,6 +417,7 @@ export function createTokenChannelSettings(definition) {
398
417
  className: `ddt-page ${pageClass} dim-channelPage`,
399
418
  'aria-label': `${channel} 设置`,
400
419
  },
420
+ operationError ? h(ConnectionError, { error: operationError }) : null,
401
421
  h('div', { className: 'ddt-heading' },
402
422
  h('div', { className: 'ddt-tools' },
403
423
  h('div', { className: 'dim-bindActions' },
@@ -422,7 +442,7 @@ export function createTokenChannelSettings(definition) {
422
442
  ? h('div', { className: 'ddt-card dim-surfaceCard' },
423
443
  h('div', { className: 'ddt-inlineError dim-inlineError' },
424
444
  h('h3', null, `无法读取 ${channel} 机器人状态`),
425
- h('p', null, model.error?.message),
445
+ h(ConnectionError, { error: model.error }),
426
446
  h(Button, { onClick: () => void loadStatus() }, '重新读取')))
427
447
  : h(React.Fragment, null,
428
448
  credentialOpen ? (CredentialPanel
@@ -433,6 +453,11 @@ export function createTokenChannelSettings(definition) {
433
453
  error: credentialError,
434
454
  onSubmit: bindCredentials,
435
455
  onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
456
+ // A transport that authorizes out of band (the Agent mailbox
457
+ // shows a QR code) drives its own endpoints through the bridge.
458
+ rpcCall,
459
+ endpoints,
460
+ onAuthorized: bindCredentials,
436
461
  })
437
462
  : h(CredentialBindingPanel, {
438
463
  channel,
@@ -1,3 +1,4 @@
1
+ import { ConnectionError } from '../../connection-error.js';
1
2
  import * as React from 'react';
2
3
 
3
4
  import { SlackLogoGlyph } from '../../channel-logos.js';
@@ -89,7 +90,7 @@ export function SlackCredentialPanel({ busy, error, onSubmit, onCancel }) {
89
90
  required: true,
90
91
  })),
91
92
  h('p', { className: 'dsl-tokenHint' }, 'Bot Token 来自 OAuth & Permissions;App Token 来自 Basic Information,并且必须包含 connections:write。')),
92
- error ? h('p', { className: 'dim-credentialError', role: 'alert' }, error.message ?? String(error)) : null,
93
+ error ? h(ConnectionError, { error: error }) : null,
93
94
  h('div', { className: 'ddt-actions dim-viewActions dim-credentialActions' },
94
95
  h('button', {
95
96
  type: 'submit',
@@ -1,3 +1,5 @@
1
+ import { normalizeConnectionError as normalizeTestError } from '../../connection-error.js';
2
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
1
3
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
4
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
3
5
  import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
@@ -53,7 +55,7 @@ function normalizeTestMessage(value) {
53
55
  const code = value.code === 'test-target-unavailable'
54
56
  ? 'test-target-unavailable'
55
57
  : 'test-message-failed';
56
- return { sent: false, code };
58
+ return { sent: false, code, ...(value.error ? { error: normalizeTestError(value.error) } : {}) };
57
59
  }
58
60
 
59
61
  export function unwrapRpcResult(result) {
@@ -61,6 +63,7 @@ export function unwrapRpcResult(result) {
61
63
  if (!result.ok) {
62
64
  const error = new Error(text(result.error?.message, '企业微信操作失败'));
63
65
  error.code = text(result.error?.code, 'WECOM_RPC_ERROR', 80);
66
+ Object.assign(error, diagnosticFields(result.error));
64
67
  throw error;
65
68
  }
66
69
  return result.value;
@@ -88,6 +91,7 @@ export function normalizeProvisioning(value, now = Date.now()) {
88
91
  if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
89
92
  if (id(source.botId)) result.botId = id(source.botId);
90
93
  if (isRecord(source.error)) result.error = {
94
+ ...diagnosticFields(source.error),
91
95
  code: text(source.error.code, 'WECOM_PROVISION_FAILED', 80),
92
96
  message: text(source.error.message, '企业微信机器人没有接入完成'),
93
97
  };
@@ -120,6 +124,7 @@ function normalizeBot(value) {
120
124
  },
121
125
  lastMessageError: normalizeLastMessageError(value.lastMessageError),
122
126
  error: isRecord(value.error) ? {
127
+ ...diagnosticFields(value.error),
123
128
  code: text(value.error.code, 'WECOM_ACCOUNT_ERROR', 80),
124
129
  message: text(value.error.message, '企业微信连接尚未就绪'),
125
130
  } : null,
@@ -143,6 +148,7 @@ export function normalizeSnapshot(value) {
143
148
 
144
149
  export function presentError(error) {
145
150
  return {
151
+ ...diagnosticFields(error),
146
152
  code: text(error?.code, 'WECOM_ERROR', 80),
147
153
  message: text(error?.message, '企业微信操作失败,请稍后重试'),
148
154
  };
@@ -1,3 +1,4 @@
1
+ import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
1
2
  import { BotName } from '../../bot-alias.js';
2
3
  import * as React from 'react';
3
4
 
@@ -150,7 +151,7 @@ function ProvisionView({ provision, busy, onRetry, onClose }) {
150
151
  return h('div', { className: 'ddt-card dim-surfaceCard' },
151
152
  h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
152
153
  h('h3', null, '企业微信机器人没有绑定完成'),
153
- h('p', null, error.message),
154
+ h(ConnectionError, { error: error }),
154
155
  h('span', { className: 'ddt-errorCode' }, error.code),
155
156
  h('div', { className: 'ddt-actions dim-viewActions' },
156
157
  h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
@@ -241,6 +242,7 @@ export function AccountCard({
241
242
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
242
243
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
243
244
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
245
+ account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
244
246
  account.lastMessageError ? h(LastMessageErrorSummary, {
245
247
  className: 'ddt-summary',
246
248
  error: account.lastMessageError,
@@ -258,6 +260,7 @@ export function AccountCard({
258
260
  }
259
261
 
260
262
  export function WecomSettingsTab({ rpcCall }) {
263
+ const [operationError, setOperationError] = React.useState(null);
261
264
  const [model, setModel] = React.useState({
262
265
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
263
266
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
@@ -309,7 +312,16 @@ export function WecomSettingsTab({ rpcCall }) {
309
312
 
310
313
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
311
314
  if (typeof rpcCall !== 'function') throw new TypeError('企业微信设置页缺少 RPC 连接');
312
- return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
315
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
316
+ if (operation && mounted.current) setOperationError(null);
317
+ try {
318
+ const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
319
+ if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
320
+ return value;
321
+ } catch (error) {
322
+ if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
323
+ throw error;
324
+ }
313
325
  }, [rpcCall]);
314
326
 
315
327
  const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
@@ -589,6 +601,7 @@ export function WecomSettingsTab({ rpcCall }) {
589
601
  }, h(AgentPresetCatalogContext.Provider, {
590
602
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
591
603
  }, h('section', { className: 'ddt-page dwecom-page dim-channelPage', 'aria-label': '企业微信设置' },
604
+ operationError ? h(ConnectionError, { error: operationError }) : null,
592
605
  h(Heading, {
593
606
  totals: model.totals,
594
607
  adding: Boolean(provision),
@@ -601,7 +614,7 @@ export function WecomSettingsTab({ rpcCall }) {
601
614
  h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
602
615
  model.phase === 'loading' ? h(LoadingView)
603
616
  : model.phase === 'error'
604
- ? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信机器人状态'), h('p', null, model.error?.message), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
617
+ ? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信机器人状态'), h(ConnectionError, { error: model.error }), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
605
618
  : h(React.Fragment, null,
606
619
  credentialView,
607
620
  provisionView,
@@ -1,3 +1,5 @@
1
+ import { normalizeConnectionError as normalizeTestError } from '../../connection-error.js';
2
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
1
3
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
4
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
3
5
  import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
@@ -55,7 +57,7 @@ function normalizeTestMessage(value) {
55
57
  const code = value.code === 'test-target-unavailable'
56
58
  ? 'test-target-unavailable'
57
59
  : 'test-message-failed';
58
- return { sent: false, code };
60
+ return { sent: false, code, ...(value.error ? { error: normalizeTestError(value.error) } : {}) };
59
61
  }
60
62
 
61
63
  export function unwrapRpcResult(result) {
@@ -63,6 +65,7 @@ export function unwrapRpcResult(result) {
63
65
  if (!result.ok) {
64
66
  const error = new Error(text(result.error?.message, '企业微信应用操作失败'));
65
67
  error.code = text(result.error?.code, 'WECOM_APP_RPC_ERROR', 80);
68
+ Object.assign(error, diagnosticFields(result.error));
66
69
  throw error;
67
70
  }
68
71
  return result.value;
@@ -99,6 +102,7 @@ function normalizeBot(value) {
99
102
  },
100
103
  lastMessageError: normalizeLastMessageError(value.lastMessageError),
101
104
  error: isRecord(value.error) ? {
105
+ ...diagnosticFields(value.error),
102
106
  code: text(value.error.code, 'WECOM_APP_ACCOUNT_ERROR', 80),
103
107
  message: text(value.error.message, '企业微信应用连接尚未就绪'),
104
108
  } : null,
@@ -121,6 +125,7 @@ export function normalizeSnapshot(value) {
121
125
 
122
126
  export function presentError(error) {
123
127
  return {
128
+ ...diagnosticFields(error),
124
129
  code: text(error?.code, 'WECOM_APP_ERROR', 80),
125
130
  message: text(error?.message, '企业微信应用操作失败,请稍后重试'),
126
131
  };