@xmanrui/dsh-im 4.22.0 → 4.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +5 -4
  3. package/lib/client.js +2584 -1153
  4. package/lib/index.js +290 -298
  5. package/package.json +14 -2
  6. package/plugin-src/client/channel-card-meta.js +2 -0
  7. package/plugin-src/client/channel-logos.js +11 -0
  8. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  9. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  10. package/plugin-src/client/channels/email/api.js +52 -0
  11. package/plugin-src/client/channels/email/index.js +728 -0
  12. package/plugin-src/client/channels/email/styles.js +51 -0
  13. package/plugin-src/client/channels/feishu/api.js +4 -2
  14. package/plugin-src/client/channels/feishu/index.js +23 -7
  15. package/plugin-src/client/channels/imessage/index.js +2 -1
  16. package/plugin-src/client/channels/office/api.js +2 -0
  17. package/plugin-src/client/channels/office/index.js +6 -5
  18. package/plugin-src/client/channels/qq/api.js +7 -0
  19. package/plugin-src/client/channels/qq/index.js +16 -3
  20. package/plugin-src/client/channels/shared/token-api.js +8 -1
  21. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  22. package/plugin-src/client/channels/slack/index.js +2 -1
  23. package/plugin-src/client/channels/telegram/index.js +3 -0
  24. package/plugin-src/client/channels/telegram/styles.js +12 -0
  25. package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
  26. package/plugin-src/client/channels/wecom/api.js +7 -1
  27. package/plugin-src/client/channels/wecom/index.js +16 -3
  28. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  29. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  30. package/plugin-src/client/channels/weixin/api.js +2 -1
  31. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  32. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  33. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  34. package/plugin-src/client/connection-error.js +87 -0
  35. package/plugin-src/client/global-settings.js +81 -1
  36. package/plugin-src/client/i18n.js +69 -1
  37. package/plugin-src/client/index.js +40 -4
  38. package/plugin-src/client/last-message-error.js +2 -1
  39. package/plugin-src/client/styles.js +8 -2
  40. package/plugin-src/host/build.mjs +4 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  43. package/plugin-src/host/channels/email/availability.mjs +60 -0
  44. package/plugin-src/host/channels/email/index.mjs +36 -0
  45. package/plugin-src/host/channels/email/production.mjs +58 -0
  46. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  47. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  48. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  49. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  50. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  51. package/plugin-src/host/channels/qq/production.mjs +2 -0
  52. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  53. package/plugin-src/host/channels/shared/production.mjs +52 -5
  54. package/plugin-src/host/channels/shared/rpc.mjs +14 -3
  55. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  56. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  57. package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
  58. package/plugin-src/host/channels/slack/production.mjs +2 -0
  59. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom/production.mjs +2 -0
  61. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
  63. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  64. package/plugin-src/host/channels/weixin/production.mjs +2 -0
  65. package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
  66. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  67. package/plugin-src/host/delivery-adapter.mjs +11 -0
  68. package/plugin-src/host/image-input-rpc.mjs +24 -0
  69. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  70. package/plugin-src/host/index.mjs +3 -0
  71. package/plugin-src/host/modern-harness-api.mjs +7 -2
  72. package/plugin-src/management-rpc.mjs +12 -2
  73. package/scripts/verify-package.mjs +13 -5
  74. package/src/channels/dingtalk/connection-error.mjs +5 -12
  75. package/src/channels/dingtalk/device-auth.mjs +4 -1
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  77. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  78. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  79. package/src/channels/discord/discord-api.mjs +3 -3
  80. package/src/channels/discord/discord-runtime.mjs +18 -12
  81. package/src/channels/email/config-store.mjs +194 -0
  82. package/src/channels/email/email-api.mjs +20 -0
  83. package/src/channels/email/email-bridge.mjs +17 -0
  84. package/src/channels/email/email-controller.mjs +824 -0
  85. package/src/channels/email/email-runtime.mjs +699 -0
  86. package/src/channels/email/harness-client.mjs +7 -0
  87. package/src/channels/email/mail-format.mjs +91 -0
  88. package/src/channels/email/state-store.mjs +176 -0
  89. package/src/channels/email/transport.mjs +64 -0
  90. package/src/channels/email/transports/agent-mail.mjs +604 -0
  91. package/src/channels/email/transports/agently-cli.mjs +288 -0
  92. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  93. package/src/channels/feishu/bridge.mjs +39 -11
  94. package/src/channels/feishu/feishu-channel.mjs +35 -0
  95. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  96. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  97. package/src/channels/feishu/live-cot.mjs +260 -0
  98. package/src/channels/feishu/message-utils.mjs +3 -2
  99. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  100. package/src/channels/feishu/registration-manager.mjs +8 -4
  101. package/src/channels/feishu/repair-manager.mjs +2 -1
  102. package/src/channels/feishu/slash-command-registry.mjs +17 -0
  103. package/src/channels/feishu/step-push-mode.mjs +10 -4
  104. package/src/channels/imessage/runtime.mjs +9 -5
  105. package/src/channels/office/office-controller.mjs +23 -12
  106. package/src/channels/office/office-runtime.mjs +6 -3
  107. package/src/channels/office/office-transport.mjs +1 -0
  108. package/src/channels/qq/qq-bridge.mjs +7 -5
  109. package/src/channels/qq/qq-controller.mjs +56 -42
  110. package/src/channels/qq/qq-runtime.mjs +16 -11
  111. package/src/channels/shared/bot-workspace-store.mjs +29 -10
  112. package/src/channels/shared/connection-error.mjs +191 -0
  113. package/src/channels/shared/connection-test.mjs +3 -1
  114. package/src/channels/shared/conversation-state-store.mjs +46 -1
  115. package/src/channels/shared/diagnostic-details.mjs +95 -0
  116. package/src/channels/shared/harness-client.mjs +287 -57
  117. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  118. package/src/channels/shared/i18n-en/email.mjs +25 -0
  119. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  120. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  121. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  122. package/src/channels/shared/i18n-en.mjs +6 -0
  123. package/src/channels/shared/image-input-policy.mjs +35 -0
  124. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  125. package/src/channels/shared/image-input.mjs +124 -0
  126. package/src/channels/shared/image-prompt.mjs +15 -7
  127. package/src/channels/shared/message-failure.mjs +12 -0
  128. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  129. package/src/channels/shared/text-harness-bridge.mjs +150 -25
  130. package/src/channels/shared/token-bot-controller.mjs +52 -47
  131. package/src/channels/slack/slack-api.mjs +3 -3
  132. package/src/channels/slack/slack-controller.mjs +59 -51
  133. package/src/channels/slack/slack-runtime.mjs +18 -12
  134. package/src/channels/telegram/config-store.mjs +4 -1
  135. package/src/channels/telegram/telegram-api.mjs +2 -2
  136. package/src/channels/telegram/telegram-controller.mjs +13 -1
  137. package/src/channels/telegram/telegram-runtime.mjs +215 -14
  138. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  139. package/src/channels/wecom/wecom-controller.mjs +51 -37
  140. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  141. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  142. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  143. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  144. package/src/channels/weixin/connection-error.mjs +26 -92
  145. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  146. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  147. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  148. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -0,0 +1,163 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
