@xmanrui/dsh-im 4.21.2 → 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 (141) hide show
  1. package/README.en.md +10 -5
  2. package/README.md +10 -5
  3. package/docs/client-integration.md +81 -0
  4. package/lib/client.js +2565 -1134
  5. package/lib/index.js +292 -301
  6. package/package.json +9 -1
  7. package/plugin-src/client/channel-card-meta.js +2 -0
  8. package/plugin-src/client/channel-logos.js +11 -0
  9. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  10. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  11. package/plugin-src/client/channels/email/api.js +52 -0
  12. package/plugin-src/client/channels/email/index.js +728 -0
  13. package/plugin-src/client/channels/email/styles.js +51 -0
  14. package/plugin-src/client/channels/feishu/api.js +4 -2
  15. package/plugin-src/client/channels/feishu/index.js +16 -3
  16. package/plugin-src/client/channels/imessage/index.js +2 -1
  17. package/plugin-src/client/channels/office/api.js +2 -0
  18. package/plugin-src/client/channels/office/index.js +6 -5
  19. package/plugin-src/client/channels/qq/api.js +7 -0
  20. package/plugin-src/client/channels/qq/index.js +16 -3
  21. package/plugin-src/client/channels/shared/token-api.js +5 -1
  22. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  23. package/plugin-src/client/channels/slack/index.js +2 -1
  24. package/plugin-src/client/channels/wecom/api.js +7 -1
  25. package/plugin-src/client/channels/wecom/index.js +16 -3
  26. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  27. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  28. package/plugin-src/client/channels/weixin/api.js +2 -1
  29. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  30. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  31. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  32. package/plugin-src/client/connection-error.js +87 -0
  33. package/plugin-src/client/global-settings.js +81 -1
  34. package/plugin-src/client/i18n.js +62 -0
  35. package/plugin-src/client/index.js +122 -28
  36. package/plugin-src/client/last-message-error.js +2 -1
  37. package/plugin-src/client/panel-error-boundary.js +21 -0
  38. package/plugin-src/client/styles.js +6 -0
  39. package/plugin-src/host/build.mjs +4 -0
  40. package/plugin-src/host/channels/dingtalk/production.mjs +5 -1
  41. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  42. package/plugin-src/host/channels/email/availability.mjs +60 -0
  43. package/plugin-src/host/channels/email/index.mjs +36 -0
  44. package/plugin-src/host/channels/email/production.mjs +58 -0
  45. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  46. package/plugin-src/host/channels/feishu/production.mjs +6 -1
  47. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  48. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  49. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  50. package/plugin-src/host/channels/qq/production.mjs +5 -1
  51. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  52. package/plugin-src/host/channels/shared/production.mjs +55 -6
  53. package/plugin-src/host/channels/shared/rpc.mjs +5 -3
  54. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  55. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  56. package/plugin-src/host/channels/slack/production.mjs +5 -1
  57. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  58. package/plugin-src/host/channels/wecom/production.mjs +5 -1
  59. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom-app/production.mjs +5 -1
  61. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/weixin/production.mjs +5 -1
  63. package/plugin-src/host/channels/whatsapp/production.mjs +5 -1
  64. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  65. package/plugin-src/host/delivery-adapter.mjs +11 -0
  66. package/plugin-src/host/image-input-rpc.mjs +24 -0
  67. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  68. package/plugin-src/host/index.mjs +3 -0
  69. package/plugin-src/management-rpc.mjs +12 -2
  70. package/scripts/verify-package.mjs +3 -1
  71. package/src/channels/dingtalk/connection-error.mjs +5 -12
  72. package/src/channels/dingtalk/device-auth.mjs +4 -1
  73. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  74. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  75. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  76. package/src/channels/discord/discord-api.mjs +3 -3
  77. package/src/channels/discord/discord-runtime.mjs +18 -12
  78. package/src/channels/email/config-store.mjs +194 -0
  79. package/src/channels/email/email-api.mjs +20 -0
  80. package/src/channels/email/email-bridge.mjs +17 -0
  81. package/src/channels/email/email-controller.mjs +824 -0
  82. package/src/channels/email/email-runtime.mjs +692 -0
  83. package/src/channels/email/harness-client.mjs +7 -0
  84. package/src/channels/email/mail-format.mjs +91 -0
  85. package/src/channels/email/state-store.mjs +176 -0
  86. package/src/channels/email/transport.mjs +64 -0
  87. package/src/channels/email/transports/agent-mail.mjs +593 -0
  88. package/src/channels/email/transports/agently-cli.mjs +288 -0
  89. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  90. package/src/channels/feishu/bridge.mjs +8 -7
  91. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  92. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  93. package/src/channels/feishu/message-utils.mjs +3 -2
  94. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  95. package/src/channels/feishu/registration-manager.mjs +8 -4
  96. package/src/channels/feishu/repair-manager.mjs +2 -1
  97. package/src/channels/imessage/runtime.mjs +9 -5
  98. package/src/channels/office/office-controller.mjs +23 -12
  99. package/src/channels/office/office-runtime.mjs +6 -3
  100. package/src/channels/office/office-transport.mjs +1 -0
  101. package/src/channels/qq/qq-bridge.mjs +7 -5
  102. package/src/channels/qq/qq-controller.mjs +56 -42
  103. package/src/channels/qq/qq-runtime.mjs +16 -11
  104. package/src/channels/shared/bot-workspace-store.mjs +37 -21
  105. package/src/channels/shared/connection-error.mjs +191 -0
  106. package/src/channels/shared/connection-test.mjs +3 -1
  107. package/src/channels/shared/conversation-state-store.mjs +46 -1
  108. package/src/channels/shared/default-workspace.mjs +28 -0
  109. package/src/channels/shared/diagnostic-details.mjs +95 -0
  110. package/src/channels/shared/harness-client.mjs +111 -20
  111. package/src/channels/shared/harness-session-binding.mjs +13 -8
  112. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  113. package/src/channels/shared/i18n-en/email.mjs +25 -0
  114. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  115. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  116. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  117. package/src/channels/shared/i18n-en.mjs +6 -0
  118. package/src/channels/shared/image-input-policy.mjs +35 -0
  119. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  120. package/src/channels/shared/image-input.mjs +124 -0
  121. package/src/channels/shared/image-prompt.mjs +15 -7
  122. package/src/channels/shared/message-failure.mjs +12 -0
  123. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  124. package/src/channels/shared/text-harness-bridge.mjs +90 -11
  125. package/src/channels/shared/token-bot-controller.mjs +52 -47
  126. package/src/channels/slack/slack-api.mjs +3 -3
  127. package/src/channels/slack/slack-controller.mjs +59 -51
  128. package/src/channels/slack/slack-runtime.mjs +18 -12
  129. package/src/channels/telegram/telegram-api.mjs +2 -2
  130. package/src/channels/telegram/telegram-runtime.mjs +17 -13
  131. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  132. package/src/channels/wecom/wecom-controller.mjs +51 -37
  133. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  134. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  135. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  136. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  137. package/src/channels/weixin/connection-error.mjs +26 -92
  138. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  139. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  140. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  141. 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 { resolve } from 'node:path';
