@xmanrui/dsh-im 4.9.0 → 4.9.1
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/lib/client.js +70 -18
- package/lib/index.js +235 -235
- package/package.json +10 -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/i18n.js +4 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +24 -0
- package/plugin-src/host/modern-harness-api.mjs +17 -8
- 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/shared/i18n-en/dingtalk.mjs +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.1",
|
|
4
4
|
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -22,10 +22,7 @@
|
|
|
22
22
|
"whatsapp",
|
|
23
23
|
"ai-office"
|
|
24
24
|
],
|
|
25
|
-
"author":
|
|
26
|
-
"name": "xmanrui",
|
|
27
|
-
"url": "https://github.com/xmanrui"
|
|
28
|
-
},
|
|
25
|
+
"author": "xmanrui (https://github.com/xmanrui)",
|
|
29
26
|
"contributors": [
|
|
30
27
|
{
|
|
31
28
|
"name": "C3H3-AI",
|
|
@@ -47,7 +44,9 @@
|
|
|
47
44
|
"url": "git+https://github.com/xmanrui/dsh-im.git"
|
|
48
45
|
},
|
|
49
46
|
"homepage": "https://github.com/xmanrui/dsh-im#readme",
|
|
50
|
-
"bugs":
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/xmanrui/dsh-im/issues"
|
|
49
|
+
},
|
|
51
50
|
"publishConfig": {
|
|
52
51
|
"access": "public"
|
|
53
52
|
},
|
|
@@ -116,5 +115,10 @@
|
|
|
116
115
|
"react-dom": "18.3.1",
|
|
117
116
|
"react-test-renderer": "18.3.1",
|
|
118
117
|
"semver": "7.8.5"
|
|
118
|
+
},
|
|
119
|
+
"directories": {
|
|
120
|
+
"doc": "docs",
|
|
121
|
+
"lib": "lib",
|
|
122
|
+
"test": "test"
|
|
119
123
|
}
|
|
120
124
|
}
|
|
@@ -85,11 +85,20 @@ function sanitizeMessage(value, fallback) {
|
|
|
85
85
|
return message.replace(/([=:]\s*)[^\s,;,。]+/g, '$1••••••').slice(0, 240);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function safeReferenceId(value) {
|
|
89
|
+
const referenceId = optionalString(value, 40);
|
|
90
|
+
return referenceId && /^DT-CONN-[A-F0-9]{8}$/.test(referenceId) ? referenceId : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
function normalizeError(value, fallbackCode, fallbackMessage) {
|
|
89
94
|
if (!isRecord(value)) return undefined;
|
|
95
|
+
const hint = sanitizeMessage(value.hint, '');
|
|
96
|
+
const referenceId = safeReferenceId(value.referenceId);
|
|
90
97
|
return {
|
|
91
98
|
code: safeErrorCode(value.code, fallbackCode),
|
|
92
99
|
message: sanitizeMessage(value.message, fallbackMessage),
|
|
100
|
+
...(hint ? { hint } : {}),
|
|
101
|
+
...(referenceId ? { referenceId } : {}),
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -108,8 +117,15 @@ export function unwrapRpcResult(result) {
|
|
|
108
117
|
throw new Error('钉钉服务返回了无法识别的响应');
|
|
109
118
|
}
|
|
110
119
|
if (!result.ok) {
|
|
111
|
-
const
|
|
112
|
-
|
|
120
|
+
const visible = normalizeError(
|
|
121
|
+
result.error,
|
|
122
|
+
'DINGTALK_RPC_ERROR',
|
|
123
|
+
'钉钉操作失败',
|
|
124
|
+
) ?? { code: 'DINGTALK_RPC_ERROR', message: '钉钉操作失败' };
|
|
125
|
+
const error = new Error(visible.message);
|
|
126
|
+
error.code = visible.code;
|
|
127
|
+
if (visible.hint) error.hint = visible.hint;
|
|
128
|
+
if (visible.referenceId) error.referenceId = visible.referenceId;
|
|
113
129
|
throw error;
|
|
114
130
|
}
|
|
115
131
|
return result.value;
|
|
@@ -227,10 +243,11 @@ export function connectionTestFeedback(result) {
|
|
|
227
243
|
}
|
|
228
244
|
|
|
229
245
|
export function presentError(error) {
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
246
|
+
return normalizeError(
|
|
247
|
+
error,
|
|
248
|
+
'DINGTALK_ERROR',
|
|
249
|
+
'钉钉操作失败,请稍后重试',
|
|
250
|
+
) ?? { code: 'DINGTALK_ERROR', message: '钉钉操作失败,请稍后重试' };
|
|
234
251
|
}
|
|
235
252
|
|
|
236
253
|
export function formatRemaining(milliseconds) {
|
|
@@ -169,19 +169,36 @@ function ProgressPanel({ status, busy, onCancel }) {
|
|
|
169
169
|
h(Button, { onClick: onCancel, disabled: busy }, '取消接入')));
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
function ConnectionErrorDiagnostic({ error }) {
|
|
173
|
+
if (!error) return null;
|
|
174
|
+
return h('div', { className: 'ddt-errorDiagnostic' },
|
|
175
|
+
error.hint ? h('p', { className: 'ddt-errorHint' }, error.hint) : null,
|
|
176
|
+
h('span', { className: 'ddt-errorCode' },
|
|
177
|
+
h('span', null, '错误码'), `: ${error.code}`,
|
|
178
|
+
error.referenceId
|
|
179
|
+
? h(React.Fragment, null, ' · ', h('span', null, '参考号'), `: ${error.referenceId}`)
|
|
180
|
+
: null));
|
|
181
|
+
}
|
|
182
|
+
|
|
172
183
|
function ProvisionError({ provision, busy, onRetry, onClose }) {
|
|
173
184
|
const error = provision.error ?? {
|
|
174
185
|
code: 'DINGTALK_PROVISION_FAILED',
|
|
175
186
|
message: '钉钉机器人没有接入完成',
|
|
176
187
|
};
|
|
188
|
+
const connectionFailed = Boolean(error.referenceId);
|
|
177
189
|
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
|
178
190
|
h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
|
|
179
|
-
h('h3', null, provision.status === 'expired'
|
|
191
|
+
h('h3', null, provision.status === 'expired'
|
|
192
|
+
? '二维码已过期'
|
|
193
|
+
: connectionFailed ? '机器人已保存,但连接未就绪' : '钉钉机器人没有接入完成'),
|
|
180
194
|
h('p', null, error.message),
|
|
181
|
-
h(
|
|
195
|
+
h(ConnectionErrorDiagnostic, { error }),
|
|
182
196
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
183
|
-
|
|
184
|
-
|
|
197
|
+
connectionFailed
|
|
198
|
+
? h(Button, { kind: 'primary', onClick: onClose, disabled: busy }, '查看已保存的机器人')
|
|
199
|
+
: h(React.Fragment, null,
|
|
200
|
+
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
|
201
|
+
h(Button, { onClick: onClose, disabled: busy }, '关闭')))));
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
function checkedTime(value) {
|
|
@@ -278,6 +295,7 @@ export function AccountCard({
|
|
|
278
295
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) },
|
|
279
296
|
'移除接入')),
|
|
280
297
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
298
|
+
account.error ? h(ConnectionErrorDiagnostic, { error: account.error }) : null,
|
|
281
299
|
account.lastMessageError ? h(LastMessageErrorSummary, {
|
|
282
300
|
className: 'ddt-summary',
|
|
283
301
|
error: account.lastMessageError,
|
|
@@ -614,6 +632,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
614
632
|
? snapshot?.bots.find((bot) => bot.botId === result.botId)
|
|
615
633
|
: snapshot?.bots.find((bot) => bot.connected);
|
|
616
634
|
if (!account?.connected) {
|
|
635
|
+
if (account?.error) {
|
|
636
|
+
setProvision((current) => current?.attemptId === attemptId
|
|
637
|
+
? { ...current, ...result, status: 'failed', error: account.error }
|
|
638
|
+
: current);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
617
641
|
setProvision((current) => current?.attemptId === attemptId
|
|
618
642
|
? { ...current, ...result, status: 'connecting' }
|
|
619
643
|
: current);
|
|
@@ -698,9 +722,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
698
722
|
return snapshot;
|
|
699
723
|
} catch (error) {
|
|
700
724
|
if (!mountedRef.current) return undefined;
|
|
725
|
+
const visibleError = presentError(error);
|
|
701
726
|
const failureMessage = operation === 'reconnect'
|
|
702
|
-
?
|
|
703
|
-
|
|
727
|
+
? visibleError.referenceId
|
|
728
|
+
? `连接检查失败:${visibleError.message}(参考号:${visibleError.referenceId})`
|
|
729
|
+
: '连接检查失败,请稍后重试。'
|
|
730
|
+
: `操作失败:${visibleError.message}`;
|
|
704
731
|
if (operation === 'reconnect') {
|
|
705
732
|
setFeedbackByBot((current) => ({
|
|
706
733
|
...current,
|
|
@@ -77,6 +77,8 @@ const CSS = String.raw`
|
|
|
77
77
|
.ddt-inlineError h3 { font-size: 17px; }
|
|
78
78
|
.ddt-inlineError p { line-height: 1.55; }
|
|
79
79
|
.ddt-errorCode { font: 11px ui-monospace, SFMono-Regular, monospace; opacity: .8; }
|
|
80
|
+
.ddt-errorDiagnostic { display: grid; gap: 5px; color: var(--ddt-error); }
|
|
81
|
+
.ddt-errorHint { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.55; }
|
|
80
82
|
.ddt-listHeading { display: flex; align-items: center; justify-content: space-between; margin: 2px 0 9px; }
|
|
81
83
|
.ddt-listHeading h3 { margin: 0; font-size: 14px; }
|
|
82
84
|
.ddt-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }
|
|
@@ -351,6 +351,8 @@ const EN = Object.freeze({
|
|
|
351
351
|
'检查连接': 'Check connection',
|
|
352
352
|
'检查中…': 'Checking…',
|
|
353
353
|
'连接检查失败,请稍后重试。': 'Connection check failed. Try again later.',
|
|
354
|
+
'机器人已保存,但连接未就绪': 'The bot was saved, but the connection is not ready',
|
|
355
|
+
'查看已保存的机器人': 'View saved bot',
|
|
354
356
|
'测试消息已发送,请到对应机器人会话中确认。': 'Test message sent. Check the matching bot conversation.',
|
|
355
357
|
'连接检查完成。机器人尚未收到可用于测试的私聊消息。': 'Connection check completed. The bot has not received a direct message it can use for testing.',
|
|
356
358
|
'连接检查完成,但测试消息发送失败。': 'Connection check completed, but the test message could not be sent.',
|
|
@@ -921,6 +923,8 @@ function translateDynamic(text) {
|
|
|
921
923
|
if (match) return `Automatic status refresh failed: ${match[1]}`;
|
|
922
924
|
match = /^操作失败:(.+)$/.exec(text);
|
|
923
925
|
if (match) return `Operation failed: ${match[1]}`;
|
|
926
|
+
match = /^连接检查失败:(.+)(参考号:(.+))$/.exec(text);
|
|
927
|
+
if (match) return `Connection check failed: ${localizeText(match[1])} (reference: ${match[2]})`;
|
|
924
928
|
match = /^连接检查失败:(.+)$/.exec(text);
|
|
925
929
|
if (match) return `Connection check failed: ${match[1]}`;
|
|
926
930
|
match = /^移除失败:(.+)$/.exec(text);
|
|
@@ -140,6 +140,27 @@ function internalFailure() {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function publicConnectionFailure(error) {
|
|
144
|
+
if (error?.name !== 'DingtalkPublicConnectionError' || !isRecord(error.publicError)) return null;
|
|
145
|
+
const source = error.publicError;
|
|
146
|
+
const code = typeof source.code === 'string' && /^[a-z][a-z\d-]{1,79}$/.test(source.code)
|
|
147
|
+
? source.code
|
|
148
|
+
: null;
|
|
149
|
+
const message = typeof source.message === 'string' && source.message.trim()
|
|
150
|
+
? source.message.trim().slice(0, 240)
|
|
151
|
+
: null;
|
|
152
|
+
const hint = typeof source.hint === 'string' && source.hint.trim()
|
|
153
|
+
? source.hint.trim().slice(0, 480)
|
|
154
|
+
: null;
|
|
155
|
+
const referenceId = typeof source.referenceId === 'string'
|
|
156
|
+
&& /^DT-CONN-[A-F0-9]{8}$/.test(source.referenceId)
|
|
157
|
+
? source.referenceId
|
|
158
|
+
: null;
|
|
159
|
+
return code && message && hint && referenceId
|
|
160
|
+
? { code, message, hint, referenceId }
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
function sanitizePublic(value) {
|
|
144
165
|
if (Array.isArray(value)) return value.map(sanitizePublic);
|
|
145
166
|
if (!isRecord(value)) return value;
|
|
@@ -293,8 +314,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
293
314
|
return signal?.aborted ? cancelled() : { ok: true, value };
|
|
294
315
|
} catch (error) {
|
|
295
316
|
const workspaceError = publicWorkspaceError(error);
|
|
317
|
+
const connectionError = publicConnectionFailure(error);
|
|
296
318
|
return signal?.aborted ? cancelled() : workspaceError
|
|
297
319
|
? { ok: false, error: workspaceError }
|
|
320
|
+
: connectionError
|
|
321
|
+
? { ok: false, error: connectionError }
|
|
298
322
|
: internalFailure();
|
|
299
323
|
}
|
|
300
324
|
};
|
|
@@ -52,6 +52,15 @@ function questionError(message, code) {
|
|
|
52
52
|
return error;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function sessionEvents(session) {
|
|
56
|
+
if (typeof session?.snapshotEvents === 'function') {
|
|
57
|
+
const events = session.snapshotEvents();
|
|
58
|
+
if (Array.isArray(events)) return events;
|
|
59
|
+
}
|
|
60
|
+
const events = session?.events;
|
|
61
|
+
return Array.isArray(events) ? events : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
function matchesQuestions(value, pending) {
|
|
56
65
|
if (!value || typeof value !== 'object'
|
|
57
66
|
|| value.sessionId !== pending.sessionId
|
|
@@ -385,15 +394,15 @@ class ModernHarnessApi {
|
|
|
385
394
|
}
|
|
386
395
|
|
|
387
396
|
#claimableAgent(agent) {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
397
|
+
const session = agent?.session;
|
|
398
|
+
const sessionId = session?.id ?? agent?.id;
|
|
399
|
+
const events = sessionEvents(session);
|
|
400
|
+
if (typeof sessionId !== 'string' || !events) return null;
|
|
392
401
|
return hasActiveHarnessInteractionOwner(
|
|
393
402
|
this.#scope,
|
|
394
403
|
sessionId,
|
|
395
|
-
|
|
396
|
-
) ? { sessionId,
|
|
404
|
+
events,
|
|
405
|
+
) ? { sessionId, events } : null;
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
#questionFrame(pending) {
|
|
@@ -464,8 +473,8 @@ class ModernHarnessApi {
|
|
|
464
473
|
const claimed = new Set([...this.#pendingApprovals.values()].map((entry) => entry.approvalId));
|
|
465
474
|
const decided = new Set();
|
|
466
475
|
let approvalId;
|
|
467
|
-
for (let index = owner.
|
|
468
|
-
const event = owner.
|
|
476
|
+
for (let index = owner.events.length - 1; index >= 0; index -= 1) {
|
|
477
|
+
const event = owner.events[index];
|
|
469
478
|
if (event.type === 'approval/decided') {
|
|
470
479
|
decided.add(event.data?.id);
|
|
471
480
|
} else if (event.type === 'approval/asked') {
|
|
@@ -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
|
+
}
|