@xmanrui/dsh-im 4.21.2 → 4.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.en.md +10 -5
  2. package/README.md +10 -5
  3. package/docs/client-integration.md +81 -0
  4. package/lib/client.js +2565 -1134
  5. package/lib/index.js +292 -301
  6. package/package.json +9 -1
  7. package/plugin-src/client/channel-card-meta.js +2 -0
  8. package/plugin-src/client/channel-logos.js +11 -0
  9. package/plugin-src/client/channels/dingtalk/api.js +5 -1
  10. package/plugin-src/client/channels/dingtalk/index.js +15 -13
  11. package/plugin-src/client/channels/email/api.js +52 -0
  12. package/plugin-src/client/channels/email/index.js +728 -0
  13. package/plugin-src/client/channels/email/styles.js +51 -0
  14. package/plugin-src/client/channels/feishu/api.js +4 -2
  15. package/plugin-src/client/channels/feishu/index.js +16 -3
  16. package/plugin-src/client/channels/imessage/index.js +2 -1
  17. package/plugin-src/client/channels/office/api.js +2 -0
  18. package/plugin-src/client/channels/office/index.js +6 -5
  19. package/plugin-src/client/channels/qq/api.js +7 -0
  20. package/plugin-src/client/channels/qq/index.js +16 -3
  21. package/plugin-src/client/channels/shared/token-api.js +5 -1
  22. package/plugin-src/client/channels/shared/token-channel.js +28 -3
  23. package/plugin-src/client/channels/slack/index.js +2 -1
  24. package/plugin-src/client/channels/wecom/api.js +7 -1
  25. package/plugin-src/client/channels/wecom/index.js +16 -3
  26. package/plugin-src/client/channels/wecom-app/api.js +6 -1
  27. package/plugin-src/client/channels/wecom-app/index.js +16 -3
  28. package/plugin-src/client/channels/weixin/api.js +2 -1
  29. package/plugin-src/client/channels/weixin/connection-error.js +1 -71
  30. package/plugin-src/client/channels/whatsapp/api.js +5 -0
  31. package/plugin-src/client/channels/whatsapp/index.js +16 -3
  32. package/plugin-src/client/connection-error.js +87 -0
  33. package/plugin-src/client/global-settings.js +81 -1
  34. package/plugin-src/client/i18n.js +62 -0
  35. package/plugin-src/client/index.js +122 -28
  36. package/plugin-src/client/last-message-error.js +2 -1
  37. package/plugin-src/client/panel-error-boundary.js +21 -0
  38. package/plugin-src/client/styles.js +6 -0
  39. package/plugin-src/host/build.mjs +4 -0
  40. package/plugin-src/host/channels/dingtalk/production.mjs +5 -1
  41. package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
  42. package/plugin-src/host/channels/email/availability.mjs +60 -0
  43. package/plugin-src/host/channels/email/index.mjs +36 -0
  44. package/plugin-src/host/channels/email/production.mjs +58 -0
  45. package/plugin-src/host/channels/email/rpc.mjs +163 -0
  46. package/plugin-src/host/channels/feishu/production.mjs +6 -1
  47. package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
  48. package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
  49. package/plugin-src/host/channels/office/rpc.mjs +3 -1
  50. package/plugin-src/host/channels/qq/production.mjs +5 -1
  51. package/plugin-src/host/channels/qq/rpc.mjs +5 -3
  52. package/plugin-src/host/channels/shared/production.mjs +55 -6
  53. package/plugin-src/host/channels/shared/rpc.mjs +5 -3
  54. package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
  55. package/plugin-src/host/channels/shared/startup.mjs +11 -6
  56. package/plugin-src/host/channels/slack/production.mjs +5 -1
  57. package/plugin-src/host/channels/slack/rpc.mjs +5 -3
  58. package/plugin-src/host/channels/wecom/production.mjs +5 -1
  59. package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
  60. package/plugin-src/host/channels/wecom-app/production.mjs +5 -1
  61. package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
  62. package/plugin-src/host/channels/weixin/production.mjs +5 -1
  63. package/plugin-src/host/channels/whatsapp/production.mjs +5 -1
  64. package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
  65. package/plugin-src/host/delivery-adapter.mjs +11 -0
  66. package/plugin-src/host/image-input-rpc.mjs +24 -0
  67. package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
  68. package/plugin-src/host/index.mjs +3 -0
  69. package/plugin-src/management-rpc.mjs +12 -2
  70. package/scripts/verify-package.mjs +3 -1
  71. package/src/channels/dingtalk/connection-error.mjs +5 -12
  72. package/src/channels/dingtalk/device-auth.mjs +4 -1
  73. package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
  74. package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
  75. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
  76. package/src/channels/discord/discord-api.mjs +3 -3
  77. package/src/channels/discord/discord-runtime.mjs +18 -12
  78. package/src/channels/email/config-store.mjs +194 -0
  79. package/src/channels/email/email-api.mjs +20 -0
  80. package/src/channels/email/email-bridge.mjs +17 -0
  81. package/src/channels/email/email-controller.mjs +824 -0
  82. package/src/channels/email/email-runtime.mjs +692 -0
  83. package/src/channels/email/harness-client.mjs +7 -0
  84. package/src/channels/email/mail-format.mjs +91 -0
  85. package/src/channels/email/state-store.mjs +176 -0
  86. package/src/channels/email/transport.mjs +64 -0
  87. package/src/channels/email/transports/agent-mail.mjs +593 -0
  88. package/src/channels/email/transports/agently-cli.mjs +288 -0
  89. package/src/channels/email/transports/imap-smtp.mjs +165 -0
  90. package/src/channels/feishu/bridge.mjs +8 -7
  91. package/src/channels/feishu/feishu-runtime.mjs +15 -8
  92. package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
  93. package/src/channels/feishu/message-utils.mjs +3 -2
  94. package/src/channels/feishu/multi-bot-controller.mjs +56 -47
  95. package/src/channels/feishu/registration-manager.mjs +8 -4
  96. package/src/channels/feishu/repair-manager.mjs +2 -1
  97. package/src/channels/imessage/runtime.mjs +9 -5
  98. package/src/channels/office/office-controller.mjs +23 -12
  99. package/src/channels/office/office-runtime.mjs +6 -3
  100. package/src/channels/office/office-transport.mjs +1 -0
  101. package/src/channels/qq/qq-bridge.mjs +7 -5
  102. package/src/channels/qq/qq-controller.mjs +56 -42
  103. package/src/channels/qq/qq-runtime.mjs +16 -11
  104. package/src/channels/shared/bot-workspace-store.mjs +37 -21
  105. package/src/channels/shared/connection-error.mjs +191 -0
  106. package/src/channels/shared/connection-test.mjs +3 -1
  107. package/src/channels/shared/conversation-state-store.mjs +46 -1
  108. package/src/channels/shared/default-workspace.mjs +28 -0
  109. package/src/channels/shared/diagnostic-details.mjs +95 -0
  110. package/src/channels/shared/harness-client.mjs +111 -20
  111. package/src/channels/shared/harness-session-binding.mjs +13 -8
  112. package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
  113. package/src/channels/shared/i18n-en/email.mjs +25 -0
  114. package/src/channels/shared/i18n-en/image-input.mjs +17 -0
  115. package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
  116. package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
  117. package/src/channels/shared/i18n-en.mjs +6 -0
  118. package/src/channels/shared/image-input-policy.mjs +35 -0
  119. package/src/channels/shared/image-input-settings-store.mjs +60 -0
  120. package/src/channels/shared/image-input.mjs +124 -0
  121. package/src/channels/shared/image-prompt.mjs +15 -7
  122. package/src/channels/shared/message-failure.mjs +12 -0
  123. package/src/channels/shared/semantic/reply-reference.mjs +3 -3
  124. package/src/channels/shared/text-harness-bridge.mjs +90 -11
  125. package/src/channels/shared/token-bot-controller.mjs +52 -47
  126. package/src/channels/slack/slack-api.mjs +3 -3
  127. package/src/channels/slack/slack-controller.mjs +59 -51
  128. package/src/channels/slack/slack-runtime.mjs +18 -12
  129. package/src/channels/telegram/telegram-api.mjs +2 -2
  130. package/src/channels/telegram/telegram-runtime.mjs +17 -13
  131. package/src/channels/wecom/wecom-bridge.mjs +15 -6
  132. package/src/channels/wecom/wecom-controller.mjs +51 -37
  133. package/src/channels/wecom/wecom-runtime.mjs +10 -6
  134. package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
  135. package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
  136. package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
  137. package/src/channels/weixin/connection-error.mjs +26 -92
  138. package/src/channels/weixin/diagnostic-details.mjs +1 -63
  139. package/src/channels/weixin/weixin-bridge.mjs +7 -5
  140. package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
  141. package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