3
4
 
@@ -12,6 +13,7 @@ import {
12
13
  createWorkspaceAwareController,
13
14
  observeBotWorkspaceRemovals,
14
15
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
16
+ import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
15
17
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
16
18
  import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
17
19
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
@@ -44,7 +46,7 @@ export async function createProductionController(ctx, config = {}, internals = {
44
46
  const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
45
47
  const paths = pluginPaths(config, 'slack');
46
48
  const configStore = await new ResolvedConfigStore(paths.config).load();
47
- const defaultWorkspace = resolve(config.workspace ?? process.cwd());
49
+ const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
48
50
  const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
49
51
  const workspaces = internals.workspaces
50
52
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
@@ -84,12 +86,14 @@ export async function createProductionController(ctx, config = {}, internals = {
84
86
  const harness = new ResolvedHarness({
85
87
  ...connection,
86
88
  workspace: defaultWorkspace,
89
+ ungroupedWorkspace,
87
90
  autostart: false,
88
91
  dshBin: config.dshBin ?? 'dsh',
89
92
  ...(commandExecutor ? { commandExecutor } : {}),
90
93
  ...(controlExecutor ? { controlExecutor } : {}),
91
94
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
92
95
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
96
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
93
97
  });
94
98
  const modelCatalog = () => listModelCatalog(harness);
95
99
  const coreController = new ResolvedController({
@@ -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';
@@ -131,6 +132,7 @@ function operationError(error) {
131
132
  }
132
133
 
133
134
  export function createSlackRpcHandler(controller) {
135
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'slack' });
134
136
  for (const method of ['status', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
135
137
  if (typeof controller?.[method] !== 'function') {
136
138
  throw new TypeError(`A complete Slack controller is required (${method})`);
@@ -171,7 +173,7 @@ export function createSlackRpcHandler(controller) {
171
173
  } catch (error) {
172
174
  testError = error;
173
175
  }
174
- value = { ...value, testMessage: publicConnectionTestResult(testError) };
176
+ value = { ...value, testMessage: publicConnectionTestResult(testError, { diagnostics, botId: payload.botId }) };
175
177
  }
176
178
  }
177
179
  else if (endpoint === SLACK_ENDPOINTS.setWorkspace) {
@@ -203,9 +205,9 @@ export function createSlackRpcHandler(controller) {
203
205
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
204
206
  : { ok: true, value: sanitizePublic(value) };
205
207
  } catch (error) {
206
- return signal?.aborted
208
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted
207
209
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
208
- : { ok: false, error: operationError(error) };
210
+ : { ok: false, error: operationError(error) }, { operation: endpoint, botId: payload?.botId });
209
211
  }
210
212
  };
211
213
  }
@@ -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';
@@ -14,6 +15,7 @@ import {
14
15
  createWorkspaceAwareController,
15
16
  observeBotWorkspaceRemovals,
16
17
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
18
+ import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
17
19
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
18
20
  import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
19
21
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
@@ -55,7 +57,7 @@ export async function createProductionController(ctx, config = {}, internals = {
55
57
  const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
56
58
  const paths = pluginPaths(config);
57
59
  const configStore = await new ConfigStore(paths.config).load();
58
- const defaultWorkspace = resolve(config.workspace ?? process.cwd());
60
+ const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
59
61
  const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
60
62
  const workspaces = internals.workspaces
61
63
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
@@ -99,12 +101,14 @@ export async function createProductionController(ctx, config = {}, internals = {
99
101
  const harness = new Harness({
100
102
  ...connection,
101
103
  workspace: defaultWorkspace,
104
+ ungroupedWorkspace,
102
105
  autostart: false,
103
106
  dshBin: config.dshBin ?? 'dsh',
104
107
  ...(commandExecutor ? { commandExecutor } : {}),
105
108
  ...(controlExecutor ? { controlExecutor } : {}),
106
109
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
107
110
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
111
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
108
112
  });
109
113
  const modelCatalog = () => listModelCatalog(harness);
110
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';
@@ -14,6 +15,7 @@ import {
14
15
  createWorkspaceAwareController,
15
16
  observeBotWorkspaceRemovals,
16
17
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
18
+ import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
17
19
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
18
20
  import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
19
21
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
@@ -69,7 +71,7 @@ export async function createProductionController(ctx, config = {}, internals = {
69
71
  const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
70
72
  const paths = pluginPaths(config);
71
73
  const configStore = await new ConfigStore(paths.config).load();
72
- const defaultWorkspace = resolve(config.workspace ?? process.cwd());
74
+ const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
73
75
  const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
74
76
  const workspaces = internals.workspaces
75
77
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
@@ -133,12 +135,14 @@ export async function createProductionController(ctx, config = {}, internals = {
133
135
  const harness = new Harness({
134
136
  ...connection,
135
137
  workspace: defaultWorkspace,
138
+ ungroupedWorkspace,
136
139
  autostart: false,
137
140
  dshBin: config.dshBin ?? 'dsh',
138
141
  ...(commandExecutor ? { commandExecutor } : {}),
139
142
  ...(controlExecutor ? { controlExecutor } : {}),
140
143
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
141
144
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
145
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
142
146
  });
143
147
  const modelCatalog = () => listModelCatalog(harness);
144
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';
@@ -18,6 +19,7 @@ import {
18
19
  createWorkspaceAwareController,
19
20
  observeBotWorkspaceRemovals,
20
21
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
22
+ import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
21
23
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
22
24
  import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
23
25
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
@@ -90,7 +92,7 @@ export async function createProductionController(ctx, config = {}, internals = {
90
92
  const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
91
93
  const paths = pluginPaths(config);
92
94
  const configStore = await new ConfigStore(paths.config).load();
93
- const defaultWorkspace = resolve(config.workspace ?? process.cwd());
95
+ const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
94
96
  const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
95
97
  const workspaces = diagnosticWorkspaces(internals.workspaces
96
98
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load());
@@ -131,12 +133,14 @@ export async function createProductionController(ctx, config = {}, internals = {
131
133
  const harness = new Harness({
132
134
  ...connection,
133
135
  workspace: defaultWorkspace,
136
+ ungroupedWorkspace,
134
137
  autostart: false,
135
138
  dshBin: config.dshBin ?? 'dsh',
136
139
  ...(commandExecutor ? { commandExecutor } : {}),
137
140
  ...(controlExecutor ? { controlExecutor } : {}),
138
141
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
139
142
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
143
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
140
144
  });
141
145
  const modelCatalog = () => listModelCatalog(harness);
142
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';
@@ -17,6 +18,7 @@ import {
17
18
  createWorkspaceAwareController,
18
19
  observeBotWorkspaceRemovals,
19
20
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
21
+ import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
20
22
  import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
21
23
  import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
22
24
  import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
@@ -65,7 +67,7 @@ export async function createProductionController(ctx, config = {}, internals = {
65
67
  const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
66
68
  const paths = pluginPaths(config);
67
69
  const configStore = await new ConfigStore(paths.config).load();
68
- const defaultWorkspace = resolve(config.workspace ?? process.cwd());
70
+ const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
69
71
  const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
70
72
  const workspaces = internals.workspaces
71
73
  ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
@@ -105,12 +107,14 @@ export async function createProductionController(ctx, config = {}, internals = {
105
107
  const harness = new Harness({
106
108
  ...connection,
107
109
  workspace: defaultWorkspace,
110
+ ungroupedWorkspace,
108
111
  autostart: false,
109
112
  dshBin: config.dshBin ?? 'dsh',
110
113
  ...(commandExecutor ? { commandExecutor } : {}),
111
114
  ...(controlExecutor ? { controlExecutor } : {}),
112
115
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
113
116
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
117
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
114
118
  });
115
119
  const modelCatalog = () => listModelCatalog(harness);
116
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({
@@ -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
  }
@@ -147,9 +147,11 @@ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.
147
147
  // template; the delivery target adds one ordinary checkbox template.
148
148
  const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
149
149
  const deliverySettingsSource = await readFile(resolve(root, 'plugin-src/client/delivery-settings.js'), 'utf8');
150
+ const emailSettingsSource = await readFile(resolve(root, 'plugin-src/client/channels/email/index.js'), 'utf8');
150
151
  const otherClientSources = clientSources
151
152
  .replace(contextEditorSource, '')
152
- .replace(deliverySettingsSource, '');
153
+ .replace(deliverySettingsSource, '')
154
+ .replace(emailSettingsSource, '');
153
155
  if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
154
156
  || (deliverySettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
155
157
  || /role:\s*["']switch["']/u.test(deliverySettingsSource)
@@ -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);