@xmanrui/dsh-im 0.9.0 → 0.11.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.
Files changed (69) hide show
  1. package/README.en.md +7 -3
  2. package/README.md +7 -3
  3. package/lib/client.js +378 -27
  4. package/lib/index.js +134 -131
  5. package/package.json +1 -1
  6. package/plugin-src/client/channels/dingtalk/api.js +19 -0
  7. package/plugin-src/client/channels/dingtalk/index.js +67 -10
  8. package/plugin-src/client/channels/feishu/index.js +35 -5
  9. package/plugin-src/client/channels/qq/api.js +21 -0
  10. package/plugin-src/client/channels/qq/index.js +39 -1
  11. package/plugin-src/client/channels/shared/token-channel.js +29 -3
  12. package/plugin-src/client/channels/wecom/api.js +11 -0
  13. package/plugin-src/client/channels/wecom/index.js +71 -1
  14. package/plugin-src/client/channels/weixin/api.js +11 -0
  15. package/plugin-src/client/channels/weixin/index.js +40 -4
  16. package/plugin-src/client/channels/whatsapp/index.js +41 -2
  17. package/plugin-src/client/i18n.js +13 -0
  18. package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
  19. package/plugin-src/host/channels/dingtalk/rpc.mjs +29 -3
  20. package/plugin-src/host/channels/feishu/production.mjs +1 -1
  21. package/plugin-src/host/channels/feishu/rpc.mjs +34 -3
  22. package/plugin-src/host/channels/qq/production.mjs +1 -1
  23. package/plugin-src/host/channels/qq/rpc.mjs +35 -2
  24. package/plugin-src/host/channels/shared/production.mjs +1 -1
  25. package/plugin-src/host/channels/shared/rpc.mjs +25 -1
  26. package/plugin-src/host/channels/slack/production.mjs +1 -1
  27. package/plugin-src/host/channels/slack/rpc.mjs +28 -2
  28. package/plugin-src/host/channels/wecom/production.mjs +1 -1
  29. package/plugin-src/host/channels/wecom/rpc.mjs +31 -2
  30. package/plugin-src/host/channels/weixin/production.mjs +1 -1
  31. package/plugin-src/host/channels/weixin/rpc.mjs +29 -3
  32. package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
  33. package/plugin-src/host/channels/whatsapp/rpc.mjs +31 -3
  34. package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
  35. package/src/channels/dingtalk/dingtalk-bridge.mjs +160 -20
  36. package/src/channels/dingtalk/dingtalk-controller.mjs +18 -0
  37. package/src/channels/dingtalk/dingtalk-runtime.mjs +21 -0
  38. package/src/channels/discord/discord-api.mjs +1 -1
  39. package/src/channels/discord/discord-runtime.mjs +54 -1
  40. package/src/channels/feishu/bridge.mjs +45 -18
  41. package/src/channels/feishu/feishu-runtime.mjs +45 -0
  42. package/src/channels/feishu/message-utils.mjs +227 -6
  43. package/src/channels/feishu/multi-bot-controller.mjs +21 -0
  44. package/src/channels/feishu/plugin-controller.mjs +1 -0
  45. package/src/channels/qq/qq-bridge.mjs +107 -20
  46. package/src/channels/qq/qq-controller.mjs +18 -0
  47. package/src/channels/qq/qq-runtime.mjs +26 -0
  48. package/src/channels/shared/bot-workspace-store.mjs +2 -0
  49. package/src/channels/shared/connection-test.mjs +54 -0
  50. package/src/channels/shared/harness-client.mjs +14 -8
  51. package/src/channels/shared/image-prompt.mjs +268 -0
  52. package/src/channels/shared/text-harness-bridge.mjs +62 -16
  53. package/src/channels/shared/token-bot-controller.mjs +21 -0
  54. package/src/channels/shared/workspace-session.mjs +2 -1
  55. package/src/channels/slack/manifest.mjs +1 -0
  56. package/src/channels/slack/slack-api.mjs +95 -0
  57. package/src/channels/slack/slack-controller.mjs +19 -0
  58. package/src/channels/slack/slack-runtime.mjs +34 -3
  59. package/src/channels/telegram/telegram-api.mjs +37 -0
  60. package/src/channels/telegram/telegram-runtime.mjs +81 -9
  61. package/src/channels/wecom/wecom-bridge.mjs +160 -23
  62. package/src/channels/wecom/wecom-controller.mjs +18 -0
  63. package/src/channels/wecom/wecom-runtime.mjs +18 -0
  64. package/src/channels/weixin/weixin-api.mjs +98 -2
  65. package/src/channels/weixin/weixin-bridge.mjs +52 -19
  66. package/src/channels/weixin/weixin-controller.mjs +18 -0
  67. package/src/channels/weixin/weixin-runtime.mjs +25 -0
  68. package/src/channels/whatsapp/whatsapp-controller.mjs +20 -0
  69. package/src/channels/whatsapp/whatsapp-runtime.mjs +160 -1