2
+ import { createTokenBotRpcHandler } from '../shared/rpc.mjs';
3
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
4
+ import { resolveRpcAuthority } from '../../rpc-authority.mjs';
5
+
6
+ export const EMAIL_RPC_CHANNEL = '/email';
7
+ export const EMAIL_ENDPOINTS = Object.freeze({
8
+ status: 'connection.status',
9
+ // Lets the client hide the mailbox entry point while the channel is closed,
10
+ // instead of offering a form whose submissions the Host would refuse.
11
+ availability: 'channel.availability',
12
+ bindMailbox: 'bot.bind-mailbox',
13
+ updateMailbox: 'bot.mailbox.update',
14
+ reconnectBot: 'bot.reconnect',
15
+ deleteBot: 'bot.delete',
16
+ setWorkspace: 'bot.workspace.set',
17
+ setModel: 'bot.model.set',
18
+ setAgentPreset: 'bot.agent-preset.set',
19
+ setContextEnhancement: 'bot.context-enhancement.set',
20
+ setAccessPolicy: 'bot.access-policy.set',
21
+ setAlias: 'bot.alias.set',
22
+ // Session binding: read the current bindings, change them, and list the
23
+ // candidate sessions for the picker.
24
+ // QR device flow for transports that authorize out of band (Agent mailbox).
25
+ startAuth: 'bot.auth.start',
26
+ pollAuth: 'bot.auth.poll',
27
+ getBinding: 'bot.session-binding.get',
28
+ setBinding: 'bot.session-binding.set',
29
+ listSessions: 'bot.session.list',
30
+ });
31
+ export const EMAIL_RPC_ENDPOINTS = Object.freeze(Object.values(EMAIL_ENDPOINTS));
32
+
33
+ /** The mailbox fields, with the addressing field removed. */
34
+ function stripBotId(payload) {
35
+ if (!payload || typeof payload !== 'object') return {};
36
+ const { botId, update, ...rest } = payload;
37
+ return rest;
38
+ }
39
+
40
+ function failure(code, error) {
41
+ return {
42
+ ok: false,
43
+ error: {
44
+ code: error?.code ?? code,
45
+ message: error?.message ?? String(error),
46
+ details: error?.details ?? {},
47
+ },
48
+ };
49
+ }
50
+
51
+ function withRpcDetails(result) {
52
+ if (result?.ok !== false) return result;
53
+ return { ...result, error: { ...result.error, details: result.error?.details ?? {} } };
54
+ }
55
+
56
+ /**
57
+ * Mailbox-specific endpoints are handled here; everything else (status,
58
+ * reconnect, delete, workspace, model, preset, alias, access policy) goes
59
+ * through the shared token-bot handler, which already knows the endpoint
60
+ * payload shapes.
61
+ */
62
+ export function createEmailRpcHandler(controller) {
63
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'email' });
64
+ // Availability is answered before anything else, including the controller
65
+ // shape check: the client asks this endpoint to decide whether to show the
66
+ // mailbox entry point at all, so it must work even while the channel is
67
+ // closed and no controller is wired.
68
+ //
69
+ // While the channel is closed the settings page must still get a clear,
70
+ // non-throwing answer rather than a crash from an unwired controller; every
71
+ // other endpoint fails closed through `controller.disabled()`.
72
+ const disabled = typeof controller?.disabled === 'function';
73
+ const closed = disabled ? controller.disabled() : null;
74
+ const shared = disabled ? null : createTokenBotRpcHandler(controller, { channel: 'Email' });
75
+ return async (endpoint, payload, signal) => {
76
+ if (endpoint === EMAIL_ENDPOINTS.availability) {
77
+ return { ok: true, value: { enabled: !disabled } };
78
+ }
79
+ if (disabled) return closed;
80
+ if (endpoint === EMAIL_ENDPOINTS.bindMailbox) {
81
+ try {
82
+ return { ok: true, value: await controller.bindMailbox(payload ?? {}) };
83
+ } catch (error) {
84
+ return diagnosticRpcResult(diagnostics, error, withRpcDetails({
85
+ ok: false,
86
+ error: {
87
+ code: error?.code ?? 'email-bind-failed',
88
+ message: error?.message ?? String(error),
89
+ details: error?.details ?? {},
90
+ },
91
+ }), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
92
+ }
93
+ }
94
+ if (endpoint === EMAIL_ENDPOINTS.startAuth) {
95
+ try {
96
+ return { ok: true, value: await controller.startAuthorization(payload ?? {}) };
97
+ } catch (error) {
98
+ return diagnosticRpcResult(diagnostics, error, failure('email-auth-failed', error), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
99
+ }
100
+ }
101
+ if (endpoint === EMAIL_ENDPOINTS.pollAuth) {
102
+ try {
103
+ return { ok: true, value: await controller.pollAuthorization(payload ?? {}) };
104
+ } catch (error) {
105
+ return diagnosticRpcResult(diagnostics, error, failure('email-auth-failed', error), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
106
+ }
107
+ }
108
+ if (endpoint === EMAIL_ENDPOINTS.getBinding) {
109
+ try {
110
+ return { ok: true, value: await controller.getSessionBinding(payload?.botId) };
111
+ } catch (error) {
112
+ return diagnosticRpcResult(diagnostics, error, failure('email-binding-failed', error), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
113
+ }
114
+ }
115
+ if (endpoint === EMAIL_ENDPOINTS.setBinding) {
116
+ try {
117
+ return { ok: true, value: await controller.setSessionBinding(payload?.botId, payload ?? {}) };
118
+ } catch (error) {
119
+ return diagnosticRpcResult(diagnostics, error, failure('email-binding-failed', error), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
120
+ }
121
+ }
122
+ if (endpoint === EMAIL_ENDPOINTS.listSessions) {
123
+ try {
124
+ return { ok: true, value: await controller.listSessions(payload?.botId) };
125
+ } catch (error) {
126
+ return diagnosticRpcResult(diagnostics, error, failure('email-sessions-failed', error), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
127
+ }
128
+ }
129
+ if (endpoint === EMAIL_ENDPOINTS.updateMailbox) {
130
+ try {
131
+ return {
132
+ ok: true,
133
+ // The client sends the mailbox fields flat alongside botId (as every
134
+ // other endpoint here does). Reading a nested `update` silently
135
+ // discarded the whole change: the call succeeded and nothing moved.
136
+ value: await controller.updateMailboxSettings(
137
+ payload?.botId,
138
+ payload?.update ?? stripBotId(payload),
139
+ ),
140
+ };
141
+ } catch (error) {
142
+ return diagnosticRpcResult(diagnostics, error, withRpcDetails({
143
+ ok: false,
144
+ error: {
145
+ code: error?.code ?? 'email-update-failed',
146
+ message: error?.message ?? String(error),
147
+ details: error?.details ?? {},
148
+ },
149
+ }), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
150
+ }
151
+ }
152
+ return withRpcDetails(await shared(endpoint, payload, signal));
153
+ };
154
+ }
155
+
156
+ export function installEmailRpc(ctx, controller, authority) {
157
+ return registerManagementRpc(
158
+ ctx,
159
+ EMAIL_RPC_CHANNEL,
160
+ createEmailRpcHandler(controller),
161
+ { authority: resolveRpcAuthority(authority) },
162
+ );
163
+ }
@@ -1,3 +1,4 @@
1
+ import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
1
2
  import { homedir } from 'node:os';
2
3
  import { join, resolve } from 'node:path';
3
4
  import { unlink } from 'node:fs/promises';
@@ -211,12 +212,14 @@ export async function createProductionController(ctx, config = {}, internals = {
211
212
  ...(controlExecutor ? { controlExecutor } : {}),
212
213
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
213
214
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
215
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
214
216
  });
215
217
  const proxyEnv = internals.proxyEnv ?? process.env;
216
218
  const wsAgent = createFeishuWebSocketAgent(proxyEnv, internals.createProxyAgent);
217
219
 
218
220
  const modelCatalog = () => listModelCatalog(harness);
219
221
  const coreController = new Controller({
222
+ logger,
220
223
  registerApp: (options) => lark.registerApp(options),
221
224
  verifyApp,
222
225
  credentials: ctx.credentials,
@@ -1,3 +1,5 @@
1
+ import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
2
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
3
  import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
2
4
  import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
3
5
  import { registerManagementRpc } from '../../../management-rpc.mjs';
@@ -137,7 +139,7 @@ function publicError(error) {
137
139
  const code = typeof error.code === 'string' && Object.hasOwn(PUBLIC_ERROR_MESSAGES, error.code)
138
140
  ? error.code
139
141
  : 'registration_failed';
140
- return { code, message: PUBLIC_ERROR_MESSAGES[code] };
142
+ return { code, message: PUBLIC_ERROR_MESSAGES[code], ...diagnosticFields(error) };
141
143
  }
142
144
 
143
145
  function publicRegistration(registration) {
@@ -560,6 +562,7 @@ function assertController(controller) {
560
562
 
561
563
  /** DSH rc.6 handler: (endpoint, payload, signal) => Promise<RpcResult>. */
562
564
  export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl } = {}) {
565
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'feishu' });
563
566
  assertController(controller);
564
567
  const qrCache = new Map();
565
568
  const attemptQr = new Map();
@@ -721,7 +724,7 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
721
724
  testError = error;
722
725
  }
723
726
  }
724
- value = { ...value, testMessage: publicConnectionTestResult(testError) };
727
+ value = { ...value, testMessage: publicConnectionTestResult(testError, { diagnostics, botId: payload.botId }) };
725
728
  }
726
729
  } else if (endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) {
727
730
  if (typeof controller.disconnectBot !== 'function') throw new Error('Multi-bot disconnect is unavailable');
@@ -802,9 +805,9 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
802
805
  return { ok: true, value };
803
806
  } catch (error) {
804
807
  const workspaceError = publicWorkspaceError(error);
805
- return signal?.aborted ? cancelled() : workspaceError
808
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted ? cancelled() : workspaceError
806
809
  ? { ok: false, error: { ...workspaceError, details: {} } }
807
- : internalFailure();
810
+ : internalFailure(), { operation: endpoint, botId: payload?.botId });
808
811
  }
809
812
  };
810
813
  }
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
2
  import { createTokenBotRpcHandler } from '../shared/rpc.mjs';
