@xmanrui/dsh-im 4.22.0 → 4.24.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 (148) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +5 -4
  3. package/lib/client.js +2584 -1153
  4. package/lib/index.js +290 -298
  5. package/package.json +14 -2
  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 +23 -7
  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 +8 -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/telegram/index.js +3 -0
  24. package/plugin-src/client/channels/telegram/styles.js +12 -0
  25. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  26. package/plugin-src/client/channels/wecom/api.js +7 -1
  27. package/plugin-src/client/channels/wecom/index.js +16 -3
  28. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  29. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  30. package/plugin-src/client/channels/weixin/api.js +2 -1
  31. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  32. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  33. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  34. package/plugin-src/client/connection-error.js +87 -0
  35. package/plugin-src/client/global-settings.js +81 -1
  36. package/plugin-src/client/i18n.js +69 -1
  37. package/plugin-src/client/index.js +40 -4
  38. package/plugin-src/client/last-message-error.js +2 -1
  39. package/plugin-src/client/styles.js +8 -2
  40. package/plugin-src/host/build.mjs +4 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  43. package/plugin-src/host/channels/email/availability.mjs +60 -0
  44. package/plugin-src/host/channels/email/index.mjs +36 -0
  45. package/plugin-src/host/channels/email/production.mjs +58 -0
  46. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  47. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  48. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  49. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  50. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  51. package/plugin-src/host/channels/qq/production.mjs +2 -0
  52. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  53. package/plugin-src/host/channels/shared/production.mjs +52 -5
  54. package/plugin-src/host/channels/shared/rpc.mjs +14 -3
  55. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  56. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  57. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  58. package/plugin-src/host/channels/slack/production.mjs +2 -0
  59. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  61. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  63. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  64. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  65. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  66. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  67. package/plugin-src/host/delivery-adapter.mjs +11 -0
  68. package/plugin-src/host/image-input-rpc.mjs +24 -0
  69. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  70. package/plugin-src/host/index.mjs +3 -0
  71. package/plugin-src/host/modern-harness-api.mjs +7 -2
  72. package/plugin-src/management-rpc.mjs +12 -2
  73. package/scripts/verify-package.mjs +13 -5
  74. package/src/channels/dingtalk/connection-error.mjs +5 -12
  75. package/src/channels/dingtalk/device-auth.mjs +4 -1
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  77. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  78. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  79. package/src/channels/discord/discord-api.mjs +3 -3
  80. package/src/channels/discord/discord-runtime.mjs +18 -12
  81. package/src/channels/email/config-store.mjs +194 -0
  82. package/src/channels/email/email-api.mjs +20 -0
  83. package/src/channels/email/email-bridge.mjs +17 -0
  84. package/src/channels/email/email-controller.mjs +824 -0
  85. package/src/channels/email/email-runtime.mjs +699 -0
  86. package/src/channels/email/harness-client.mjs +7 -0
  87. package/src/channels/email/mail-format.mjs +91 -0
  88. package/src/channels/email/state-store.mjs +176 -0
  89. package/src/channels/email/transport.mjs +64 -0
  90. package/src/channels/email/transports/agent-mail.mjs +604 -0
  91. package/src/channels/email/transports/agently-cli.mjs +288 -0
  92. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  93. package/src/channels/feishu/bridge.mjs +39 -11
  94. package/src/channels/feishu/feishu-channel.mjs +35 -0
  95. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  96. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  97. package/src/channels/feishu/live-cot.mjs +260 -0
  98. package/src/channels/feishu/message-utils.mjs +3 -2
  99. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  100. package/src/channels/feishu/registration-manager.mjs +8 -4
  101. package/src/channels/feishu/repair-manager.mjs +2 -1
  102. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  103. package/src/channels/feishu/step-push-mode.mjs +10 -4
  104. package/src/channels/imessage/runtime.mjs +9 -5
  105. package/src/channels/office/office-controller.mjs +23 -12
  106. package/src/channels/office/office-runtime.mjs +6 -3
  107. package/src/channels/office/office-transport.mjs +1 -0
  108. package/src/channels/qq/qq-bridge.mjs +7 -5
  109. package/src/channels/qq/qq-controller.mjs +56 -42
  110. package/src/channels/qq/qq-runtime.mjs +16 -11
  111. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  112. package/src/channels/shared/connection-error.mjs +191 -0
  113. package/src/channels/shared/connection-test.mjs +3 -1
  114. package/src/channels/shared/conversation-state-store.mjs +46 -1
  115. package/src/channels/shared/diagnostic-details.mjs +95 -0
  116. package/src/channels/shared/harness-client.mjs +287 -57
  117. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  118. package/src/channels/shared/i18n-en/email.mjs +25 -0
  119. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  120. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  121. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  122. package/src/channels/shared/i18n-en.mjs +6 -0
  123. package/src/channels/shared/image-input-policy.mjs +35 -0
  124. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  125. package/src/channels/shared/image-input.mjs +124 -0
  126. package/src/channels/shared/image-prompt.mjs +15 -7
  127. package/src/channels/shared/message-failure.mjs +12 -0
  128. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  129. package/src/channels/shared/text-harness-bridge.mjs +150 -25
  130. package/src/channels/shared/token-bot-controller.mjs +52 -47
  131. package/src/channels/slack/slack-api.mjs +3 -3
  132. package/src/channels/slack/slack-controller.mjs +59 -51
  133. package/src/channels/slack/slack-runtime.mjs +18 -12
  134. package/src/channels/telegram/config-store.mjs +4 -1
  135. package/src/channels/telegram/telegram-api.mjs +2 -2
  136. package/src/channels/telegram/telegram-controller.mjs +13 -1
  137. package/src/channels/telegram/telegram-runtime.mjs +215 -14
  138. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  139. package/src/channels/wecom/wecom-controller.mjs +51 -37
  140. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  141. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  142. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  143. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  144. package/src/channels/weixin/connection-error.mjs +26 -92
  145. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  146. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  147. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  148. 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 },