@@ -1,8 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { mkdir } from 'node:fs/promises';
3
4
  import { isAbsolute } from 'node:path';
4
5
 
5
6
  import { adoptRegisteredWorkspaceSession } from './harness-session-binding.mjs';
7
+ import { sameWorkspacePath } from './default-workspace.mjs';
6
8
  import {
7
9
  appendInboundFilesToPrompt,
8
10
  InboundFileError,
@@ -13,6 +15,8 @@ import {
13
15
  imageFileSourcesFromContent,
14
16
  isModelImageRejection,
15
17
  } from './image-prompt.mjs';
18
+ import { imageInputLimits } from './image-input-policy.mjs';
19
+ import { inboundImagesAsFiles, imageContentFromStaged, isImageAdmissionRejection, IMAGE_HOST_LIMIT_FALLBACK_PROMPT } from './image-input.mjs';
16
20
  import { imSourceGuidance } from './im-source-guidance.mjs';
17
21
  import { outboundArtifactRegistry } from './semantic/artifact.mjs';
18
22
  import { t } from './i18n.mjs';
@@ -704,7 +708,6 @@ function harnessTurnError(reason) {
704
708
  return new HarnessTurnError('turn-interrupted', { reason });
705
709
  }
706
710
  if (kind === 'aborted') return new HarnessTurnError('turn-aborted', { reason });
707
- if (kind === 'completed') return new HarnessTurnError('model-empty-response', { reason });
708
711
  return new HarnessTurnError('harness-turn-failed', { reason });
709
712
  }
710
713
 
