@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
@@ -1,3 +1,4 @@
1
+ import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
1
2
  import { unlink } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
@@ -107,6 +108,7 @@ export async function createProductionController(ctx, config = {}, internals = {
107
108
  ...(controlExecutor ? { controlExecutor } : {}),
108
109
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
109
110
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
111
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
110
112
  });
111
113
  const modelCatalog = () => listModelCatalog(harness);
112
114
  const coreController = new Controller({
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
2
  import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
2
3
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
4
  import QRCode from 'qrcode';
@@ -131,6 +132,7 @@ async function publicStatus(status, encodeQr) {
131
132
  }
132
133
 
133
134
  export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
135
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'wecom' });
134
136
  for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
135
137
  if (typeof controller?.[method] !== 'function') {
136
138
  throw new TypeError(`A complete Enterprise WeChat controller is required (${method})`);
@@ -185,7 +187,7 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
185
187
  await controller.sendConnectionTest(payload.botId);
186
188
  testMessage = publicConnectionTestResult();
187
189
  } catch (error) {
188
- testMessage = publicConnectionTestResult(error);
190
+ testMessage = publicConnectionTestResult(error, { diagnostics, botId: payload.botId });
189
191
  }
190
192
  }
191
193
  }
@@ -231,10 +233,10 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
231
233
  : { ok: true, value };
232
234
  } catch (error) {
233
235
  const workspaceError = publicWorkspaceError(error);
234
- return signal?.aborted
236
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted
235
237
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
236
238
  : { ok: false, error: workspaceError
237
- ?? { code: 'wecom-operation-failed', message: '企业微信操作失败,请稍后重试。' } };
239
+ ?? { code: 'wecom-operation-failed', message: '企业微信操作失败,请稍后重试。' } }, { operation: endpoint, botId: payload?.botId });
238
240
  }
239
241
  };
240
242
  }
@@ -1,3 +1,4 @@
1
+ import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
1
2
  import { unlink } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
@@ -141,6 +142,7 @@ export async function createProductionController(ctx, config = {}, internals = {
141
142
  ...(controlExecutor ? { controlExecutor } : {}),
142
143
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
143
144
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
145
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
144
146
  });
145
147
  const modelCatalog = () => listModelCatalog(harness);
146
148
  const coreController = new Controller({
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
2
  import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
2
3
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
4
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
@@ -136,6 +137,7 @@ function publicStatus(status) {
136
137
  }
137
138
 
138
139
  export function createWecomAppRpcHandler(controller) {
140
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'wecom-app' });
139
141
  for (const method of ['status', 'bindApp', 'updateAppSettings', 'resetCallbackSecret', 'reconnectBot', 'deleteBot']) {
140
142
  if (typeof controller?.[method] !== 'function') {
141
143
  throw new TypeError(`A complete Enterprise WeChat app controller is required (${method})`);
@@ -178,7 +180,7 @@ export function createWecomAppRpcHandler(controller) {
178
180
  await controller.sendConnectionTest(payload.botId);
179
181
  testMessage = publicConnectionTestResult();
180
182
  } catch (error) {
181
- testMessage = publicConnectionTestResult(error);
183
+ testMessage = publicConnectionTestResult(error, { diagnostics, botId: payload.botId });
182
184
  }
183
185
  }
184
186
  }
@@ -215,11 +217,11 @@ export function createWecomAppRpcHandler(controller) {
215
217
  : { ok: true, value };
216
218
  } catch (error) {
217
219
  const workspaceError = publicWorkspaceError(error);
218
- return signal?.aborted
220
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted
219
221
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } }
220
222
  : { ok: false, error: workspaceError
221
223
  ? { ...workspaceError, details: {} }
222
- : { code: 'wecom-app-operation-failed', message: '企业微信应用操作失败,请稍后重试。', details: {} } };
224
+ : { code: 'wecom-app-operation-failed', message: '企业微信应用操作失败,请稍后重试。', details: {} } }, { operation: endpoint, botId: payload?.botId });
223
225
  }
224
226
  };
225
227
  }
@@ -1,3 +1,4 @@
1
+ import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
1
2
  import { unlink } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
@@ -139,6 +140,7 @@ export async function createProductionController(ctx, config = {}, internals = {
139
140
  ...(controlExecutor ? { controlExecutor } : {}),
140
141
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
141
142
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
143
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
142
144
  });
