@xmanrui/dsh-im 4.9.1 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/PROACTIVE_DELIVERY.en.md +8 -0
  2. package/PROACTIVE_DELIVERY.md +8 -0
  3. package/lib/client.js +610 -56
  4. package/lib/index.js +273 -272
  5. package/package.json +12 -1
  6. package/plugin-src/client/channels/feishu/api.js +2 -0
  7. package/plugin-src/client/channels/feishu/index.js +54 -0
  8. package/plugin-src/client/global-settings.js +341 -0
  9. package/plugin-src/client/i18n.js +29 -0
  10. package/plugin-src/client/index.js +47 -6
  11. package/plugin-src/client/styles.js +49 -0
  12. package/plugin-src/host/channels/dingtalk/production.mjs +12 -0
  13. package/plugin-src/host/channels/feishu/production.mjs +14 -0
  14. package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
  15. package/plugin-src/host/channels/qq/production.mjs +12 -0
  16. package/plugin-src/host/channels/shared/production.mjs +12 -0
  17. package/plugin-src/host/channels/slack/production.mjs +12 -0
  18. package/plugin-src/host/channels/wecom/production.mjs +12 -0
  19. package/plugin-src/host/channels/weixin/production.mjs +12 -0
  20. package/plugin-src/host/channels/whatsapp/production.mjs +12 -0
  21. package/plugin-src/host/delivery-adapter.mjs +1 -0
  22. package/plugin-src/host/delivery-service.mjs +15 -0
  23. package/plugin-src/host/harness-session-coordinator.mjs +26 -5
  24. package/plugin-src/host/inbound-ttl-rpc.mjs +83 -0
  25. package/plugin-src/host/inbound-ttl-runtime.mjs +85 -0
  26. package/plugin-src/host/inbound-ttl-service.mjs +179 -0
  27. package/plugin-src/host/index.mjs +8 -0
  28. package/plugin-src/host/modern-harness-api.mjs +58 -2
  29. package/src/channels/feishu/bridge.mjs +164 -4
  30. package/src/channels/feishu/feishu-channel.mjs +45 -10
  31. package/src/channels/feishu/feishu-runtime.mjs +9 -0
  32. package/src/channels/feishu/message-utils.mjs +13 -0
  33. package/src/channels/feishu/multi-bot-controller.mjs +16 -0
  34. package/src/channels/feishu/plugin-config-store.mjs +1 -0
  35. package/src/channels/feishu/state-store.mjs +25 -0
  36. package/src/channels/shared/bot-workspace-store.mjs +6 -0
  37. package/src/channels/shared/inbound-file.mjs +117 -2
  38. package/src/channels/shared/inbound-ttl-store.mjs +110 -0
  39. package/src/channels/shared/inbound-ttl.mjs +22 -0