@@ -717,6 +720,7 @@ export class HarnessClient {
717
720
  #baseUrl;
718
721
  #apiProxy;
719
722
  #workspace;
723
+ #ungroupedWorkspace;
720
724
  #agentPreset;
721
725
  #autostart;
722
726
  #dshBin;
@@ -729,6 +733,7 @@ export class HarnessClient {
729
733
  #controlExecutor;
730
734
  #sessionMaintenanceExecutor;
731
735
  #fileIngressExecutor;
736
+ #imageInputPolicy;
732
737
  #managedProcess = null;
733
738
  #interactionRegistry;
734
739
  #interactionOwnerships;
@@ -740,6 +745,7 @@ export class HarnessClient {
740
745
  apiProxy,
741
746
  interactionScope = apiProxy,
742
747
  workspace,
748
+ ungroupedWorkspace,
743
749
  agentPreset,
744
750
  autostart = false,
745
751
  dshBin = 'dsh',
@@ -752,6 +758,7 @@ export class HarnessClient {
752
758
  controlExecutor,
753
759
  sessionMaintenanceExecutor,
754
760
  fileIngressExecutor,
761
+ imageInputPolicy = () => undefined,
755
762
  }) {
756
763
  if (typeof createWebSocket !== 'function') {
757
764
  throw new TypeError('createWebSocket must be a function');
@@ -788,6 +795,8 @@ export class HarnessClient {
788
795
  throw new TypeError('interactionScope must identify the current Host');
789
796
  }
790
797
  this.#workspace = workspace;
798
+ // Only the reserved IM directory bypasses grouping, even after /ws switches.
799
+ this.#ungroupedWorkspace = ungroupedWorkspace;
791
800
  // Keep an omitted preset absent so session.create resolves the Host's current default.
792
801
  this.#agentPreset = agentPreset ?? undefined;
793
802
  this.#autostart = Boolean(this.#baseUrl && autostart);
@@ -801,6 +810,7 @@ export class HarnessClient {
801
810
  this.#controlExecutor = controlExecutor;
802
811
  this.#sessionMaintenanceExecutor = sessionMaintenanceExecutor;
803
812
  this.#fileIngressExecutor = fileIngressExecutor;
813
+ this.#imageInputPolicy = imageInputPolicy;
804
814
  this.#interactionRegistry = interactionRegistry(this.#baseUrl?.origin ?? interactionScope);
805
815
  this.#interactionOwnerships = this.#interactionRegistry.ownerships;
806
816
  this.#interactionClaims = this.#interactionRegistry.claims;
@@ -921,6 +931,38 @@ export class HarnessClient {
921
931
  await this.ensureRunning(options);
922
932
  const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
923
933
  const workspace = workspaceFromList(workspacePath, workspaceList);
934
+ if (await this.isUngroupedWorkspace(workspacePath)) {
935
+ const sessionList = await this.rpc('session.list', {}, 30_000, options);
936
+ if (!Array.isArray(sessionList?.items)) {
937
+ throw new Error('Harness returned an invalid response for session.list');
938
+ }
939
+ const registered = new Set();
940
+ const selected = new Set();
941
+ for (const item of workspaceList.items) {
942
+ if (!Array.isArray(item?.sessionIds)
943
+ || item.sessionIds.some((id) => typeof id !== 'string' || !id)) {
944
+ throw new Error('Harness returned invalid session IDs for workspace.list');
945
+ }
946
+ for (const id of item.sessionIds) registered.add(id);
947
+ if (await sameWorkspacePath(item.path, workspacePath)) {
948
+ for (const id of item.sessionIds) selected.add(id);
949
+ }
950
+ }
951
+ for (const item of sessionList.items) {
952
+ if (typeof item?.sessionId !== 'string' || !item.sessionId) {
953
+ throw new Error('Harness returned an invalid response for session.list');
954
+ }
955
+ // A matching cwd never overrides an explicit group assignment.
956
+ if (!registered.has(item.sessionId) && await this.isUngroupedWorkspace(item.cwd)) {
957
+ selected.add(item.sessionId);
958
+ }
959
+ }
960
+ return workspaceSessions(
961
+ { path: workspacePath, sessionIds: [...selected] },
962
+ workspaceList.archivedSessionIds,
963
+ sessionList,
964
+ );
965
+ }
924
966
  if (!workspace) return { workspace: workspacePath, sessions: [] };
925
967
  const sessionList = await this.rpc('session.list', {}, 30_000, options);
926
968
  return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
@@ -988,6 +1030,10 @@ export class HarnessClient {
988
1030
  return adoptRegisteredWorkspaceSession(this, value, options);
989
1031
  }
990
1032
 
1033
+ async isUngroupedWorkspace(workspace) {
1034
+ return sameWorkspacePath(workspace, this.#ungroupedWorkspace);
1035
+ }
1036
+
991
1037
  async workspaceId(options = {}) {
992
1038
  const { workspace = this.#workspace, ...rpcOptions } = options;
993
1039
  const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
@@ -999,9 +1045,13 @@ export class HarnessClient {
999
1045
 
1000
1046
  async createSession(options = {}) {
1001
1047
  const { agentPreset: requestedPreset, ...rpcOptions } = options;
1048
+ const workspace = rpcOptions.workspace ?? this.#workspace;
1049
+ const ungrouped = await this.isUngroupedWorkspace(workspace);
1050
+ if (ungrouped) await mkdir(workspace, { recursive: true });
1002
1051
  await this.ensureRunning(rpcOptions);
1003
- const workspaceId = await this.workspaceId(rpcOptions);
1004
- const payload = { workspaceId };
1052
+ const payload = ungrouped
1053
+ ? { cwd: workspace }
1054
+ : { workspaceId: await this.workspaceId(rpcOptions) };
1005
1055
  const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
1006
1056
  if (agentPreset != null) payload.agentPreset = agentPreset;
1007
1057
  const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
@@ -1390,6 +1440,10 @@ export class HarnessClient {
1390
1440
  });
1391
1441
  }
1392
1442
 
1443
+ async getImageInputLimits() {
1444
+ return imageInputLimits(await this.#imageInputPolicy());
1445
+ }
1446
+
1393
1447
  async ask(sessionId, prompt, options = {}) {
1394
1448
  if (typeof options === 'number') options = { timeoutMs: options };
1395
1449
  const timeoutMs = options.timeoutMs ?? 600_000;
@@ -1405,6 +1459,7 @@ export class HarnessClient {
1405
1459
  : undefined;
1406
1460
  const control = normalizeControl(options.control);
1407
1461
  const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
1462
+ const inboundImages = Array.isArray(options.images) ? options.images.filter(Boolean) : [];
1408
1463
  await this.ensureRunning({ signal });
1409
1464
  const before = await this.rpc(
1410
1465
  'session.history',
@@ -1447,6 +1502,7 @@ export class HarnessClient {
1447
1502
  let interactionTask = null;
1448
1503
  let artifactsDelivered = false;
1449
1504
  let deliveredArtifactCount = 0;
1505
+ let artifactHandoffError = null;
1450
1506
  const stagedBatches = [];
1451
1507
  let promptAccepted = false;
1452
1508
  let turnFinished = false;
@@ -1462,6 +1518,7 @@ export class HarnessClient {
1462
1518
  await onArtifact(artifact);
1463
1519
  deliveredArtifactCount += 1;
1464
1520
  } catch (error) {
1521
+ artifactHandoffError ??= error;
1465
1522
  outboundArtifactRegistry.release(artifact);
1466
1523
  console.warn('[dsh-im] ignored an artifact handoff failure:', this.#logPrefix, error.message);
1467
1524
  }
@@ -1484,6 +1541,26 @@ export class HarnessClient {
1484
1541
  stagedBatches.push(staged);
1485
1542
  prompt = appendInboundFilesToPrompt(prompt, staged);
1486
1543
  }
1544
+ if (inboundImages.length > 0) {
1545
+ const limits = await this.getImageInputLimits();
1546
+ const sources = inboundImagesAsFiles(inboundImages, limits);
1547
+ let staged;
1548
+ try {
1549
+ staged = await this.#stageWorkspaceFiles(sessionId, sources, signal);
1550
+ } catch (error) {
1551
+ // Ingress wraps loader errors; retain the original image diagnostic.
1552
+ if (signal?.aborted) throw signal.reason ?? error;
1553
+ if (error?.cause?.name === 'ImagePromptError') throw error.cause;
1554
+ throw error;
1555
+ }
1556
+ stagedBatches.push(staged);
1557
+ const images = await imageContentFromStaged(staged, limits, { signal });
1558
+ const originalContent = typeof basePrompt === 'string'
1559
+ ? [{ type: 'text', text: basePrompt }] : basePrompt;
1560
+ prompt = appendInboundFilesToPrompt([...originalContent, ...images], {
1561
+ files: stagedBatches.flatMap((batch) => batch.files),
1562
+ });
1563
+ }
1487
1564
  if (interactionSignal) {
1488
1565
  let markOpen;
1489
1566
  const opened = new Promise((resolve) => { markOpen = resolve; });
@@ -1531,29 +1608,32 @@ export class HarnessClient {
1531
1608
  // Session workspace and named in a text manifest — then retry once
1532
1609
  // with a text-only prompt. The retry reuses promptRpcId so reply
1533
1610
  // tracking, control and interaction ownership stay bound to this ask.
1534
- const imageSources = isModelImageRejection(error)
1535
- ? imageFileSourcesFromContent(content)
1536
- : [];
1537
- if (imageSources.length === 0) throw error;
1538
- let stagedImages;
1539
- try {
1540
- stagedImages = await this.#stageWorkspaceFiles(sessionId, imageSources, signal);
1541
- } catch (stagingError) {
1542
- if (signal?.aborted) throw signal.reason ?? stagingError;
1543
- console.warn(
1544
- '[dsh-im] unable to restage rejected images as workspace files:',
1545
- this.#logPrefix,
1546
- stagingError?.message ?? String(stagingError),
1547
- );
1548
- throw error;
1611
+ const originalsStaged = inboundImages.length > 0 && isImageAdmissionRejection(error)
1612
+ && content.some((part) => part.type === 'image');
1613
+ const imageSources = !originalsStaged && isModelImageRejection(error)
1614
+ ? imageFileSourcesFromContent(content) : [];
1615
+ if (!originalsStaged && imageSources.length === 0) throw error;
1616
+ if (!originalsStaged) {
1617
+ let stagedImages;
1618
+ try {
1619
+ stagedImages = await this.#stageWorkspaceFiles(sessionId, imageSources, signal);
1620
+ } catch (stagingError) {
1621
+ if (signal?.aborted) throw signal.reason ?? stagingError;
1622
+ console.warn(
1623
+ '[dsh-im] unable to restage rejected images as workspace files:',
1624
+ this.#logPrefix,
1625
+ stagingError?.message ?? String(stagingError),
1626
+ );
1627
+ throw error;
1628
+ }
1629
+ stagedBatches.push(stagedImages);
1549
1630
  }
1550
- stagedBatches.push(stagedImages);
1551
1631
  const baseContent = typeof basePrompt === 'string'
1552
1632
  ? [{ type: 'text', text: basePrompt }]
1553
1633
  : basePrompt;
1554
1634
  const fallbackPrompt = appendInboundFilesToPrompt([
1555
1635
  ...contentWithoutImages(baseContent),
1556
- { type: 'text', text: t(IMAGE_FILE_FALLBACK_PROMPT) },
1636
+ { type: 'text', text: t(isModelImageRejection(error) ? IMAGE_FILE_FALLBACK_PROMPT : IMAGE_HOST_LIMIT_FALLBACK_PROMPT) },
1557
1637
  ], { files: stagedBatches.flatMap((batch) => batch?.files ?? []) });
1558
1638
  await sendPrompt(fallbackPrompt);
1559
1639
  }
@@ -1613,6 +1693,12 @@ export class HarnessClient {
1613
1693
  }
1614
1694
  if (artifactCount > 0) return '';
1615
1695
  if (ownership?.stopRequested) throw turnStoppedError();
1696
+ if (artifactHandoffError) throw artifactHandoffError;
1697
+ // A completed turn can do all its work through tools without text.
1698
+ // Missing end reasons retain the existing empty-reply failure.
1699
+ if (tracker.reason != null && harnessTurnSucceeded(tracker.reason)) {
1700
+ return t('本轮处理已结束,没有文本回复。');
1701
+ }
1616
1702
  throw harnessTurnError(tracker.reason);
1617
1703
  }
1618
1704
 
@@ -1635,6 +1721,11 @@ export class HarnessClient {
1635
1721
  throw timeoutError;
1636
1722
  }
1637
1723
  } catch (error) {
1724
+ if (error instanceof HarnessTurnError) {
1725
+ error.details ??= {
1726
+ sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq,
1727
+ };
1728
+ }
1638
1729
  // Once cancellation was accepted, transport/poll failures and timeouts
1639
1730
  // describe the convergence of that stop, not an unrelated ask failure.
1640
1731
  if (!ownership?.stopRequested) throw error;
@@ -37,16 +37,13 @@ function sessionWorkspace(sessionId, value) {
37
37
  }
38
38
  }
39
39
 
40
- if (owners.length === 0) {
41
- throw bindingError('session-not-registered', 'The session is not registered to a Harness workspace');
42
- }
43
- if (owners.length !== 1) {
40
+ if (owners.length > 1) {
44
41
  throw bindingError(
45
42
  'session-workspace-ambiguous',
46
43
  'The session is registered to more than one Harness workspace',
47
44
  );
48
45
  }
49
- if (UNSAFE_WORKSPACE_PATH.test(owners[0].path)) {
46
+ if (owners.length && UNSAFE_WORKSPACE_PATH.test(owners[0].path)) {
50
47
  throw new Error('Harness returned an unsafe workspace path for the session');
51
48
  }
52
49
  return {
@@ -82,20 +79,28 @@ function sessionSummary(sessionId, value) {
82
79
  if (title !== undefined && title !== null && typeof title !== 'string') {
83
80
  throw new Error('Harness returned an invalid session title for session.list');
84
81
  }
85
- return { title: typeof title === 'string' ? title : null };
82
+ return { cwd: summary.cwd, title: typeof title === 'string' ? title : null };
86
83
  }
87
84
 
88
85
  export async function adoptRegisteredWorkspaceSession(client, value, options = {}, timeoutMs = 30_000) {
89
86
  const sessionId = validatedSessionId(value);
90
87
  await client.ensureRunning(options);
91
88
  const workspaceList = await client.rpc('workspace.list', {}, timeoutMs, options);
92
- const { workspace, archived } = sessionWorkspace(sessionId, workspaceList);
89
+ let { workspace, archived } = sessionWorkspace(sessionId, workspaceList);
93
90
  const summary = sessionSummary(
94
91
  sessionId,
95
92
  await client.rpc('session.list', {}, timeoutMs, options),
96
93
  );
94
+ if (!workspace) {
95
+ if (typeof summary.cwd !== 'string' || !isAbsolute(summary.cwd)
96
+ || UNSAFE_WORKSPACE_PATH.test(summary.cwd)
97
+ || !await client.isUngroupedWorkspace?.(summary.cwd)) {
98
+ throw bindingError('session-not-registered', 'The session is not registered to a Harness workspace');
99
+ }
100
+ workspace = { path: summary.cwd };
101
+ }
97
102
  const adopted = await client.rpc('session.create', {
98
- workspaceId: workspace.workspaceId,
103
+ ...(workspace.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: workspace.path }),
99
104
  sessionId,
100
105
  }, timeoutMs, options);
101
106
  if (!adopted || adopted.sessionId !== sessionId) {
@@ -0,0 +1,55 @@
1
+ export default Object.freeze({
2
+ "DSH 版本": "DSH version",
3
+ "依赖版本": "Dependency versions",
4
+ "代理已配置": "Proxy configured",
5
+ "原因链已截断": "Cause chain truncated",
6
+ "请在 macOS 系统设置中授予 DSH 完全磁盘访问权限,以读取 Messages 数据库。": "Grant DSH Full Disk Access in macOS System Settings to read the Messages database.",
7
+ "请在 macOS 系统设置中允许 DSH 自动控制 Messages。": "Allow DSH to control Messages in macOS System Settings.",
8
+ "先在 macOS 系统设置中授予 Messages 权限。": "Grant Messages permissions in macOS System Settings first.",
9
+
10
+ "加载渠道配置": "Load channel configuration",
11
+ "激活账号": "Activate account",
12
+ "启动连接": "Start connection",
13
+ "监测连接与消息": "Monitor connection and messages",
14
+ "停止连接": "Stop connection",
15
+ "服务返回码": "Provider code",
16
+ "验证凭据": "Verify credentials",
17
+ "检查权限": "Check permissions",
18
+ "加载渠道组件": "Load channel component",
19
+ "执行渠道操作": "Perform channel operation",
20
+ "接收消息": "Receive message",
21
+ "发送消息": "Send message",
22
+ "发送测试消息": "Send test message",
23
+ "暂未识别": "Unknown",
24
+ "操作已取消。": "The operation was cancelled.",
25
+ "本地会话状态清理失败,请查看诊断详情。": "Local session state cleanup failed. See the diagnostic details.",
26
+ "账号已移除,但本地状态清理失败。": "The account was removed, but local state cleanup failed.",
27
+ "账号已移除,但登录凭据清理失败。": "The account was removed, but credential cleanup failed.",
28
+ "连接已移除,但登录凭据清理失败。": "The connection was removed, but credential cleanup failed.",
29
+
30
+ "操作失败,请查看原因和诊断详情。": "The operation failed. See the cause and diagnostic details.",
31
+ "检测到多个底层原因,请展开诊断详情逐项检查。": "Multiple underlying causes were detected. Expand the diagnostic details to inspect them.",
32
+ "服务域名解析失败,请检查运行 DSH 的机器的 DNS 和网络设置。": "Service DNS resolution failed. Check DNS and network settings on the machine running DSH.",
33
+ "目标拒绝连接,请检查服务地址、端口和访问限制。": "The connection was refused. Check the service address, port, and access restrictions.",
34
+ "连接已中断,请检查网络和目标服务状态。": "The connection was interrupted. Check the network and target service.",
35
+ "目标网络不可达,请检查运行 DSH 的机器的网络出口和访问限制。": "The target network is unreachable. Check network routing and access restrictions on the machine running DSH.",
36
+ "TLS 或证书校验失败,请检查系统时间、证书链和网络设置。": "TLS or certificate validation failed. Check the system clock, certificate chain, and network settings.",
37
+ "请求超时,请检查目标服务和网络;诊断详情中可查看已知的耗时。": "The request timed out. Check the service and network; available timings are in the diagnostic details.",
38
+ "代理连接或认证失败,请检查 DSH 进程使用的代理配置。": "Proxy connection or authentication failed. Check the proxy configuration used by the DSH process.",
39
+ "服务认证失败,请检查账号和授权信息。": "Service authentication failed. Check the account and credentials.",
40
+ "操作权限不足,请检查对应资源的访问权限。": "Permission was denied. Check access to the affected resource.",
41
+ "所需文件或本地工具不存在,请检查安装和配置。": "A required file or local tool is missing. Check installation and configuration.",
42
+ "服务正在限流,请稍后重试。": "The service is rate limiting requests. Try again later.",
43
+ "服务拒绝了请求,请检查凭据、权限或访问限制;HTTP 状态本身不能确认登录失效。": "The service rejected the request. Check credentials, permissions, or access restrictions; the HTTP status alone does not prove that the login expired.",
44
+ "目标服务暂时异常,请稍后重试。": "The target service is temporarily unavailable. Try again later.",
45
+ "目标服务返回了 HTTP 错误,请查看诊断详情。": "The target service returned an HTTP error. See the diagnostic details.",
46
+ "数据格式无效,请按失败阶段检查服务响应或本地配置。": "The data format is invalid. Check the service response or local configuration indicated by the failing stage.",
47
+ "暂未识别具体原因,请复制诊断信息,并查看相同参考号的 DSH 日志。": "The underlying cause is unknown. Copy the diagnostics and find the matching reference in the DSH logs.",
48
+ "多个原因": "Multiple causes",
49
+ "服务域名": "Service host",
50
+ "请求耗时(毫秒)": "Request duration (ms)",
51
+ "超时阈值(毫秒)": "Timeout (ms)",
52
+ "Node 版本": "Node version",
53
+ "运行平台": "Platform",
54
+ "无法访问 DSH 管理接口,请检查 DSH 连接或重新登录后重试。": "Cannot access the DSH management API. Check the DSH connection or sign in again."
55
+ });
@@ -0,0 +1,25 @@
1
+ /** English projections for host-side Email channel text. */
2
+ export default Object.freeze({
3
+ '邮箱': 'Email',
4
+ '邮箱地址格式不正确': 'The email address format is invalid',
5
+ '邮箱地址与应用密码均为必填': 'Both the email address and the app password are required',
6
+ '请选择邮箱服务商或填写 IMAP/SMTP 服务器地址':
7
+ 'Choose a mail provider or fill in the IMAP/SMTP server addresses',
8
+ '必须至少配置一个允许发件人':
9
+ 'At least one allowed sender must be configured',
10
+ '邮箱连接失败,请检查地址、应用密码与服务器设置':
11
+ 'Could not connect to the mailbox; check the address, app password, and server settings',
12
+ '未找到该邮箱配置': 'No mailbox configuration was found',
13
+ '邮箱凭据已丢失,请重新绑定':
14
+ 'The mailbox credential is missing; please bind the mailbox again',
15
+ '邮箱尚未连接': 'The mailbox is not connected yet',
16
+ '邮箱通道连接正常。': 'The Email channel is connected.',
17
+ '邮箱通道运行正常': 'The Email channel is running normally',
18
+ '邮箱通道尚未连接': 'The Email channel is not connected yet',
19
+ '邮箱访问策略同步失败,请重试': 'Failed to sync the mailbox access policy; please retry',
20
+ '当前环境不支持会话绑定': 'Session binding is not supported in this environment',
21
+ '该接入方式不需要扫码授权': 'This access method does not use QR authorization',
22
+ '授权信息缺失,请重新扫码授权': 'The authorization is missing; please scan again',
23
+ '扫码授权尚未开始': 'QR authorization has not been started',
24
+ '扫码授权已超时,请重新发起': 'QR authorization timed out; please start it again',
25
+ });
@@ -0,0 +1,17 @@
1
+ export default {
2
+ "图片原文件已保存到工作区,文件清单中的图片序号与本条消息一致。图片内容块可能经过缩放或压缩;需要查看细节时可用工具读取原文件。": "Original images are saved in the workspace. Image numbers in the file list match this message. Image blocks may be resized or compressed; use tools to read originals for details.",
3
+ "部分图片未直接提供给模型,图片序号:{indexes}。原文件已保存,请使用可用工具读取或分析;不要假设自己已看到这些图片。": "Some images were not provided directly to the model: {indexes}. Originals are saved; use available tools to read or analyze them. Do not assume you have seen these images.",
4
+ "宿主拒绝了本次图片输入。原图已保存到工作区,请根据文件清单使用可用工具读取或分析,不要假设自己已看到图片。": "The Host rejected the image input. Originals are saved in the workspace; use the file list and available tools to read or analyze them. Do not assume you have seen the images.",
5
+ "图片超过原图接收上限 {maxMb} MB,请压缩后重试,或在通用设置的附件中调整上限。": "The image exceeds the original-image receipt limit of {maxMb} MB. Compress it and retry, or adjust the limit in General settings > Attachments.",
6
+ "图片 {index}(原图文件:{name})": "Image {index} (original file: {name})",
7
+ "图片限制无效:请输入正整数,原图接收上限和总量上限不能小于单图上限。": "Invalid image limits: enter positive integers. The receipt and total limits must be at least the per-image limit.",
8
+ "无法读取或保存图片设置,请稍后重试。": "Could not read or save image settings. Try again later.",
9
+ "原图接收上限 (MB)": "Original-image receipt limit (MB)",
10
+ "模型单图上限 (MB)": "Model per-image limit (MB)",
11
+ "模型图片总量上限 (MB)": "Model total image limit (MB)",
12
+ "每条消息最多图片数": "Maximum images per message",
13
+ "图片输入": "Image input",
14
+ "适用于支持图片的聊天渠道。原图按附件保留时长保存,发送给模型的副本会自动缩放或压缩;无法直接发送时交给模型按文件处理。": "Applies to chat channels that support images. Originals follow attachment retention settings. Model copies are resized or compressed automatically; images that cannot be sent directly are provided as files.",
15
+ "保存图片设置": "Save image settings",
16
+ "正在读取图片设置…": "Loading image settings…"
17
+ };
@@ -3,6 +3,7 @@ export default {
3
3
  '这个问题已在其他客户端处理,无需再次回答。':
4
4
  'This question has already been answered from another client; no further reply is needed.',
5
5
  '任务已完成。': 'Task completed.',
6
+ '本轮处理已结束,没有文本回复。': 'This turn has ended with no text reply.',
6
7
  '结果文件': 'result file',
7
8
  '结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。':
8
9
  'The delivery result of the result file "{name}" could not be confirmed. Please check whether it already arrived in the chat before retrying.',
@@ -8,7 +8,7 @@ export default {
8
8
  '{channel}配置格式错误。请检查其数据目录中的 config.json 和 workspaces.json,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration is invalid. Check config.json and workspaces.json in its data directory, then restart DSH. See the startup log for details.',
9
9
  '{channel}配置格式错误。请检查其数据目录中的 config.json,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration is invalid. Check config.json in its data directory, then restart DSH. See the startup log for details.',
10
10
  '无法读取或写入{channel}配置。请检查其数据目录的访问权限,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration could not be read or written. Check access to its data directory, then restart DSH. See the startup log for details.',
11
- '{channel}初始化失败。请查看 DSH 启动日志中 failed to activate {id} 后的错误,修复后重启 DSH。': '{channel} initialization failed. Check the error after "failed to activate {id}" in the DSH startup log, fix it, then restart DSH.',
11
+ '{channel}初始化失败。请展开诊断详情,并通过参考号查找 DSH 启动日志,修复后重启 DSH。': '{channel} initialization failed. Expand the diagnostic details and find the reference in the DSH startup log, fix the issue, then restart DSH.',
12
12
  // history-command.mjs / command help
13
13
  '/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)':
14
14
  '/history [count] Preview recent messages (default 3, maximum 5)',
@@ -1,3 +1,5 @@
1
+ import imageInput from './i18n-en/image-input.mjs';
2
+ import diagnostics from './i18n-en/diagnostics.mjs';
1
3
  // English translations for host-side user-facing text.
2
4
  // Keys are the exact Chinese literals passed to t() in src/channels/**.
3
5
  // Chinese output is the identity default and needs no entries here.
@@ -17,9 +19,12 @@ import telegram from './i18n-en/telegram.mjs';
17
19
  import discord from './i18n-en/discord.mjs';
18
20
  import whatsapp from './i18n-en/whatsapp.mjs';
19
21
  import office from './i18n-en/office.mjs';
22
+ import email from './i18n-en/email.mjs';
20
23
 
21
24
  export const EN = Object.freeze(Object.assign(
22
25
  {},
26
+ diagnostics,
27
+ imageInput,
23
28
  sharedA,
24
29
  sharedB,
25
30
  sharedC,
@@ -34,4 +39,5 @@ export const EN = Object.freeze(Object.assign(
34
39
  discord,
35
40
  whatsapp,
36
41
  office,
42
+ email,
37
43
  ));
@@ -0,0 +1,35 @@
1
+ export const IMAGE_MEGABYTE = 1024 * 1024;
2
+
3
+ export const DEFAULT_IMAGE_INPUT_SETTINGS = Object.freeze({
4
+ maxDownloadMb: 30,
5
+ maxImageMb: 5,
6
+ maxTotalMb: 20,
7
+ maxImages: 20,
8
+ });
9
+
10
+ /** Shared by the settings form and Host; reject incomplete or unsafe values. */
11
+ export function normalizeImageInputSettings(value) {
12
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
13
+ const result = {};
14
+ for (const key of Object.keys(DEFAULT_IMAGE_INPUT_SETTINGS)) {
15
+ const input = value[key];
16
+ const number = typeof input === 'string' && /^\d+$/.test(input.trim())
17
+ ? Number(input.trim()) : input;
18
+ if (!Number.isSafeInteger(number) || number < 1
19
+ || !Number.isSafeInteger(number * IMAGE_MEGABYTE)) return null;
20
+ result[key] = number;
21
+ }
22
+ if (result.maxDownloadMb < result.maxImageMb || result.maxTotalMb < result.maxImageMb) return null;
23
+ return Object.freeze(result);
24
+ }
25
+
26
+ export function imageInputLimits(settings = DEFAULT_IMAGE_INPUT_SETTINGS) {
27
+ const normalized = normalizeImageInputSettings(settings);
28
+ if (!normalized) throw new TypeError('Invalid image input settings');
29
+ return Object.freeze({
30
+ maxDownloadBytes: normalized.maxDownloadMb * IMAGE_MEGABYTE,
31
+ maxImageBytes: normalized.maxImageMb * IMAGE_MEGABYTE,
32
+ maxTotalImageBytes: normalized.maxTotalMb * IMAGE_MEGABYTE,
33
+ maxImages: normalized.maxImages,
34
+ });
35
+ }
@@ -0,0 +1,60 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join, resolve } from 'node:path';
5
+
6
+ import { DEFAULT_IMAGE_INPUT_SETTINGS, normalizeImageInputSettings } from './image-input-policy.mjs';
7
+
8
+ const stores = new Map();
9
+
10
+ export class ImageInputSettingsStore {
11
+ #path;
12
+ #settings = DEFAULT_IMAGE_INPUT_SETTINGS;
13
+ #ready;
14
+ #writes = Promise.resolve();
15
+
16
+ constructor(path) { this.#path = path; }
17
+
18
+ async get() {
19
+ this.#ready ??= this.#load().catch((error) => { this.#ready = null; throw error; });
20
+ await this.#ready;
21
+ return { ...this.#settings };
22
+ }
23
+
24
+ async #load() {
25
+ let raw;
26
+ try { raw = await readFile(this.#path, 'utf8'); }
27
+ catch (error) { if (error.code === 'ENOENT') return; throw error; }
28
+ const document = JSON.parse(raw);
29
+ const settings = document.version === 1 && normalizeImageInputSettings(document.imageInput);
30
+ if (!settings) throw new Error('Invalid image input settings document');
31
+ this.#settings = settings;
32
+ }
33
+
34
+ async set(value) {
35
+ const settings = normalizeImageInputSettings(value);
36
+ if (!settings) throw new TypeError('Invalid image input settings');
37
+ const operation = this.#writes.catch(() => {}).then(async () => {
38
+ await this.get();
39
+ await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
40
+ const temporary = `${this.#path}.${randomUUID()}.tmp`;
41
+ try {
42
+ await writeFile(temporary, `${JSON.stringify({ version: 1, imageInput: settings }, null, 2)}\n`,
43
+ { mode: 0o600, flag: 'wx' });
44
+ await rename(temporary, this.#path);
45
+ this.#settings = settings;
46
+ } finally { await rm(temporary, { force: true }); }
47
+ return { ...settings };
48
+ });
49
+ this.#writes = operation;
50
+ return operation;
51
+ }
52
+ }
53
+
54
+ /** One settings owner per DSH home, shared by the UI and channel Harness clients. */
55
+ export function getImageInputSettingsStore(config = {}) {
56
+ const root = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
57
+ const path = join(root, 'integrations', 'dsh-im', 'image-input-settings.json');
58
+ if (!stores.has(path)) stores.set(path, new ImageInputSettingsStore(path));
59
+ return stores.get(path);
60
+ }