@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
|
@@ -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';
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
createWorkspaceAwareController,
|
|
27
28
|
observeBotWorkspaceRemovals,
|
|
28
29
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
30
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
29
31
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
30
32
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
31
33
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -117,7 +119,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
117
119
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
118
120
|
const paths = pluginPaths(config);
|
|
119
121
|
const configStore = await new ConfigStore(paths.config).load();
|
|
120
|
-
const defaultWorkspace =
|
|
122
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
121
123
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
122
124
|
const workspaces = internals.workspaces
|
|
123
125
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -201,6 +203,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
201
203
|
const harness = new Harness({
|
|
202
204
|
...connection,
|
|
203
205
|
workspace: defaultWorkspace,
|
|
206
|
+
ungroupedWorkspace,
|
|
204
207
|
// This plugin is already hosted by a running DSH process. Starting a
|
|
205
208
|
// second DSH would create a competing server and lifecycle.
|
|
206
209
|
autostart: false,
|
|
@@ -209,12 +212,14 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
209
212
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
210
213
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
211
214
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
215
|
+
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
212
216
|
});
|
|
213
217
|
const proxyEnv = internals.proxyEnv ?? process.env;
|
|
214
218
|
const wsAgent = createFeishuWebSocketAgent(proxyEnv, internals.createProxyAgent);
|
|
215
219
|
|
|
216
220
|
const modelCatalog = () => listModelCatalog(harness);
|
|
217
221
|
const coreController = new Controller({
|
|
222
|
+
logger,
|
|
218
223
|
registerApp: (options) => lark.registerApp(options),
|
|
219
224
|
verifyApp,
|
|
220
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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
|
|
1
2
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
2
3
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
4
|
import { OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS } from '../../../../src/channels/office/protocol.mjs';
|
|
@@ -15,6 +16,7 @@ function validConfigure(payload) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function createOfficeRpcHandler(controller) {
|
|
19
|
+
const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'office' });
|
|
18
20
|
for (const method of ['status', 'configure', 'reconnect', 'test', 'remove']) {
|
|
19
21
|
if (typeof controller?.[method] !== 'function') throw new TypeError(`AI Office controller requires ${method}()`);
|
|
20
22
|
}
|
|
@@ -35,7 +37,7 @@ export function createOfficeRpcHandler(controller) {
|
|
|
35
37
|
const message = code === 'invalid-device-token' ? 'AI Office Device Token 无效。'
|
|
36
38
|
: code === 'office-hook-unavailable' ? 'AI Office Hook 尚未上线或地址不正确。'
|
|
37
39
|
: error instanceof TypeError ? error.message : 'AI Office 连接操作失败,请稍后重试。';
|
|
38
|
-
return { ok: false, error: { code, message } };
|
|
40
|
+
return diagnosticRpcResult(diagnostics, error, { ok: false, error: { code, message } }, { operation: endpoint, botId: payload?.botId, untrustedPublicError: true });
|
|
39
41
|
}
|
|
40
42
|
};
|
|
41
43
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
|
|
1
2
|
import { unlink } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
createWorkspaceAwareController,
|
|
15
16
|
observeBotWorkspaceRemovals,
|
|
16
17
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
18
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
17
19
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
18
20
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
19
21
|
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
@@ -55,7 +57,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
55
57
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
56
58
|
const paths = pluginPaths(config);
|
|
57
59
|
const configStore = await new ConfigStore(paths.config).load();
|
|
58
|
-
const defaultWorkspace =
|
|
60
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
59
61
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
60
62
|
const workspaces = internals.workspaces
|
|
61
63
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -96,12 +98,14 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
96
98
|
const harness = new Harness({
|
|
97
99
|
...connection,
|
|
98
100
|
workspace: defaultWorkspace,
|
|
101
|
+
ungroupedWorkspace,
|
|
99
102
|
autostart: false,
|
|
100
103
|
dshBin: config.dshBin ?? 'dsh',
|
|
101
104
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
102
105
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
103
106
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
104
107
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
108
|
+
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
105
109
|
});
|
|
106
110
|
const modelCatalog = () => listModelCatalog(harness);
|
|
107
111
|
const coreController = new Controller({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
|
|
1
2
|
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
2
3
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
3
4
|
import QRCode from 'qrcode';
|
|
@@ -132,6 +133,7 @@ async function publicStatus(status, encodeQr) {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
136
|
+
const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: 'qq' });
|
|
135
137
|
for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
|
|
136
138
|
if (typeof controller?.[method] !== 'function') throw new TypeError(`A complete QQ controller is required (${method})`);
|
|
137
139
|
}
|
|
@@ -183,7 +185,7 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
|
183
185
|
} catch (error) {
|
|
184
186
|
testError = error;
|
|
185
187
|
}
|
|
186
|
-
testMessage = publicConnectionTestResult(testError);
|
|
188
|
+
testMessage = publicConnectionTestResult(testError, { diagnostics, botId: payload.botId });
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
value = await publicStatus({
|
|
@@ -231,10 +233,10 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
|
|
231
233
|
: { ok: true, value };
|
|
232
234
|
} catch (error) {
|
|
233
235
|
const workspaceError = publicWorkspaceError(error);
|
|
234
|
-
return signal?.aborted
|
|
236
|
+
return diagnosticRpcResult(diagnostics, error, signal?.aborted
|
|
235
237
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
236
238
|
: { ok: false, error: publicQqStateError(error) ?? workspaceError
|
|
237
|
-
?? { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } };
|
|
239
|
+
?? { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } }, { operation: endpoint, botId: payload?.botId });
|
|
238
240
|
}
|
|
239
241
|
};
|
|
240
242
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getImageInputSettingsStore } from '../../../../src/channels/shared/image-input-settings-store.mjs';
|
|
1
2
|
import { unlink } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
createWorkspaceAwareController,
|
|
17
18
|
observeBotWorkspaceRemovals,
|
|
18
19
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
20
|
+
import { prepareBotWorkspace } from '../../../../src/channels/shared/default-workspace.mjs';
|
|
19
21
|
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
20
22
|
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
21
23
|
import {
|
|
@@ -65,7 +67,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
65
67
|
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
66
68
|
const paths = pluginPaths(config, channel);
|
|
67
69
|
const configStore = await new ResolvedConfigStore(paths.config).load();
|
|
68
|
-
const defaultWorkspace =
|
|
70
|
+
const { defaultWorkspace, ungroupedWorkspace } = await prepareBotWorkspace(config);
|
|
69
71
|
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
70
72
|
const workspaces = internals.workspaces
|
|
71
73
|
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
@@ -78,6 +80,16 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
78
80
|
const observedConfigStore = typeof configStore.remove === 'function'
|
|
79
81
|
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
80
82
|
: configStore;
|
|
83
|
+
// Idempotent, and shared with createRuntime below so both paths seed a new
|
|
84
|
+
// bot's workspace identically. A channel that must write something into the
|
|
85
|
+
// workspace store before the runtime exists (email pushes its sender
|
|
86
|
+
// allowlist into the access policy) needs the record to exist first —
|
|
87
|
+
// setAccessPolicy refuses a bot the store has never seen. The call is safe to
|
|
88
|
+
// repeat: ensure() only seeds the policy when the bot has none yet.
|
|
89
|
+
const ensureWorkspace = (botId, botConfig) => workspaces.ensure(botId, {
|
|
90
|
+
defaultAgentPreset: config.agentPreset,
|
|
91
|
+
initialAccessPolicy: seedAccessPolicy(botConfig),
|
|
92
|
+
});
|
|
81
93
|
const stateStores = new Map();
|
|
82
94
|
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
83
95
|
const stateFor = async (botId) => {
|
|
@@ -105,32 +117,68 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
105
117
|
const harness = new ResolvedHarness({
|
|
106
118
|
...connection,
|
|
107
119
|
workspace: defaultWorkspace,
|
|
120
|
+
ungroupedWorkspace,
|
|
108
121
|
autostart: false,
|
|
109
122
|
dshBin: config.dshBin ?? 'dsh',
|
|
110
123
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
111
124
|
...(controlExecutor ? { controlExecutor } : {}),
|
|
112
125
|
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
113
126
|
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
127
|
+
imageInputPolicy: () => getImageInputSettingsStore(config).get(),
|
|
114
128
|
});
|
|
115
129
|
const modelCatalog = () => listModelCatalog(harness);
|
|
130
|
+
// Assigned just below. A transport may need to write a rotated token back
|
|
131
|
+
// through the controller, which is only constructible after createRuntime is
|
|
132
|
+
// defined, so the reference is held in a slot rather than captured directly.
|
|
133
|
+
let controllerRef = null;
|
|
116
134
|
const coreController = new ResolvedController({
|
|
117
135
|
credentials: ctx.credentials,
|
|
118
136
|
configStore: observedConfigStore,
|
|
119
137
|
logger,
|
|
120
138
|
...(internals.inspectToken ? { inspectToken: internals.inspectToken } : {}),
|
|
121
|
-
|
|
139
|
+
// Optional per-channel hook: a channel whose own settings also express an
|
|
140
|
+
// access rule (email's sender allowlist) can push the derived policy into
|
|
141
|
+
// the workspace store, which is what the runtime actually reads.
|
|
142
|
+
...(typeof definitions.accessPolicyForBot === 'function' ? {
|
|
143
|
+
syncAccessPolicy: async (botId, policy) => {
|
|
144
|
+
await workspaces.setAccessPolicy(botId, policy, {
|
|
145
|
+
incarnation: workspaces.incarnationFor(botId),
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
// Exposed so the controller can create the workspace record before it
|
|
149
|
+
// pushes a policy into it.
|
|
150
|
+
ensureWorkspace,
|
|
151
|
+
} : {}),
|
|
152
|
+
// Channels that pin conversations to an existing session need the per-bot
|
|
153
|
+
// state and the session catalog to drive their settings UI.
|
|
154
|
+
...(definitions.supportsSessionBinding ? {
|
|
155
|
+
stateFor,
|
|
156
|
+
listWorkspaceSessions: (workspace) => harness.listWorkspaceSessions(workspace),
|
|
157
|
+
botWorkspaceFor: (botId) => workspaces.workspaceFor(botId),
|
|
158
|
+
defaultWorkspace,
|
|
159
|
+
} : {}),
|
|
160
|
+
createRuntime: async ({ botId, config: botConfig, token, credential, createTransport }) => {
|
|
122
161
|
const state = await stateFor(botId);
|
|
123
|
-
await
|
|
124
|
-
defaultAgentPreset: config.agentPreset,
|
|
125
|
-
initialAccessPolicy: seedAccessPolicy(botConfig),
|
|
126
|
-
});
|
|
162
|
+
await ensureWorkspace(botId, botConfig);
|
|
127
163
|
const workspaceScope = createBotWorkspaceScope(harness, {
|
|
128
164
|
botId, workspaces, state, agentPresetCatalog,
|
|
129
165
|
});
|
|
166
|
+
const persistTokens = typeof definitions.persistBotCredential === 'function'
|
|
167
|
+
? (tokens) => definitions.persistBotCredential({
|
|
168
|
+
botId, config: botConfig, tokens, controller: controllerRef,
|
|
169
|
+
})
|
|
170
|
+
: null;
|
|
130
171
|
return new ResolvedRuntime({
|
|
131
172
|
...channelRuntimeOptions,
|
|
132
173
|
config: botConfig,
|
|
133
174
|
token,
|
|
175
|
+
// The mailbox may authenticate with an OAuth pair instead of a password.
|
|
176
|
+
...(credential ? { credential } : {}),
|
|
177
|
+
// A transport that rotates its tokens needs them written back.
|
|
178
|
+
...(persistTokens ? { onTokensRefreshed: persistTokens } : {}),
|
|
179
|
+
// The channel decides which transport a mailbox uses; without this the
|
|
180
|
+
// runtime falls back to its own IMAP/SMTP default.
|
|
181
|
+
...(typeof createTransport === 'function' ? { createTransport } : {}),
|
|
134
182
|
harness: workspaceScope.harness,
|
|
135
183
|
state: workspaceScope.state,
|
|
136
184
|
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
|
|
@@ -159,6 +207,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|
|
159
207
|
}
|
|
160
208
|
},
|
|
161
209
|
});
|
|
210
|
+
controllerRef = coreController;
|
|
162
211
|
const controller = createWorkspaceAwareController(coreController, {
|
|
163
212
|
workspaces,
|
|
164
213
|
stateFor,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
|
|
1
2
|
import { SET_ALIAS_ENDPOINT, validAliasPayload } from './bot-alias-rpc.mjs';
|
|
2
3
|
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
3
4
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
|
|
@@ -131,6 +132,7 @@ function operationError(channel, error) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
export function createTokenBotRpcHandler(controller, { channel }) {
|
|
135
|
+
const diagnostics = controller.diagnostics ?? createConnectionDiagnostics({ channel: channel.toLowerCase() });
|
|
134
136
|
for (const method of ['status', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
|
|
135
137
|
if (typeof controller?.[method] !== 'function') {
|
|
136
138
|
throw new TypeError(`A complete ${channel} controller is required (${method})`);
|
|
@@ -172,7 +174,7 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
|
|
172
174
|
} catch (error) {
|
|
173
175
|
testError = error;
|
|
174
176
|
}
|
|
175
|
-
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
177
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError, { diagnostics, botId: payload.botId }) };
|
|
176
178
|
}
|
|
177
179
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
|
|
178
180
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
@@ -199,9 +201,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
|
|
199
201
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
200
202
|
: { ok: true, value: sanitizePublic(value) };
|
|
201
203
|
} catch (error) {
|
|
202
|
-
return signal?.aborted
|
|
204
|
+
return diagnosticRpcResult(diagnostics, error, signal?.aborted
|
|
203
205
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
204
|
-
: { ok: false, error: operationError(channel, error) };
|
|
206
|
+
: { ok: false, error: operationError(channel, error) }, { operation: endpoint, botId: payload?.botId });
|
|
205
207
|
}
|
|
206
208
|
};
|
|
207
209
|
}
|
|
@@ -3,7 +3,8 @@ import { t } from '../../../../src/channels/shared/i18n.mjs';
|
|
|
3
3
|
const CHANNEL_NAMES = {
|
|
4
4
|
weixin: '微信', feishu: '飞书', dingtalk: '钉钉', wecom: '企业微信',
|
|
5
5
|
'wecom-app': '企业微信应用', qq: 'QQ', slack: 'Slack', telegram: 'Telegram',
|
|
6
|
-
discord: 'Discord', whatsapp: 'WhatsApp',
|
|
6
|
+
discord: 'Discord', whatsapp: 'WhatsApp', imessage: 'iMessage',
|
|
7
|
+
email: '邮箱', office: 'AI Office',
|
|
7
8
|
};
|
|
8
9
|
const INVALID_CONFIG_MESSAGES = new Set([
|
|
9
10
|
'dsh-weixin config contains invalid account data',
|
|
@@ -11,7 +12,7 @@ const INVALID_CONFIG_MESSAGES = new Set([
|
|
|
11
12
|
'dsh-feishu config contains duplicate bot identities',
|
|
12
13
|
'dsh-feishu config is incomplete or invalid',
|
|
13
14
|
'dsh-dingtalk config contains invalid bot data',
|
|
14
|
-
...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord']
|
|
15
|
+
...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord', 'Email']
|
|
15
16
|
.map(channel => `dsh-im ${channel} config contains invalid bot data`),
|
|
16
17
|
'dsh-im WhatsApp config contains invalid account data',
|
|
17
18
|
'dsh-im AI Office config is invalid',
|
|
@@ -47,7 +48,7 @@ export function publicChannelStartupError(channel, error) {
|
|
|
47
48
|
}
|
|
48
49
|
return {
|
|
49
50
|
code: `${channel}-startup-failed`,
|
|
50
|
-
message: t('{channel}
|
|
51
|
+
message: t('{channel}初始化失败。请展开诊断详情,并通过参考号查找 DSH 启动日志,修复后重启 DSH。', params),
|
|
51
52
|
details: {},
|
|
52
53
|
};
|
|
53
54
|
}
|
|
@@ -2,6 +2,7 @@ import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
|
2
2
|
import { onImHostLanguageChange } from '../../../../src/channels/shared/i18n.mjs';
|
|
3
3
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
4
|
import { publicChannelInitializing, publicChannelStartupError } from './startup-error.mjs';
|
|
5
|
+
import { createConnectionDiagnostics, diagnosticRpcResult } from '../../../../src/channels/shared/connection-error.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Keep a channel's platform-side command menu in the current host message
|
|
@@ -30,13 +31,18 @@ export async function installProductionChannel(ctx, config, {
|
|
|
30
31
|
}) {
|
|
31
32
|
let startupError = publicChannelInitializing(channel);
|
|
32
33
|
let handler = async () => ({ ok: false, error: startupError });
|
|
33
|
-
const
|
|
34
|
+
const logger = typeof ctx.logger === 'function' ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
|
|
35
|
+
const diagnostics = createConnectionDiagnostics({ channel, logger });
|
|
36
|
+
const disposeRpc = registerManagementRpc(ctx, rpcChannel, async (endpoint, payload, signal) => {
|
|
34
37
|
if (signal?.aborted) {
|
|
35
38
|
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
36
39
|
}
|
|
37
|
-
return handler(endpoint, payload, signal);
|
|
40
|
+
try { return await handler(endpoint, payload, signal); }
|
|
41
|
+
catch (error) {
|
|
42
|
+
// Preserve a final Host diagnostic if an endpoint misses its own catch.
|
|
43
|
+
return diagnosticRpcResult(diagnostics, error, { ok: false, error: { code: `${channel}-operation-failed` } }, { operation: endpoint });
|
|
44
|
+
}
|
|
38
45
|
}, { authority: resolveRpcAuthority(config.rpcAuthority) });
|
|
39
|
-
const logger = typeof ctx.logger === 'function' ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
|
|
40
46
|
let production;
|
|
41
47
|
let unregisterDelivery;
|
|
42
48
|
let closing;
|
|
@@ -58,13 +64,12 @@ export async function installProductionChannel(ctx, config, {
|
|
|
58
64
|
} catch (error) {
|
|
59
65
|
startupError = reportStartupError
|
|
60
66
|
? reportStartupError(error, false)
|
|
61
|
-
: publicChannelStartupError(channel, error);
|
|
62
|
-
if (!reportStartupError) logger.error?.(`[dsh-im] failed to activate ${channel}; management RPC remains available`, error);
|
|
67
|
+
: diagnostics.report(error, { operation: 'startup', stage: 'startup.load', publicError: publicChannelStartupError(channel, error) }).publicError;
|
|
63
68
|
try {
|
|
64
69
|
await closeProduction();
|
|
65
70
|
} catch (cleanupError) {
|
|
66
71
|
if (reportStartupError) reportStartupError(cleanupError, true);
|
|
67
|
-
else
|
|
72
|
+
else diagnostics.report(cleanupError, { operation: 'startup', stage: 'connection.stop', warning: true });
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
return disposeRpc;
|