@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.
- package/README.en.md +10 -5
- package/README.md +10 -5
- package/docs/client-integration.md +81 -0
- package/lib/client.js +2565 -1134
- package/lib/index.js +292 -301
- package/package.json +9 -1
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +16 -3
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +5 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +62 -0
- package/plugin-src/client/index.js +122 -28
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/panel-error-boundary.js +21 -0
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +5 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +6 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +5 -1
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +55 -6
- package/plugin-src/host/channels/shared/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/slack/production.mjs +5 -1
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +5 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +5 -1
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +5 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +5 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +692 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +593 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +8 -7
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +37 -21
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/default-workspace.mjs +28 -0
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +111 -20
- package/src/channels/shared/harness-session-binding.mjs +13 -8
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +90 -11
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-runtime.mjs +17 -13
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -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,
|
|
@@ -74,6 +78,7 @@ import { installImStyles } from './styles.js';
|
|
|
74
78
|
import { installSessionChannelLogos } from './session-channel-logos.js';
|
|
75
79
|
import { UpdatePanel, UPDATE_RPC_CHANNEL } from './update-panel.js';
|
|
76
80
|
import { WorkspaceDirectoryPickerContext } from './workspace-editor.js';
|
|
81
|
+
import { IMPanelErrorBoundary } from './panel-error-boundary.js';
|
|
77
82
|
|
|
78
83
|
export const name = 'im-settings';
|
|
79
84
|
export const inject = ['slots', 'connection', 'locale', 'workspaces'];
|
|
@@ -101,6 +106,7 @@ const CHANNELS = Object.freeze([
|
|
|
101
106
|
{ id: 'whatsapp', label: 'WhatsApp' },
|
|
102
107
|
{ id: 'wecomApp', label: '企业微信应用', note: '(实验功能)' },
|
|
103
108
|
{ id: 'imessage', label: 'iMessage', note: '(实验功能)' },
|
|
109
|
+
{ id: 'email', label: '邮箱', note: '(实验功能)' },
|
|
104
110
|
{ id: 'office', label: 'AI Office', note: '(实验功能)' },
|
|
105
111
|
]);
|
|
106
112
|
|
|
@@ -156,6 +162,11 @@ function IMessageLogo() {
|
|
|
156
162
|
h(IMessageLogoGlyph));
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
function EmailLogo() {
|
|
166
|
+
return h('span', { className: 'dim-logo dim-logoEmail', 'aria-hidden': 'true' },
|
|
167
|
+
h(EmailLogoGlyph));
|
|
168
|
+
}
|
|
169
|
+
|
|
159
170
|
function OfficeLogo() {
|
|
160
171
|
return h('span', { className: 'dim-logo dim-logoOffice', 'aria-hidden': 'true' },
|
|
161
172
|
h(OfficeLogoGlyph));
|
|
@@ -173,6 +184,7 @@ function ChannelLogo({ channel }) {
|
|
|
173
184
|
if (channel === 'discord') return h(DiscordLogo);
|
|
174
185
|
if (channel === 'whatsapp') return h(WhatsappLogo);
|
|
175
186
|
if (channel === 'imessage') return h(IMessageLogo);
|
|
187
|
+
if (channel === 'email') return h(EmailLogo);
|
|
176
188
|
return h(OfficeLogo);
|
|
177
189
|
}
|
|
178
190
|
|
|
@@ -197,6 +209,7 @@ export function IMSettingsTab({
|
|
|
197
209
|
discordRpcCall,
|
|
198
210
|
feishuRpcCall,
|
|
199
211
|
imessageRpcCall,
|
|
212
|
+
emailRpcCall,
|
|
200
213
|
qqRpcCall,
|
|
201
214
|
slackRpcCall,
|
|
202
215
|
telegramRpcCall,
|
|
@@ -209,17 +222,30 @@ export function IMSettingsTab({
|
|
|
209
222
|
deliveryRpcCall,
|
|
210
223
|
globalSettingsRpcCall,
|
|
211
224
|
workspaceDirectoryPicker,
|
|
225
|
+
preferredSectionId,
|
|
212
226
|
browserLocation = globalThis.location,
|
|
213
227
|
navigateToRecoveryUrl = replacePageLocation,
|
|
214
228
|
}) {
|
|
215
|
-
const [selected, setSelected] = React.useState(
|
|
229
|
+
const [selected, setSelected] = React.useState(() => (
|
|
230
|
+
preferredSectionId === GLOBAL_SETTINGS_TAB_ID
|
|
231
|
+
|| CHANNELS.some((channel) => channel.id === preferredSectionId)
|
|
232
|
+
? preferredSectionId : 'weixin'
|
|
233
|
+
));
|
|
216
234
|
const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
|
|
217
235
|
const [runningVersion, setRunningVersion] = React.useState(IM_PLUGIN_VERSION);
|
|
218
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
|
+
);
|
|
219
245
|
const githubTooltipId = React.useId();
|
|
220
246
|
const generalSettingsTooltipId = React.useId();
|
|
221
247
|
const globalSettingsSelected = selected === GLOBAL_SETTINGS_TAB_ID;
|
|
222
|
-
const active =
|
|
248
|
+
const active = visibleChannels.find((channel) => channel.id === selected) ?? visibleChannels[0];
|
|
223
249
|
const activeTabId = globalSettingsSelected
|
|
224
250
|
? 'dim-general-settings-trigger'
|
|
225
251
|
: `dim-tab-${active.id}`;
|
|
@@ -248,6 +274,7 @@ export function IMSettingsTab({
|
|
|
248
274
|
deliveryRpcCall,
|
|
249
275
|
globalSettingsRpcCall,
|
|
250
276
|
imessageRpcCall,
|
|
277
|
+
emailRpcCall,
|
|
251
278
|
}, {
|
|
252
279
|
location: browserLocation,
|
|
253
280
|
onRecovery: reportLoopbackRecovery,
|
|
@@ -259,6 +286,7 @@ export function IMSettingsTab({
|
|
|
259
286
|
feishuRpcCall,
|
|
260
287
|
globalSettingsRpcCall,
|
|
261
288
|
imessageRpcCall,
|
|
289
|
+
emailRpcCall,
|
|
262
290
|
officeRpcCall,
|
|
263
291
|
qqRpcCall,
|
|
264
292
|
reportLoopbackRecovery,
|
|
@@ -325,7 +353,7 @@ export function IMSettingsTab({
|
|
|
325
353
|
),
|
|
326
354
|
h('div', { className: 'dim-layout' },
|
|
327
355
|
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 设置导航' },
|
|
328
|
-
|
|
356
|
+
visibleChannels.map((channel) => h('button', {
|
|
329
357
|
key: channel.id,
|
|
330
358
|
type: 'button',
|
|
331
359
|
role: 'tab',
|
|
@@ -389,6 +417,8 @@ export function IMSettingsTab({
|
|
|
389
417
|
? h(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall })
|
|
390
418
|
: active.id === 'imessage'
|
|
391
419
|
? h(IMessageSettingsTab, { rpcCall: rpcCalls.imessageRpcCall })
|
|
420
|
+
: active.id === 'email'
|
|
421
|
+
? h(EmailSettingsTab, { rpcCall: rpcCalls.emailRpcCall })
|
|
392
422
|
: h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
|
|
393
423
|
),
|
|
394
424
|
));
|
|
@@ -427,6 +457,7 @@ export function apply(ctx) {
|
|
|
427
457
|
installDiscordStyles(),
|
|
428
458
|
installWhatsappStyles(),
|
|
429
459
|
installIMessageStyles(),
|
|
460
|
+
installEmailStyles(),
|
|
430
461
|
installOfficeStyles(),
|
|
431
462
|
installImStyles(),
|
|
432
463
|
];
|
|
@@ -455,6 +486,8 @@ export function apply(ctx) {
|
|
|
455
486
|
callManagementRpc(ctx.connection, WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
|
|
456
487
|
const imessageRpcCall = (endpoint, payload, signal) =>
|
|
457
488
|
callManagementRpc(ctx.connection, IMESSAGE_RPC_CHANNEL, endpoint, payload, signal);
|
|
489
|
+
const emailRpcCall = (endpoint, payload, signal) =>
|
|
490
|
+
callManagementRpc(ctx.connection, EMAIL_RPC_CHANNEL, endpoint, payload, signal);
|
|
458
491
|
const slackRpcCall = (endpoint, payload, signal) =>
|
|
459
492
|
callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
|
|
460
493
|
const officeRpcCall = (endpoint, payload, signal) =>
|
|
@@ -471,29 +504,90 @@ export function apply(ctx) {
|
|
|
471
504
|
pickDirectory: () => callWorkspaceDirectoryApi(ctx, 'pickDirectory'),
|
|
472
505
|
});
|
|
473
506
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
507
|
+
const panelDependencies = {
|
|
508
|
+
dingtalkRpcCall,
|
|
509
|
+
discordRpcCall,
|
|
510
|
+
feishuRpcCall,
|
|
511
|
+
qqRpcCall,
|
|
512
|
+
slackRpcCall,
|
|
513
|
+
telegramRpcCall,
|
|
514
|
+
wecomRpcCall,
|
|
515
|
+
wecomAppRpcCall,
|
|
516
|
+
weixinRpcCall,
|
|
517
|
+
whatsappRpcCall,
|
|
518
|
+
imessageRpcCall,
|
|
519
|
+
officeRpcCall,
|
|
520
|
+
emailRpcCall,
|
|
521
|
+
updateRpcCall,
|
|
522
|
+
deliveryRpcCall,
|
|
523
|
+
globalSettingsRpcCall,
|
|
524
|
+
workspaceDirectoryPicker,
|
|
525
|
+
};
|
|
526
|
+
const subscribeLocale = (listener) => typeof ctx.on === 'function'
|
|
527
|
+
? ctx.on('locale/change', listener) : () => {};
|
|
528
|
+
const localeSnapshot = () => ctx.locale.getLocale?.()?.active ?? '';
|
|
529
|
+
|
|
530
|
+
// Stable for this plugin lifetime: creating an element must not create a
|
|
531
|
+
// new component type and reset the reader's selected tab or unsaved input.
|
|
532
|
+
function IMPanel({ preferredSectionId }) {
|
|
533
|
+
React.useSyncExternalStore(subscribeLocale, localeSnapshot, localeSnapshot);
|
|
534
|
+
return h(IMPanelErrorBoundary, null,
|
|
535
|
+
h(IMSettingsTab, { ...panelDependencies, preferredSectionId }));
|
|
536
|
+
}
|
|
537
|
+
const buildPanelElement = (props = {}) => h(IMPanel, {
|
|
538
|
+
preferredSectionId: props.preferredSectionId,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
ctx.effect(() => {
|
|
542
|
+
let disposed = false;
|
|
543
|
+
let stopSettings = null;
|
|
544
|
+
let registered = false;
|
|
545
|
+
const setSettingsVisible = (visible) => {
|
|
546
|
+
if (disposed) return;
|
|
547
|
+
if (typeof visible !== 'boolean') throw new TypeError('visible must be a boolean');
|
|
548
|
+
if (visible === (stopSettings !== null)) return;
|
|
549
|
+
if (!visible) {
|
|
550
|
+
const stop = stopSettings;
|
|
551
|
+
stopSettings = null;
|
|
552
|
+
stop();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// The existing slot controller owns late declarations, withdrawal and
|
|
556
|
+
// re-declaration. Cancelling it also cancels a pending registration.
|
|
557
|
+
stopSettings = ctx.slots.inject('settings.section', () => {
|
|
558
|
+
const unregister = ctx.slots.register({
|
|
559
|
+
name: 'settings.section',
|
|
560
|
+
id: 'xmanrui-dsh-im',
|
|
561
|
+
order: 21,
|
|
562
|
+
label: () => t('IM机器人'),
|
|
563
|
+
locale: IM_LOCALE_NAMESPACE,
|
|
564
|
+
inject: () => panelDependencies,
|
|
565
|
+
}, buildPanelElement);
|
|
566
|
+
registered = true;
|
|
567
|
+
return () => {
|
|
568
|
+
registered = false;
|
|
569
|
+
unregister();
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// Publish only after the default registration is established, so a
|
|
575
|
+
// consumer hiding it during service discovery cannot be overridden.
|
|
576
|
+
setSettingsVisible(true);
|
|
577
|
+
if (typeof ctx.provide === 'function') {
|
|
578
|
+
ctx.provide('dshImClient', Object.freeze({
|
|
579
|
+
version: 1,
|
|
580
|
+
render: (props) => disposed ? null : buildPanelElement(props),
|
|
581
|
+
setSettingsVisible,
|
|
582
|
+
settingsVisible: () => !disposed && registered,
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
return () => {
|
|
586
|
+
disposed = true;
|
|
587
|
+
const stop = stopSettings;
|
|
588
|
+
stopSettings = null;
|
|
589
|
+
registered = false;
|
|
590
|
+
stop?.();
|
|
591
|
+
};
|
|
592
|
+
}, 'im-settings: client panel service');
|
|
499
593
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { h } from './i18n.js';
|
|
3
|
+
|
|
4
|
+
/** Keep a broken IM subtree local when a shell renders it outside a slot. */
|
|
5
|
+
export class IMPanelErrorBoundary extends React.Component {
|
|
6
|
+
state = { failed: false };
|
|
7
|
+
|
|
8
|
+
static getDerivedStateFromError() {
|
|
9
|
+
return { failed: true };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
if (!this.state.failed) return this.props.children;
|
|
14
|
+
return h('section', { className: 'dim-page', role: 'alert' },
|
|
15
|
+
h('p', null, 'IM 面板加载失败'),
|
|
16
|
+
h('button', {
|
|
17
|
+
type: 'button',
|
|
18
|
+
onClick: () => this.setState({ failed: false }),
|
|
19
|
+
}, '重试'));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -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; }
|
|
@@ -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';
|
|
@@ -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';
|
|
@@ -58,7 +60,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
58
60
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
59
61
|
const paths = pluginPaths(config);
|
|
60
62
|
const configStore = await new ConfigStore(paths.config).load();
|
|
61
|
-
const defaultWorkspace =
|
|
63
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
62
64
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
63
65
|
const workspaces = internals.workspaces
|
|
64
66
|
?? 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,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
|
+
}
|