@xmanrui/dsh-im 4.9.0 → 4.10.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/PROACTIVE_DELIVERY.en.md +8 -0
- package/PROACTIVE_DELIVERY.md +8 -0
- package/lib/client.js +679 -73
- package/lib/index.js +276 -275
- package/package.json +21 -6
- package/plugin-src/client/channels/dingtalk/api.js +23 -6
- package/plugin-src/client/channels/dingtalk/index.js +33 -6
- package/plugin-src/client/channels/dingtalk/styles.js +2 -0
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +54 -0
- package/plugin-src/client/global-settings.js +341 -0
- package/plugin-src/client/i18n.js +33 -0
- package/plugin-src/client/index.js +47 -6
- package/plugin-src/client/styles.js +49 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +24 -0
- package/plugin-src/host/channels/feishu/production.mjs +14 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
- package/plugin-src/host/channels/qq/production.mjs +12 -0
- package/plugin-src/host/channels/shared/production.mjs +12 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -0
- package/plugin-src/host/channels/wecom/production.mjs +12 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +12 -0
- package/plugin-src/host/delivery-adapter.mjs +1 -0
- package/plugin-src/host/delivery-service.mjs +15 -0
- package/plugin-src/host/harness-session-coordinator.mjs +26 -5
- package/plugin-src/host/inbound-ttl-rpc.mjs +83 -0
- package/plugin-src/host/inbound-ttl-runtime.mjs +85 -0
- package/plugin-src/host/inbound-ttl-service.mjs +179 -0
- package/plugin-src/host/index.mjs +8 -0
- package/plugin-src/host/modern-harness-api.mjs +75 -10
- package/src/channels/dingtalk/connection-error.mjs +299 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +79 -34
- package/src/channels/dingtalk/dingtalk-runtime.mjs +9 -2
- package/src/channels/feishu/bridge.mjs +164 -4
- package/src/channels/feishu/feishu-channel.mjs +45 -10
- package/src/channels/feishu/feishu-runtime.mjs +9 -0
- package/src/channels/feishu/message-utils.mjs +13 -0
- package/src/channels/feishu/multi-bot-controller.mjs +16 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/feishu/state-store.mjs +25 -0
- package/src/channels/shared/bot-workspace-store.mjs +6 -0
- package/src/channels/shared/i18n-en/dingtalk.mjs +23 -0
- package/src/channels/shared/inbound-file.mjs +117 -2
- package/src/channels/shared/inbound-ttl-store.mjs +110 -0
- package/src/channels/shared/inbound-ttl.mjs +22 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import { t } from '../shared/i18n.mjs';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const STAGE_CODES = new Set([
|
|
8
|
+
'dingtalk-harness-connect-failed',
|
|
9
|
+
'dingtalk-runtime-prepare-failed',
|
|
10
|
+
'dingtalk-stream-client-load-failed',
|
|
11
|
+
'dingtalk-stream-listener-failed',
|
|
12
|
+
'dingtalk-stream-connect-failed',
|
|
13
|
+
]);
|
|
14
|
+
const PROXY_VARIABLES = Object.freeze([
|
|
15
|
+
'HTTPS_PROXY',
|
|
16
|
+
'https_proxy',
|
|
17
|
+
'HTTP_PROXY',
|
|
18
|
+
'http_proxy',
|
|
19
|
+
'ALL_PROXY',
|
|
20
|
+
'all_proxy',
|
|
21
|
+
]);
|
|
22
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
|
|
23
|
+
const PUBLIC_REFERENCE_PATTERN = /^DT-CONN-[A-F0-9]{8}$/;
|
|
24
|
+
let installedDependencyVersions;
|
|
25
|
+
|
|
26
|
+
function nonEmptyString(value, maxLength = 500) {
|
|
27
|
+
if (typeof value !== 'string') return null;
|
|
28
|
+
const text = value.trim();
|
|
29
|
+
return text ? text.slice(0, maxLength) : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeVersion(value) {
|
|
33
|
+
const version = nonEmptyString(value, 80);
|
|
34
|
+
return version && VERSION_PATTERN.test(version) ? version : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolvedPackage(name, from = require) {
|
|
38
|
+
try {
|
|
39
|
+
const packagePath = from.resolve(`${name}/package.json`);
|
|
40
|
+
return {
|
|
41
|
+
version: safeVersion(from(packagePath)?.version),
|
|
42
|
+
require: createRequire(packagePath),
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Returns only non-sensitive versions involved in the external DingTalk Stream dependency chain. */
|
|
50
|
+
export function installedDingtalkConnectionDependencies() {
|
|
51
|
+
if (installedDependencyVersions) return installedDependencyVersions;
|
|
52
|
+
const dingtalkStream = resolvedPackage('dingtalk-stream');
|
|
53
|
+
const axios = dingtalkStream ? resolvedPackage('axios', dingtalkStream.require) : null;
|
|
54
|
+
const httpsProxyAgent = axios ? resolvedPackage('https-proxy-agent', axios.require) : null;
|
|
55
|
+
const agentBase = httpsProxyAgent ? resolvedPackage('agent-base', httpsProxyAgent.require) : null;
|
|
56
|
+
installedDependencyVersions = Object.freeze({
|
|
57
|
+
dingtalkStream: dingtalkStream?.version ?? null,
|
|
58
|
+
axios: axios?.version ?? null,
|
|
59
|
+
httpsProxyAgent: httpsProxyAgent?.version ?? null,
|
|
60
|
+
agentBase: agentBase?.version ?? null,
|
|
61
|
+
});
|
|
62
|
+
return installedDependencyVersions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function errorChain(error) {
|
|
66
|
+
const chain = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
let current = error;
|
|
69
|
+
while (current && typeof current === 'object' && chain.length < 4 && !seen.has(current)) {
|
|
70
|
+
seen.add(current);
|
|
71
|
+
chain.push(current);
|
|
72
|
+
current = current.cause;
|
|
73
|
+
}
|
|
74
|
+
return chain;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function statusFrom(error) {
|
|
78
|
+
if (Number.isInteger(error?.status)) return error.status;
|
|
79
|
+
if (Number.isInteger(error?.statusCode)) return error.statusCode;
|
|
80
|
+
if (Number.isInteger(error?.response?.status)) return error.response.status;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function providerCodeFrom(error) {
|
|
85
|
+
const value = nonEmptyString(
|
|
86
|
+
error?.providerCode
|
|
87
|
+
?? error?.response?.data?.code
|
|
88
|
+
?? error?.response?.data?.errorCode
|
|
89
|
+
?? error?.response?.data?.errcode,
|
|
90
|
+
100,
|
|
91
|
+
);
|
|
92
|
+
return value && /^[A-Za-z0-9_.:-]+$/.test(value) ? value : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function redactMessage(value, sensitiveValues) {
|
|
96
|
+
let message = nonEmptyString(value);
|
|
97
|
+
if (!message) return null;
|
|
98
|
+
for (const sensitive of sensitiveValues) {
|
|
99
|
+
const text = nonEmptyString(sensitive, 2_048);
|
|
100
|
+
if (text && text.length >= 4) message = message.replaceAll(text, '••••');
|
|
101
|
+
}
|
|
102
|
+
return message
|
|
103
|
+
.replace(/(https?:\/\/)[^/\s@]+@/giu, '$1••••@')
|
|
104
|
+
.replace(/([?&](?:appsecret|client_secret|clientsecret|access_token|token|password)=)[^&\s]*/giu, '$1••••')
|
|
105
|
+
.replace(/((?:app|client|access)[_-]?secret|authorization|password|token)\s*[=:]\s*[^\s,;,。]+/giu, '$1=••••')
|
|
106
|
+
.replace(/\b[A-Za-z0-9_.-]*secret[A-Za-z0-9_.-]*\b/giu, '••••')
|
|
107
|
+
.slice(0, 500);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function safeDependencyVersions(value) {
|
|
111
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
112
|
+
return {
|
|
113
|
+
dingtalkStream: safeVersion(source.dingtalkStream),
|
|
114
|
+
axios: safeVersion(source.axios),
|
|
115
|
+
httpsProxyAgent: safeVersion(source.httpsProxyAgent),
|
|
116
|
+
agentBase: safeVersion(source.agentBase),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function diagnosticErrors(chain, sensitiveValues) {
|
|
121
|
+
return chain.map((error) => {
|
|
122
|
+
const name = nonEmptyString(error?.name, 80);
|
|
123
|
+
const code = nonEmptyString(error?.code, 100);
|
|
124
|
+
const status = statusFrom(error);
|
|
125
|
+
const providerCode = providerCodeFrom(error);
|
|
126
|
+
const message = redactMessage(error?.message, sensitiveValues);
|
|
127
|
+
return {
|
|
128
|
+
...(name ? { name } : {}),
|
|
129
|
+
...(code ? { code } : {}),
|
|
130
|
+
...(status !== null ? { status } : {}),
|
|
131
|
+
...(providerCode ? { providerCode } : {}),
|
|
132
|
+
...(message ? { message } : {}),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function publicReference(value) {
|
|
138
|
+
return PUBLIC_REFERENCE_PATTERN.test(value ?? '')
|
|
139
|
+
? value
|
|
140
|
+
: `DT-CONN-${randomUUID().replaceAll('-', '').slice(0, 8).toUpperCase()}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function fixedPublicError(code, message, hint, referenceId) {
|
|
144
|
+
return Object.freeze({ code, message: t(message), hint: t(hint), referenceId });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Adds a stable startup stage without discarding the original exception as `cause`. */
|
|
148
|
+
export function dingtalkRuntimeStartError(code, cause) {
|
|
149
|
+
if (cause?.name === 'AbortError' || STAGE_CODES.has(cause?.code)) return cause;
|
|
150
|
+
const error = new Error(
|
|
151
|
+
nonEmptyString(cause?.message) ?? 'DingTalk runtime startup failed',
|
|
152
|
+
{ cause },
|
|
153
|
+
);
|
|
154
|
+
error.name = 'DingtalkRuntimeStartError';
|
|
155
|
+
error.code = STAGE_CODES.has(code) ? code : 'dingtalk-runtime-prepare-failed';
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Creates browser-safe guidance plus a redacted Host-log diagnostic for one connection failure. */
|
|
160
|
+
export function describeDingtalkConnectionFailure(error, {
|
|
161
|
+
fallbackMessage = '钉钉连接未就绪,请稍后重试。',
|
|
162
|
+
clientId,
|
|
163
|
+
clientSecret,
|
|
164
|
+
environment = process.env,
|
|
165
|
+
dependencies = installedDingtalkConnectionDependencies(),
|
|
166
|
+
nodeVersion = process.versions.node,
|
|
167
|
+
referenceId: suppliedReferenceId,
|
|
168
|
+
} = {}) {
|
|
169
|
+
const referenceId = publicReference(suppliedReferenceId);
|
|
170
|
+
const chain = errorChain(error);
|
|
171
|
+
const codes = new Set(chain
|
|
172
|
+
.map((entry) => nonEmptyString(entry?.code, 100)?.toUpperCase())
|
|
173
|
+
.filter(Boolean));
|
|
174
|
+
const messages = chain
|
|
175
|
+
.map((entry) => nonEmptyString(entry?.message)?.toLowerCase())
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
const statuses = chain.map(statusFrom).filter((status) => status !== null);
|
|
178
|
+
const stage = chain.map((entry) => entry?.code).find((code) => STAGE_CODES.has(code)) ?? null;
|
|
179
|
+
const proxyVariables = PROXY_VARIABLES.filter((name) => nonEmptyString(environment?.[name], 4_096));
|
|
180
|
+
const proxyConfigured = proxyVariables.length > 0;
|
|
181
|
+
const versions = safeDependencyVersions(dependencies);
|
|
182
|
+
const messageContains = (pattern) => messages.some((message) => pattern.test(message));
|
|
183
|
+
const codeContains = (pattern) => [...codes].some((code) => pattern.test(code));
|
|
184
|
+
const proxyFailureSignal = statuses.includes(407)
|
|
185
|
+
|| codeContains(/(?:PROXY|ERR_INVALID_PROTOCOL)/u)
|
|
186
|
+
|| messageContains(/proxy|tunneling socket/u);
|
|
187
|
+
|
|
188
|
+
let publicError;
|
|
189
|
+
if (stage === 'dingtalk-stream-connect-failed'
|
|
190
|
+
&& proxyConfigured
|
|
191
|
+
&& versions.agentBase === '6.0.0') {
|
|
192
|
+
publicError = fixedPublicError(
|
|
193
|
+
'stream-proxy-dependency-incompatible',
|
|
194
|
+
'钉钉 Stream 连接失败:检测到代理依赖 agent-base 6.0.0。',
|
|
195
|
+
'请将 DSH profile 中的 agent-base@6 固定为 6.0.2 后重新安装依赖,或升级 pnpm 后重新解析 lockfile。',
|
|
196
|
+
referenceId,
|
|
197
|
+
);
|
|
198
|
+
} else if (stage === 'dingtalk-harness-connect-failed') {
|
|
199
|
+
publicError = fixedPublicError(
|
|
200
|
+
'harness-unavailable',
|
|
201
|
+
'插件无法连接本机 Harness。',
|
|
202
|
+
'请确认 dsh web 正常运行,并查看 dsh web 日志中相同参考号对应的诊断信息。',
|
|
203
|
+
referenceId,
|
|
204
|
+
);
|
|
205
|
+
} else if (stage === 'dingtalk-stream-client-load-failed') {
|
|
206
|
+
publicError = fixedPublicError(
|
|
207
|
+
'stream-sdk-load-failed',
|
|
208
|
+
'钉钉 Stream SDK 加载失败。',
|
|
209
|
+
'请重新安装当前 DSH profile 的插件依赖,并查看 dsh web 日志中相同参考号对应的诊断信息。',
|
|
210
|
+
referenceId,
|
|
211
|
+
);
|
|
212
|
+
} else if (statuses.some((status) => status === 401 || status === 403)) {
|
|
213
|
+
publicError = fixedPublicError(
|
|
214
|
+
'stream-credentials-rejected',
|
|
215
|
+
'钉钉拒绝了当前应用凭据。',
|
|
216
|
+
'请核对 Client ID、Client Secret 和机器人权限后重试。',
|
|
217
|
+
referenceId,
|
|
218
|
+
);
|
|
219
|
+
} else if (codeContains(/(?:TIMEOUT|ETIMEDOUT)/u) || messageContains(/timed? out|timeout/u)) {
|
|
220
|
+
publicError = fixedPublicError(
|
|
221
|
+
'stream-handshake-timeout',
|
|
222
|
+
'连接钉钉 Stream 超时。',
|
|
223
|
+
'请检查网络、代理和防火墙后重试;详细原因可在 dsh web 日志中按参考号查找。',
|
|
224
|
+
referenceId,
|
|
225
|
+
);
|
|
226
|
+
} else if (codeContains(/^(?:ENOTFOUND|EAI_AGAIN)$/u)) {
|
|
227
|
+
publicError = fixedPublicError(
|
|
228
|
+
'stream-dns-failed',
|
|
229
|
+
'无法解析钉钉服务地址。',
|
|
230
|
+
'请检查 DNS、网络和代理设置;详细原因可在 dsh web 日志中按参考号查找。',
|
|
231
|
+
referenceId,
|
|
232
|
+
);
|
|
233
|
+
} else if (codeContains(/(?:CERT|TLS|SSL|UNABLE_TO_VERIFY)/u)) {
|
|
234
|
+
publicError = fixedPublicError(
|
|
235
|
+
'stream-tls-failed',
|
|
236
|
+
'钉钉 Stream 的 TLS 连接校验失败。',
|
|
237
|
+
'请检查系统证书、代理证书或 HTTPS 中间代理;详细原因可在 dsh web 日志中按参考号查找。',
|
|
238
|
+
referenceId,
|
|
239
|
+
);
|
|
240
|
+
} else if (stage === 'dingtalk-stream-connect-failed'
|
|
241
|
+
&& proxyFailureSignal) {
|
|
242
|
+
publicError = fixedPublicError(
|
|
243
|
+
'stream-proxy-failed',
|
|
244
|
+
'钉钉 Stream 无法通过当前代理建立连接。',
|
|
245
|
+
'请检查 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 和代理连通性;详细原因可在 dsh web 日志中按参考号查找。',
|
|
246
|
+
referenceId,
|
|
247
|
+
);
|
|
248
|
+
} else if (stage === 'dingtalk-stream-connect-failed') {
|
|
249
|
+
publicError = fixedPublicError(
|
|
250
|
+
'stream-connect-failed',
|
|
251
|
+
'钉钉 Stream 消息连接建立失败。',
|
|
252
|
+
'请检查网络和机器人配置;详细原因可在 dsh web 日志中按参考号查找。',
|
|
253
|
+
referenceId,
|
|
254
|
+
);
|
|
255
|
+
} else if (stage === 'dingtalk-stream-listener-failed') {
|
|
256
|
+
publicError = fixedPublicError(
|
|
257
|
+
'stream-listener-failed',
|
|
258
|
+
'钉钉 Stream 消息监听初始化失败。',
|
|
259
|
+
'请确认 dsh-im 与 dingtalk-stream 版本兼容,并按参考号查看 dsh web 日志。',
|
|
260
|
+
referenceId,
|
|
261
|
+
);
|
|
262
|
+
} else if (stage === 'dingtalk-runtime-prepare-failed') {
|
|
263
|
+
publicError = fixedPublicError(
|
|
264
|
+
'runtime-prepare-failed',
|
|
265
|
+
'钉钉机器人运行环境初始化失败。',
|
|
266
|
+
'请检查 DSH 数据目录、工作区和插件依赖,并按参考号查看 dsh web 日志。',
|
|
267
|
+
referenceId,
|
|
268
|
+
);
|
|
269
|
+
} else {
|
|
270
|
+
publicError = fixedPublicError(
|
|
271
|
+
'connection-failed',
|
|
272
|
+
fallbackMessage,
|
|
273
|
+
'请在 dsh web 日志中查找相同参考号,以获取已脱敏的具体错误。',
|
|
274
|
+
referenceId,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return Object.freeze({
|
|
279
|
+
publicError,
|
|
280
|
+
diagnostic: Object.freeze({
|
|
281
|
+
referenceId,
|
|
282
|
+
category: publicError.code,
|
|
283
|
+
stage,
|
|
284
|
+
runtime: { node: safeVersion(nodeVersion) },
|
|
285
|
+
proxy: { configured: proxyConfigured, variables: proxyVariables },
|
|
286
|
+
dependencies: versions,
|
|
287
|
+
errors: diagnosticErrors(chain, [clientId, clientSecret]),
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Carries only a pre-built public projection across the Host RPC boundary. */
|
|
293
|
+
export function dingtalkPublicConnectionError(publicError, cause) {
|
|
294
|
+
const error = new Error(publicError.message, { cause });
|
|
295
|
+
error.name = 'DingtalkPublicConnectionError';
|
|
296
|
+
error.code = publicError.code;
|
|
297
|
+
error.publicError = structuredClone(publicError);
|
|
298
|
+
return error;
|
|
299
|
+
}
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
} from '../shared/connection-test.mjs';
|
|
13
13
|
import { t } from '../shared/i18n.mjs';
|
|
14
14
|
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
15
|
+
import {
|
|
16
|
+
describeDingtalkConnectionFailure,
|
|
17
|
+
dingtalkPublicConnectionError,
|
|
18
|
+
dingtalkRuntimeStartError,
|
|
19
|
+
} from './connection-error.mjs';
|
|
15
20
|
|
|
16
21
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
|
17
22
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
|
@@ -216,12 +221,14 @@ export class DingtalkController {
|
|
|
216
221
|
try {
|
|
217
222
|
await this.#startRuntime(latest, clientSecret);
|
|
218
223
|
this.#errors.delete(latest.botId);
|
|
219
|
-
} catch {
|
|
220
|
-
this.#
|
|
221
|
-
latest
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.#rememberConnectionFailure({
|
|
226
|
+
config: latest,
|
|
227
|
+
clientSecret,
|
|
228
|
+
error,
|
|
229
|
+
fallbackMessage: '钉钉连接未就绪,请稍后重试。',
|
|
230
|
+
context: `bot ${latest.botId} failed to initialize`,
|
|
231
|
+
});
|
|
225
232
|
}
|
|
226
233
|
this.#touch();
|
|
227
234
|
});
|
|
@@ -318,12 +325,14 @@ export class DingtalkController {
|
|
|
318
325
|
try {
|
|
319
326
|
await this.#startRuntime(config, normalizedSecret);
|
|
320
327
|
this.#errors.delete(identity.botId);
|
|
321
|
-
} catch {
|
|
322
|
-
this.#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this.#rememberConnectionFailure({
|
|
330
|
+
config,
|
|
331
|
+
clientSecret: normalizedSecret,
|
|
332
|
+
error,
|
|
333
|
+
fallbackMessage: '钉钉已接入,但消息连接暂未就绪,请稍后重试。',
|
|
334
|
+
context: `credential-bound bot ${identity.botId} is not ready`,
|
|
335
|
+
});
|
|
327
336
|
}
|
|
328
337
|
this.#touch();
|
|
329
338
|
});
|
|
@@ -380,11 +389,14 @@ export class DingtalkController {
|
|
|
380
389
|
await this.#startRuntime(config, clientSecret);
|
|
381
390
|
this.#errors.delete(botId);
|
|
382
391
|
} catch (error) {
|
|
383
|
-
this.#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
const publicError = this.#rememberConnectionFailure({
|
|
393
|
+
config,
|
|
394
|
+
clientSecret,
|
|
395
|
+
error,
|
|
396
|
+
fallbackMessage: '钉钉连接仍未就绪,请稍后重试。',
|
|
397
|
+
context: `bot ${botId} failed to reconnect`,
|
|
398
|
+
});
|
|
399
|
+
throw dingtalkPublicConnectionError(publicError, error);
|
|
388
400
|
} finally {
|
|
389
401
|
this.#touch();
|
|
390
402
|
}
|
|
@@ -675,11 +687,13 @@ export class DingtalkController {
|
|
|
675
687
|
await rollback();
|
|
676
688
|
throw abortError();
|
|
677
689
|
}
|
|
678
|
-
this.#
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
690
|
+
this.#rememberConnectionFailure({
|
|
691
|
+
config,
|
|
692
|
+
clientSecret,
|
|
693
|
+
error,
|
|
694
|
+
fallbackMessage: '钉钉已接入,但消息连接暂未就绪,请稍后重试。',
|
|
695
|
+
context: `authorized bot ${identity.botId} is not ready`,
|
|
696
|
+
});
|
|
683
697
|
}
|
|
684
698
|
return { botId: identity.botId, alreadyConnected: Boolean(previousConfig) };
|
|
685
699
|
});
|
|
@@ -696,11 +710,14 @@ export class DingtalkController {
|
|
|
696
710
|
} catch (error) {
|
|
697
711
|
await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
698
712
|
await this.#startRuntime(previousConfig, clientSecret).catch(() => undefined);
|
|
699
|
-
this.#
|
|
700
|
-
previousConfig
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
713
|
+
const publicError = this.#rememberConnectionFailure({
|
|
714
|
+
config: previousConfig,
|
|
715
|
+
clientSecret,
|
|
716
|
+
error,
|
|
717
|
+
fallbackMessage: '钉钉连接未就绪,请稍后重试。',
|
|
718
|
+
context: `bot ${previousConfig.botId} failed to apply updated settings`,
|
|
719
|
+
});
|
|
720
|
+
throw dingtalkPublicConnectionError(publicError, error);
|
|
704
721
|
} finally {
|
|
705
722
|
this.#touch();
|
|
706
723
|
}
|
|
@@ -711,13 +728,21 @@ export class DingtalkController {
|
|
|
711
728
|
if (this.#closed) throw abortError();
|
|
712
729
|
await this.#stopRuntime(config.botId);
|
|
713
730
|
if (this.#closed) throw abortError();
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
731
|
+
let runtime;
|
|
732
|
+
try {
|
|
733
|
+
runtime = await this.#createRuntime({
|
|
734
|
+
botId: config.botId,
|
|
735
|
+
config: structuredClone(config),
|
|
736
|
+
clientSecret,
|
|
737
|
+
});
|
|
738
|
+
} catch (error) {
|
|
739
|
+
throw dingtalkRuntimeStartError('dingtalk-runtime-prepare-failed', error);
|
|
740
|
+
}
|
|
719
741
|
if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
|
|
720
|
-
throw
|
|
742
|
+
throw dingtalkRuntimeStartError(
|
|
743
|
+
'dingtalk-runtime-prepare-failed',
|
|
744
|
+
new TypeError('createRuntime returned an invalid DingTalk runtime'),
|
|
745
|
+
);
|
|
721
746
|
}
|
|
722
747
|
if (this.#closed) {
|
|
723
748
|
await runtime.stop().catch(() => undefined);
|
|
@@ -733,10 +758,30 @@ export class DingtalkController {
|
|
|
733
758
|
} catch (error) {
|
|
734
759
|
if (this.#runtimes.get(config.botId) === runtime) this.#runtimes.delete(config.botId);
|
|
735
760
|
await runtime.stop().catch(() => undefined);
|
|
736
|
-
throw error;
|
|
761
|
+
throw dingtalkRuntimeStartError('dingtalk-stream-connect-failed', error);
|
|
737
762
|
}
|
|
738
763
|
}
|
|
739
764
|
|
|
765
|
+
#rememberConnectionFailure({
|
|
766
|
+
config,
|
|
767
|
+
clientSecret,
|
|
768
|
+
error,
|
|
769
|
+
fallbackMessage,
|
|
770
|
+
context,
|
|
771
|
+
}) {
|
|
772
|
+
const failure = describeDingtalkConnectionFailure(error, {
|
|
773
|
+
fallbackMessage,
|
|
774
|
+
clientId: config.clientId,
|
|
775
|
+
clientSecret,
|
|
776
|
+
});
|
|
777
|
+
this.#errors.set(config.botId, failure.publicError);
|
|
778
|
+
this.#logger.error?.(
|
|
779
|
+
`[dsh-dingtalk] ${context} [${failure.publicError.referenceId}]`,
|
|
780
|
+
failure.diagnostic,
|
|
781
|
+
);
|
|
782
|
+
return failure.publicError;
|
|
783
|
+
}
|
|
784
|
+
|
|
740
785
|
async #stopRuntime(botId) {
|
|
741
786
|
const runtime = this.#runtimes.get(botId);
|
|
742
787
|
this.#runtimes.delete(botId);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
7
7
|
import { t } from '../shared/i18n.mjs';
|
|
8
8
|
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
|
|
9
|
+
import { dingtalkRuntimeStartError } from './connection-error.mjs';
|
|
9
10
|
|
|
10
11
|
function nonEmptyString(value) {
|
|
11
12
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -223,10 +224,12 @@ export class DingtalkRuntime {
|
|
|
223
224
|
this.#status.startedAt = new Date().toISOString();
|
|
224
225
|
this.#status.dingtalkStreamState = 'connecting';
|
|
225
226
|
this.#status.lastError = null;
|
|
227
|
+
let startStage = 'dingtalk-harness-connect-failed';
|
|
226
228
|
|
|
227
229
|
try {
|
|
228
230
|
await this.#harness.ensureRunning({ signal });
|
|
229
231
|
this.#status.harnessReachable = true;
|
|
232
|
+
startStage = 'dingtalk-runtime-prepare-failed';
|
|
230
233
|
if (typeof this.#state.removePendingSenderByStaffId === 'function') {
|
|
231
234
|
for (const staffId of approvedSenderIds(this.#config)) {
|
|
232
235
|
await this.#state.removePendingSenderByStaffId(staffId);
|
|
@@ -249,6 +252,7 @@ export class DingtalkRuntime {
|
|
|
249
252
|
signal,
|
|
250
253
|
});
|
|
251
254
|
|
|
255
|
+
startStage = 'dingtalk-stream-client-load-failed';
|
|
252
256
|
const created = await this.#streamFactory({
|
|
253
257
|
clientId: this.#config.clientId,
|
|
254
258
|
clientSecret: this.#clientSecret,
|
|
@@ -266,6 +270,7 @@ export class DingtalkRuntime {
|
|
|
266
270
|
|
|
267
271
|
const client = this.#client;
|
|
268
272
|
const bridge = this.#bridge;
|
|
273
|
+
startStage = 'dingtalk-stream-listener-failed';
|
|
269
274
|
client.registerCallbackListener(this.#topic, (response) => {
|
|
270
275
|
if (this.#client !== client || this.#bridge !== bridge) return;
|
|
271
276
|
const callbackMessageId = nonEmptyString(response?.headers?.messageId);
|
|
@@ -314,6 +319,7 @@ export class DingtalkRuntime {
|
|
|
314
319
|
this.#callbackTasks.add(task);
|
|
315
320
|
});
|
|
316
321
|
|
|
322
|
+
startStage = 'dingtalk-stream-connect-failed';
|
|
317
323
|
await connectStream(
|
|
318
324
|
client,
|
|
319
325
|
this.#connectTimeoutMs,
|
|
@@ -336,11 +342,12 @@ export class DingtalkRuntime {
|
|
|
336
342
|
return this.status;
|
|
337
343
|
} catch (error) {
|
|
338
344
|
const aborted = signal.aborted;
|
|
345
|
+
const failure = aborted ? error : dingtalkRuntimeStartError(startStage, error);
|
|
339
346
|
this.#status.ready = false;
|
|
340
347
|
this.#status.dingtalkStreamState = aborted ? 'idle' : 'failed';
|
|
341
|
-
this.#status.lastError = aborted ? null : (
|
|
348
|
+
this.#status.lastError = aborted ? null : (failure?.message ?? String(failure));
|
|
342
349
|
await this.stop({ preserveError: !aborted });
|
|
343
|
-
throw
|
|
350
|
+
throw failure;
|
|
344
351
|
}
|
|
345
352
|
}
|
|
346
353
|
|