@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,5 @@
1
+ import imageInput from '../../src/channels/shared/i18n-en/image-input.mjs';
2
+ import diagnostics from '../../src/channels/shared/i18n-en/diagnostics.mjs';
1
3
  import weixinDiagnostics from '../../src/channels/weixin/connection-error.en.mjs';
2
4
  import * as React from 'react';
3
5
 
@@ -5,6 +7,8 @@ export const IM_LOCALE_NAMESPACE = 'dsh-im';
5
7
 
6
8
  const EN = Object.freeze({
7
9
  ...weixinDiagnostics,
10
+ ...diagnostics,
11
+ ...imageInput,
8
12
  '$locale': 'en',
9
13
  '修改别名': 'Edit alias',
10
14
  '关闭修改别名': 'Close alias editor',
@@ -258,11 +262,13 @@ const EN = Object.freeze({
258
262
  '查看分步直推说明': 'View step push help',
259
263
  '开启后逐步推送工具调用与过程说明': 'Push tool calls and process notes step by step',
260
264
  '任务过程展示': 'Task progress display',
261
- '设置任务执行过程的呈现方式:不显示、实时卡片或逐步消息': 'Choose how the execution is presented: hidden, one live card, or step-by-step messages',
265
+ '设置任务执行过程的呈现方式:不显示、原生直播、实时卡片或逐步消息': 'Choose how execution is presented: hidden, native live process, one live card, or step-by-step messages',
262
266
  '不显示过程(只发送最终答案)': 'Hide the process (send the final answer only)',
267
+ '实时直播(飞书原生思考过程)': 'Live process (native Feishu thinking process)',
263
268
  '实时过程卡(全程一张卡片动态更新)': 'Live process card (one card updated throughout)',
264
269
  '逐步直播(每一步单独发一条消息)': 'Step-by-step feed (one message per step)',
265
270
  '适合日常问答:执行过程中不显示工具调用等中间步骤,只回复最终结果': 'For everyday Q&A: tool calls and other interim steps stay hidden, only the final result is replied',
271
+ '使用飞书原生思考过程展示推理、工具调用与结果,最终答案单独发送': 'Use Feishu’s native thinking process for reasoning, tool calls, and results, then send the final answer separately',
266
272
  '推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏': 'Recommended for long tasks: the process and the final answer update live in one card without flooding the chat',
267
273
  '每一步都单独发一条消息(含工具调用和过程说明);注意长任务会连续发送较多消息': 'Every step is sent as its own message (including tool calls and notes); long tasks may send many messages in a row',
268
274
  '分步直推设置保存失败,请重试。': 'Could not save the step push setting. Try again.',
@@ -663,6 +669,11 @@ const EN = Object.freeze({
663
669
  '接入 Telegram 机器人': 'Connect a Telegram bot',
664
670
  '先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
665
671
  '填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
672
+ '会话行为': 'Conversation behavior',
673
+ '思考过程留痕': 'Thinking traces',
674
+ '实时显示思考步骤与工具调用,最终答案单独成条': 'Show thinking steps and tool calls as they happen; the final answer arrives as its own message',
675
+ '请提交有效的思考过程留痕设置。': 'Submit a valid thinking-traces setting.',
676
+ '正在使用{name}…': 'Using {name}…',
666
677
  '访问模式': 'Access mode',
667
678
  'Telegram 访问模式': 'Telegram access mode',
668
679
  '查看 Telegram 访问模式说明': 'View Telegram access mode details',
@@ -1005,6 +1016,63 @@ const EN = Object.freeze({
1005
1016
  '企业微信应用操作失败,请稍后重试': 'The WeCom app operation failed; try again later',
1006
1017
  '企业微信应用服务返回了无法识别的响应': 'The WeCom app service returned an unrecognized response',
1007
1018
  '企业微信应用服务没有返回有效的机器人列表': 'The WeCom app service did not return a valid bot list',
1019
+ '邮箱': 'Email',
1020
+ ' IMAP/SMTP 邮箱': ' IMAP/SMTP mailbox',
1021
+ 'QQ 邮箱': 'QQ Mail',
1022
+ '163 邮箱': '163 Mail',
1023
+ '自定义服务器': 'Custom servers',
1024
+ '在 QQ 邮箱「设置 → 账户」中开启 IMAP/SMTP 服务,并生成 16 位授权码(不是登录密码)。': 'In QQ Mail open Settings → Account, enable IMAP/SMTP, and generate a 16-character authorization code (not your login password).',
1025
+ '在 163 邮箱「设置 → POP3/SMTP/IMAP」中开启服务,并新增授权密码。': 'In 163 Mail open Settings → POP3/SMTP/IMAP, enable the service, and add an authorization password.',
1026
+ '需要先开启两步验证,再生成「应用专用密码」。': 'Enable two-step verification first, then generate an app-specific password.',
1027
+ '请填写邮箱服务商提供的 IMAP 与 SMTP 服务器地址及端口。': 'Enter the IMAP and SMTP host and port provided by your mail provider.',
1028
+ '接入邮箱': 'Connect a mailbox',
1029
+ 'DeepSeek Harness 会读取该邮箱的新邮件作为指令,并在同一邮件线程内回复处理结果。': 'DeepSeek Harness reads new mail in this mailbox as instructions and replies with the result inside the same mail thread.',
1030
+ '邮箱服务商': 'Mail provider',
1031
+ '邮箱地址': 'Email address',
1032
+ '应用密码 / 授权码': 'App password / authorization code',
1033
+ 'IMAP/SMTP 授权码': 'IMAP/SMTP authorization code',
1034
+ '不是邮箱登录密码;请在邮箱设置中单独生成。': 'This is not your login password; generate it separately in your mailbox settings.',
1035
+ 'IMAP 服务器': 'IMAP server',
1036
+ '端口': 'Port',
1037
+ 'SMTP 服务器': 'SMTP server',
1038
+ '允许的发件人': 'Allowed senders',
1039
+ 'me@example.com\\n同事@example.com': 'me@example.com\\ncolleague@example.com',
1040
+ '必填。只有这些地址发来的邮件会触发 Harness;多个地址用换行或逗号分隔。': 'Required. Only mail from these addresses triggers the Harness; separate multiple addresses with newlines or commas.',
1041
+ '正在连接邮箱…': 'Connecting the mailbox…',
1042
+ '连接邮箱': 'Connect mailbox',
1043
+ '保存后会重新连接邮箱以使设置立即生效。': 'Saving reconnects the mailbox so the change takes effect immediately.',
1044
+ '使用现有邮箱收发指令:邮件进来触发 Harness,处理结果以回信形式送达。': 'Use an existing mailbox to send instructions: incoming mail triggers the Harness and the result is delivered as a reply.',
1045
+ '配置邮箱收发': 'Configure email send and receive',
1046
+ '配置邮箱': 'Configure mailbox',
1047
+ '邮箱配置': 'Mailbox configuration',
1048
+ '会话绑定': 'Session binding',
1049
+ '会话绑定已保存。': 'Session binding saved.',
1050
+ '不绑定则每封新邮件开启一个新会话;绑定固定会话后,来信都在该会话内继续。': 'With no binding every new mail starts a new session; with a fixed session, incoming mail continues in that session.',
1051
+ '固定会话(账号级)': 'Fixed session (account level)',
1052
+ '不绑定(每封新邮件新建会话)': 'No binding (new session per mail)',
1053
+ '按发件人覆盖(优先于账号级)': 'Per-sender override (takes priority)',
1054
+ '跟随账号级': 'Follow the account setting',
1055
+ '尚无可覆盖的发件人(先在上方配置允许的发件人)。': 'No senders to override yet (configure allowed senders above first).',
1056
+ '保存绑定': 'Save binding',
1057
+ '清除绑定': 'Clear binding',
1058
+ '所有发件人同上': 'Point every sender at the same session',
1059
+ '已选:': 'Selected: ',
1060
+ '接入方式': 'Access method',
1061
+ '生成授权链接': 'Generate authorization link',
1062
+ '重新生成': 'Regenerate',
1063
+ '授权链接': 'Authorization link',
1064
+ '配对码': 'Pairing code',
1065
+ '等待授权中…': 'Waiting for authorization…',
1066
+ '等待授权中,': 'Waiting for authorization — valid for ',
1067
+ ' 分钟': ' minutes',
1068
+ '内有效,可保持本页打开。': '; you can keep this page open.',
1069
+ '授权成功,正在接入邮箱…': 'Authorized; connecting the mailbox…',
1070
+ '授权成功;请填写邮箱地址与允许的发件人后完成接入。': 'Authorized. Enter the mailbox address and at least one allowed sender to finish connecting.',
1071
+ '授权后自动填入': 'Filled in after authorization',
1072
+ '由授权结果自动填入,无需手工填写。': 'Filled in from the authorization; no need to type it.',
1073
+ '刷新会话列表': 'Refresh the session list',
1074
+ '腾讯 Agent 邮箱需要微信扫码授权。点下面的按钮生成授权链接,在打开的页面里用微信扫码登录并确认。': 'The Tencent Agent mailbox authorizes with a WeChat QR code. Generate an authorization link, then scan it with WeChat on the page that opens and confirm.',
1075
+ '请求失败': 'The request failed',
1008
1076
  });
1009
1077
 
1010
1078
  export const en = EN;
@@ -14,6 +14,7 @@ import {
14
14
  WeixinLogoGlyph,
15
15
  WhatsappLogoGlyph,
16
16
  IMessageLogoGlyph,
17
+ EmailLogoGlyph,
17
18
  } from './channel-logos.js';
18
19
  import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
19
20
  import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
@@ -48,8 +49,11 @@ import { WHATSAPP_RPC_CHANNEL } from './channels/whatsapp/api.js';
48
49
  import { WhatsappSettingsTab } from './channels/whatsapp/index.js';
49
50
  import { installWhatsappStyles } from './channels/whatsapp/styles.js';
50
51
  import { IMESSAGE_RPC_CHANNEL } from './channels/imessage/api.js';
52
+ import { EMAIL_RPC_CHANNEL } from './channels/email/api.js';
51
53
  import { IMessageSettingsTab } from './channels/imessage/index.js';
54
+ import { EmailSettingsTab, useEmailChannelEnabled } from './channels/email/index.js';
52
55
  import { installIMessageStyles } from './channels/imessage/styles.js';
56
+ import { installEmailStyles } from './channels/email/styles.js';
53
57
  import { en, h, IM_LOCALE_NAMESPACE, setImTranslator, zh } from './i18n.js';
54
58
  import {
55
59
  HOST_LANGUAGE_RPC_CHANNEL,
@@ -102,6 +106,7 @@ const CHANNELS = Object.freeze([
102
106
  { id: 'whatsapp', label: 'WhatsApp' },
103
107
  { id: 'wecomApp', label: '企业微信应用', note: '(实验功能)' },
104
108
  { id: 'imessage', label: 'iMessage', note: '(实验功能)' },
109
+ { id: 'email', label: '邮箱', note: '(实验功能)' },
105
110
  { id: 'office', label: 'AI Office', note: '(实验功能)' },
106
111
  ]);
107
112
 
@@ -157,6 +162,11 @@ function IMessageLogo() {
157
162
  h(IMessageLogoGlyph));
158
163
  }
159
164
 
165
+ function EmailLogo() {
166
+ return h('span', { className: 'dim-logo dim-logoEmail', 'aria-hidden': 'true' },
167
+ h(EmailLogoGlyph));
168
+ }
169
+
160
170
  function OfficeLogo() {
161
171
  return h('span', { className: 'dim-logo dim-logoOffice', 'aria-hidden': 'true' },
162
172
  h(OfficeLogoGlyph));
@@ -174,6 +184,7 @@ function ChannelLogo({ channel }) {
174
184
  if (channel === 'discord') return h(DiscordLogo);
175
185
  if (channel === 'whatsapp') return h(WhatsappLogo);
176
186
  if (channel === 'imessage') return h(IMessageLogo);
187
+ if (channel === 'email') return h(EmailLogo);
177
188
  return h(OfficeLogo);
178
189
  }
179
190
 
@@ -198,6 +209,7 @@ export function IMSettingsTab({
198
209
  discordRpcCall,
199
210
  feishuRpcCall,
200
211
  imessageRpcCall,
212
+ emailRpcCall,
201
213
  qqRpcCall,
202
214
  slackRpcCall,
203
215
  telegramRpcCall,
@@ -222,10 +234,18 @@ export function IMSettingsTab({
222
234
  const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
223
235
  const [runningVersion, setRunningVersion] = React.useState(IM_PLUGIN_VERSION);
224
236
  const [deliverySettings, setDeliverySettings] = React.useState(null);
237
+ // The Host owns email availability and reports it over RPC. The
238
+ // mailbox entry point is omitted entirely while it is closed, and the visible
239
+ // channel list is what every later lookup (active tab, rail) reads from.
240
+ const emailEnabled = useEmailChannelEnabled(emailRpcCall);
241
+ const visibleChannels = React.useMemo(
242
+ () => CHANNELS.filter((channel) => channel.id !== 'email' || emailEnabled),
243
+ [emailEnabled],
244
+ );
225
245
  const githubTooltipId = React.useId();
226
246
  const generalSettingsTooltipId = React.useId();
227
247
  const globalSettingsSelected = selected === GLOBAL_SETTINGS_TAB_ID;
228
- const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
248
+ const active = visibleChannels.find((channel) => channel.id === selected) ?? visibleChannels[0];
229
249
  const activeTabId = globalSettingsSelected
230
250
  ? 'dim-general-settings-trigger'
231
251
  : `dim-tab-${active.id}`;
@@ -254,6 +274,7 @@ export function IMSettingsTab({
254
274
  deliveryRpcCall,
255
275
  globalSettingsRpcCall,
256
276
  imessageRpcCall,
277
+ emailRpcCall,
257
278
  }, {
258
279
  location: browserLocation,
259
280
  onRecovery: reportLoopbackRecovery,
@@ -265,6 +286,7 @@ export function IMSettingsTab({
265
286
  feishuRpcCall,
266
287
  globalSettingsRpcCall,
267
288
  imessageRpcCall,
289
+ emailRpcCall,
268
290
  officeRpcCall,
269
291
  qqRpcCall,
270
292
  reportLoopbackRecovery,
@@ -302,8 +324,16 @@ export function IMSettingsTab({
302
324
  'aria-label': 'dsh-im GitHub',
303
325
  'aria-describedby': githubTooltipId,
304
326
  },
305
- h('span', null, 'GitHub'),
306
- h('span', { className: 'dim-githubArrow', 'aria-hidden': 'true' }, '↗')),
327
+ h('svg', {
328
+ width: 18,
329
+ height: 18,
330
+ viewBox: '0 0 16 16',
331
+ fill: 'currentColor',
332
+ focusable: 'false',
333
+ 'aria-hidden': 'true',
334
+ }, h('path', {
335
+ d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z',
336
+ }))),
307
337
  h('span', {
308
338
  id: githubTooltipId,
309
339
  className: 'dim-githubTooltip',
@@ -331,7 +361,7 @@ export function IMSettingsTab({
331
361
  ),
332
362
  h('div', { className: 'dim-layout' },
333
363
  h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 设置导航' },
334
- CHANNELS.map((channel) => h('button', {
364
+ visibleChannels.map((channel) => h('button', {
335
365
  key: channel.id,
336
366
  type: 'button',
337
367
  role: 'tab',
@@ -395,6 +425,8 @@ export function IMSettingsTab({
395
425
  ? h(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall })
396
426
  : active.id === 'imessage'
397
427
  ? h(IMessageSettingsTab, { rpcCall: rpcCalls.imessageRpcCall })
428
+ : active.id === 'email'
429
+ ? h(EmailSettingsTab, { rpcCall: rpcCalls.emailRpcCall })
398
430
  : h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
399
431
  ),
400
432
  ));
@@ -433,6 +465,7 @@ export function apply(ctx) {
433
465
  installDiscordStyles(),
434
466
  installWhatsappStyles(),
435
467
  installIMessageStyles(),
468
+ installEmailStyles(),
436
469
  installOfficeStyles(),
437
470
  installImStyles(),
438
471
  ];
@@ -461,6 +494,8 @@ export function apply(ctx) {
461
494
  callManagementRpc(ctx.connection, WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
462
495
  const imessageRpcCall = (endpoint, payload, signal) =>
463
496
  callManagementRpc(ctx.connection, IMESSAGE_RPC_CHANNEL, endpoint, payload, signal);
497
+ const emailRpcCall = (endpoint, payload, signal) =>
498
+ callManagementRpc(ctx.connection, EMAIL_RPC_CHANNEL, endpoint, payload, signal);
464
499
  const slackRpcCall = (endpoint, payload, signal) =>
465
500
  callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
466
501
  const officeRpcCall = (endpoint, payload, signal) =>
@@ -490,6 +525,7 @@ export function apply(ctx) {
490
525
  whatsappRpcCall,
491
526
  imessageRpcCall,
492
527
  officeRpcCall,
528
+ emailRpcCall,
493
529
  updateRpcCall,
494
530
  deliveryRpcCall,
495
531
  globalSettingsRpcCall,
@@ -1,3 +1,4 @@
1
+ import { diagnosticFields } from '../../src/channels/shared/diagnostic-details.mjs';
1
2
  function text(value, maxLength) {
2
3
  if (typeof value !== 'string') return null;
3
4
  const trimmed = value.trim();
@@ -12,6 +13,6 @@ export function normalizeLastMessageError(value) {
12
13
  const referenceId = text(value.referenceId, 40);
13
14
  const at = Number.isFinite(value.at) ? value.at : null;
14
15
  return code && reason && message && referenceId && at !== null
15
- ? { code, reason, message, referenceId, at }
16
+ ? { code, reason, message, referenceId, at, ...(value.details ? diagnosticFields(value) : {}) }
16
17
  : null;
17
18
  }
@@ -1,6 +1,12 @@
1
1
  export const IM_STYLE_ID = 'xmanrui-dsh-im-settings';
2
2
 
3
3
  const CSS = String.raw`
4
+ .dim-connectionDiagnostic { min-width: 0; width: 100%; color: var(--dsw-alias-label-secondary, #646a73); overflow-wrap: anywhere; font-size: 13px; line-height: 1.6; }
5
+ .dim-connectionDiagnostic[data-warning="true"] { color: var(--dsw-alias-state-warn-primary, #d97706); }
6
+ .dim-connectionDiagnostic p { margin: 4px 0; }
7
+ .dim-connectionDiagnostic button { border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 6px; padding: 6px 10px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; cursor: pointer; }
8
+ .dim-connectionDiagnostic button:focus-visible, .dim-connectionDiagnostic summary:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
9
+
4
10
  .dim-aliasName { display: flex; align-items: center; gap: 4px; min-width: 0; }
5
11
  .dim-aliasName h3 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6
12
  .dim-aliasName h3:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 3px; }
@@ -87,10 +93,10 @@ const CSS = String.raw`
87
93
  .dim-updateFooter .dim-updateButton:first-child { margin-right: auto; }
88
94
  .dim-updatePrimary, .dim-updatePrimary:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #3370ff); color: #fff; background: var(--dsw-alias-state-business-primary, #3370ff); }
89
95
  .dim-githubAction { position: relative; display: inline-flex; flex: none; }
90
- .dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
96
+ .dim-githubLink { width: 30px; height: 30px; display: grid; place-items: center; flex: none; padding: 0; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
97
+ .dim-githubLink svg { display: block; }
91
98
  .dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
92
99
  .dim-githubLink:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
93
- .dim-githubArrow { font-size: 13px; line-height: 1; }
94
100
  .dim-githubTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 20; width: max-content; max-width: min(220px, 80vw); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
95
101
  .dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
96
102
  .dim-generalSettingsAction { position: relative; display: inline-flex; flex: none; }
@@ -14,7 +14,11 @@ const externalRuntimePackages = [
14
14
  '@tencent-connect/qqbot-nodejs',
15
15
  '@wecom/aibot-node-sdk',
16
16
  'dingtalk-stream',
17
+ 'imapflow',
18
+ 'mailparser',
19
+ 'nodemailer',
17
20
  'qrcode',
21
+ 'sharp',
18
22
  'undici',
19
23
  ];
20
24
  const external = externalRuntimePackages.flatMap((name) => [name, `${name}/*`]);
@@ -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';
@@ -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,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 { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
2
4
  import { registerManagementRpc } from '../../../management-rpc.mjs';
3
5
  import QRCode from 'qrcode';
@@ -169,7 +171,7 @@ function publicConnectionFailure(error) {
169
171
  ? source.referenceId
170
172
  : null;
171
173
  return code && message && hint && referenceId
172
- ? { code, message, hint, referenceId }
174
+ ? { code, message, hint, referenceId, ...diagnosticFields(source) }
173
175
  : null;
174
176
  }
175
177
 
@@ -227,6 +229,7 @@ function assertController(controller) {
227
229
  }
228
230
 
229
231
  export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
232
+ const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'dingtalk' });
230
233
  assertController(controller);
231
234
  const qrCache = new Map();
232
235
  const cachedEncode = (url) => {
@@ -283,7 +286,7 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
283
286
  await controller.sendConnectionTest(payload.botId);
284
287
  testMessage = publicConnectionTestResult();
285
288
  } catch (error) {
286
- testMessage = publicConnectionTestResult(error);
289
+ testMessage = publicConnectionTestResult(error, { diagnostics, botId: payload.botId });
287
290
  }
288
291
  }
289
292
  }
@@ -338,11 +341,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
338
341
  } catch (error) {
339
342
  const workspaceError = publicWorkspaceError(error);
340
343
  const connectionError = publicConnectionFailure(error);
341
- return signal?.aborted ? cancelled() : workspaceError
344
+ return diagnosticRpcResult(diagnostics, error, signal?.aborted ? cancelled() : workspaceError
342
345
  ? { ok: false, error: workspaceError }
343
346
  : connectionError
344
347
  ? { ok: false, error: connectionError }
345
- : internalFailure();
348
+ : internalFailure(), { operation: endpoint, botId: payload?.botId });
346
349
  }
347
350
  };
348
351
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The single switch that opens or closes the email channel.
3
+ *
4
+ * Email is enabled by default. A deployment can opt out with
5
+ * `EMAIL_CHANNEL_ENABLED=0` or `emailChannelEnabled: false` in the channel config.
6
+ * Setting either option to true enables the entry point, runtime and config
7
+ * restore again without a code change.
8
+ *
9
+ * Closing is deliberately non-destructive: it never deletes a mailbox config or
10
+ * a credential. It only refuses to expose the entry point and to start the
11
+ * runtime, so every existing mailbox is still there when the switch reopens.
12
+ */
13
+
14
+ /** Environment variable that controls the email channel. */
15
+ export const EMAIL_CHANNEL_ENABLED_ENV = 'EMAIL_CHANNEL_ENABLED';
16
+
17
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
18
+ const FALSY = new Set(['0', 'false', 'no', 'off', '']);
19
+
20
+ function readBoolean(value) {
21
+ if (typeof value === 'boolean') return value;
22
+ if (typeof value === 'string') {
23
+ const normalized = value.trim().toLowerCase();
24
+ if (TRUTHY.has(normalized)) return true;
25
+ if (FALSY.has(normalized)) return false;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Decide whether the email channel is open.
32
+ *
33
+ * An explicit config value wins over the environment so a deployment can pin
34
+ * the channel without changing its environment; an unreadable value falls back
35
+ * to the environment, and an absent one keeps the channel enabled.
36
+ */
37
+ export function isEmailChannelEnabled(config = {}, env = process.env) {
38
+ return readBoolean(config.emailChannelEnabled)
39
+ ?? readBoolean(env?.[EMAIL_CHANNEL_ENABLED_ENV])
40
+ ?? true;
41
+ }
42
+
43
+ /**
44
+ * The single host-side gate.
45
+ *
46
+ * Returns `null` when email is open, or a public "not available" result when it
47
+ * is closed. Both the management RPC and the production startup consult this so
48
+ * a closed channel can neither be configured nor connected.
49
+ */
50
+ export function emailChannelGate(config = {}, env = process.env) {
51
+ if (isEmailChannelEnabled(config, env)) return null;
52
+ return Object.freeze({
53
+ ok: false,
54
+ error: Object.freeze({
55
+ code: 'email-channel-disabled',
56
+ message: 'Email is not available yet.',
57
+ details: Object.freeze({}),
58
+ }),
59
+ });
60
+ }
@@ -0,0 +1,36 @@
1
+ import { createProductionController } from './production.mjs';
2
+ import { createEmailRpcHandler, installEmailRpc, EMAIL_RPC_CHANNEL } from './rpc.mjs';
3
+ import { installProductionChannel } from '../shared/startup.mjs';
4
+ import { emailChannelGate } from './availability.mjs';
5
+
6
+ export const name = 'dsh-im-email-host';
7
+ export const inject = ['connection', 'credentials', 'typertGateway'];
8
+
9
+ /**
10
+ * Cordis/DSH Host plugin entry. The management RPC is mounted before the
11
+ * fallible production initialization so the channel's settings page stays
12
+ * reachable (and can report why the mailbox failed) even when startup throws.
13
+ * Tests and embedded distributions may inject a controller through config.
14
+ *
15
+ * While `EMAIL_CHANNEL_ENABLED` is off this mounts the management RPC only and
16
+ * returns: no mailbox runtime is started and no configured mailbox is restored,
17
+ * so a machine that already holds mailboxes sends no network request for them.
18
+ * Nothing is deleted — reopening the switch restores every mailbox untouched.
19
+ */
20
+ export async function apply(ctx, config = {}) {
21
+ if (config?.controller) {
22
+ return installEmailRpc(ctx, config.controller, config.rpcAuthority);
23
+ }
24
+ const closed = emailChannelGate(config);
25
+ if (closed) return installEmailRpc(ctx, { disabled: () => closed }, config.rpcAuthority);
26
+ return installProductionChannel(ctx, config, {
27
+ channel: 'email',
28
+ rpcChannel: EMAIL_RPC_CHANNEL,
29
+ createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
30
+ createHandler: controller => createEmailRpcHandler(controller),
31
+ });
32
+ }
33
+
34
+ export { createProductionController } from './production.mjs';
35
+ export { EMAIL_ENDPOINTS, EMAIL_RPC_CHANNEL, EMAIL_RPC_ENDPOINTS,
36
+ createEmailRpcHandler, installEmailRpc } from './rpc.mjs';
@@ -0,0 +1,58 @@
1
+ import { EmailConfigStore } from '../../../../src/channels/email/config-store.mjs';
2
+ import { EmailHarnessClient } from '../../../../src/channels/email/harness-client.mjs';
3
+ import { EmailStateStore } from '../../../../src/channels/email/state-store.mjs';
4
+ import { EmailController } from '../../../../src/channels/email/email-controller.mjs';
5
+ import { EmailRuntime } from '../../../../src/channels/email/email-runtime.mjs';
6
+ import {
7
+ createAccessPolicy,
8
+ createAccessPolicyScope,
9
+ } from '../../../../src/channels/shared/access-policy.mjs';
10
+ import { createTokenProductionController } from '../shared/production.mjs';
11
+
12
+ /**
13
+ * Email has no group concept, and a mail address is trivially forgeable, so
14
+ * both scopes start as an allowlist seeded from the mailbox's configured
15
+ * senders instead of the open baseline other token channels use. With no
16
+ * senders configured the allowlist is empty, which denies everyone until the
17
+ * user adds one — the channel fails closed.
18
+ */
19
+ function emailAccessPolicyFor(bot) {
20
+ const senders = Array.isArray(bot?.allowedSenders) ? bot.allowedSenders : [];
21
+ const scope = createAccessPolicyScope({
22
+ mode: 'allowlist',
23
+ open: { defaultCanExecuteCommands: false, commandPermissionOverrides: [] },
24
+ // Every policy user entry carries its command permission alongside the id.
25
+ allowlist: { users: senders.map((id) => ({ id, canExecuteCommands: true })) },
26
+ });
27
+ return createAccessPolicy({ direct: scope, group: scope });
28
+ }
29
+
30
+ /**
31
+ * Email is a token-shaped channel: one mailbox identity plus one secret. It
32
+ * reuses the shared token production assembly, which wires the workspace
33
+ * scope, access policy, delivery adapter, and connection supervisor.
34
+ */
35
+ export function createProductionController(ctx, config = {}, internals = {}) {
36
+ return createTokenProductionController(ctx, config, internals, {
37
+ channel: 'email',
38
+ ConfigStore: EmailConfigStore,
39
+ StateStore: EmailStateStore,
40
+ HarnessClient: EmailHarnessClient,
41
+ Controller: EmailController,
42
+ Runtime: EmailRuntime,
43
+ runtimeOptions: (channelConfig) => ({
44
+ ...(channelConfig.pollIntervalMs === undefined
45
+ ? {} : { pollIntervalMs: channelConfig.pollIntervalMs }),
46
+ }),
47
+ initialAccessPolicyForBot: emailAccessPolicyFor,
48
+ // Declares that this channel keeps its allowlist in its own config and
49
+ // pushes the derived policy into the workspace store on change.
50
+ accessPolicyForBot: emailAccessPolicyFor,
51
+ // The mailbox settings page can pin the chat to an existing session.
52
+ supportsSessionBinding: true,
53
+ // The Agent mailbox rotates its refresh token on every refresh, so the new
54
+ // pair must be written back through the controller.
55
+ persistBotCredential: ({ botId, tokens, controller }) =>
56
+ controller?.persistTokens?.(botId, tokens) ?? Promise.resolve(false),
57
+ });
58
+ }