@@ -465,7 +466,7 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
465
466
  );
466
467
  }
467
468
 
468
- /** One select for the step-push presentation: off / per-step posts / process card. */
469
+ /** One select for the step-push presentation. */
469
470
  function StepPushEditor({ value = false, mode = "post", disabled = false, onSave, onModeSave }) {
470
471
  const titleId = React.useId();
471
472
  const helpId = `${titleId}-help`;
@@ -495,7 +496,7 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
495
496
  if (value === true) await onSave?.(false);
496
497
  return;
497
498
  }
498
- const nextMode = next === "streaming_card" ? "streaming_card" : "post";
499
+ const nextMode = ["streaming_card", "live_cot"].includes(next) ? next : "post";
499
500
  // Enabling (or switching presentation) may need both writes; the flag
500
501
  // must land before the mode so the runtime never sees a mode without
501
502
  // step push enabled.
@@ -506,7 +507,9 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
506
507
 
507
508
  const helpText = current === "off"
508
509
  ? "适合日常问答:执行过程中不显示工具调用等中间步骤,只回复最终结果"
509
- : current === "streaming_card"
510
+ : current === "live_cot"
511
+ ? "使用飞书原生思考过程展示推理、工具调用与结果,最终答案单独发送"
512
+ : current === "streaming_card"
510
513
  ? "推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏"
511
514
  : "每一步都单独发一条消息(含工具调用和过程说明);注意长任务会连续发送较多消息";
512
515
 
@@ -528,7 +531,7 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
528
531
  id: helpId,
529
532
  className: "dim-presetTooltip",
530
533
  role: "tooltip",
531
- }, "设置任务执行过程的呈现方式:不显示、实时卡片或逐步消息"))),
534
+ }, "设置任务执行过程的呈现方式:不显示、原生直播、实时卡片或逐步消息"))),
532
535
  saving
533
536
  ? h("span", { className: "dim-feishuGroupControlStatus", role: "status" }, "保存中…")
534
537
  : null),
@@ -540,6 +543,7 @@ function StepPushEditor({ value = false, mode = "post", disabled = false, onSave
540
543
  onChange: change,
541
544
  },
542
545
  h("option", { value: "off" }, "不显示过程(只发送最终答案)"),
546
+ h("option", { value: "live_cot" }, "实时直播(飞书原生思考过程)"),
543
547
  h("option", { value: "streaming_card" }, "实时过程卡(全程一张卡片动态更新)"),
544
548
  h("option", { value: "post" }, "逐步直播(每一步单独发一条消息)")),
545
549
  h("p", { className: "dim-feishuGroupHelp" }, helpText),