143
145
  const modelCatalog = () => listModelCatalog(harness);
144
146
  const coreController = new Controller({
@@ -1,3 +1,4 @@
1
+ import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
1
2
  import { rm, unlink } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
@@ -113,6 +114,7 @@ export async function createProductionController(ctx, config = {}, internals = {
113
114
  ...(controlExecutor ? { controlExecutor } : {}),
114
115
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
115
116
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
117
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
116
118
  });
117
119
  const modelCatalog = () => listModelCatalog(harness);
118
120
  const coreController = new Controller({
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
2
  import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
2
3
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
4
  import QRCode from 'qrcode';
@@ -116,6 +117,7 @@ async function publicStatus(value, encodeQr) {
116
117
  }
117
118
 
118
119
  export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
120
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'whatsapp' });
119
121
  for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot']) {
120
122
  if (typeof controller?.[method] !== 'function') {
121
123
  throw new TypeError(`A complete WhatsApp controller is required (${method})`);
@@ -176,7 +178,7 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
176
178
  testError = error;
177
179
  }
178
180
  }
179
- value = { ...value, testMessage: publicConnectionTestResult(testError) };
181
+ value = { ...value, testMessage: publicConnectionTestResult(testError, { diagnostics, botId: payload.botId }) };
180
182
  }
181
183
  } else if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
182
184
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
@@ -223,10 +225,10 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
223
225
  : { ok: true, value };
224
226
  } catch (error) {
225
227
  const workspaceError = publicWorkspaceError(error);
226
- return signal?.aborted
228
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted
227
229
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
228
230
  : { ok: false, error: workspaceError
229
- ?? { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' } };
231
+ ?? { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' } }, { operation: endpoint, botId: payload?.botId });
230
232
  }
231
233
  };
232
234
  }
@@ -16,6 +16,7 @@ const CHANNELS = new Set([
16
16
  'discord',
17
17
  'whatsapp',
18
18
  'imessage',
19
+ 'email',
19
20
  ]);
20
21
 
21
22
  export function supportsDeliveryChannel(channel) {
@@ -126,6 +127,16 @@ function normalizeRoute(channel, kind, route) {
126
127
  case 'imessage':
127
128
  oneOf(kind, ['user']);
128
129
  return routeWithStrings(route, ['chatGuid']);
130
+ case 'email': {
131
+ oneOf(kind, ['user']);
132
+ const normalized = routeWithStrings(route, ['address']);
133
+ // Mail addresses are normalized to lower case everywhere so a target
134
+ // created from a mixed-case address still matches.
135
+ if (!/^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(normalized.address)) {
136
+ throw invalidTarget('route.address must be a valid email address');
137
+ }
138
+ return { address: normalized.address.toLowerCase() };
139
+ }
129
140
  default:
130
141
  throw new TypeError(`Unsupported delivery channel: ${channel}`);
131
142
  }
@@ -0,0 +1,24 @@
1
+ import { normalizeImageInputSettings } from '../../src/channels/shared/image-input-policy.mjs';
2
+
3
+ export const IMAGE_INPUT_ENDPOINTS = Object.freeze({
4
+ get: 'settings.image-input.get',
5
+ set: 'settings.image-input.set',
6
+ });
7
+
8
+ export function createImageInputRpcHandler(store) {
9
+ return async (endpoint, payload, signal) => {
10
+ const object = payload && typeof payload === 'object' && !Array.isArray(payload);
11
+ const valid = object && (endpoint === IMAGE_INPUT_ENDPOINTS.get
12
+ ? Object.keys(payload).length === 0
13
+ : endpoint === IMAGE_INPUT_ENDPOINTS.set && Object.keys(payload).length === 4
14
+ && normalizeImageInputSettings(payload));
15
+ if (!valid) return { ok: false, error: { code: 'bad-request', message: '图片限制无效:请输入正整数,原图接收上限和总量上限不能小于单图上限。' } };
16
+ if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'Request cancelled.' } };
17
+ try {
18
+ const value = endpoint === IMAGE_INPUT_ENDPOINTS.get ? await store.get() : await store.set(payload);
19
+ return { ok: true, value };
20
+ } catch {
21
+ return { ok: false, error: { code: 'image-input-settings-unavailable', message: '无法读取或保存图片设置,请稍后重试。' } };
22
+ }
23
+ };
24
+ }
@@ -1,6 +1,8 @@
1
1
  import { registerManagementRpc } from '../management-rpc.mjs';