2
3
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
@@ -28,32 +29,38 @@ function withRpcDetails(result) {
28
29
  }
29
30
 
30
31
  export function createIMessageRpcHandler(controller) {
32
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'imessage' });
31
33
  const tokenHandler = createTokenBotRpcHandler(controller, {
32
34
  channel: 'iMessage',
33
35
  });
34
36
  return async (endpoint, payload, signal) => {
35
37
  if (endpoint === IMESSAGE_ENDPOINTS.status) {
36
- const value = await controller.status();
37
- return { ok: true, value: { ...value, permissions: await controller.permissions() } };
38
+ try {
39
+ const value = await controller.status();
40
+ return { ok: true, value: { ...value, permissions: await controller.permissions() } };
41
+ } catch (error) {
42
+ return diagnosticRpcResult(diagnostics, error, { ok: false, error: { code: 'status-failed' } },
43
+ { operation: endpoint, untrustedPublicError: true });
44
+ }
38
45
  }
39
46
  if (endpoint === IMESSAGE_ENDPOINTS.permissions) {
40
47
  try {
41
48
  return { ok: true, value: await controller.permissions() };
42
49
  } catch (error) {
43
- return withRpcDetails({
50
+ return diagnosticRpcResult(diagnostics, error, withRpcDetails({
44
51
  ok: false,
45
52
  error: { code: 'permissions-check-failed', message: error.message },
46
- });
53
+ }), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
47
54
  }
48
55
  }
49
56
  if (endpoint === IMESSAGE_ENDPOINTS.bindNative) {
50
57
  try {
51
58
  return { ok: true, value: await controller.bindNative() };
52
59
  } catch (error) {
53
- return withRpcDetails({
60
+ return diagnosticRpcResult(diagnostics, error, withRpcDetails({
54
61
  ok: false,
55
62
  error: { code: error.code ?? 'imessage-bind-failed', message: error.message },
56
- });
63
+ }), { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
57
64
  }
58
65
  }
59
66
  const translated = endpoint === IMESSAGE_ENDPOINTS.setModel ? 'bot.model.set' : endpoint;
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
2
  import { registerManagementRpc } from '../../../management-rpc.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import { OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS } from '../../../../src/channels/office/protocol.mjs';
@@ -15,6 +16,7 @@ function validConfigure(payload) {
15
16
  }
16
17
 
17
18
  export function createOfficeRpcHandler(controller) {
19
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'office' });
18
20
  for (const method of ['status', 'configure', 'reconnect', 'test', 'remove']) {
19
21
  if (typeof controller?.[method] !== 'function') throw new TypeError(`AI Office controller requires ${method}()`);
20
22
  }
@@ -35,7 +37,7 @@ export function createOfficeRpcHandler(controller) {
35
37
  const message = code === 'invalid-device-token' ? 'AI Office Device Token 无效。'
36
38
  : code === 'office-hook-unavailable' ? 'AI Office Hook 尚未上线或地址不正确。'
37
39
  : error instanceof TypeError ? error.message : 'AI Office 连接操作失败,请稍后重试。';
38
- return { ok: false, error: { code, message } };
40
+ return diagnosticRpcResult(diagnostics, error, { ok: false, error: { code, message } }, { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
39
41
  }
40
42
  };
41
43
  }
@@ -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';
@@ -104,6 +105,7 @@ export async function createProductionController(ctx, config = {}, internals = {
104
105
  ...(controlExecutor ? { controlExecutor } : {}),
105
106
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
106
107
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
108
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
107
109
  });
108
110
  const modelCatalog = () => listModelCatalog(harness);
109
111
  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';
@@ -132,6 +133,7 @@ async function publicStatus(status, encodeQr) {
132
133
  }
133
134
 
134
135
  export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
136
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'qq' });
135
137
  for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
136
138
  if (typeof controller?.[method] !== 'function') throw new TypeError(`A complete QQ controller is required (${method})`);
137
139
  }
