@xmanrui/dsh-im 4.8.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.
Files changed (32) hide show
  1. package/README.en.md +8 -79
  2. package/README.md +8 -79
  3. package/lib/client.js +73 -20
  4. package/lib/index.js +244 -240
  5. package/package.json +10 -6
  6. package/plugin-src/client/channels/dingtalk/api.js +23 -6
  7. package/plugin-src/client/channels/dingtalk/index.js +33 -6
  8. package/plugin-src/client/channels/dingtalk/styles.js +2 -0
  9. package/plugin-src/client/i18n.js +4 -0
  10. package/plugin-src/client/styles.js +3 -2
  11. package/plugin-src/host/channels/dingtalk/rpc.mjs +24 -0
  12. package/plugin-src/host/modern-harness-api.mjs +17 -8
  13. package/src/channels/dingtalk/connection-error.mjs +299 -0
  14. package/src/channels/dingtalk/dingtalk-bridge.mjs +1 -0
  15. package/src/channels/dingtalk/dingtalk-controller.mjs +79 -34
  16. package/src/channels/dingtalk/dingtalk-runtime.mjs +9 -2
  17. package/src/channels/feishu/bridge.mjs +174 -20
  18. package/src/channels/feishu/feishu-cards.mjs +69 -0
  19. package/src/channels/feishu/feishu-runtime.mjs +5 -0
  20. package/src/channels/feishu/slash-command-registry.mjs +3 -0
  21. package/src/channels/qq/qq-bridge.mjs +1 -0
  22. package/src/channels/shared/harness-approval.mjs +24 -1
  23. package/src/channels/shared/harness-client.mjs +45 -16
  24. package/src/channels/shared/i18n-en/dingtalk.mjs +23 -0
  25. package/src/channels/shared/i18n-en/feishu.mjs +11 -0
  26. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  27. package/src/channels/shared/semantic/reply-reference.mjs +5 -11
  28. package/src/channels/shared/text-harness-bridge.mjs +1 -0
  29. package/src/channels/shared/workspace-command.mjs +2 -2
  30. package/src/channels/telegram/telegram-runtime.mjs +6 -9
  31. package/src/channels/wecom/wecom-bridge.mjs +1 -0
  32. package/src/channels/weixin/weixin-bridge.mjs +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.8.0",
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": "https://github.com/xmanrui/dsh-im/issues",
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 error = new Error(sanitizeMessage(result.error?.message, '钉钉操作失败'));
112
- error.code = safeErrorCode(result.error?.code, 'DINGTALK_RPC_ERROR');
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
- code: safeErrorCode(error?.code, 'DINGTALK_ERROR'),
232
- message: sanitizeMessage(error?.message, '钉钉操作失败,请稍后重试'),
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('span', { className: 'ddt-errorCode' }, error.code),
195
+ h(ConnectionErrorDiagnostic, { error }),
182
196
  h('div', { className: 'ddt-actions dim-viewActions' },
183
- h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
184
- h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
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
- : `操作失败:${presentError(error).message}`;
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);
@@ -245,14 +245,15 @@ const CSS = String.raw`
245
245
  .dim-contextSwitch:checked { border-color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-state-business-primary, #3370ff); }
246
246
  .dim-contextSwitch:checked::before { transform: translateX(13px); background: #fff; }
247
247
  .dim-contextFields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px 12px; }
248
- .dim-contextField { min-width: 0; min-height: 30px; display: flex; align-items: center; gap: 6px; }
248
+ .dim-contextField { position: relative; min-width: 0; min-height: 30px; display: flex; align-items: center; gap: 6px; }
249
249
  .dim-contextField input { flex: none; width: 14px; height: 14px; margin: 0; accent-color: var(--dsw-alias-state-business-primary, #3370ff); }
250
250
  .dim-contextFieldText { min-width: 0; display: grid; grid-template-columns: max-content max-content; align-items: center; column-gap: 5px; overflow-wrap: anywhere; }
251
251
  .dim-contextFieldName { min-width: 0; line-height: 17px; cursor: pointer; }
252
252
  .dim-contextFieldKey { min-width: 0; grid-column: 1 / -1; color: var(--dsw-alias-label-tertiary, #8f959e); font: 10px/14px ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; cursor: pointer; }
253
- .dim-contextFieldHelp { position: relative; }
253
+ .dim-contextFieldHelp { position: static; }
254
254
  .dim-contextFieldHelpButton { width: 16px; height: 16px; font-size: 10px; }
255
255
  .dim-contextTooltip.dim-contextFieldTooltip { top: calc(100% + 6px); right: 0; left: auto; width: min(280px, calc(100vw - 72px)); }
256
+ .dim-contextField:nth-child(odd) .dim-contextFieldTooltip { right: auto; left: 0; }
256
257
  .dim-contextEditorHeader { position: relative; flex-wrap: wrap; }
257
258
  .dim-contextEditorTitle { min-width: 0; display: inline-flex; align-items: center; gap: 6px; }
258
259
  .dim-contextEditorTitle > label { font-weight: 500; }
@@ -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 sessionId = agent?.session?.id ?? agent?.id;
389
- if (typeof sessionId !== 'string' || !agent?.session || !Array.isArray(agent.session.events)) {
390
- return null;
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
- agent.session.events,
396
- ) ? { sessionId, session: agent.session } : null;
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.session.events.length - 1; index >= 0; index -= 1) {
468
- const event = owner.session.events[index];
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
+ }
@@ -81,6 +81,7 @@ const HELP_TEXT_LINES = [
81
81
  '/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
82
82
  '/workspace 工作区序号或绝对路径 切换工作区',
83
83
  '/workspacelist 列出工作区绝对路径',
84
+ '/ws、/wsl、/workspaces 工作区命令别名',
84
85
  '/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题',
85
86
  '/sessionlist --limit N 仅列出当前工作区前 N 个会话',
86
87
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',