@@ -700,6 +704,7 @@ export function BotCard({
700
704
  }, "移除接入")),
701
705
  summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
702
706
  summary) : null,
707
+ (actionError || connection.error) ? h(ConnectionError, { error: actionError || connection.error, showMessage: false }) : null,
703
708
  connection.lastMessageError ? h(LastMessageErrorSummary, {
704
709
  className: "bxf-healthSummary",
705
710
  error: connection.lastMessageError,
@@ -770,7 +775,7 @@ function PageError({ error, onRetry, busy }) {
770
775
  h("div", { className: "bxf-error dim-inlineError", role: "alert" },
771
776
  h("div", null,
772
777
  h("h3", null, "无法读取飞书机器人"),
773
- h("p", null, error.message),
778
+ h(ConnectionError, { error: error }),
774
779
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
775
780
  h("div", { className: "bxf-actions dim-viewActions" },
776
781
  h(Button, { kind: "primary", onClick: onRetry, disabled: busy },
@@ -812,6 +817,7 @@ export function mergeFeishuSnapshotState(
812
817
  }
813
818
 
814
819
  export function FeishuSettingsTab({ rpcCall }) {
820
+ const [operationError, setOperationError] = React.useState(null);
815
821
  const [model, setModel] = React.useState({
816
822
  phase: "loading",
817
823
  revision: 0,
@@ -857,7 +863,16 @@ export function FeishuSettingsTab({ rpcCall }) {
857
863
  }, [scheduleAnimationFrame]);
858
864
 
859
865
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
860
- return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
866
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
867
+ if (operation && mountedRef.current) setOperationError(null);
868
+ try {
869
+ const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
870
+ if (operation && mountedRef.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
871
+ return value;
872
+ } catch (error) {
873
+ if (endpoint === 'bot.delete' && mountedRef.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
874
+ throw error;
875
+ }
861
876
  }, [rpcCall]);
862
877
 
863
878
  const mergeSnapshot = React.useCallback((snapshot, { restoreProvisioning = false } = {}) => {
@@ -1490,6 +1505,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1490
1505
  }, h(AgentPresetCatalogContext.Provider, {
1491
1506
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
1492
1507
  }, h("section", { className: "bxf-page dim-channelPage", "aria-label": "飞书机器人设置" },
1508
+ operationError ? h(ConnectionError, { error: operationError }) : null,
1493
1509
  h(Heading, {
1494
1510
  totals: model.totals,
1495
1511
  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';
@@ -37,6 +38,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
37
38
  setContextEnhancement: 'bot.context-enhancement.set',
38
39
  setAccessPolicy: 'bot.access-policy.set',
39
40
  setAlias: 'bot.alias.set',
41
+ setThinkingTraces: 'bot.thinking-traces.set',
40
42
  });
41
43
 
42
44
  export function createTokenChannelApi(channel, connectionSummary, {
@@ -49,7 +51,8 @@ export function createTokenChannelApi(channel, connectionSummary, {
49
51
  if (!result.ok) {
50
52
  const error = new Error(text(result.error?.message, `${channel} 操作失败`));
51
53
  error.code = text(result.error?.code, `${channel.toUpperCase()}_RPC_ERROR`, 80);
52
- throw error;
54
+ Object.assign(error, diagnosticFields(result.error));
55
+ throw error;
53
56
  }
54
57
  return result.value;
55
58
  };
@@ -67,6 +70,8 @@ export function createTokenChannelApi(channel, connectionSummary, {
67
70
  model: normalizeModelSelection(value.model),
68
71
  agentPreset: normalizeAgentPresetId(value.agentPreset),
69
72
  contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
73
+ // Absent from the backend means ON; only an explicit false opts out.
74
+ thinkingTraces: value.thinkingTraces !== false,
70
75
  ...(Object.hasOwn(value, 'accessPolicy')
71
76
  ? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
72
77
  : {}),
@@ -85,6 +90,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
85
90
  },
86
91
  lastMessageError: normalizeLastMessageError(value.lastMessageError),
87
92
  error: isRecord(value.error) ? {
93
+ ...diagnosticFields(value.error),
88
94
  code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
89
95
  message: text(value.error.message, `${channel}连接尚未就绪`),
90
96
  } : null,
@@ -109,6 +115,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
109
115
  };
110
116
 
111
117
  const presentError = (error) => ({
118
+ ...diagnosticFields(error),
112
119
  code: text(error?.code, `${channel.toUpperCase()}_ERROR`, 80),
113
120
  message: text(error?.message, `${channel}操作失败,请稍后重试`),
114
121
  });
@@ -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',
@@ -4,6 +4,7 @@ import {
4
4
  TELEGRAM_ENDPOINTS,
5
5
  telegramClientApi,
6
6
  } from './api.js';
7
+ import { ThinkingTracesSettings } from './thinking-traces.js';
7
8
  import { installTelegramStyles } from './styles.js';
8
9
 
9
10
  const channel = createTokenChannelSettings({
@@ -19,6 +20,8 @@ const channel = createTokenChannelSettings({
19
20
  emptyTitle: '接入 Telegram 机器人',
20
21
  emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
21
22
  platformLabel: 'Telegram',
23
+ AccountSettings: ThinkingTracesSettings,
24
+ accountSettingsEndpoint: 'bot.thinking-traces.set',
22
25
  });
23
26
 
24
27
  export const TelegramSettingsTab = channel.SettingsTab;
@@ -4,6 +4,18 @@ const CSS = String.raw`
4
4
  .dtg-page { --ddt-accent: #229ed9; --ddt-accent-deep: #1687bd; --ddt-accent-wash: #eaf7fd; }
5
5
  .dtg-avatar { color: #fff; background: #229ed9; }
6
6
  .dtg-avatar svg { display: block; }
7
+ .dim-accountSettings { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l2, #eceef1); }
8
+ .dim-accountSettingsTitle { margin: 0 0 8px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #646a73); letter-spacing: 0.02em; }
9
+ .dim-accountSettingsRow { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 40px; cursor: pointer; }
10
+ .dim-accountSettingsText { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
11
+ .dim-accountSettingsLabel { font-size: 13px; font-weight: 500; color: var(--dsw-alias-label, #1f2329); }
12
+ .dim-accountSettingsHelp { font-size: 12px; color: var(--dsw-alias-label-tertiary, #8a919f); }
13
+ .dim-accountSettingsSwitch { appearance: none; flex: none; width: 32px; height: 19px; margin: 0; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; background: var(--dsw-alias-interactive-bg-hover, #eef0f3); cursor: pointer; position: relative; }
14
+ .dim-accountSettingsSwitch::before { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: var(--dsw-alias-label-secondary, #646a73); transition: transform 120ms ease, background 120ms ease; }
15
+ .dim-accountSettingsSwitch:checked { border-color: #229ed9; background: #229ed9; }
16
+ .dim-accountSettingsSwitch:checked::before { transform: translateX(13px); background: #fff; }
17
+ .dim-accountSettingsSwitch:disabled { opacity: 0.5; cursor: not-allowed; }
18
+ @media (prefers-reduced-motion: reduce) { .dim-accountSettingsSwitch::before { transition: none; } }
7
19
  `;
8
20
 
9
21
  export function installTelegramStyles() {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 会话行为设置块:思考过程留痕开关(Telegram 专属)。
3
+ *
4
+ * 开启后,机器人把 agent 的推理与工具调用作为独立的 💭/🔧 消息实时发出来,
5
+ * 最终答案单独成条;关闭则回到「占位符被最终答案覆盖」的原有单消息行为。
6
+ */
7
+ import { h } from '../../i18n.js';
8
+
9
+ export function ThinkingTracesSettings({ account, busy, onSave }) {
10
+ const checked = account.thinkingTraces !== false;
11
+ return h('div', { className: 'dim-accountSettings' },
12
+ h('h4', { className: 'dim-accountSettingsTitle' }, '会话行为'),
13
+ h('label', { className: 'dim-accountSettingsRow' },
14
+ h('span', { className: 'dim-accountSettingsText' },
15
+ h('span', { className: 'dim-accountSettingsLabel' }, '思考过程留痕'),
16
+ h('span', { className: 'dim-accountSettingsHelp' },
17
+ '实时显示思考步骤与工具调用,最终答案单独成条')),
18
+ h('input', {
19
+ type: 'checkbox',
20
+ className: 'dim-accountSettingsSwitch',
21
+ checked,
22
+ disabled: Boolean(busy),
23
+ onChange: (event) => { onSave?.({ thinkingTraces: event.target.checked }); },
24
+ })));
25
+ }
@@ -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
  };