@@ -183,7 +185,7 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
183
185
  } catch (error) {
184
186
  testError = error;
185
187
  }
186
- testMessage = publicConnectionTestResult(testError);
188
+ testMessage = publicConnectionTestResult(testError, { diagnostics, botId: payload.botId });
187
189
  }
188
190
  }
189
191
  value = await publicStatus({
@@ -231,10 +233,10 @@ export function createQqRpcHandler(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: publicQqStateError(error) ?? workspaceError
237
- ?? { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } };
239
+ ?? { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } }, { 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';
@@ -79,6 +80,16 @@ export async function createTokenProductionController(ctx, config, internals, de
79
80
  const observedConfigStore = typeof configStore.remove === 'function'
80
81
  ? observeBotWorkspaceRemovals(configStore, { workspaces })
81
82
  : configStore;
83
+ // Idempotent, and shared with createRuntime below so both paths seed a new
84
+ // bot's workspace identically. A channel that must write something into the
85
+ // workspace store before the runtime exists (email pushes its sender
86
+ // allowlist into the access policy) needs the record to exist first —
87
+ // setAccessPolicy refuses a bot the store has never seen. The call is safe to
88
+ // repeat: ensure() only seeds the policy when the bot has none yet.
89
+ const ensureWorkspace = (botId, botConfig) => workspaces.ensure(botId, {
90
+ defaultAgentPreset: config.agentPreset,
91
+ initialAccessPolicy: seedAccessPolicy(botConfig),
92
+ });
82
93
  const stateStores = new Map();
83
94
  const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
84
95
  const stateFor = async (botId) => {
@@ -113,26 +124,61 @@ export async function createTokenProductionController(ctx, config, internals, de
113
124
  ...(controlExecutor ? { controlExecutor } : {}),
114
125
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
115
126
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
127
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
116
128
  });
117
129
  const modelCatalog = () => listModelCatalog(harness);
130
+ // Assigned just below. A transport may need to write a rotated token back
131
+ // through the controller, which is only constructible after createRuntime is
132
+ // defined, so the reference is held in a slot rather than captured directly.
133
+ let controllerRef = null;
118
134
  const coreController = new ResolvedController({
119
135
  credentials: ctx.credentials,
120
136
  configStore: observedConfigStore,
121
137
  logger,
122
138
  ...(internals.inspectToken ? { inspectToken: internals.inspectToken } : {}),
123
- createRuntime: async ({ botId, config: botConfig, token }) => {
139
+ // Optional per-channel hook: a channel whose own settings also express an
140
+ // access rule (email's sender allowlist) can push the derived policy into
141
+ // the workspace store, which is what the runtime actually reads.
142
+ ...(typeof definitions.accessPolicyForBot === 'function' ? {
143
+ syncAccessPolicy: async (botId, policy) => {
144
+ await workspaces.setAccessPolicy(botId, policy, {
145
+ incarnation: workspaces.incarnationFor(botId),
146
+ });
147
+ },
148
+ // Exposed so the controller can create the workspace record before it
149
+ // pushes a policy into it.
150
+ ensureWorkspace,
151
+ } : {}),
152
+ // Channels that pin conversations to an existing session need the per-bot
153
+ // state and the session catalog to drive their settings UI.
154
+ ...(definitions.supportsSessionBinding ? {
155
+ stateFor,
156
+ listWorkspaceSessions: (workspace) => harness.listWorkspaceSessions(workspace),
157
+ botWorkspaceFor: (botId) => workspaces.workspaceFor(botId),
158
+ defaultWorkspace,
159
+ } : {}),
160
+ createRuntime: async ({ botId, config: botConfig, token, credential, createTransport }) => {
124
161
  const state = await stateFor(botId);
125
- await workspaces.ensure(botId, {
126
- defaultAgentPreset: config.agentPreset,
127
- initialAccessPolicy: seedAccessPolicy(botConfig),
128
- });
162
+ await ensureWorkspace(botId, botConfig);
129
163
  const workspaceScope = createBotWorkspaceScope(harness, {
130
164
  botId, workspaces, state, agentPresetCatalog,
131
165
  });
166
+ const persistTokens = typeof definitions.persistBotCredential === 'function'
167
+ ? (tokens) => definitions.persistBotCredential({
168
+ botId, config: botConfig, tokens, controller: controllerRef,
169
+ })
170
+ : null;
132
171
  return new ResolvedRuntime({
133
172
  ...channelRuntimeOptions,
134
173
  config: botConfig,
135
174
  token,
175
+ // The mailbox may authenticate with an OAuth pair instead of a password.
176
+ ...(credential ? { credential } : {}),
177
+ // A transport that rotates its tokens needs them written back.
178
+ ...(persistTokens ? { onTokensRefreshed: persistTokens } : {}),
179
+ // The channel decides which transport a mailbox uses; without this the
180
+ // runtime falls back to its own IMAP/SMTP default.
181
+ ...(typeof createTransport === 'function' ? { createTransport } : {}),
136
182
  harness: workspaceScope.harness,
137
183
  state: workspaceScope.state,
138
184
  contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
@@ -161,6 +207,7 @@ export async function createTokenProductionController(ctx, config, internals, de
161
207
  }
162
208
  },
163
209
  });
210
+ controllerRef = coreController;
164
211
  const controller = createWorkspaceAwareController(coreController, {
165
212
  workspaces,
166
213
  stateFor,
@@ -1,3 +1,4 @@
1
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
1
2
  import { SET_ALIAS_ENDPOINT, validAliasPayload } from './bot-alias-rpc.mjs';
2
3
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
4
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
@@ -14,6 +15,7 @@ import {
14
15
  validAgentPresetPayload,
15
16
  } from './agent-preset-rpc.mjs';
16
17
  import { SET_MODEL_ENDPOINT, validModelPayload } from './model-setting-rpc.mjs';
18
+ import { SET_THINKING_TRACES_ENDPOINT, validThinkingTracesPayload } from './thinking-traces-rpc.mjs';
17
19
 
18
20
  export const TOKEN_BOT_ENDPOINTS = Object.freeze({
19
21
  status: 'connection.status',
@@ -26,6 +28,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
26
28
  setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
27
29
  setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
28
30
  setAlias: SET_ALIAS_ENDPOINT,
31
+ setThinkingTraces: SET_THINKING_TRACES_ENDPOINT,
29
32
  });
30
33
 
31
34
  const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@@ -96,6 +99,10 @@ function payloadFailure(endpoint, payload) {
96
99
  return validAliasPayload(payload)
97
100
  ? null : '请输入有效的别名(最多 80 个字符)。';
98
101
  }
102
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setThinkingTraces) {
103
+ return validThinkingTracesPayload(payload)
104
+ ? null : '请提交有效的思考过程留痕设置。';
105
+ }
99
106
  return 'Unknown bot endpoint.';
100
107
  }
101
108
 
@@ -131,6 +138,7 @@ function operationError(channel, error) {
131
138
  }
132
139
 
133
140
  export function createTokenBotRpcHandler(controller, { channel }) {
141
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: channel.toLowerCase() });
134
142
  for (const method of ['status', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
135
143
  if (typeof controller?.[method] !== 'function') {
136
144
  throw new TypeError(`A complete ${channel} controller is required (${method})`);
@@ -172,7 +180,7 @@ export function createTokenBotRpcHandler(controller, { channel }) {
172
180
  } catch (error) {
173
181
  testError = error;
174
182
  }
175
- value = { ...value, testMessage: publicConnectionTestResult(testError) };
183
+ value = { ...value, testMessage: publicConnectionTestResult(testError, { diagnostics, botId: payload.botId }) };
176
184
  }
177
185
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
178
186
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
@@ -192,6 +200,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
192
200
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
193
201
  if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
194
202
  value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
203
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setThinkingTraces) {
204
+ if (typeof controller.setThinkingTraces !== 'function') throw new Error('Thinking traces update is unavailable');
205
+ value = await controller.setThinkingTraces(payload.botId, payload.thinkingTraces);
195
206
  } else {
196
207
  value = await controller.deleteBot(payload.botId);
197
208
  }
@@ -199,9 +210,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
199
210
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
200
211
  : { ok: true, value: sanitizePublic(value) };
201
212
  } catch (error) {
202
- return signal?.aborted
213
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted
203
214
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
204
- : { ok: false, error: operationError(channel, error) };
215
+ : { ok: false, error: operationError(channel, error) }, { operation: endpoint, botId: payload?.botId });
205
216
  }
206
217
  };
207
218
  }
@@ -3,7 +3,8 @@ import { t } from '../../../../src/channels/shared/i18n.mjs';
3
3
  const CHANNEL_NAMES = {
4
4
  weixin: '微信', feishu: '飞书', dingtalk: '钉钉', wecom: '企业微信',
5
5
  'wecom-app': '企业微信应用', qq: 'QQ', slack: 'Slack', telegram: 'Telegram',
6
- discord: 'Discord', whatsapp: 'WhatsApp', office: 'AI Office',
6
+ discord: 'Discord', whatsapp: 'WhatsApp', imessage: 'iMessage',
7
+ email: '邮箱', office: 'AI Office',
7
8
  };
8
9
  const INVALID_CONFIG_MESSAGES = new Set([
9
10
  'dsh-weixin config contains invalid account data',
@@ -11,7 +12,7 @@ const INVALID_CONFIG_MESSAGES = new Set([
11
12
  'dsh-feishu config contains duplicate bot identities',
12
13
  'dsh-feishu config is incomplete or invalid',
13
14
  'dsh-dingtalk config contains invalid bot data',
14
- ...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord']
15
+ ...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord', 'Email']
15
16
  .map(channel => `dsh-im ${channel} config contains invalid bot data`),
16
17
  'dsh-im WhatsApp config contains invalid account data',
17
18
  'dsh-im AI Office config is invalid',
@@ -47,7 +48,7 @@ export function publicChannelStartupError(channel, error) {
47
48
  }
48
49
  return {
49
50
  code: `${channel}-startup-failed`,
50
- message: t('{channel}初始化失败。请查看 DSH 启动日志中 failed to activate {id} 后的错误,修复后重启 DSH。', params),
51
+ message: t('{channel}初始化失败。请展开诊断详情,并通过参考号查找 DSH 启动日志,修复后重启 DSH。', params),
51
52
  details: {},
52
53
  };
53
54
  }
@@ -2,6 +2,7 @@ import { registerManagementRpc } from '../../../management-rpc.mjs';
2
2
  import { onImHostLanguageChange } from '../../../../src/channels/shared/i18n.mjs';
3
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
4
4
  import { publicChannelInitializing, publicChannelStartupError } from './startup-error.mjs';
5
+ import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
5
6
 
6
7
  /**
7
8
  * Keep a channel's platform-side command menu in the current host message
@@ -30,13 +31,18 @@ export async function installProductionChannel(ctx, config, {
30
31
  }) {
31
32
  let startupError = publicChannelInitializing(channel);
32
33
  let handler = async () => ({ ok: false, error: startupError });
33
- const disposeRpc = registerManagementRpc(ctx, rpcChannel, (endpoint, payload, signal) => {
34
+ const logger = typeof ctx.logger === 'function' ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
35
+ const diagnostics = createConnectionDiagnostics({ channel, logger });
36
+ const disposeRpc = registerManagementRpc(ctx, rpcChannel, async (endpoint, payload, signal) => {
34
37
  if (signal?.aborted) {
35
38
  return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
36
39
  }
37
- return handler(endpoint, payload, signal);
40
+ try { return await handler(endpoint, payload, signal); }
41
+ catch (error) {
42
+ // Preserve a final Host diagnostic if an endpoint misses its own catch.
43
+ return diagnosticRpcResult(diagnostics, error, { ok: false, error: { code: `${channel}-operation-failed` } }, { operation: endpoint });
44
+ }
38
45
  }, { authority: resolveRpcAuthority(config.rpcAuthority) });
39
- const logger = typeof ctx.logger === 'function' ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
40
46
  let production;
41
47
  let unregisterDelivery;
42
48
  let closing;
@@ -58,13 +64,12 @@ export async function installProductionChannel(ctx, config, {
58
64
  } catch (error) {
59
65
  startupError = reportStartupError
60
66
  ? reportStartupError(error, false)
61
- : publicChannelStartupError(channel, error);
62
- if (!reportStartupError) logger.error?.(`[dsh-im] failed to activate ${channel}; management RPC remains available`, error);
67
+ : diagnostics.report(error, { operation: 'startup', stage: 'startup.load', publicError: publicChannelStartupError(channel, error) }).publicError;
63
68
  try {
64
69
  await closeProduction();
65
70
  } catch (cleanupError) {
66
71
  if (reportStartupError) reportStartupError(cleanupError, true);
67
- else logger.error?.(`[dsh-im] failed to close partially initialized ${channel} resources`, cleanupError);
72
+ else diagnostics.report(cleanupError, { operation: 'startup', stage: 'connection.stop', warning: true });
68
73
  }
69
74
  }
70
75
  return disposeRpc;
@@ -0,0 +1,11 @@
1
+ export const SET_THINKING_TRACES_ENDPOINT = 'bot.thinking-traces.set';
2
+
3
+ export function validThinkingTracesPayload(payload) {
4
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
5
+ || Reflect.ownKeys(payload).length !== 2
6
+ || !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'thinkingTraces')
7
+ || typeof payload.botId !== 'string'
8
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)
9
+ || typeof payload.thinkingTraces !== 'boolean') return false;
10
+ return true;
11
+ }
@@ -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
 
@@ -92,6 +93,7 @@ export async function createProductionController(ctx, config = {}, internals = {
92
93
  ...(controlExecutor ? { controlExecutor } : {}),
93
94
  ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
94
95
  ...(fileIngressExecutor ? { fileIngressExecutor } : {}),
96
+ imageInputPolicy: () => getImageInputSettingsStore(config).get(),
95
97
  });
96
98
  const modelCatalog = () => listModelCatalog(harness);
97
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
  }