2
2
  import { normalizeInboundTtlHours } from '../../src/channels/shared/inbound-ttl.mjs';
3
3
  import { getInboundTtlRuntime } from './inbound-ttl-runtime.mjs';
4
+ import { getImageInputSettingsStore } from '../../src/channels/shared/image-input-settings-store.mjs';
5
+ import { createImageInputRpcHandler, IMAGE_INPUT_ENDPOINTS } from './image-input-rpc.mjs';
4
6
 
5
7
  export const INBOUND_TTL_RPC_CHANNEL = '/dsh-im-settings';
6
8
  export const INBOUND_TTL_ENDPOINTS = Object.freeze({
@@ -73,9 +75,12 @@ export function installInboundTtlRpc(ctx, options = {}) {
73
75
  const runtime = options.runtime ?? getInboundTtlRuntime(ctx, options.config);
74
76
  const logger = typeof ctx?.logger === 'function'
75
77
  ? ctx.logger('dsh-im:inbound-ttl') : (ctx?.logger ?? null);
78
+ const ttl = createInboundTtlRpcHandler({ ...runtime, logger });
79
+ const image = createImageInputRpcHandler(getImageInputSettingsStore(options.config));
76
80
  return registerManagementRpc(ctx,
77
81
  INBOUND_TTL_RPC_CHANNEL,
78
- createInboundTtlRpcHandler({ ...runtime, logger }),
82
+ (endpoint, payload, signal) => Object.values(IMAGE_INPUT_ENDPOINTS).includes(endpoint)
83
+ ? image(endpoint, payload, signal) : ttl(endpoint, payload, signal),
79
84
  { authority: 'loopback' },
80
85
  );
81
86
  }
@@ -10,6 +10,7 @@ import { apply as applyWecomApp } from './channels/wecom-app/index.mjs';
10
10
  import { apply as applyWeixin } from './channels/weixin/index.mjs';
11
11
  import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
12
12
  import { apply as applyIMessage } from './channels/imessage/index.mjs';
13
+ import { apply as applyEmail } from './channels/email/index.mjs';
13
14
  import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
14
15
  import { installHostLanguage } from './host-language.mjs';
15
16
  import { installHostLanguageRpc } from './host-language-rpc.mjs';
@@ -60,6 +61,7 @@ export function createImHostPlugin(internals = {}) {
60
61
  const startOffice = internals.applyOffice ?? applyOffice;
61
62
  const startWhatsapp = internals.applyWhatsapp ?? applyWhatsapp;
62
63
  const startIMessage = internals.applyIMessage ?? applyIMessage;
64
+ const startEmail = internals.applyEmail ?? applyEmail;
63
65
  const channels = [
64
66
  ['feishu', startFeishu],
65
67
  ['weixin', startWeixin],
@@ -72,6 +74,7 @@ export function createImHostPlugin(internals = {}) {
72
74
  ['discord', startDiscord],
73
75
  ['whatsapp', startWhatsapp],
74
76
  ['imessage', startIMessage],
77
+ ['email', startEmail],
75
78
  ['office', startOffice],
76
79
  ];
77
80
  return Object.freeze({
@@ -382,7 +382,7 @@ class ModernHarnessApi {
382
382
  return;
383
383
  }
384
384
  if (frame.type !== 'chunk' || frame.index !== stream.entries.length) return;
385
- stream.entries.push({
385
+ const entry = {
386
386
  type: 'transient',
387
387
  event: {
388
388
  type: 'assistant/chunk',
@@ -392,7 +392,12 @@ class ModernHarnessApi {
392
392
  time: frame.time,
393
393
  data: { turn: stream.turn, step: stream.step, chunk: frame.chunk },
394
394
  },
395
- });
395
+ };
396
+ stream.entries.push(entry);
397
+ // History only exposes these transient chunks while the attempt is open.
398
+ // Broadcast each one as it arrives so live IM renderers cannot miss a
399
+ // short reasoning phase that starts and commits between two history polls.
400
+ this.#broadcast({ type: 'session/event', sessionId, event: entry.event });
396
401
  }
397
402
 
398
403
  #withAssistantStream(sessionId, entries) {
@@ -72,6 +72,16 @@ export function registerManagementRpc(ctx, channel, handler, { authority } = {})
72
72
  }
73
73
 
74
74
  /** Keep channel UI callers unchanged while using DSH's native correlation and response validation. */
75
- export function callManagementRpc(connection, channel, method, payload, signal) {
76
- return connection.rpc.call('/api', rpcEndpoint(channel), { method, payload }, signal);
75
+ export async function callManagementRpc(connection, channel, method, payload, signal) {
76
+ try {
77
+ return await connection.rpc.call('/api', rpcEndpoint(channel), { method, payload }, signal);
78
+ } catch (cause) {
79
+ if (signal?.aborted || cause?.name === 'AbortError') throw cause;
80
+ const error = new Error('无法访问 DSH 管理接口,请检查 DSH 连接或重新登录后重试。', { cause });
81
+ error.code = 'management-unreachable';
82
+ error.details = { operation: method, stage: 'management.request', occurredAt: new Date().toISOString(),
83
+ ...(Number.isInteger(cause?.status) && cause.status >= 100 && cause.status <= 599 ? { httpStatus: cause.status } : {}),
84
+ };
85
+ throw error;
86
+ }
77
87
  }
@@ -142,20 +142,28 @@ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.
142
142
  throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
143
143
  }
144
144
  // Connections still have no channel-enable toggle. Checkable inputs are owned
145
- // only by the shared context editor and the saved-target Session sync row.
145
+ // only by the shared context editor, the saved-target Session sync row, the
146
+ // email settings, and the Telegram thinking-traces block.
146
147
  // The context editor contains one switch template and one mapped field-input
147
- // template; the delivery target adds one ordinary checkbox template.
148
+ // template; the delivery target adds one ordinary checkbox template; the
149
+ // Telegram thinking-traces block adds one ordinary checkbox template.
148
150
  const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
149
151
  const deliverySettingsSource = await readFile(resolve(root, 'plugin-src/client/delivery-settings.js'), 'utf8');
152
+ const emailSettingsSource = await readFile(resolve(root, 'plugin-src/client/channels/email/index.js'), 'utf8');
153
+ const thinkingTracesSource = await readFile(resolve(root, 'plugin-src/client/channels/telegram/thinking-traces.js'), 'utf8');
150
154
  const otherClientSources = clientSources
151
155
  .replace(contextEditorSource, '')
152
- .replace(deliverySettingsSource, '');
156
+ .replace(deliverySettingsSource, '')
157
+ .replace(emailSettingsSource, '')
158
+ .replace(thinkingTracesSource, '');
153
159
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
154
160
  || (deliverySettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
155
161
  || /role:\s*["']switch["']/u.test(deliverySettingsSource)
162
+ || (thinkingTracesSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
163
+ || /role:\s*["']switch["']/u.test(thinkingTracesSource)
156
164
  || (client.match(/role:\s*["']switch["']/g) ?? []).length !== 1
157
- || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
158
- throw new Error('checkable inputs must be limited to context enhancement and Session sync');
165
+ || (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 4) {
166
+ throw new Error('checkable inputs must be limited to context enhancement, Session sync, email settings, and the Telegram thinking-traces toggle');
159
167
  }
160
168
  for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
161
169
  if (!host.includes(marker) || !client.includes(marker)) {
@@ -1,3 +1,4 @@
1
+ import { connectionErrorChain, connectionStageError } from '../shared/connection-error.mjs';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { createRequire } from 'node:module';
3
4
 
@@ -62,17 +63,7 @@ export function installedDingtalkConnectionDependencies() {
62
63
  return installedDependencyVersions;
63
64
  }
64
65
 
65
- function errorChain(error) {
66
- const chain = [];
67
- const seen = new Set();
68
- let current = error;
69
- while (current && typeof current === 'object' && chain.length < 4 && !seen.has(current)) {
70
- seen.add(current);
71
- chain.push(current);
72
- current = current.cause;
73
- }
74
- return chain;
75
- }
66
+ function errorChain(error) { return connectionErrorChain(error).chain; }
76
67
 
77
68
  function statusFrom(error) {
78
69
  if (Number.isInteger(error?.status)) return error.status;
@@ -153,7 +144,9 @@ export function dingtalkRuntimeStartError(code, cause) {
153
144
  );
154
145
  error.name = 'DingtalkRuntimeStartError';
155
146
  error.code = STAGE_CODES.has(code) ? code : 'dingtalk-runtime-prepare-failed';
156
- return error;
147
+ return connectionStageError(error, ({ 'dingtalk-harness-connect-failed': 'harness.check',
148
+ 'dingtalk-runtime-prepare-failed': 'runtime.prepare', 'dingtalk-stream-client-load-failed': 'sdk.load',
149
+ 'dingtalk-stream-listener-failed': 'runtime.prepare', 'dingtalk-stream-connect-failed': 'connection.start' })[error.code]);
157
150
  }
158
151
 
159
152
  /** Creates browser-safe guidance plus a redacted Host-log diagnostic for one connection failure. */
@@ -68,6 +68,8 @@ export class DingtalkDeviceAuthError extends Error {
68
68
  this.name = 'DingtalkDeviceAuthError';
69
69
  this.code = code;
70
70
  this.action = action;
71
+ if (Number.isInteger(options.status)) this.status = options.status;
72
+ if (Number.isFinite(options.timeoutMs)) this.timeoutMs = options.timeoutMs;
71
73
  }
72
74
  }
73
75
 
@@ -199,7 +201,7 @@ export class DingtalkDeviceAuth {
199
201
  'timeout',
200
202
  `DingTalk ${action} request timed out`,
201
203
  action,
202
- { cause: error },
204
+ { cause: error, timeoutMs: this.#timeoutMs },
203
205
  );
204
206
  }
205
207
  if (error?.name === 'AbortError') throw error;
@@ -215,6 +217,7 @@ export class DingtalkDeviceAuth {
215
217
  'http-error',
216
218
  `DingTalk ${action} request failed`,
217
219
  action,
220
+ { status: response?.status },
218
221
  );
219
222
  }
220
223
  let value;
@@ -65,6 +65,7 @@ import {
65
65
  import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
66
66
  import { DINGTALK_RECENT_OUTBOUND_MATCH_TOLERANCE_MS } from './state-store.mjs';
67
67
  import {
68
+ messageFailureDiagnostic,
68
69
  channelDeliveryFailure,
69
70
  clearLastMessageFailure,
70
71
  messageFailureText,
@@ -740,7 +741,7 @@ export class DingtalkHarnessBridge {
740
741
  const failure = setLastMessageFailure(this.#status, error);
741
742
  this.#logger.error?.(
742
743
  `[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
743
- safeErrorDiagnostic(error),
744
+ messageFailureDiagnostic(error, failure),
744
745
  );
745
746
  return this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined);
746
747
  }).finally(() => {
@@ -1207,7 +1208,7 @@ export class DingtalkHarnessBridge {
1207
1208
  const failure = setLastMessageFailure(this.#status, error);
1208
1209
  this.#logger.error?.(
1209
1210
  `[dsh-dingtalk] failed to process a batch input message [${failure.referenceId}]`,
1210
- safeErrorDiagnostic(error),
1211
+ messageFailureDiagnostic(error, failure),
1211
1212
  );
1212
1213
  await this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
1213
1214
  }).finally(() => {
@@ -1338,7 +1339,7 @@ export class DingtalkHarnessBridge {
1338
1339
  signal: this.#signal,
1339
1340
  });
1340
1341
  let content = hasInboundImages(modelMessage) || hasReply
1341
- ? await promptContentForInboundMessage(modelMessage, { signal: this.#signal })
1342
+ ? await promptContentForInboundMessage(modelMessage, { signal: this.#signal, deferImages: true })
1342
1343
  : undefined;
1343
1344
  const snapshot = this.#acceptedMessageIds.get(messageId);
1344
1345
  let contextEnhanced = false;
@@ -1395,6 +1396,7 @@ export class DingtalkHarnessBridge {
1395
1396
  }),
1396
1397
  onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
1397
1398
  files: modelMessage.files,
1399
+ images: modelMessage.images,
1398
1400
  },
1399
1401
  });
1400
1402
  if (batchSubmission) {
@@ -1486,7 +1488,7 @@ export class DingtalkHarnessBridge {
1486
1488
  });
1487
1489
  this.#logger.error?.(
1488
1490
  `[dsh-dingtalk] failed to process an inbound message [${failure.referenceId}]`,
1489
- safeErrorDiagnostic(error),
1491
+ messageFailureDiagnostic(error, failure),
1490
1492
  );
1491
1493
  try {
1492
1494
  const errorText = messageFailureText(failure);