@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
@@ -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
 
@@ -159,7 +160,7 @@ function BindForm({ busy, error, onSubmit, onCancel }) {
159
160
  disabled: busy,
160
161
  }, streamEnabled ? '流式回复:开' : '流式回复:关'),
161
162
  h('span', { className: 'dim-switchHint' }, '企业微信客户端实时出字;微信端不支持时自动改为整段发送'))),
162
- error ? h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' }, h('p', null, error.message)) : null,
163
+ error ? h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' }, h(ConnectionError, { error: error })) : null,
163
164
  h('div', { className: 'ddt-actions dim-viewActions' },
164
165
  h(Button, { kind: 'primary', onClick: () => formRef.current?.requestSubmit(), disabled: busy }, busy ? '正在绑定…' : '保存并连接'),
165
166
  h(Button, { kind: 'quiet', onClick: onCancel, disabled: busy }, '取消'))));
@@ -327,6 +328,7 @@ export function AccountCard({
327
328
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
328
329
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
329
330
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
331
+ account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
330
332
  account.lastMessageError ? h(LastMessageErrorSummary, {
331
333
  className: 'ddt-summary',
332
334
  error: account.lastMessageError,
@@ -344,6 +346,7 @@ export function AccountCard({
344
346
  }
345
347
 
346
348
  export function WecomAppSettingsTab({ rpcCall }) {
349
+ const [operationError, setOperationError] = React.useState(null);
347
350
  const [model, setModel] = React.useState({
348
351
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
349
352
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
@@ -393,7 +396,16 @@ export function WecomAppSettingsTab({ rpcCall }) {
393
396
 
394
397
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
395
398
  if (typeof rpcCall !== 'function') throw new TypeError('企业微信应用设置页缺少 RPC 连接');
396
- return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
399
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
400
+ if (operation && mounted.current) setOperationError(null);
401
+ try {
402
+ const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
403
+ if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
404
+ return value;
405
+ } catch (error) {
406
+ if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
407
+ throw error;
408
+ }
397
409
  }, [rpcCall]);
398
410
 
399
411
  const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
@@ -601,6 +613,7 @@ export function WecomAppSettingsTab({ rpcCall }) {
601
613
  }, h(AgentPresetCatalogContext.Provider, {
602
614
  value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
603
615
  }, h('section', { className: 'ddt-page dwecomapp-page dim-channelPage', 'aria-label': '企业微信应用设置' },
616
+ operationError ? h(ConnectionError, { error: operationError }) : null,
604
617
  h(Heading, {
605
618
  totals: model.totals,
606
619
  adding: bindOpen,
@@ -611,7 +624,7 @@ export function WecomAppSettingsTab({ rpcCall }) {
611
624
  h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
612
625
  model.phase === 'loading' ? h(LoadingView)
613
626
  : model.phase === 'error'
614
- ? 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() }, '重新读取')))
627
+ ? 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() }, '重新读取')))
615
628
  : h(React.Fragment, null,
616
629
  bindView,
617
630
  model.bots.length === 0 && !bindOpen
@@ -1,3 +1,4 @@
1
+ import { normalizeConnectionError as normalizeTestError } from '../../connection-error.js';
1
2
  import { normalizeWeixinDiagnosticDetails } from '../../../../src/channels/weixin/diagnostic-details.mjs';
2
3
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
3
4
  import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
@@ -55,7 +56,7 @@ function normalizeTestMessage(value) {
55
56
  const code = value.code === 'test-target-unavailable'
56
57
  ? 'test-target-unavailable'
57
58
  : 'test-message-failed';
58
- return { sent: false, code };
59
+ return { sent: false, code, ...(value.error ? { error: normalizeTestError(value.error) } : {}) };
59
60
  }
60
61
 
61
62
  export function normalizeConnectionError(value, fallbackCode = 'WEIXIN_ERROR', fallbackMessage = '微信操作失败,请稍后重试') {
@@ -1,71 +1 @@
1
- import * as React from 'react';
2
- import { h, localizeText } from '../../i18n.js';
3
- import { normalizeConnectionError } from './api.js';
4
- import { CONFIG_ISSUE_LABELS } from '../../../../src/channels/weixin/diagnostic-details.mjs';
5
-
6
- const STAGE_LABELS = {
7
- 'startup.load': '加载微信配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
8
- 'qr.verify': '提交配对码', 'qr.cancel': '取消绑定', 'credential.read': '读取登录凭据', 'credential.save': '保存登录凭据',
9
- 'credential.remove': '移除登录凭据', 'account.save': '保存账号配置', 'account.remove': '移除账号配置',
10
- 'state.load': '读取账号状态', 'state.write': '保存账号状态', 'state.cleanup': '清理账号状态',
11
- 'workspace.write': '保存工作区设置', 'workspace.cleanup': '清理工作区设置', 'runtime.prepare': '准备消息连接',
12
- activation: '激活微信账号', 'harness.check': '检查 DSH 宿主', 'connection.start': '启动微信连接',
13
- 'connection.poll': '同步微信消息', 'connection.stop': '停止微信连接', 'status.read': '读取连接状态',
14
- rollback: '恢复原账号状态', 'management.request': '访问 DSH 管理接口',
15
- };
16
-
17
- export function provisioningErrorTitle(error) {
18
- const stage = error?.details?.stage;
19
- if (stage === 'qr.begin' || stage === 'qr.encode') return '无法生成微信二维码';
20
- if (stage === 'qr.poll') return '查询微信扫码状态失败';
21
- if (stage === 'management.request') return '无法完成微信管理请求';
22
- return '微信没有绑定完成';
23
- }
24
-
25
- export function formatWeixinDiagnostic(value) {
26
- const error = normalizeConnectionError(value);
27
- const details = error.details ?? {};
28
- return [
29
- localizeText(error.message), details.hint ? localizeText(details.hint) : null,
30
- localizeText('错误码') + ': ' + error.code,
31
- ...['operation', 'stage', 'reason', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
32
- .filter(field => details[field] !== undefined)
33
- .map(field => `${field}: ${details[field]}`),
34
- ].filter(Boolean).join('\n');
35
- }
36
-
37
- export function WeixinConnectionError({ error: value, warning = false }) {
38
- const error = normalizeConnectionError(value);
39
- const details = error.details ?? {};
40
- const [copyState, setCopyState] = React.useState(null);
41
- React.useEffect(() => setCopyState(null), [details.referenceId, error.code, error.message]);
42
- const copy = async () => {
43
- try {
44
- if (typeof globalThis.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable');
45
- await globalThis.navigator.clipboard.writeText(formatWeixinDiagnostic(error));
46
- setCopyState('copied');
47
- } catch { setCopyState('manual'); }
48
- };
49
- const fields = [
50
- ['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
51
- ['底层原因', details.reason], ['HTTP 状态', details.httpStatus], ['微信返回码', details.providerCode],
52
- ['配置文件', details.file], ['配置字段', details.field],
53
- ['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
54
- ['参考号', details.referenceId], ['发生时间', details.occurredAt], ['插件版本', details.pluginVersion],
55
- ].filter(([, text]) => text !== undefined && text !== '');
56
- return h('div', { className: 'dxw-summary dim-cardSummary', 'data-weixin-diagnostic': true, role: warning ? 'status' : undefined },
57
- h('p', null, error.message),
58
- details.hint ? h('p', null, details.hint) : null,
59
- h('details', { style: { marginTop: 8 } },
60
- h('summary', { style: { cursor: 'pointer' } }, '诊断详情'),
61
- h('dl', { style: { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)', gap: '4px 12px', margin: '10px 0' } },
62
- ...fields.flatMap(([label, text]) => [h('dt', { key: `${label}-label` }, label),
63
- React.createElement('dd', { key: label, style: { margin: 0, overflowWrap: 'anywhere' } }, String(text))])),
64
- !details.referenceId ? h('p', null, '未取得 Host 诊断参考号。') : null,
65
- h('div', { className: 'dim-viewActions' },
66
- h('button', { type: 'button', className: 'dxw-button', onClick: copy }, copyState === 'copied' ? '诊断信息已复制' : '复制诊断信息')),
67
- copyState === 'manual' ? h('div', null,
68
- h('p', null, '无法访问剪贴板,请选择并复制以下诊断信息。'),
69
- React.createElement('textarea', { readOnly: true, value: formatWeixinDiagnostic(error), rows: 7,
70
- 'aria-label': localizeText('诊断信息'), style: { width: '100%', boxSizing: 'border-box', marginTop: 8 } })) : null));
71
- }
1
+ export { ConnectionError as WeixinConnectionError, formatConnectionDiagnostic as formatWeixinDiagnostic, provisioningErrorTitle } from '../../connection-error.js';
@@ -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';
@@ -52,6 +53,7 @@ export function unwrapRpcResult(result) {
52
53
  if (!result.ok) {
53
54
  const error = new Error(text(result.error?.message, 'WhatsApp 操作失败'));
54
55
  error.code = text(result.error?.code, 'WHATSAPP_RPC_ERROR', 80);
56
+ Object.assign(error, diagnosticFields(result.error));
55
57
  throw error;
56
58
  }
57
59
  return result.value;
@@ -79,6 +81,7 @@ export function normalizeProvisioning(value, now = Date.now()) {
79
81
  if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
80
82
  if (id(source.botId)) result.botId = id(source.botId);
81
83
  if (isRecord(source.error)) result.error = {
84
+ ...diagnosticFields(source.error),
82
85
  code: text(source.error.code, 'WHATSAPP_PROVISION_FAILED', 80),
83
86
  message: text(source.error.message, 'WhatsApp 没有接入完成'),
84
87
  };
@@ -112,6 +115,7 @@ function normalizeBot(value) {
112
115
  },
113
116
  lastMessageError: normalizeLastMessageError(value.lastMessageError),
114
117
  error: isRecord(value.error) ? {
118
+ ...diagnosticFields(value.error),
115
119
  code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
116
120
  message: text(value.error.message, 'WhatsApp 连接尚未就绪'),
117
121
  } : null,
@@ -136,6 +140,7 @@ export function normalizeSnapshot(value) {
136
140
 
137
141
  export function presentError(error) {
138
142
  return {
143
+ ...diagnosticFields(error),
139
144
  code: text(error?.code, 'WHATSAPP_ERROR', 80),
140
145
  message: text(error?.message, 'WhatsApp 操作失败,请稍后重试'),
141
146
  };
@@ -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
 
@@ -168,7 +169,7 @@ export function ProvisionView({ provision, busy, onRetry, onClose }) {
168
169
  return h('div', { className: 'ddt-card dim-surfaceCard' },
169
170
  h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
170
171
  h('h3', null, 'WhatsApp 没有接入完成'),
171
- h('p', null, error.message),
172
+ h(ConnectionError, { error: error }),
172
173
  h('span', { className: 'ddt-errorCode' }, error.code),
173
174
  h('div', { className: 'ddt-actions dim-viewActions' },
174
175
  h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
@@ -268,6 +269,7 @@ export function WhatsappAccountCard({
268
269
  className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
269
270
  }, '移除接入')),
270
271
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
272
+ account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
271
273
  account.lastMessageError ? h(LastMessageErrorSummary, {
272
274
  className: 'ddt-summary',
273
275
  error: account.lastMessageError,
@@ -287,6 +289,7 @@ export function WhatsappAccountCard({
287
289
  }
288
290
 
289
291
  export function WhatsappSettingsTab({ rpcCall }) {
292
+ const [operationError, setOperationError] = React.useState(null);
290
293
  const [model, setModel] = React.useState({
291
294
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
292
295
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
@@ -315,7 +318,16 @@ export function WhatsappSettingsTab({ rpcCall }) {
315
318
 
316
319
  const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
317
320
  if (typeof rpcCall !== 'function') throw new TypeError('WhatsApp 设置页缺少 RPC 连接');
318
- return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
321
+ const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
322
+ if (operation && mounted.current) setOperationError(null);
323
+ try {
324
+ const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
325
+ if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
326
+ return value;
327
+ } catch (error) {
328
+ if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
329
+ throw error;
330
+ }
319
331
  }, [rpcCall]);
320
332
 
321
333
  const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
@@ -558,6 +570,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
558
570
  className: 'ddt-page dwa-page dim-channelPage',
559
571
  'aria-label': 'WhatsApp 设置',
560
572
  },
573
+ operationError ? h(ConnectionError, { error: operationError }) : null,
561
574
  h(Heading, {
562
575
  totals: model.totals,
563
576
  busy,
@@ -570,7 +583,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
570
583
  ? h('div', { className: 'ddt-card dim-surfaceCard' },
571
584
  h('div', { className: 'ddt-inlineError dim-inlineError' },
572
585
  h('h3', null, '无法读取 WhatsApp 机器人状态'),
573
- h('p', null, model.error?.message),
586
+ h(ConnectionError, { error: model.error }),
574
587
  h(Button, { onClick: () => void loadStatus() }, '重新读取')))
575
588
  : h(React.Fragment, null,
576
589
  provision?.status === 'pending'
@@ -0,0 +1,87 @@
1
+ import * as React from 'react';
2
+ import { h, localizeText } from './i18n.js';
3
+ import { normalizeDiagnosticDetails } from '../../src/channels/shared/diagnostic-details.mjs';
4
+ import { CONFIG_ISSUE_LABELS } from '../../src/channels/shared/diagnostic-details.mjs';
5
+
6
+ const STAGE_LABELS = {
7
+ 'credential.verify': '验证凭据', 'permission.check': '检查权限', 'sdk.load': '加载渠道组件',
8
+ operation: '执行渠道操作', 'message.receive': '接收消息', 'message.send': '发送消息', 'connection.test': '发送测试消息',
9
+ 'startup.load': '加载渠道配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
10
+ 'qr.verify': '提交配对码', 'qr.cancel': '取消绑定', 'credential.read': '读取登录凭据', 'credential.save': '保存登录凭据',
11
+ 'credential.remove': '移除登录凭据', 'account.save': '保存账号配置', 'account.remove': '移除账号配置',
12
+ 'state.load': '读取账号状态', 'state.write': '保存账号状态', 'state.cleanup': '清理账号状态',
13
+ 'workspace.write': '保存工作区设置', 'workspace.cleanup': '清理工作区设置', 'runtime.prepare': '准备消息连接',
14
+ activation: '激活账号', 'harness.check': '检查 DSH 宿主', 'connection.start': '启动连接',
15
+ 'connection.poll': '监测连接与消息', 'connection.stop': '停止连接', 'status.read': '读取连接状态',
16
+ rollback: '恢复原账号状态', 'management.request': '访问 DSH 管理接口',
17
+ };
18
+
19
+ export function provisioningErrorTitle(error) {
20
+ const stage = error?.details?.stage;
21
+ if (stage === 'qr.begin' || stage === 'qr.encode') return '无法生成微信二维码';
22
+ if (stage === 'qr.poll') return '查询微信扫码状态失败';
23
+ if (stage === 'management.request') return '无法完成微信管理请求';
24
+ return '微信没有绑定完成';
25
+ }
26
+
27
+ export function formatConnectionDiagnostic(value) {
28
+ const error = normalizeConnectionError(value);
29
+ const details = error.details ?? {};
30
+ return [
31
+ localizeText(error.message), details.hint ? localizeText(details.hint) : null,
32
+ localizeText('错误码') + ': ' + error.code,
33
+ ...['channel', 'operation', 'stage', 'reason', 'reasons', 'host', 'durationMs', 'timeoutMs', 'nodeVersion', 'dshVersion', 'platform', 'dependencies', 'proxyConfigured', 'truncated', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
34
+ .filter(field => details[field] !== undefined)
35
+ .map(field => `${field}: ${typeof details[field] === 'object' ? JSON.stringify(details[field]) : details[field]}`),
36
+ ].filter(Boolean).join('\n');
37
+ }
38
+
39
+ export function ConnectionError({ error: value, warning = false, showMessage = true }) {
40
+ const error = normalizeConnectionError(value);
41
+ const details = error.details ?? {};
42
+ const isWarning = warning || error.code.endsWith('cleanup-failed');
43
+ const [copyState, setCopyState] = React.useState(null);
44
+ React.useEffect(() => setCopyState(null), [details.referenceId, error.code, error.message]);
45
+ const copy = async () => {
46
+ try {
47
+ if (typeof globalThis.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable');
48
+ await globalThis.navigator.clipboard.writeText(formatConnectionDiagnostic(error));
49
+ setCopyState('copied');
50
+ } catch { setCopyState('manual'); }
51
+ };
52
+ const fields = [
53
+ ['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
54
+ ['底层原因', details.reason === 'unknown' ? localizeText('暂未识别') : details.reason], ['多个原因', details.reasons?.join(', ')], ['服务域名', details.host], ['请求耗时(毫秒)', details.durationMs], ['超时阈值(毫秒)', details.timeoutMs], ['Node 版本', details.nodeVersion], ['DSH 版本', details.dshVersion], ['依赖版本', details.dependencies ? Object.entries(details.dependencies).map(([name, version]) => `${name}: ${version}`).join(', ') : undefined], ['代理已配置', details.proxyConfigured], ['原因链已截断', details.truncated], ['运行平台', details.platform], ['HTTP 状态', details.httpStatus], ['服务返回码', details.providerCode],
55
+ ['配置文件', details.file], ['配置字段', details.field],
56
+ ['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
57
+ ['参考号', details.referenceId], ['发生时间', details.occurredAt], ['插件版本', details.pluginVersion],
58
+ ].filter(([, text]) => text !== undefined && text !== '');
59
+ return h('div', { className: 'dim-connectionDiagnostic', 'data-connection-diagnostic': true, 'data-weixin-diagnostic': details.channel === 'weixin' || /^WX-CONN/.test(details.referenceId ?? '') || undefined, role: isWarning ? 'status' : undefined, 'data-warning': isWarning || undefined },
60
+ showMessage ? h('p', null, error.message) : null,
61
+ details.hint ? h('p', null, details.hint) : null,
62
+ h('details', { style: { marginTop: 8 } },
63
+ h('summary', { style: { cursor: 'pointer' } }, '诊断详情'),
64
+ h('dl', { style: { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)', gap: '4px 12px', margin: '10px 0' } },
65
+ ...fields.flatMap(([label, text]) => [h('dt', { key: `${label}-label` }, label),
66
+ React.createElement('dd', { key: label, style: { margin: 0, overflowWrap: 'anywhere' } }, String(text))])),
67
+ !details.referenceId ? h('p', null, '未取得 Host 诊断参考号。') : null,
68
+ h('div', { className: 'dim-viewActions' },
69
+ h('button', { type: 'button', className: 'dxw-button', onClick: copy }, copyState === 'copied' ? '诊断信息已复制' : '复制诊断信息')),
70
+ copyState === 'manual' ? h('div', null,
71
+ h('p', null, '无法访问剪贴板,请选择并复制以下诊断信息。'),
72
+ React.createElement('textarea', { readOnly: true, value: formatConnectionDiagnostic(error), rows: 7,
73
+ 'aria-label': localizeText('诊断信息'), style: { width: '100%', boxSizing: 'border-box', marginTop: 8 } })) : null));
74
+ }
75
+
76
+ export function normalizeConnectionError(value, fallbackCode = 'operation-failed', fallbackMessage = '操作失败,请查看原因和诊断详情。') {
77
+ const source = value && typeof value === 'object' ? value : {};
78
+ const text = (v, fallback, n) => typeof v === 'string' && v.trim() ? v.trim().slice(0, n) : fallback;
79
+ const details = normalizeDiagnosticDetails({ ...source.details,
80
+ referenceId: source.details?.referenceId ?? source.referenceId,
81
+ hint: source.details?.hint ?? source.hint,
82
+ });
83
+ return { code: text(source.code, fallbackCode, 100), message: text(source.message, fallbackMessage, 500),
84
+ ...(Object.keys(details).length ? { details } : {}),
85
+ ...(source.hint ? { hint: details.hint } : {}), ...(source.referenceId ? { referenceId: details.referenceId } : {}),
86
+ };
87
+ }
@@ -6,6 +6,7 @@ import {
6
6
  normalizeInboundTtlHours,
7
7
  } from '../../src/channels/shared/inbound-ttl.mjs';
8
8
  import { h } from './i18n.js';
9
+ import { DEFAULT_IMAGE_INPUT_SETTINGS, normalizeImageInputSettings } from '../../src/channels/shared/image-input-policy.mjs';
9
10
 
10
11
  export const GLOBAL_SETTINGS_RPC_CHANNEL = '/dsh-im-settings';
11
12
 
@@ -18,6 +19,8 @@ export const GLOBAL_SETTINGS_ENDPOINTS = Object.freeze({
18
19
  getTtl: 'settings.inbound-ttl.get',
19
20
  setTtl: 'settings.inbound-ttl.set',
20
21
  sweep: 'settings.inbound-ttl.sweep',
22
+ getImages: 'settings.image-input.get',
23
+ setImages: 'settings.image-input.set',
21
24
  });
22
25
 
23
26
  function presentError(error, fallback) {
@@ -56,6 +59,82 @@ function GlobalButton({ children, kind = 'secondary', className = '', ...props }
56
59
  }, children);
57
60
  }
58
61
 
62
+ export function ImageInputSettings({ rpcCall }) {
63
+ const [values, setValues] = React.useState(DEFAULT_IMAGE_INPUT_SETTINGS);
64
+ const [phase, setPhase] = React.useState('loading');
65
+ const [error, setError] = React.useState(null);
66
+ const [saved, setSaved] = React.useState(false);
67
+ const mounted = React.useRef(false);
68
+ const saving = React.useRef(false);
69
+ const id = React.useId();
70
+ const load = React.useCallback(async (signal) => {
71
+ setPhase('loading');
72
+ setError(null);
73
+ try {
74
+ const result = unwrapRpcResult(await rpcCall(GLOBAL_SETTINGS_ENDPOINTS.getImages, {}, signal));
75
+ if (signal?.aborted || !mounted.current) return;
76
+ const settings = normalizeImageInputSettings(result);
77
+ if (!settings) throw new Error('通用设置返回了无法识别的响应。');
78
+ setValues(settings);
79
+ setPhase('ready');
80
+ } catch (caught) {
81
+ if (signal?.aborted || !mounted.current) return;
82
+ setError(presentError(caught, '无法读取或保存图片设置,请稍后重试。'));
83
+ setPhase('error');
84
+ }
85
+ }, [rpcCall]);
86
+ React.useEffect(() => {
87
+ mounted.current = true;
88
+ const controller = new AbortController();
89
+ void load(controller.signal);
90
+ return () => { mounted.current = false; controller.abort(); };
91
+ }, [load]);
92
+ const save = async (event) => {
93
+ event.preventDefault();
94
+ if (saving.current || phase !== 'ready') return;
95
+ const settings = normalizeImageInputSettings(values);
96
+ setSaved(false);
97
+ if (!settings) {
98
+ setError('图片限制无效:请输入正整数,原图接收上限和总量上限不能小于单图上限。');
99
+ return;
100
+ }
101
+ saving.current = true;
102
+ setPhase('saving');
103
+ setError(null);
104
+ try {
105
+ const result = unwrapRpcResult(await rpcCall(GLOBAL_SETTINGS_ENDPOINTS.setImages, settings));
106
+ if (!normalizeImageInputSettings(result)) throw new Error('通用设置返回了无法识别的响应。');
107
+ if (mounted.current) { setValues(result); setSaved(true); }
108
+ } catch (caught) {
109
+ if (mounted.current) setError(presentError(caught, '无法读取或保存图片设置,请稍后重试。'));
110
+ } finally {
111
+ saving.current = false;
112
+ if (mounted.current) setPhase('ready');
113
+ }
114
+ };
115
+ const fields = [
116
+ ['maxDownloadMb', '原图接收上限 (MB)'],
117
+ ['maxImageMb', '模型单图上限 (MB)'],
118
+ ['maxTotalMb', '模型图片总量上限 (MB)'],
119
+ ['maxImages', '每条消息最多图片数'],
120
+ ];
121
+ return h('section', { className: 'dim-globalSection', 'aria-label': '图片输入', 'aria-busy': phase === 'loading' || phase === 'saving' },
122
+ h('h3', null, '图片输入'),
123
+ h('p', null, '适用于支持图片的聊天渠道。原图按附件保留时长保存,发送给模型的副本会自动缩放或压缩;无法直接发送时交给模型按文件处理。'),
124
+ h('form', { onSubmit: save },
125
+ ...fields.map(([key, label]) => h('div', { className: 'dim-globalTtlRow', key },
126
+ h('label', { htmlFor: `${id}-${key}` }, label),
127
+ h('input', { id: `${id}-${key}`, className: 'dim-globalTtlInput', type: 'number', min: 1, step: 1,
128
+ value: values[key], disabled: phase !== 'ready',
129
+ onChange: (event) => { setValues((current) => ({ ...current, [key]: event.target.value })); setSaved(false); setError(null); },
130
+ }))),
131
+ h(GlobalButton, { type: 'submit', kind: 'primary', disabled: phase !== 'ready' }, phase === 'saving' ? '保存中…' : '保存图片设置'),
132
+ phase === 'error' ? h(GlobalButton, { onClick: () => void load() }, '重试') : null),
133
+ error ? h('p', { role: 'alert' }, error) : null,
134
+ saved ? h('p', { role: 'status' }, '已保存') : null,
135
+ phase === 'loading' ? h('p', { role: 'status' }, '正在读取图片设置…') : null);
136
+ }
137
+
59
138
  export function GlobalSettingsPanel({ rpcCall }) {
60
139
  const [phase, setPhase] = React.useState('loading');
61
140
  const [loadError, setLoadError] = React.useState(null);
@@ -337,5 +416,6 @@ export function GlobalSettingsPanel({ rpcCall }) {
337
416
  role: inlineStatus.role,
338
417
  'aria-live': inlineStatus.role === 'status' ? 'polite' : undefined,
339
418
  }, inlineStatus.message)
340
- : null))));
419
+ : null)),
420
+ h(ImageInputSettings, { rpcCall })));
341
421
  }
@@ -1,3 +1,5 @@
1
+ import imageInput from '../../src/channels/shared/i18n-en/image-input.mjs';
2
+ import diagnostics from '../../src/channels/shared/i18n-en/diagnostics.mjs';
1
3
  import weixinDiagnostics from '../../src/channels/weixin/connection-error.en.mjs';
2
4
  import * as React from 'react';
3
5
 
@@ -5,6 +7,8 @@ export const IM_LOCALE_NAMESPACE = 'dsh-im';
5
7
 
6
8
  const EN = Object.freeze({
7
9
  ...weixinDiagnostics,
10
+ ...diagnostics,
11
+ ...imageInput,
8
12
  '$locale': 'en',
9
13
  '修改别名': 'Edit alias',
10
14
  '关闭修改别名': 'Close alias editor',
@@ -1005,6 +1009,63 @@ const EN = Object.freeze({
1005
1009
  '企业微信应用操作失败,请稍后重试': 'The WeCom app operation failed; try again later',
1006
1010
  '企业微信应用服务返回了无法识别的响应': 'The WeCom app service returned an unrecognized response',
1007
1011
  '企业微信应用服务没有返回有效的机器人列表': 'The WeCom app service did not return a valid bot list',
1012
+ '邮箱': 'Email',
1013
+ ' IMAP/SMTP 邮箱': ' IMAP/SMTP mailbox',
1014
+ 'QQ 邮箱': 'QQ Mail',
1015
+ '163 邮箱': '163 Mail',
1016
+ '自定义服务器': 'Custom servers',
1017
+ '在 QQ 邮箱「设置 → 账户」中开启 IMAP/SMTP 服务,并生成 16 位授权码(不是登录密码)。': 'In QQ Mail open Settings → Account, enable IMAP/SMTP, and generate a 16-character authorization code (not your login password).',
1018
+ '在 163 邮箱「设置 → POP3/SMTP/IMAP」中开启服务,并新增授权密码。': 'In 163 Mail open Settings → POP3/SMTP/IMAP, enable the service, and add an authorization password.',
1019
+ '需要先开启两步验证,再生成「应用专用密码」。': 'Enable two-step verification first, then generate an app-specific password.',
1020
+ '请填写邮箱服务商提供的 IMAP 与 SMTP 服务器地址及端口。': 'Enter the IMAP and SMTP host and port provided by your mail provider.',
1021
+ '接入邮箱': 'Connect a mailbox',
1022
+ 'DeepSeek Harness 会读取该邮箱的新邮件作为指令,并在同一邮件线程内回复处理结果。': 'DeepSeek Harness reads new mail in this mailbox as instructions and replies with the result inside the same mail thread.',
1023
+ '邮箱服务商': 'Mail provider',
1024
+ '邮箱地址': 'Email address',
1025
+ '应用密码 / 授权码': 'App password / authorization code',
1026
+ 'IMAP/SMTP 授权码': 'IMAP/SMTP authorization code',
1027
+ '不是邮箱登录密码;请在邮箱设置中单独生成。': 'This is not your login password; generate it separately in your mailbox settings.',
1028
+ 'IMAP 服务器': 'IMAP server',
1029
+ '端口': 'Port',
1030
+ 'SMTP 服务器': 'SMTP server',
1031
+ '允许的发件人': 'Allowed senders',
1032
+ 'me@example.com\\n同事@example.com': 'me@example.com\\ncolleague@example.com',
1033
+ '必填。只有这些地址发来的邮件会触发 Harness;多个地址用换行或逗号分隔。': 'Required. Only mail from these addresses triggers the Harness; separate multiple addresses with newlines or commas.',
1034
+ '正在连接邮箱…': 'Connecting the mailbox…',
1035
+ '连接邮箱': 'Connect mailbox',
1036
+ '保存后会重新连接邮箱以使设置立即生效。': 'Saving reconnects the mailbox so the change takes effect immediately.',
1037
+ '使用现有邮箱收发指令:邮件进来触发 Harness,处理结果以回信形式送达。': 'Use an existing mailbox to send instructions: incoming mail triggers the Harness and the result is delivered as a reply.',
1038
+ '配置邮箱收发': 'Configure email send and receive',
1039
+ '配置邮箱': 'Configure mailbox',
1040
+ '邮箱配置': 'Mailbox configuration',
1041
+ '会话绑定': 'Session binding',
1042
+ '会话绑定已保存。': 'Session binding saved.',
1043
+ '不绑定则每封新邮件开启一个新会话;绑定固定会话后,来信都在该会话内继续。': 'With no binding every new mail starts a new session; with a fixed session, incoming mail continues in that session.',
1044
+ '固定会话(账号级)': 'Fixed session (account level)',
1045
+ '不绑定(每封新邮件新建会话)': 'No binding (new session per mail)',
1046
+ '按发件人覆盖(优先于账号级)': 'Per-sender override (takes priority)',
1047
+ '跟随账号级': 'Follow the account setting',
1048
+ '尚无可覆盖的发件人(先在上方配置允许的发件人)。': 'No senders to override yet (configure allowed senders above first).',
1049
+ '保存绑定': 'Save binding',
1050
+ '清除绑定': 'Clear binding',
1051
+ '所有发件人同上': 'Point every sender at the same session',
1052
+ '已选:': 'Selected: ',
1053
+ '接入方式': 'Access method',
1054
+ '生成授权链接': 'Generate authorization link',
1055
+ '重新生成': 'Regenerate',
1056
+ '授权链接': 'Authorization link',
1057
+ '配对码': 'Pairing code',
1058
+ '等待授权中…': 'Waiting for authorization…',
1059
+ '等待授权中,': 'Waiting for authorization — valid for ',
1060
+ ' 分钟': ' minutes',
1061
+ '内有效,可保持本页打开。': '; you can keep this page open.',
1062
+ '授权成功,正在接入邮箱…': 'Authorized; connecting the mailbox…',
1063
+ '授权成功;请填写邮箱地址与允许的发件人后完成接入。': 'Authorized. Enter the mailbox address and at least one allowed sender to finish connecting.',
1064
+ '授权后自动填入': 'Filled in after authorization',
1065
+ '由授权结果自动填入,无需手工填写。': 'Filled in from the authorization; no need to type it.',
1066
+ '刷新会话列表': 'Refresh the session list',
1067
+ '腾讯 Agent 邮箱需要微信扫码授权。点下面的按钮生成授权链接,在打开的页面里用微信扫码登录并确认。': 'The Tencent Agent mailbox authorizes with a WeChat QR code. Generate an authorization link, then scan it with WeChat on the page that opens and confirm.',
1068
+ '请求失败': 'The request failed',
1008
1069
  });
1009
1070
 
1010
1071
  export const en = EN;