@@ -151,6 +151,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
151
151
  export function AccountCard({
152
152
  account,
153
153
  busy,
154
+ feedback,
154
155
  removing,
155
156
  onReconnect,
156
157
  onWorkspaceSave,
@@ -180,6 +181,11 @@ export function AccountCard({
180
181
  }),
181
182
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
182
183
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
184
+ feedback ? h('div', {
185
+ className: 'ddt-summary dim-cardSummary',
186
+ role: 'status',
187
+ 'aria-live': 'polite',
188
+ }, feedback) : null,
183
189
  h('div', { className: 'ddt-actions dim-cardActions' },
184
190
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
185
191
  h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')))),
@@ -193,13 +199,31 @@ export function WecomSettingsTab({ rpcCall }) {
193
199
  const [provision, setProvision] = React.useState(null);
194
200
  const [busy, setBusy] = React.useState(false);
195
201
  const [busyByBot, setBusyByBot] = React.useState({});
202
+ const [feedbackByBot, setFeedbackByBot] = React.useState({});
196
203
  const [removeTarget, setRemoveTarget] = React.useState(null);
197
204
  const [credentialOpen, setCredentialOpen] = React.useState(false);
198
205
  const [credentialError, setCredentialError] = React.useState(null);
206
+ const [notice, setNotice] = React.useState('');
199
207
  const [now, setNow] = React.useState(Date.now());
200
208
  const mounted = React.useRef(true);
201
209
  const workspaceFence = useWorkspaceSnapshotFence();
202
210
  const addButtonRef = React.useRef(null);
211
+ const noticeFrameRef = React.useRef(null);
212
+
213
+ const announce = React.useCallback((message) => {
214
+ if (!mounted.current) return;
215
+ if (noticeFrameRef.current !== null) {
216
+ window.cancelAnimationFrame(noticeFrameRef.current);
217
+ noticeFrameRef.current = null;
218
+ }
219
+ setNotice('');
220
+ if (message) {
221
+ noticeFrameRef.current = window.requestAnimationFrame(() => {
222
+ noticeFrameRef.current = null;
223
+ if (mounted.current) setNotice(message);
224
+ });
225
+ }
226
+ }, []);
203
227
 
204
228
  React.useEffect(() => {
205
229
  const disposeDingtalk = installDingtalkStyles();
@@ -207,6 +231,10 @@ export function WecomSettingsTab({ rpcCall }) {
207
231
  mounted.current = true;
208
232
  return () => {
209
233
  mounted.current = false;
234
+ if (noticeFrameRef.current !== null) {
235
+ window.cancelAnimationFrame(noticeFrameRef.current);
236
+ noticeFrameRef.current = null;
237
+ }
210
238
  disposeWecom();
211
239
  disposeDingtalk();
212
240
  };
@@ -361,6 +389,46 @@ export function WecomSettingsTab({ rpcCall }) {
361
389
  }
362
390
  }, [invoke, loadStatus, workspaceFence]);
363
391
 
392
+ const reconnect = React.useCallback(async (account) => {
393
+ setFeedbackByBot((current) => {
394
+ const next = { ...current };
395
+ delete next[account.botId];
396
+ return next;
397
+ });
398
+ try {
399
+ const snapshot = await botAction(
400
+ account,
401
+ 'reconnect',
402
+ WECOM_ENDPOINTS.reconnectBot,
403
+ { botId: account.botId, sendTest: true },
404
+ );
405
+ if (!snapshot) return;
406
+ const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
407
+ let feedback;
408
+ if (!refreshed?.connected) {
409
+ feedback = '企业微信仍未连接,插件会继续自动重试。';
410
+ } else if (snapshot.testMessage?.sent) {
411
+ feedback = '企业微信连接检查完成,测试消息已发送。';
412
+ } else if (snapshot.testMessage?.code === 'test-target-unavailable') {
413
+ feedback = '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
414
+ } else if (snapshot.testMessage) {
415
+ feedback = '企业微信连接检查完成,但测试消息发送失败。';
416
+ } else {
417
+ feedback = '企业微信连接检查完成。';
418
+ }
419
+ if (mounted.current) {
420
+ setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
421
+ }
422
+ announce(feedback);
423
+ } catch {
424
+ const feedback = '连接检查失败,请稍后重试。';
425
+ if (mounted.current) {
426
+ setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
427
+ }
428
+ announce(feedback);
429
+ }
430
+ }, [announce, botAction]);
431
+
364
432
  let provisionView = null;
365
433
  if (provision?.status === 'starting') provisionView = h('div', { className: 'ddt-card ddt-loading dim-surfaceCard' }, h('div', { className: 'ddt-spinner' }), '正在申请企业微信二维码…');
366
434
  else if (['pending', 'refreshing'].includes(provision?.status)) provisionView = h(QrPanel, {
@@ -378,8 +446,9 @@ export function WecomSettingsTab({ rpcCall }) {
378
446
  h('li', { key: account.botId }, h(AccountCard, {
379
447
  account,
380
448
  busy: busyByBot[account.botId],
449
+ feedback: feedbackByBot[account.botId],
381
450
  removing: removeTarget === account.botId,
382
- onReconnect: () => void botAction(account, 'reconnect', WECOM_ENDPOINTS.reconnectBot, { botId: account.botId }),
451
+ onReconnect: () => void reconnect(account),
383
452
  onWorkspaceSave: (workspace) => botAction(
384
453
  account,
385
454
  'workspace',
@@ -419,6 +488,7 @@ export function WecomSettingsTab({ rpcCall }) {
419
488
  credentialOpen,
420
489
  addButtonRef,
421
490
  }),
491
+ h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
422
492
  model.phase === 'loading' ? h(LoadingView)
423
493
  : model.phase === 'error'
424
494
  ? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信机器人状态'), h('p', null, model.error?.message), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
@@ -35,6 +35,16 @@ function timestamp(value) {
35
35
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
36
36
  }
37
37
 
38
+ function normalizeTestMessage(value) {
39
+ if (!isRecord(value)) return null;
40
+ if (value.sent === true) return { sent: true };
41
+ if (value.sent !== false) return null;
42
+ const code = value.code === 'test-target-unavailable'
43
+ ? 'test-target-unavailable'
44
+ : 'test-message-failed';
45
+ return { sent: false, code };
46
+ }
47
+
38
48
  export function unwrapRpcResult(result) {
39
49
  if (!isRecord(result) || typeof result.ok !== 'boolean') {
40
50
  throw new Error('微信服务返回了无法识别的响应');
@@ -142,6 +152,7 @@ export function normalizeSnapshot(value) {
142
152
  connected: bots.filter((bot) => bot.connected).length,
143
153
  },
144
154
  provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : null,
155
+ testMessage: normalizeTestMessage(value.testMessage),
145
156
  };
146
157
  }
147
158
 
@@ -198,6 +198,7 @@ function checkedTime(timestamp) {
198
198
  export function AccountCard({
199
199
  account,
200
200
  busy,
201
+ feedback,
201
202
  removing,
202
203
  onReconnect,
203
204
  onWorkspaceSave,
@@ -229,6 +230,11 @@ export function AccountCard({
229
230
  }),
230
231
  h('div', { className: 'dxw-accountFooter dim-cardFooter' },
231
232
  summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
233
+ feedback ? h('div', {
234
+ className: 'dxw-summary dim-cardSummary',
235
+ role: 'status',
236
+ 'aria-live': 'polite',
237
+ }, feedback) : null,
232
238
  h('div', { className: 'dxw-actions dim-cardActions' },
233
239
  h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) },
234
240
  busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
@@ -250,6 +256,7 @@ function AccountList(props) {
250
256
  h(AccountCard, {
251
257
  account,
252
258
  busy: props.busyByBot[account.botId],
259
+ feedback: props.feedbackByBot[account.botId],
253
260
  removing: props.removeTarget === account.botId,
254
261
  onReconnect: () => props.onReconnect(account),
255
262
  onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
@@ -282,6 +289,7 @@ export function WeixinSettingsTab({ rpcCall }) {
282
289
  const [provision, setProvision] = React.useState(null);
283
290
  const [busy, setBusy] = React.useState(false);
284
291
  const [busyByBot, setBusyByBot] = React.useState({});
292
+ const [feedbackByBot, setFeedbackByBot] = React.useState({});
285
293
  const [removeTarget, setRemoveTarget] = React.useState(null);
286
294
  const [notice, setNotice] = React.useState('');
287
295
  const [now, setNow] = React.useState(() => Date.now());
@@ -496,15 +504,42 @@ export function WeixinSettingsTab({ rpcCall }) {
496
504
  const reconnect = React.useCallback(async (account) => {
497
505
  const snapshotVersion = workspaceFence.beginMutation();
498
506
  setBotBusy(account.botId, 'reconnect');
507
+ setFeedbackByBot((current) => {
508
+ const next = { ...current };
509
+ delete next[account.botId];
510
+ return next;
511
+ });
499
512
  try {
500
- const snapshot = normalizeSnapshot(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId }));
513
+ const snapshot = normalizeSnapshot(await invoke(
514
+ WEIXIN_ENDPOINTS.reconnectBot,
515
+ { botId: account.botId, sendTest: true },
516
+ ));
501
517
  if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
502
518
  setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
503
519
  }
504
520
  const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
505
- announce(refreshed?.connected ? '微信连接检查完成。' : '微信仍未连接,插件会继续自动重试。');
506
- } catch (error) {
507
- announce(`连接检查失败:${presentError(error).message}`);
521
+ let feedback;
522
+ if (!refreshed?.connected) {
523
+ feedback = '微信仍未连接,插件会继续自动重试。';
524
+ } else if (snapshot.testMessage?.sent) {
525
+ feedback = '微信连接检查完成,测试消息已发送。';
526
+ } else if (snapshot.testMessage?.code === 'test-target-unavailable') {
527
+ feedback = '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
528
+ } else if (snapshot.testMessage) {
529
+ feedback = '微信连接检查完成,但测试消息发送失败。';
530
+ } else {
531
+ feedback = '微信连接检查完成。';
532
+ }
533
+ if (mountedRef.current) {
534
+ setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
535
+ }
536
+ announce(feedback);
537
+ } catch {
538
+ const feedback = '连接检查失败,请稍后重试。';
539
+ if (mountedRef.current) {
540
+ setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
541
+ }
542
+ announce(feedback);
508
543
  } finally {
509
544
  const shouldRefresh = workspaceFence.endMutation();
510
545
  if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
@@ -611,6 +646,7 @@ export function WeixinSettingsTab({ rpcCall }) {
611
646
  ? h(AccountList, {
612
647
  bots: model.bots,
613
648
  busyByBot,
649
+ feedbackByBot,
614
650
  removeTarget,
615
651
  onReconnect: (account) => void reconnect(account),
616
652
  onWorkspaceSave: saveWorkspace,
@@ -43,6 +43,16 @@ function checkedTime(value) {
43
43
  }
44
44
  }
45
45
 
46
+ function connectionTestNotice(value) {
47
+ if (value?.testMessage?.sent === true) {
48
+ return '测试消息已发送,请到 WhatsApp 自聊会话中确认。';
49
+ }
50
+ if (value?.testMessage?.code === 'test-target-unavailable') {
51
+ return '连接检查完成,但当前没有可用的 WhatsApp 自聊目标。';
52
+ }
53
+ return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
54
+ }
55
+
46
56
  function Heading({ totals, busy, onAdd, addButtonRef }) {
47
57
  return h('div', { className: 'ddt-heading' },
48
58
  h('div', { className: 'ddt-tools' },
@@ -159,6 +169,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
159
169
  export function WhatsappAccountCard({
160
170
  account,
161
171
  busy,
172
+ testNotice,
162
173
  removing,
163
174
  onReconnect,
164
175
  onWorkspaceSave,
@@ -196,6 +207,10 @@ export function WhatsappAccountCard({
196
207
  }),
197
208
  h('div', { className: 'ddt-accountFooter dim-cardFooter' },
198
209
  summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
210
+ testNotice ? h('div', {
211
+ className: 'ddt-summary dim-cardSummary',
212
+ role: 'status',
213
+ }, testNotice) : null,
199
214
  h('div', { className: 'ddt-actions dim-cardActions' },
200
215
  h(Button, {
201
216
  className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy),
@@ -218,6 +233,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
218
233
  const [provision, setProvision] = React.useState(null);
219
234
  const [busy, setBusy] = React.useState(false);
220
235
  const [busyByBot, setBusyByBot] = React.useState({});
236
+ const [testNoticeByBot, setTestNoticeByBot] = React.useState({});
221
237
  const [removeTarget, setRemoveTarget] = React.useState(null);
222
238
  const [now, setNow] = React.useState(Date.now());
223
239
  const mounted = React.useRef(true);
@@ -365,10 +381,32 @@ export function WhatsappSettingsTab({ rpcCall }) {
365
381
  const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
366
382
  const snapshotVersion = workspaceFence.beginMutation();
367
383
  setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
384
+ if (operation === 'reconnect') {
385
+ setTestNoticeByBot((current) => {
386
+ const next = { ...current };
387
+ delete next[account.botId];
388
+ return next;
389
+ });
390
+ }
368
391
  try {
369
- const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
392
+ const value = await invoke(endpoint, payload);
393
+ const snapshot = normalizeSnapshot(value);
370
394
  if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
371
395
  setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
396
+ if (operation === 'reconnect') {
397
+ setTestNoticeByBot((current) => ({
398
+ ...current,
399
+ [account.botId]: connectionTestNotice(value),
400
+ }));
401
+ }
402
+ }
403
+ } catch (error) {
404
+ if (operation !== 'reconnect') throw error;
405
+ if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
406
+ setTestNoticeByBot((current) => ({
407
+ ...current,
408
+ [account.botId]: '连接检查失败,请稍后重试。',
409
+ }));
372
410
  }
373
411
  } finally {
374
412
  const shouldRefresh = workspaceFence.endMutation();
@@ -389,12 +427,13 @@ export function WhatsappSettingsTab({ rpcCall }) {
389
427
  h('li', { key: account.botId }, h(WhatsappAccountCard, {
390
428
  account,
391
429
  busy: busyByBot[account.botId],
430
+ testNotice: testNoticeByBot[account.botId],
392
431
  removing: removeTarget === account.botId,
393
432
  onReconnect: () => void botAction(
394
433
  account,
395
434
  'reconnect',
396
435
  WHATSAPP_ENDPOINTS.reconnectBot,
397
- { botId: account.botId },
436
+ { botId: account.botId, sendTest: true },
398
437
  ),
399
438
  onWorkspaceSave: (workspace) => botAction(
400
439
  account,
@@ -75,6 +75,19 @@ const EN = Object.freeze({
75
75
  '刚刚': 'Just now',
76
76
  '检查连接': 'Check connection',
77
77
  '检查中…': 'Checking…',
78
+ '连接检查失败,请稍后重试。': 'Connection check failed. Try again later.',
79
+ '测试消息已发送,请到对应机器人会话中确认。': 'Test message sent. Check the matching bot conversation.',
80
+ '连接检查完成。机器人尚未收到可用于测试的私聊消息。': 'Connection check completed. The bot has not received a direct message it can use for testing.',
81
+ '连接检查完成,但测试消息发送失败。': 'Connection check completed, but the test message could not be sent.',
82
+ '测试消息已发送,请到飞书会话中确认。': 'Test message sent. Check the Feishu conversation.',
83
+ '测试消息已发送,请到 WhatsApp 自聊会话中确认。': 'Test message sent. Check the WhatsApp self-chat.',
84
+ '连接检查完成,但当前没有可用的 WhatsApp 自聊目标。': 'Connection check completed, but no WhatsApp self-chat target is available.',
85
+ '钉钉连接检查完成,测试消息已发送。': 'DingTalk connection check completed and the test message was sent.',
86
+ '钉钉连接检查完成,但测试消息发送失败。': 'DingTalk connection check completed, but the test message could not be sent.',
87
+ '微信连接检查完成,测试消息已发送。': 'WeChat connection check completed and the test message was sent.',
88
+ '微信连接检查完成,但测试消息发送失败。': 'WeChat connection check completed, but the test message could not be sent.',
89
+ '企业微信连接检查完成,测试消息已发送。': 'WeCom connection check completed and the test message was sent.',
90
+ '企业微信连接检查完成,但测试消息发送失败。': 'WeCom connection check completed, but the test message could not be sent.',
78
91
  '重试连接': 'Reconnect',
79
92
  '重试中…': 'Retrying…',
80
93
  '移除接入': 'Remove connection',
@@ -84,7 +84,7 @@ export async function createProductionController(ctx, config = {}, internals = {
84
84
  const harness = new Harness({
85
85
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
86
86
  workspace: defaultWorkspace,
87
- agentPreset: config.agentPreset ?? 'standard',
87
+ ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
88
88
  autostart: false,
89
89
  dshBin: config.dshBin ?? 'dsh',
90
90
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -1,6 +1,10 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
3
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
+ import {
5
+ connectionTestTargetUnavailable,
6
+ publicConnectionTestResult,
7
+ } from '../../../../src/channels/shared/connection-test.mjs';
4
8
 
5
9
  export const DINGTALK_RPC_CHANNEL = '/dingtalk';
6
10
  export const DINGTALK_ENDPOINTS = Object.freeze({
@@ -69,9 +73,11 @@ function payloadFailure(endpoint, payload) {
69
73
  : 'bot.bind-credentials requires Client ID and Client Secret.';
70
74
  }
71
75
  if (endpoint === DINGTALK_ENDPOINTS.reconnectBot) {
72
- return exactKeys(payload, ['botId']) && validId(payload.botId)
76
+ return exactKeys(payload, ['botId', 'sendTest'])
77
+ && validId(payload.botId)
78
+ && (payload.sendTest === undefined || payload.sendTest === true)
73
79
  ? null
74
- : 'bot.reconnect requires a botId.';
80
+ : 'bot.reconnect requires a botId and optional sendTest=true.';
75
81
  }
76
82
  if (endpoint === DINGTALK_ENDPOINTS.deleteBot) {
77
83
  return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
@@ -210,7 +216,27 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
210
216
  } else if (endpoint === DINGTALK_ENDPOINTS.bindCredentials) {
211
217
  value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
212
218
  } else if (endpoint === DINGTALK_ENDPOINTS.reconnectBot) {
213
- value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
219
+ const snapshot = await controller.reconnectBot(payload.botId);
220
+ if (signal?.aborted) return cancelled();
221
+ let testMessage;
222
+ if (payload.sendTest === true) {
223
+ const connected = snapshot?.bots?.some(
224
+ (bot) => bot?.botId === payload.botId && bot?.connected === true,
225
+ );
226
+ if (!connected || typeof controller.sendConnectionTest !== 'function') {
227
+ testMessage = publicConnectionTestResult(
228
+ connectionTestTargetUnavailable('钉钉机器人'),
229
+ );
230
+ } else {
231
+ try {
232
+ await controller.sendConnectionTest(payload.botId);
233
+ testMessage = publicConnectionTestResult();
234
+ } catch (error) {
235
+ testMessage = publicConnectionTestResult(error);
236
+ }
237
+ }
238
+ }
239
+ value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) }, cachedEncode);
214
240
  } else if (endpoint === DINGTALK_ENDPOINTS.deleteBot) {
215
241
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
216
242
  } else if (endpoint === DINGTALK_ENDPOINTS.setWorkspace) {
@@ -110,7 +110,7 @@ export async function createProductionController(ctx, config = {}, internals = {
110
110
  const harness = new Harness({
111
111
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
112
112
  workspace: defaultWorkspace,
113
- agentPreset: config.agentPreset ?? 'standard',
113
+ ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
114
114
  // This plugin is already hosted by a running DSH process. Starting a
115
115
  // second DSH would create a competing server and lifecycle.
116
116
  autostart: false,
@@ -1,4 +1,5 @@
1
1
  import QRCode from 'qrcode';
2
+ import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
2
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
4
  import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
5
  import {
@@ -258,8 +259,14 @@ function validPayload(endpoint, payload) {
258
259
  ? null
259
260
  : 'Disconnect requires removeCredentials=true.';
260
261
  }
261
- if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot
262
- || endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) {
262
+ if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot) {
263
+ return hasOnlyKeys(payload, new Set(['botId', 'sendTest']))
264
+ && safeOpaqueId(payload.botId)
265
+ && (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
266
+ ? null
267
+ : 'A valid botId and optional sendTest flag are required.';
268
+ }
269
+ if (endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) {
263
270
  return hasOnlyKeys(payload, new Set(['botId'])) && safeOpaqueId(payload.botId)
264
271
  ? null
265
272
  : 'A single valid botId is required.';
@@ -431,7 +438,31 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
431
438
  value = await toPublicFeishuStatus(await controller.disconnect(), { encodeQr: cachedEncodeQr });
432
439
  } else if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot) {
433
440
  if (typeof controller.reconnectBot !== 'function') throw new Error('Multi-bot reconnect is unavailable');
434
- value = await toPublicFeishuStatus(await controller.reconnectBot(payload.botId), { encodeQr: cachedEncodeQr });
441
+ const checked = await controller.reconnectBot(payload.botId);
442
+ if (signal?.aborted) return cancelled();
443
+ value = await toPublicFeishuStatus(checked, { encodeQr: cachedEncodeQr });
444
+ if (payload.sendTest === true) {
445
+ let testError = null;
446
+ const connected = checked?.bots?.some(
447
+ (bot) => bot?.botId === payload.botId && bot.connected === true,
448
+ ) === true;
449
+ if (!connected) {
450
+ testError = new Error('Feishu bot is not connected');
451
+ testError.code = 'test-target-unavailable';
452
+ } else {
453
+ try {
454
+ if (typeof controller.sendConnectionTest !== 'function') {
455
+ const unavailable = new Error('Connection test is unavailable');
456
+ unavailable.code = 'test-target-unavailable';
457
+ throw unavailable;
458
+ }
459
+ await controller.sendConnectionTest(payload.botId);
460
+ } catch (error) {
461
+ testError = error;
462
+ }
463
+ }
464
+ value = { ...value, testMessage: publicConnectionTestResult(testError) };
465
+ }
435
466
  } else if (endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) {
436
467
  if (typeof controller.disconnectBot !== 'function') throw new Error('Multi-bot disconnect is unavailable');
437
468
  value = await toPublicFeishuStatus(await controller.disconnectBot(payload.botId), { encodeQr: cachedEncodeQr });
@@ -75,7 +75,7 @@ export async function createProductionController(ctx, config = {}, internals = {
75
75
  const harness = new Harness({
76
76
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
77
77
  workspace: defaultWorkspace,
78
- agentPreset: config.agentPreset ?? 'standard',
78
+ ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
79
79
  autostart: false,
80
80
  dshBin: config.dshBin ?? 'dsh',
81
81
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -1,4 +1,8 @@
1
1
  import QRCode from 'qrcode';
2
+ import {
3
+ connectionTestTargetUnavailable,
4
+ publicConnectionTestResult,
5
+ } from '../../../../src/channels/shared/connection-test.mjs';
2
6
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
7
  import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
8
 
@@ -53,7 +57,10 @@ function payloadFailure(endpoint, payload) {
53
57
  ? null : 'bot.bind-credentials requires AppID and AppSecret.';
54
58
  }
55
59
  if (endpoint === QQ_ENDPOINTS.reconnectBot) {
56
- return exactKeys(payload, ['botId']) && validId(payload.botId) ? null : 'bot.reconnect requires a botId.';
60
+ return exactKeys(payload, ['botId', 'sendTest'])
61
+ && validId(payload.botId)
62
+ && (payload.sendTest === undefined || payload.sendTest === true)
63
+ ? null : 'bot.reconnect requires a botId and accepts only sendTest=true.';
57
64
  }
58
65
  if (endpoint === QQ_ENDPOINTS.deleteBot) {
59
66
  return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
@@ -125,7 +132,33 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
125
132
  } else if (endpoint === QQ_ENDPOINTS.bindCredentials) {
126
133
  value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
127
134
  } else if (endpoint === QQ_ENDPOINTS.reconnectBot) {
128
- value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
135
+ const snapshot = await controller.reconnectBot(payload.botId);
136
+ if (signal?.aborted) {
137
+ return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
138
+ }
139
+ let testMessage;
140
+ if (payload.sendTest === true) {
141
+ const connected = snapshot?.bots?.some(
142
+ (bot) => bot?.botId === payload.botId && bot?.connected === true,
143
+ ) === true;
144
+ if (!connected || typeof controller.sendConnectionTest !== 'function') {
145
+ testMessage = publicConnectionTestResult(
146
+ connectionTestTargetUnavailable('QQ机器人'),
147
+ );
148
+ } else {
149
+ let testError = null;
150
+ try {
151
+ await controller.sendConnectionTest(payload.botId);
152
+ } catch (error) {
153
+ testError = error;
154
+ }
155
+ testMessage = publicConnectionTestResult(testError);
156
+ }
157
+ }
158
+ value = await publicStatus({
159
+ ...snapshot,
160
+ ...(testMessage ? { testMessage } : {}),
161
+ }, cachedEncode);
129
162
  } else if (endpoint === QQ_ENDPOINTS.setWorkspace) {
130
163
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
131
164
  value = await publicStatus(
@@ -69,7 +69,7 @@ export async function createTokenProductionController(ctx, config, internals, de
69
69
  const harness = new ResolvedHarness({
70
70
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
71
71
  workspace: defaultWorkspace,
72
- agentPreset: config.agentPreset ?? 'standard',
72
+ ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
73
73
  autostart: false,
74
74
  dshBin: config.dshBin ?? 'dsh',
75
75
  ...(commandExecutor ? { commandExecutor } : {}),
@@ -1,4 +1,5 @@
1
1
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
+ import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
2
3
  import {
3
4
  publicWorkspaceError,
4
5
  SET_WORKSPACE_ENDPOINT,
@@ -44,7 +45,8 @@ function payloadFailure(endpoint, payload) {
44
45
  ? null : 'bot.bind-credentials requires a Bot Token.';
45
46
  }
46
47
  if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
47
- return exactKeys(payload, ['botId']) && validId(payload.botId)
48
+ return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
49
+ && (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
48
50
  ? null : 'bot.reconnect requires a botId.';
49
51
  }
50
52
  if (endpoint === TOKEN_BOT_ENDPOINTS.deleteBot) {
@@ -105,6 +107,28 @@ export function createTokenBotRpcHandler(controller, { channel }) {
105
107
  value = await controller.bindCredentials(payload);
106
108
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
107
109
  value = await controller.reconnectBot(payload.botId);
110
+ if (signal?.aborted) {
111
+ return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
112
+ }
113
+ if (payload.sendTest === true) {
114
+ let testError = null;
115
+ try {
116
+ if (value?.bots?.find((bot) => bot?.botId === payload.botId)?.connected !== true) {
117
+ const unavailable = new Error('Bot is not connected');
118
+ unavailable.code = 'test-target-unavailable';
119
+ throw unavailable;
120
+ }
121
+ if (typeof controller.sendConnectionTest !== 'function') {
122
+ const unavailable = new Error('Connection test is unavailable');
123
+ unavailable.code = 'test-target-unavailable';
124
+ throw unavailable;
125
+ }
126
+ await controller.sendConnectionTest(payload.botId);
127
+ } catch (error) {
128
+ testError = error;
129
+ }
130
+ value = { ...value, testMessage: publicConnectionTestResult(testError) };
131
+ }
108
132
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
109
133
  if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
110
134
  value = await controller.updateWorkspace(payload.botId, payload.workspace);
@@ -54,7 +54,7 @@ export async function createProductionController(ctx, config = {}, internals = {
54
54
  const harness = new ResolvedHarness({
55
55
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
56
56
  workspace: defaultWorkspace,
57
- agentPreset: config.agentPreset ?? 'standard',
57
+ ...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
58
58
  autostart: false,
59
59
  dshBin: config.dshBin ?? 'dsh',
60
60
  ...(commandExecutor ? { commandExecutor } : {}),