@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.
- package/PROACTIVE_DELIVERY.en.md +8 -0
- package/PROACTIVE_DELIVERY.md +8 -0
- package/lib/client.js +610 -56
- package/lib/index.js +273 -272
- package/package.json +12 -1
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +54 -0
- package/plugin-src/client/global-settings.js +341 -0
- package/plugin-src/client/i18n.js +29 -0
- package/plugin-src/client/index.js +47 -6
- package/plugin-src/client/styles.js +49 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -0
- package/plugin-src/host/channels/feishu/production.mjs +14 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +16 -0
- package/plugin-src/host/channels/qq/production.mjs +12 -0
- package/plugin-src/host/channels/shared/production.mjs +12 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -0
- package/plugin-src/host/channels/wecom/production.mjs +12 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +12 -0
- package/plugin-src/host/delivery-adapter.mjs +1 -0
- package/plugin-src/host/delivery-service.mjs +15 -0
- package/plugin-src/host/harness-session-coordinator.mjs +26 -5
- package/plugin-src/host/inbound-ttl-rpc.mjs +83 -0
- package/plugin-src/host/inbound-ttl-runtime.mjs +85 -0
- package/plugin-src/host/inbound-ttl-service.mjs +179 -0
- package/plugin-src/host/index.mjs +8 -0
- package/plugin-src/host/modern-harness-api.mjs +58 -2
- package/src/channels/feishu/bridge.mjs +164 -4
- package/src/channels/feishu/feishu-channel.mjs +45 -10
- package/src/channels/feishu/feishu-runtime.mjs +9 -0
- package/src/channels/feishu/message-utils.mjs +13 -0
- package/src/channels/feishu/multi-bot-controller.mjs +16 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/feishu/state-store.mjs +25 -0
- package/src/channels/shared/bot-workspace-store.mjs +6 -0
- package/src/channels/shared/inbound-file.mjs +117 -2
- package/src/channels/shared/inbound-ttl-store.mjs +110 -0
- 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?.
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|