@@ -61,6 +61,15 @@ const CSS = String.raw`
61
61
  .dim-githubArrow { font-size: 13px; line-height: 1; }
62
62
  .dim-githubTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 20; width: max-content; max-width: min(220px, 80vw); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
63
63
  .dim-githubAction:hover .dim-githubTooltip, .dim-githubAction:focus-within .dim-githubTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
64
+ .dim-generalSettingsAction { position: relative; display: inline-flex; flex: none; }
65
+ .dim-generalSettingsButton { width: 30px; height: 30px; display: grid; place-items: center; flex: none; padding: 0; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
66
+ .dim-generalSettingsButton svg { display: block; }
67
+ .dim-generalSettingsButton:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
68
+ .dim-generalSettingsButton[aria-current="page"] { border-color: color-mix(in srgb, var(--dim-blue) 42%, var(--dsw-alias-border-l2, #dfe1e5)); color: var(--dim-blue); background: var(--dim-blue-soft); }
69
+ .dim-generalSettingsButton:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
70
+ .dim-generalSettingsTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 20; width: max-content; padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
71
+ .dim-generalSettingsAction:hover .dim-generalSettingsTooltip, .dim-generalSettingsButton:focus-visible + .dim-generalSettingsTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
72
+ .dim-generalSettingsButton[aria-current="page"] + .dim-generalSettingsTooltip { opacity: 0; visibility: hidden; transform: translateY(-3px); }
64
73
  .dim-layout { display: grid; grid-template-columns: 174px 1px minmax(0, 1fr); gap: 24px; align-items: start; }
65
74
  .dim-rail { max-height: 520px; display: grid; align-content: start; gap: 8px; overflow-y: auto; padding: 0 4px 1px 1px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
66
75
  .dim-rail::-webkit-scrollbar { width: 4px; }
@@ -468,6 +477,46 @@ const CSS = String.raw`
468
477
  .dim-accessFeedback[data-tone="success"] { color: var(--dsw-alias-state-success-primary, #20a162); }
469
478
  .dim-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
470
479
  .dim-accessActions { display: flex; justify-content: flex-end; }
480
+ .dim-generalSettingsPage { min-width: 0; display: grid; }
481
+ .dim-generalSettingsHeader { min-width: 0; margin: 0 0 8px; }
482
+ .dim-generalSettingsHeader h2 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 18px; line-height: 25px; font-weight: 680; }
483
+ .dim-generalSettingsTabsBar { min-width: 0; border-bottom: 1px solid var(--dsw-alias-border-l2, #dfe1e5); }
484
+ .dim-generalSettingsTabs { min-width: 0; display: flex; align-items: flex-end; gap: 24px; overflow-x: auto; scrollbar-width: none; }
485
+ .dim-generalSettingsTabs::-webkit-scrollbar { display: none; }
486
+ .dim-generalSettingsTab { position: relative; min-height: 38px; flex: none; display: inline-flex; align-items: center; justify-content: center; padding: 6px 2px 9px; border: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 13px; line-height: 20px; font-weight: 560; white-space: nowrap; cursor: pointer; transition: color .15s ease; }
487
+ .dim-generalSettingsTab::after { content: ''; position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; border-radius: 2px 2px 0 0; background: transparent; transform: scaleX(.45); transition: background .15s ease, transform .15s ease; }
488
+ .dim-generalSettingsTab:hover:not([aria-selected="true"]) { color: var(--dsw-alias-label-primary, #1f2329); }
489
+ .dim-generalSettingsTab[aria-selected="true"] { color: var(--dim-blue); font-weight: 650; }
490
+ .dim-generalSettingsTab[aria-selected="true"]::after { background: var(--dim-blue); transform: scaleX(1); }
491
+ .dim-generalSettingsTab:focus-visible { outline: 2px solid var(--dim-blue); outline-offset: -2px; border-radius: 5px; }
492
+ .dim-generalSettingsTabPanel { min-width: 0; padding-top: 14px; }
493
+ .dim-globalSection { min-width: 0; padding: 14px 16px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; background: var(--dsw-alias-bg-layer-3, #fff); }
494
+ .dim-globalHead { min-width: 0; display: flex; align-items: center; }
495
+ .dim-globalHeadTitle { min-width: 0; display: inline-flex; align-items: center; gap: 6px; }
496
+ .dim-globalHead h3 { min-width: 0; overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; line-height: 22px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
497
+ .dim-globalTtlHelp { position: relative; display: inline-flex; flex: none; }
498
+ .dim-globalTtlTooltip { position: absolute; top: calc(100% + 7px); left: 0; z-index: 30; width: max-content; max-width: min(280px, calc(100vw - 48px)); padding: 9px 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
499
+ .dim-globalTtlHelp:hover .dim-globalTtlTooltip, .dim-globalTtlHelpButton:focus-visible + .dim-globalTtlTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
500
+ .dim-globalTtlHints { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; }
501
+ .dim-globalTtlHints li { min-width: 0; display: flex; align-items: baseline; gap: 8px; }
502
+ .dim-globalTtlHints code { flex: none; min-width: 44px; padding: 0 6px; border-radius: 5px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-module-platform, #f7f8fa); font: 10px/16px ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; }
503
+ .dim-globalTtlHints span { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
504
+ .dim-globalTtlRow { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; margin-top: 12px; }
505
+ .dim-globalTtlInput { width: min(160px, 100%); min-width: 110px; max-width: 160px; flex: 1 1 130px; height: 34px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; transition: border-color .16s ease, box-shadow .16s ease; }
506
+ .dim-globalTtlInput:hover:not(:disabled) { border-color: var(--dsw-alias-border-l3, #bbbfc4); }
507
+ .dim-globalTtlInput:focus { outline: none; border-color: var(--dim-blue); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dim-blue) 14%, transparent); }
508
+ .dim-globalTtlInput[aria-invalid="true"] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 62%, var(--dsw-alias-border-l2, #dfe1e5)); }
509
+ .dim-globalTtlInput:disabled { cursor: not-allowed; opacity: .55; }
510
+ .dim-globalSaveButton { min-width: 58px; min-height: 34px; }
511
+ .dim-globalSweepAction { position: relative; display: inline-flex; flex: none; margin-left: auto; }
512
+ .dim-globalSweepButton { flex: none; }
513
+ .dim-globalSweepConfirm { position: absolute; top: calc(100% + 8px); right: 0; z-index: 35; width: 218px; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 12px 30px rgb(31 35 41 / 18%); }
514
+ .dim-globalSweepConfirm p { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 18px; }
515
+ .dim-globalSweepConfirmActions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 10px; }
516
+ .dim-deliveryButton.dim-globalSweepConfirmButton { border-color: var(--dsw-alias-state-error-primary, #d54941); color: #fff; background: var(--dsw-alias-state-error-primary, #d54941); }
517
+ .dim-deliveryButton.dim-globalSweepConfirmButton:hover:not(:disabled) { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 82%, #000); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 88%, #000); }
518
+ .dim-globalInline { flex: 1 0 100%; min-width: 0; margin: 2px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
519
+ .dim-globalInline[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
471
520
  .dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
472
521
  .dim-panel .ddt-headingCopy { display: none; }
473
522
  .dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
@@ -20,6 +20,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
20
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
21
21
  import { harnessConnection } from '../../harness-connection.mjs';
22
22
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
23
+ import {
24
+ getInboundTtlRuntime,
25
+ registerInboundTtlWorkspaces,
26
+ } from '../../inbound-ttl-runtime.mjs';
23
27
  import {
24
28
  accessPolicyProvider,
25
29
  initialAccessPolicyFor,
@@ -84,10 +88,18 @@ export async function createProductionController(ctx, config = {}, internals = {
84
88
  return state;
85
89
  };
86
90
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
91
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
92
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
93
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
94
+ workspaces,
95
+ configStore: observedConfigStore,
96
+ defaultWorkspace,
97
+ });
87
98
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
88
99
  controlExecutor: internals.controlExecutor,
89
100
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
90
101
  fileIngressExecutor: internals.fileIngressExecutor,
102
+ inboundTtlService,
91
103
  });
92
104
  const harness = new Harness({
93
105
  ...connection,
@@ -7,6 +7,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
7
7
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
8
8
  import { harnessConnection } from '../../harness-connection.mjs';
9
9
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
10
+ import {
11
+ getInboundTtlRuntime,
12
+ registerInboundTtlWorkspaces,
13
+ } from '../../inbound-ttl-runtime.mjs';
10
14
  import { verifyFeishuApp } from '../../../../src/channels/feishu/feishu-app.mjs';
11
15
  import { FeishuRuntime } from '../../../../src/channels/feishu/feishu-runtime.mjs';
12
16
  import { HarnessClient } from '../../../../src/channels/feishu/harness-client.mjs';
@@ -158,10 +162,19 @@ export async function createProductionController(ctx, config = {}, internals = {
158
162
  return stateFor(botConfig);
159
163
  };
160
164
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
165
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
166
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
167
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
168
+ workspaces,
169
+ configStore: observedConfigStore,
170
+ defaultWorkspace,
171
+ botIdFrom: (bot) => bot?.id,
172
+ });
161
173
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
162
174
  controlExecutor: internals.controlExecutor,
163
175
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
164
176
  fileIngressExecutor: internals.fileIngressExecutor,
177
+ inboundTtlService,
165
178
  });
166
179
  const harness = new Harness({
167
180
  ...connection,
@@ -202,6 +215,7 @@ export async function createProductionController(ctx, config = {}, internals = {
202
215
  domain: botConfig.domain,
203
216
  botOpenId: botConfig.botOpenId,
204
217
  groupResponseMode: botConfig.groupResponseMode,
218
+ groupTopicReply: botConfig.groupTopicReply,
205
219
  ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
206
220
  harness: workspaceScope.harness,
207
221
  state: workspaceScope.state,
@@ -287,6 +287,7 @@ function publicBotEntry(entry) {
287
287
  contextEnhancement: normalizeContextEnhancementConfig(source.contextEnhancement),
288
288
  accessPolicy: normalizeAccessPolicy(source.accessPolicy),
289
289
  groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
290
+ groupTopicReply: source.groupTopicReply === true,
290
291
  groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
291
292
  bot: publicBot(source.bot),
292
293
  health: publicHealth(source, connected),
@@ -440,6 +441,13 @@ function validPayload(endpoint, payload) {
440
441
  ? null
441
442
  : '请选择群聊响应方式。';
442
443
  }
444
+ if (endpoint === FEISHU_ENDPOINTS.setGroupTopicReply) {
445
+ return hasOnlyKeys(payload, new Set(['botId', 'groupTopicReply']))
446
+ && safeOpaqueId(payload.botId)
447
+ && typeof payload.groupTopicReply === 'boolean'
448
+ ? null
449
+ : '请选择是否以话题方式回复。';
450
+ }
443
451
  return 'Unknown Feishu endpoint.';
444
452
  }
445
453
 
@@ -710,6 +718,14 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
710
718
  await controller.updateGroupResponseMode(payload.botId, payload.groupResponseMode),
711
719
  { encodeQr: cachedEncodeQr },
712
720
  );
721
+ } else if (endpoint === FEISHU_ENDPOINTS.setGroupTopicReply) {
722
+ if (typeof controller.updateGroupTopicReply !== 'function') {
723
+ throw new Error('Group topic reply update is unavailable');
724
+ }
725
+ value = await toPublicFeishuStatus(
726
+ await controller.updateGroupTopicReply(payload.botId, payload.groupTopicReply),
727
+ { encodeQr: cachedEncodeQr },
728
+ );
713
729
  } else {
714
730
  if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
715
731
  value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
@@ -20,6 +20,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
20
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
21
21
  import { harnessConnection } from '../../harness-connection.mjs';
22
22
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
23
+ import {
24
+ getInboundTtlRuntime,
25
+ registerInboundTtlWorkspaces,
26
+ } from '../../inbound-ttl-runtime.mjs';
23
27
  import {
24
28
  accessPolicyProvider,
25
29
  initialAccessPolicyFor,
@@ -75,10 +79,18 @@ export async function createProductionController(ctx, config = {}, internals = {
75
79
  return state;
76
80
  };
77
81
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
82
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
83
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
84
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
85
+ workspaces,
86
+ configStore: observedConfigStore,
87
+ defaultWorkspace,
88
+ });
78
89
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
79
90
  controlExecutor: internals.controlExecutor,
80
91
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
81
92
  fileIngressExecutor: internals.fileIngressExecutor,
93
+ inboundTtlService,
82
94
  });
83
95
  const harness = new Harness({
84
96
  ...connection,
@@ -6,6 +6,10 @@ import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
6
6
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
7
7
  import { harnessConnection } from '../../harness-connection.mjs';
8
8
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
9
+ import {
10
+ getInboundTtlRuntime,
11
+ registerInboundTtlWorkspaces,
12
+ } from '../../inbound-ttl-runtime.mjs';
9
13
  import {
10
14
  BotWorkspaceStore,
11
15
  createBotWorkspaceScope,
@@ -84,10 +88,18 @@ export async function createTokenProductionController(ctx, config, internals, de
84
88
  return state;
85
89
  };
86
90
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
91
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
92
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
93
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
94
+ workspaces,
95
+ configStore: observedConfigStore,
96
+ defaultWorkspace,
97
+ });
87
98
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
88
99
  controlExecutor: internals.controlExecutor,
89
100
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
90
101
  fileIngressExecutor: internals.fileIngressExecutor,
102
+ inboundTtlService,
91
103
  });
92
104
  const harness = new ResolvedHarness({
93
105
  ...connection,
@@ -19,6 +19,10 @@ import { pluginPaths } from '../shared/production.mjs';
19
19
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
20
20
  import { harnessConnection } from '../../harness-connection.mjs';
21
21
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
22
+ import {
23
+ getInboundTtlRuntime,
24
+ registerInboundTtlWorkspaces,
25
+ } from '../../inbound-ttl-runtime.mjs';
22
26
  import {
23
27
  accessPolicyProvider,
24
28
  initialAccessPolicyFor,
@@ -63,10 +67,18 @@ export async function createProductionController(ctx, config = {}, internals = {
63
67
  return state;
64
68
  };
65
69
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
70
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
71
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
72
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
73
+ workspaces,
74
+ configStore: observedConfigStore,
75
+ defaultWorkspace,
76
+ });
66
77
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
67
78
  controlExecutor: internals.controlExecutor,
68
79
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
69
80
  fileIngressExecutor: internals.fileIngressExecutor,
81
+ inboundTtlService,
70
82
  });
71
83
  const harness = new ResolvedHarness({
72
84
  ...connection,
@@ -20,6 +20,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
20
20
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
21
21
  import { harnessConnection } from '../../harness-connection.mjs';
22
22
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
23
+ import {
24
+ getInboundTtlRuntime,
25
+ registerInboundTtlWorkspaces,
26
+ } from '../../inbound-ttl-runtime.mjs';
23
27
  import {
24
28
  accessPolicyProvider,
25
29
  initialAccessPolicyFor,
@@ -78,10 +82,18 @@ export async function createProductionController(ctx, config = {}, internals = {
78
82
  return state;
79
83
  };
80
84
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
85
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
86
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
87
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
88
+ workspaces,
89
+ configStore: observedConfigStore,
90
+ defaultWorkspace,
91
+ });
81
92
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
82
93
  controlExecutor: internals.controlExecutor,
83
94
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
84
95
  fileIngressExecutor: internals.fileIngressExecutor,
96
+ inboundTtlService,
85
97
  });
86
98
  const harness = new Harness({
87
99
  ...connection,
@@ -23,6 +23,10 @@ import { createConnectionSupervisor } from './connection-supervisor.mjs';
23
23
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
24
24
  import { harnessConnection } from '../../harness-connection.mjs';
25
25
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
26
+ import {
27
+ getInboundTtlRuntime,
28
+ registerInboundTtlWorkspaces,
29
+ } from '../../inbound-ttl-runtime.mjs';
26
30
  import {
27
31
  accessPolicyProvider,
28
32
  initialAccessPolicyFor,
@@ -81,10 +85,18 @@ export async function createProductionController(ctx, config = {}, internals = {
81
85
  return state;
82
86
  };
83
87
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
88
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
89
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
90
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
91
+ workspaces,
92
+ configStore: observedConfigStore,
93
+ defaultWorkspace,
94
+ });
84
95
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
85
96
  controlExecutor: internals.controlExecutor,
86
97
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
87
98
  fileIngressExecutor: internals.fileIngressExecutor,
99
+ inboundTtlService,
88
100
  });
89
101
  const harness = new Harness({
90
102
  ...connection,
@@ -23,6 +23,10 @@ import { createTokenConnectionSupervisor } from '../shared/connection-supervisor
23
23
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
24
24
  import { harnessConnection } from '../../harness-connection.mjs';
25
25
  import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
26
+ import {
27
+ getInboundTtlRuntime,
28
+ registerInboundTtlWorkspaces,
29
+ } from '../../inbound-ttl-runtime.mjs';
26
30
  import {
27
31
  accessPolicyProvider,
28
32
  initialAccessPolicyFor,
@@ -84,10 +88,18 @@ export async function createProductionController(ctx, config = {}, internals = {
84
88
  return state;
85
89
  };
86
90
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
91
+ const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
92
+ const inboundTtlService = inboundTtl?.service ?? inboundTtl;
93
+ registerInboundTtlWorkspaces(ctx, inboundTtlService, {
94
+ workspaces,
95
+ configStore: observedConfigStore,
96
+ defaultWorkspace,
97
+ });
87
98
  const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
88
99
  controlExecutor: internals.controlExecutor,
89
100
  sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
90
101
  fileIngressExecutor: internals.fileIngressExecutor,
102
+ inboundTtlService,
91
103
  });
92
104
  const harness = new Harness({
93
105
  ...connection,
@@ -144,6 +144,7 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
144
144
  return Object.freeze({
145
145
  channel,
146
146
  ownsBot: (botId) => workspaces.has(botId),
147
+ listBots: () => workspaces.listBotIds(),
147
148
  listTargets: (botId) => workspaces.listDeliveryTargets(botId),
148
149
  async listSuggestions(botId) {
149
150
  const state = await stateFor(botId);
@@ -7,6 +7,7 @@ const DRAFT_TARGET_ID = '__test__';
7
7
 
8
8
  const ADAPTER_METHODS = Object.freeze([
9
9
  'ownsBot',
10
+ 'listBots',
10
11
  'listTargets',
11
12
  'listSuggestions',
12
13
  'createTarget',
@@ -118,6 +119,20 @@ export class DeliveryService {
118
119
  }
119
120
  }
120
121
 
122
+ async listBots() {
123
+ const bots = [];
124
+ for (const { adapter } of this.#adapters.values()) {
125
+ try {
126
+ const ids = await adapter.listBots();
127
+ if (!Array.isArray(ids)) throw new TypeError('Adapter returned invalid bots');
128
+ for (const botId of ids) bots.push({ botId: botIdOf(botId), channel: adapter.channel });
129
+ } catch (error) {
130
+ throw publicOperationError(error);
131
+ }
132
+ }
133
+ return bots;
134
+ }
135
+
121
136
  async listSuggestions(botId) {
122
137
  const id = botIdOf(botId);
123
138
  const adapter = await this.#adapterFor(id);
@@ -22,7 +22,9 @@ function agentsFromContext(ctx) {
22
22
 
23
23
  function currentOwnedTurn(agent, expectedTurn, promptRpcId) {
24
24
  if (agent?.status !== 'running') return false;
25
- const events = agent?.session?.events;
25
+ const events = typeof agent?.session?.snapshotEvents === 'function'
26
+ ? agent.session.snapshotEvents()
27
+ : agent?.session?.events;
26
28
  if (!Array.isArray(events)) return false;
27
29
 
28
30
  let openTurn = null;
@@ -113,7 +115,7 @@ function createSessionMaintenanceExecutor(agents) {
113
115
  };
114
116
  }
115
117
 
116
- function createFileIngressExecutor(agents) {
118
+ function createFileIngressExecutor(agents, { inboundTtlService } = {}) {
117
119
  return ({ sessionId, workspace, files, signal }) => {
118
120
  const agent = agents.get(sessionId);
119
121
  const attachedWorkspace = agent?.session?.header?.cwd;
@@ -126,7 +128,19 @@ function createFileIngressExecutor(agents) {
126
128
  'The Harness Session workspace is unavailable for inbound files.',
127
129
  );
128
130
  }
129
- return stageInboundFiles({ files }, { workspace: exactWorkspace, signal });
131
+ const staged = stageInboundFiles({ files }, {
132
+ workspace: exactWorkspace,
133
+ signal,
134
+ ...(inboundTtlService ? { retention: inboundTtlService.stagingRetention() } : {}),
135
+ // Registration at creation time closes the mid-staging sweep window;
136
+ // trackStaged later re-registers idempotently and owns the release.
137
+ ...(typeof inboundTtlService?.trackDirectory === 'function'
138
+ ? { onStagedDirectory: (directory) => inboundTtlService.trackDirectory(directory) }
139
+ : {}),
140
+ });
141
+ return inboundTtlService
142
+ ? staged.then((result) => (result ? inboundTtlService.trackStaged(result) : result))
143
+ : staged;
130
144
  };
131
145
  }
132
146
 
@@ -136,7 +150,9 @@ function createFileIngressExecutor(agents) {
136
150
  * when an Agent is attached, its live Session header remains authoritative.
137
151
  */
138
152
  export function createHarnessSessionExecutors(ctx, provided = {}) {
139
- const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = provided;
153
+ const {
154
+ controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor, inboundTtlService,
155
+ } = provided;
140
156
  if (controlExecutor !== undefined && typeof controlExecutor !== 'function') {
141
157
  throw new TypeError('controlExecutor must be a function');
142
158
  }
@@ -147,6 +163,11 @@ export function createHarnessSessionExecutors(ctx, provided = {}) {
147
163
  if (fileIngressExecutor !== undefined && typeof fileIngressExecutor !== 'function') {
148
164
  throw new TypeError('fileIngressExecutor must be a function');
149
165
  }
166
+ if (inboundTtlService !== undefined
167
+ && (typeof inboundTtlService?.stagingRetention !== 'function'
168
+ || typeof inboundTtlService?.trackStaged !== 'function')) {
169
+ throw new TypeError('inboundTtlService must expose stagingRetention() and trackStaged()');
170
+ }
150
171
 
151
172
  const agents = controlExecutor && sessionMaintenanceExecutor && fileIngressExecutor
152
173
  ? undefined
@@ -156,6 +177,6 @@ export function createHarnessSessionExecutors(ctx, provided = {}) {
156
177
  sessionMaintenanceExecutor: sessionMaintenanceExecutor
157
178
  ?? (agents ? createSessionMaintenanceExecutor(agents) : undefined),
158
179
  fileIngressExecutor: fileIngressExecutor
159
- ?? createFileIngressExecutor(agents ?? { get: () => undefined }),
180
+ ?? createFileIngressExecutor(agents ?? { get: () => undefined }, { inboundTtlService }),
160
181
  };
161
182
  }
@@ -0,0 +1,83 @@
1
+ import { normalizeInboundTtlHours } from '../../src/channels/shared/inbound-ttl.mjs';
2
+ import { getInboundTtlRuntime } from './inbound-ttl-runtime.mjs';
3
+
4
+ export const INBOUND_TTL_RPC_CHANNEL = '/dsh-im-settings';
5
+ export const INBOUND_TTL_ENDPOINTS = Object.freeze({
6
+ get: 'settings.inbound-ttl.get',
7
+ set: 'settings.inbound-ttl.set',
8
+ sweep: 'settings.inbound-ttl.sweep',
9
+ });
10
+
11
+ const ENDPOINTS = new Set(Object.values(INBOUND_TTL_ENDPOINTS));
12
+
13
+ export function validInboundTtlPayload(endpoint, payload) {
14
+ if (!ENDPOINTS.has(endpoint)) return false;
15
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
16
+ const keys = Object.keys(payload);
17
+ if (endpoint === INBOUND_TTL_ENDPOINTS.set) {
18
+ return keys.length === 1 && keys[0] === 'ttlHours'
19
+ && normalizeInboundTtlHours(payload.ttlHours) !== null;
20
+ }
21
+ return keys.length === 0;
22
+ }
23
+
24
+ export function createInboundTtlRpcHandler({ store, service, logger = null } = {}) {
25
+ if (!store || typeof store.getTtlHours !== 'function'
26
+ || typeof store.setTtlHours !== 'function'
27
+ || !service || typeof service.sweepNow !== 'function') {
28
+ throw new TypeError('createInboundTtlRpcHandler requires an inbound TTL store and service');
29
+ }
30
+ return async (endpoint, payload, signal) => {
31
+ if (!validInboundTtlPayload(endpoint, payload)) {
32
+ return { ok: false, error: { code: 'bad-request', message: 'Invalid inbound TTL request.' } };
33
+ }
34
+ if (signal?.aborted) {
35
+ return { ok: false, error: { code: 'cancelled', message: 'Request cancelled.' } };
36
+ }
37
+ try {
38
+ if (endpoint === INBOUND_TTL_ENDPOINTS.get) {
39
+ return { ok: true, value: { ttlHours: store.getTtlHours() } };
40
+ }
41
+ if (endpoint === INBOUND_TTL_ENDPOINTS.set) {
42
+ const ttlHours = await store.setTtlHours(payload.ttlHours);
43
+ // Re-arm the sweep interval so the saved value gets a full window
44
+ // before its first scheduled enforcement.
45
+ service.resetSweepSchedule?.();
46
+ return { ok: true, value: { ttlHours } };
47
+ }
48
+ const summary = await service.sweepNow();
49
+ // The UI shows no result text, so the host log is the only place a
50
+ // manual sweep can be diagnosed (TTL applied, coverage, deletions).
51
+ logger?.info?.(
52
+ `[dsh-im] manual inbound attachment sweep: ${summary.deletedDirectories} deleted`
53
+ + ` across ${summary.sweptWorkspaces} workspaces (ttl ${store.getTtlHours()}h)`,
54
+ );
55
+ return { ok: true, value: summary };
56
+ } catch (error) {
57
+ if (error?.code === 'inbound-ttl-invalid') {
58
+ return {
59
+ ok: false,
60
+ error: {
61
+ code: 'inbound-ttl-invalid',
62
+ message: '附件保留时长无效:请输入 -1、0 或 1-8760 之间的整数小时数。',
63
+ },
64
+ };
65
+ }
66
+ return { ok: false, error: { code: 'inbound-ttl-unavailable', message: 'inbound-ttl-unavailable' } };
67
+ }
68
+ };
69
+ }
70
+
71
+ export function installInboundTtlRpc(ctx, options = {}) {
72
+ if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
73
+ throw new TypeError('DSH Host Connection RPC is required');
74
+ }
75
+ const runtime = options.runtime ?? getInboundTtlRuntime(ctx, options.config);
76
+ const logger = typeof ctx?.logger === 'function'
77
+ ? ctx.logger('dsh-im:inbound-ttl') : (ctx?.logger ?? null);
78
+ return ctx.connection.rpc.handle(
79
+ INBOUND_TTL_RPC_CHANNEL,
80
+ createInboundTtlRpcHandler({ ...runtime, logger }),
81
+ { authority: 'loopback' },
82
+ );
83
+ }
@@ -0,0 +1,85 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ import { InboundTtlStore } from '../../src/channels/shared/inbound-ttl-store.mjs';
5
+ import { createInboundTtlService } from './inbound-ttl-service.mjs';
6
+
7
+ const runtimes = new Map();
8
+
9
+ /**
10
+ * Resolve the global settings file shared by every channel. The dshHome
11
+ * resolution mirrors pluginPaths: config.dshHome, then DSH_HOME, then the
12
+ * user's home directory. Channel-specific dataDir values never apply.
13
+ */
14
+ export function inboundTtlSettingsPath(config = {}) {
15
+ const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
16
+ return resolve(dshHome, 'integrations', 'dsh-im', 'settings.json');
17
+ }
18
+
19
+ /**
20
+ * Per-process inbound TTL runtime, cached by resolved settings path so every
21
+ * channel shares one store, one staged registry, and one sweep timer. The
22
+ * entry is dropped (and the sweeper stopped) when the owning context is
23
+ * disposed, letting a later activation rebuild from disk.
24
+ */
25
+ export function getInboundTtlRuntime(ctx, config = {}) {
26
+ const settingsPath = inboundTtlSettingsPath(config);
27
+ const existing = runtimes.get(settingsPath);
28
+ if (existing) return existing.runtime;
29
+ const logger = typeof ctx?.logger === 'function'
30
+ ? ctx.logger('dsh-im:inbound-ttl') : (ctx?.logger ?? console);
31
+ const store = new InboundTtlStore(settingsPath);
32
+ const service = createInboundTtlService({ store, logger });
33
+ const runtime = Object.freeze({ store, service });
34
+ const entry = { runtime };
35
+ runtimes.set(settingsPath, entry);
36
+ // Load persisted settings before the first sweep so a keep-forever TTL can
37
+ // never be overtaken by the in-memory default. An unreadable store keeps
38
+ // sweeping disabled rather than deleting files of unknown intent.
39
+ void store.load().then(
40
+ () => service.start(),
41
+ (error) => logger.error?.('[dsh-im] unable to load inbound attachment settings; sweeping stays disabled', error),
42
+ );
43
+ if (typeof ctx?.effect === 'function') {
44
+ ctx.effect(() => () => {
45
+ if (runtimes.get(settingsPath) !== entry) return;
46
+ runtimes.delete(settingsPath);
47
+ service.stop();
48
+ }, 'dsh-im: stop inbound attachment TTL sweeper');
49
+ }
50
+ return runtime;
51
+ }
52
+
53
+ /**
54
+ * Advertise one channel's bot workspaces to the global sweeper. The provider
55
+ * is consulted at sweep time, so bots added or removed later are always
56
+ * current. The default workspace is always included.
57
+ */
58
+ export function registerInboundTtlWorkspaces(ctx, service, {
59
+ workspaces,
60
+ configStore,
61
+ defaultWorkspace,
62
+ botIdFrom = (bot) => bot?.botId,
63
+ } = {}) {
64
+ if (!service || typeof service.registerWorkspaceProvider !== 'function') return undefined;
65
+ if (!workspaces || typeof workspaces.workspaceFor !== 'function'
66
+ || typeof defaultWorkspace !== 'string' || !defaultWorkspace) {
67
+ return undefined;
68
+ }
69
+ const unregister = service.registerWorkspaceProvider(() => {
70
+ const bots = typeof configStore?.list === 'function' ? configStore.list() : [];
71
+ const paths = [defaultWorkspace];
72
+ for (const bot of Array.isArray(bots) ? bots : []) {
73
+ try {
74
+ paths.push(workspaces.workspaceFor(botIdFrom(bot)));
75
+ } catch {
76
+ // A malformed config entry must not hide the remaining workspaces.
77
+ }
78
+ }
79
+ return paths;
80
+ });
81
+ if (typeof ctx?.effect === 'function') {
82
+ ctx.effect(() => unregister, 'dsh-im: unregister inbound TTL workspaces');
83
+ }
84
+ return unregister;
85
+ }