@xmanrui/dsh-im 4.22.0 → 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 +3 -3
- package/README.md +3 -3
- package/lib/client.js +2505 -1152
- package/lib/index.js +290 -299
- package/package.json +8 -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 +61 -0
- package/plugin-src/client/index.js +30 -2
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- 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 +3 -0
- 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 +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- 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 +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- 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 +29 -10
- 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/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +63 -18
- 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,
|
|
@@ -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 =
|
|
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,
|
|
@@ -331,7 +353,7 @@ export function IMSettingsTab({
|
|
|
331
353
|
),
|
|
332
354
|
h('div', { className: 'dim-layout' },
|
|
333
355
|
h('nav', { className: 'dim-rail', role: 'tablist', 'aria-label': 'IM 设置导航' },
|
|
334
|
-
|
|
356
|
+
visibleChannels.map((channel) => h('button', {
|
|
335
357
|
key: channel.id,
|
|
336
358
|
type: 'button',
|
|
337
359
|
role: 'tab',
|
|
@@ -395,6 +417,8 @@ export function IMSettingsTab({
|
|
|
395
417
|
? h(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall })
|
|
396
418
|
: active.id === 'imessage'
|
|
397
419
|
? h(IMessageSettingsTab, { rpcCall: rpcCalls.imessageRpcCall })
|
|
420
|
+
: active.id === 'email'
|
|
421
|
+
? h(EmailSettingsTab, { rpcCall: rpcCalls.emailRpcCall })
|
|
398
422
|
: h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
|
|
399
423
|
),
|
|
400
424
|
));
|
|
@@ -433,6 +457,7 @@ export function apply(ctx) {
|
|
|
433
457
|
installDiscordStyles(),
|
|
434
458
|
installWhatsappStyles(),
|
|
435
459
|
installIMessageStyles(),
|
|
460
|
+
installEmailStyles(),
|
|
436
461
|
installOfficeStyles(),
|
|
437
462
|
installImStyles(),
|
|
438
463
|
];
|
|
@@ -461,6 +486,8 @@ export function apply(ctx) {
|
|
|
461
486
|
callManagementRpc(ctx.connection, WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
|
|
462
487
|
const imessageRpcCall = (endpoint, payload, signal) =>
|
|
463
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);
|
|
464
491
|
const slackRpcCall = (endpoint, payload, signal) =>
|
|
465
492
|
callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
|
|
466
493
|
const officeRpcCall = (endpoint, payload, signal) =>
|
|
@@ -490,6 +517,7 @@ export function apply(ctx) {
|
|
|
490
517
|
whatsappRpcCall,
|
|
491
518
|
imessageRpcCall,
|
|
492
519
|
officeRpcCall,
|
|
520
|
+
emailRpcCall,
|
|
493
521
|
updateRpcCall,
|
|
494
522
|
deliveryRpcCall,
|
|
495
523
|
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; }
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
37
|
-
|
|
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;
|