@xmanrui/dsh-im 4.9.1 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROACTIVE_DELIVERY.en.md +23 -1
- package/PROACTIVE_DELIVERY.md +23 -1
- package/README.en.md +5 -1
- package/README.md +6 -2
- package/lib/client.js +2077 -673
- package/lib/index.js +277 -272
- package/package.json +13 -1
- package/plugin-src/client/channel-card-meta.js +9 -1
- package/plugin-src/client/channels/dingtalk/api.js +4 -0
- package/plugin-src/client/channels/dingtalk/index.js +25 -2
- package/plugin-src/client/channels/feishu/api.js +6 -0
- package/plugin-src/client/channels/feishu/group-settings.js +581 -0
- package/plugin-src/client/channels/feishu/index.js +26 -150
- package/plugin-src/client/channels/qq/api.js +4 -0
- package/plugin-src/client/channels/qq/index.js +25 -2
- package/plugin-src/client/channels/shared/token-api.js +4 -0
- package/plugin-src/client/channels/shared/token-channel.js +25 -3
- package/plugin-src/client/channels/wecom/api.js +4 -0
- package/plugin-src/client/channels/wecom/index.js +25 -2
- package/plugin-src/client/channels/weixin/api.js +4 -0
- package/plugin-src/client/channels/weixin/index.js +25 -2
- package/plugin-src/client/channels/whatsapp/api.js +4 -0
- package/plugin-src/client/channels/whatsapp/index.js +24 -2
- package/plugin-src/client/delivery-settings.js +73 -5
- package/plugin-src/client/global-settings.js +341 -0
- package/plugin-src/client/i18n.js +49 -0
- package/plugin-src/client/index.js +47 -6
- package/plugin-src/client/model-setting.js +104 -0
- package/plugin-src/client/styles.js +99 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +15 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +17 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +32 -0
- package/plugin-src/host/channels/qq/production.mjs +15 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +19 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +2 -0
- package/plugin-src/host/channels/slack/production.mjs +15 -0
- package/plugin-src/host/channels/slack/rpc.mjs +9 -0
- package/plugin-src/host/channels/wecom/production.mjs +15 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +15 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +15 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/delivery-adapter.mjs +142 -3
- package/plugin-src/host/delivery-rpc.mjs +12 -0
- package/plugin-src/host/delivery-service.mjs +125 -3
- package/plugin-src/host/delivery-suggestions.mjs +46 -0
- package/plugin-src/host/harness-session-coordinator.mjs +32 -10
- 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 +26 -1
- package/plugin-src/host/modern-harness-api.mjs +58 -2
- package/plugin-src/host/session-sync-coordinator.mjs +217 -0
- package/scripts/verify-package.mjs +12 -6
- 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 +291 -38
- package/src/channels/shared/harness-client.mjs +124 -43
- 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
- package/src/channels/shared/model-setting.mjs +110 -0
|
@@ -13,6 +13,8 @@ import { setImHostLanguage } from '../../src/channels/shared/i18n.mjs';
|
|
|
13
13
|
import { installDeliveryRpc } from './delivery-rpc.mjs';
|
|
14
14
|
import { installDeliveryHttp } from './delivery-http.mjs';
|
|
15
15
|
import { createDeliveryService } from './delivery-service.mjs';
|
|
16
|
+
import { installInboundTtlRpc } from './inbound-ttl-rpc.mjs';
|
|
17
|
+
import { installSessionSyncCoordinator } from './session-sync-coordinator.mjs';
|
|
16
18
|
import { installUpdateRpc } from './update-rpc.mjs';
|
|
17
19
|
|
|
18
20
|
export const name = 'dsh-im-host';
|
|
@@ -32,8 +34,11 @@ function channelConfig(config, name, deliveryService) {
|
|
|
32
34
|
|
|
33
35
|
export function createImHostPlugin(internals = {}) {
|
|
34
36
|
const startUpdate = internals.installUpdateRpc ?? installUpdateRpc;
|
|
37
|
+
const startInboundTtl = internals.installInboundTtlRpc ?? installInboundTtlRpc;
|
|
35
38
|
const startDelivery = internals.installDeliveryRpc ?? installDeliveryRpc;
|
|
36
39
|
const startDeliveryHttp = internals.installDeliveryHttp ?? installDeliveryHttp;
|
|
40
|
+
const startSessionSync = internals.installSessionSyncCoordinator
|
|
41
|
+
?? installSessionSyncCoordinator;
|
|
37
42
|
const makeDeliveryService = internals.createDeliveryService ?? createDeliveryService;
|
|
38
43
|
const startFeishu = internals.applyFeishu ?? applyFeishu;
|
|
39
44
|
const startWeixin = internals.applyWeixin ?? applyWeixin;
|
|
@@ -61,13 +66,18 @@ export function createImHostPlugin(internals = {}) {
|
|
|
61
66
|
name,
|
|
62
67
|
inject,
|
|
63
68
|
async apply(ctx, config = {}) {
|
|
64
|
-
const
|
|
69
|
+
const unavailableSessionSyncChannels = channels
|
|
70
|
+
.map(([channel]) => channel)
|
|
71
|
+
.filter((channel) => channel !== 'office'
|
|
72
|
+
&& config[channel]?.harnessBaseUrl !== undefined);
|
|
73
|
+
const deliveryService = makeDeliveryService({ unavailableSessionSyncChannels });
|
|
65
74
|
if (typeof ctx?.provide === 'function') {
|
|
66
75
|
ctx.provide('dshIm', Object.freeze({
|
|
67
76
|
send: (botId, targetId, text, options) => (
|
|
68
77
|
deliveryService.send(botId, targetId, text, options)
|
|
69
78
|
),
|
|
70
79
|
listTargets: async (botId) => (await deliveryService.listTargets(botId)).targets,
|
|
80
|
+
listBots: () => deliveryService.listBots(),
|
|
71
81
|
}));
|
|
72
82
|
}
|
|
73
83
|
const activate = async (readyCtx) => {
|
|
@@ -109,6 +119,11 @@ export function createImHostPlugin(internals = {}) {
|
|
|
109
119
|
} catch (error) {
|
|
110
120
|
logger.error?.('[dsh-im] failed to activate update management; continuing with channels', error);
|
|
111
121
|
}
|
|
122
|
+
try {
|
|
123
|
+
startInboundTtl(ctx, { config });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.error?.('[dsh-im] failed to activate inbound TTL settings; continuing with channels', error);
|
|
126
|
+
}
|
|
112
127
|
try {
|
|
113
128
|
startDelivery(ctx, deliveryService, { authority: config.rpcAuthority });
|
|
114
129
|
} catch (error) {
|
|
@@ -127,6 +142,16 @@ export function createImHostPlugin(internals = {}) {
|
|
|
127
142
|
if (failures.length === channels.length) {
|
|
128
143
|
throw new AggregateError(failures, 'dsh-im failed to activate every channel');
|
|
129
144
|
}
|
|
145
|
+
if (typeof ctx?.on === 'function') {
|
|
146
|
+
try {
|
|
147
|
+
startSessionSync(ctx, deliveryService, {
|
|
148
|
+
logger,
|
|
149
|
+
inputScope: ctx.root ?? ctx,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error?.('[dsh-im] failed to activate Session sync; continuing with channels', error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
130
155
|
}
|
|
131
156
|
}
|
|
132
157
|
|
|
@@ -200,6 +200,7 @@ class ModernHarnessApi {
|
|
|
200
200
|
#pendingQuestions = new Map();
|
|
201
201
|
#pendingApprovals = new Map();
|
|
202
202
|
#sessionCursors = new Map();
|
|
203
|
+
#assistantStreams = new Map();
|
|
203
204
|
#disposers = [];
|
|
204
205
|
#disposed = false;
|
|
205
206
|
|
|
@@ -261,6 +262,13 @@ class ModernHarnessApi {
|
|
|
261
262
|
if (Number.isSafeInteger(event.seq)) this.#rememberCursor(sessionId, event.seq);
|
|
262
263
|
this.#broadcast({ type: 'session/event', sessionId, event });
|
|
263
264
|
}, { global: true }));
|
|
265
|
+
this.#disposers.push(ctx.on('agent/assistant-stream', ({ agent, frame }) => {
|
|
266
|
+
this.#acceptAssistantStream(agent, frame);
|
|
267
|
+
}, { global: true }));
|
|
268
|
+
this.#disposers.push(ctx.on('agent/disposed', ({ agent }) => {
|
|
269
|
+
const sessionId = agent?.session?.id;
|
|
270
|
+
if (typeof sessionId === 'string') this.#assistantStreams.delete(sessionId);
|
|
271
|
+
}, { global: true }));
|
|
264
272
|
this.#disposers.push(ctx.on(
|
|
265
273
|
'approval/request',
|
|
266
274
|
(request, next) => this.#requestApproval(request, next),
|
|
@@ -328,7 +336,7 @@ class ModernHarnessApi {
|
|
|
328
336
|
cursor = this.#sessionCursors.get(sessionId) ?? snapshot.cursor;
|
|
329
337
|
if (beforeSeq === undefined && cursor === snapshot.cursor) {
|
|
330
338
|
return {
|
|
331
|
-
events: historyEntries(snapshot.records),
|
|
339
|
+
events: this.#withAssistantStream(sessionId, historyEntries(snapshot.records)),
|
|
332
340
|
hasMore: snapshot.hasMore === true,
|
|
333
341
|
...(snapshot.projections === undefined ? {} : { projections: snapshot.projections }),
|
|
334
342
|
};
|
|
@@ -342,7 +350,54 @@ class ModernHarnessApi {
|
|
|
342
350
|
...(beforeSeq === undefined ? {} : { beforeSeq }),
|
|
343
351
|
},
|
|
344
352
|
}, signal);
|
|
345
|
-
return {
|
|
353
|
+
return {
|
|
354
|
+
events: beforeSeq === undefined
|
|
355
|
+
? this.#withAssistantStream(sessionId, historyEntries(page.records))
|
|
356
|
+
: historyEntries(page.records),
|
|
357
|
+
hasMore: page.hasMore === true,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#acceptAssistantStream(agent, frame) {
|
|
362
|
+
const sessionId = agent?.session?.id;
|
|
363
|
+
if (typeof sessionId !== 'string' || !frame || typeof frame !== 'object') return;
|
|
364
|
+
if (frame.type === 'start') {
|
|
365
|
+
const nextSeq = agent?.session?.seq;
|
|
366
|
+
const startedAfterSeq = Number.isSafeInteger(nextSeq) && nextSeq >= 0
|
|
367
|
+
? nextSeq - 1
|
|
368
|
+
: this.#sessionCursors.get(sessionId) ?? -1;
|
|
369
|
+
this.#assistantStreams.set(sessionId, {
|
|
370
|
+
attemptId: frame.attemptId,
|
|
371
|
+
turn: frame.turn,
|
|
372
|
+
step: frame.step,
|
|
373
|
+
startedAfterSeq,
|
|
374
|
+
entries: [],
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const stream = this.#assistantStreams.get(sessionId);
|
|
379
|
+
if (!stream || stream.attemptId !== frame.attemptId) return;
|
|
380
|
+
if (frame.type === 'end') {
|
|
381
|
+
this.#assistantStreams.delete(sessionId);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (frame.type !== 'chunk' || frame.index !== stream.entries.length) return;
|
|
385
|
+
stream.entries.push({
|
|
386
|
+
type: 'transient',
|
|
387
|
+
event: {
|
|
388
|
+
type: 'assistant/chunk',
|
|
389
|
+
// Match DSH's Web client ordering: live chunks sit in the numeric gap
|
|
390
|
+
// before the durable assistant/message that replaces them.
|
|
391
|
+
seq: stream.startedAfterSeq + 1 - 1 / (frame.index + 2),
|
|
392
|
+
time: frame.time,
|
|
393
|
+
data: { turn: stream.turn, step: stream.step, chunk: frame.chunk },
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#withAssistantStream(sessionId, entries) {
|
|
399
|
+
const transient = this.#assistantStreams.get(sessionId)?.entries ?? [];
|
|
400
|
+
return transient.length === 0 ? entries : [...entries, ...transient];
|
|
346
401
|
}
|
|
347
402
|
|
|
348
403
|
#rememberCursor(sessionId, cursor) {
|
|
@@ -556,6 +611,7 @@ class ModernHarnessApi {
|
|
|
556
611
|
if (this.#disposed) return;
|
|
557
612
|
this.#disposed = true;
|
|
558
613
|
for (const subscription of [...this.#mux]) subscription.close();
|
|
614
|
+
this.#assistantStreams.clear();
|
|
559
615
|
for (const pending of [...this.#pendingApprovals.values()]) pending.settle('cancelled');
|
|
560
616
|
for (const pending of [...this.#pendingQuestions.values()]) pending.settle(
|
|
561
617
|
'cancelled',
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssistantTextAccumulator,
|
|
3
|
+
consumeDshImInputOrigin,
|
|
4
|
+
textFromHarnessContent,
|
|
5
|
+
} from '../../src/channels/shared/harness-client.mjs';
|
|
6
|
+
|
|
7
|
+
const DSH_USER_PREFIX = '[来自 DSH]\n';
|
|
8
|
+
const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
|
|
9
|
+
|
|
10
|
+
function completedTurn(reason) {
|
|
11
|
+
return (typeof reason === 'string' ? reason : reason?.kind) === 'completed';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function recipientKey(target) {
|
|
15
|
+
return JSON.stringify([target.channel, target.botId, target.targetId]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sessionIdOf(session) {
|
|
19
|
+
const value = session?.id ?? session?.sessionId;
|
|
20
|
+
return typeof value === 'string' && value ? value : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function userInputOrigin(scope, event) {
|
|
24
|
+
const source = event?.data?.source;
|
|
25
|
+
if (source?.kind !== 'user' || typeof source.rpcId !== 'string' || !source.rpcId) {
|
|
26
|
+
return 'other';
|
|
27
|
+
}
|
|
28
|
+
return consumeDshImInputOrigin(scope, source.rpcId) ? 'im' : 'dsh';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validRecipient(target) {
|
|
32
|
+
return target && typeof target === 'object'
|
|
33
|
+
&& typeof target.channel === 'string' && target.channel
|
|
34
|
+
&& typeof target.botId === 'string' && target.botId
|
|
35
|
+
&& typeof target.targetId === 'string' && target.targetId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createSessionSyncCoordinator({ deliveryService, logger = console }) {
|
|
39
|
+
if (typeof deliveryService?.listSessionSyncTargets !== 'function'
|
|
40
|
+
|| typeof deliveryService?.sendSessionSyncText !== 'function') {
|
|
41
|
+
throw new TypeError('Session sync requires a complete delivery service');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const turns = new Map();
|
|
45
|
+
const tails = new Map();
|
|
46
|
+
let closed = false;
|
|
47
|
+
|
|
48
|
+
const logFailure = (phase, target, error) => {
|
|
49
|
+
logger.warn?.(
|
|
50
|
+
`[dsh-im] ignored Session sync ${phase} failure`
|
|
51
|
+
+ (target ? ` (${target.channel}/${target.botId}/${target.targetId})` : '')
|
|
52
|
+
+ ` [${error?.code ?? error?.name ?? 'unknown-error'}]`,
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const deliver = async (sessionId, targets, text, phase) => {
|
|
57
|
+
const unique = new Map();
|
|
58
|
+
for (const target of targets) {
|
|
59
|
+
if (validRecipient(target)) unique.set(recipientKey(target), target);
|
|
60
|
+
}
|
|
61
|
+
const entries = [...unique.entries()];
|
|
62
|
+
const results = await Promise.allSettled(entries.map(([, target]) => (
|
|
63
|
+
deliveryService.sendSessionSyncText(
|
|
64
|
+
target.botId,
|
|
65
|
+
target.targetId,
|
|
66
|
+
sessionId,
|
|
67
|
+
text,
|
|
68
|
+
)
|
|
69
|
+
)));
|
|
70
|
+
const successful = new Map();
|
|
71
|
+
results.forEach((result, index) => {
|
|
72
|
+
const [key, target] = entries[index];
|
|
73
|
+
if (result.status === 'fulfilled') successful.set(key, target);
|
|
74
|
+
else logFailure(phase, target, result.reason);
|
|
75
|
+
});
|
|
76
|
+
return successful;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const processEvent = async (sessionId, event, origin) => {
|
|
80
|
+
if (closed || !event || typeof event !== 'object') return;
|
|
81
|
+
|
|
82
|
+
if (event.type === 'turn/start') {
|
|
83
|
+
const turn = event.data?.turn;
|
|
84
|
+
if (!Number.isSafeInteger(turn)) return;
|
|
85
|
+
turns.set(sessionId, {
|
|
86
|
+
turn,
|
|
87
|
+
step: null,
|
|
88
|
+
origin: 'unknown',
|
|
89
|
+
recipients: null,
|
|
90
|
+
assistant: new AssistantTextAccumulator(),
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const state = turns.get(sessionId);
|
|
96
|
+
if (!state) return;
|
|
97
|
+
|
|
98
|
+
if (event.type === 'step/start') {
|
|
99
|
+
if (event.data?.turn !== undefined && event.data.turn !== state.turn) return;
|
|
100
|
+
state.step = Number.isSafeInteger(event.data?.step) ? event.data.step : null;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (event.type === 'step/end') {
|
|
105
|
+
if (event.data?.turn !== undefined && event.data.turn !== state.turn) return;
|
|
106
|
+
if (event.data?.step === undefined || event.data.step === state.step) state.step = null;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (event.type === 'user/message') {
|
|
111
|
+
if (event.surfaceOp !== 'append') return;
|
|
112
|
+
if (state.origin === 'unknown') state.origin = origin;
|
|
113
|
+
if (state.origin !== 'dsh' || origin !== 'dsh') return;
|
|
114
|
+
const text = textFromHarnessContent(event.data?.content);
|
|
115
|
+
if (!text) return;
|
|
116
|
+
|
|
117
|
+
let targets;
|
|
118
|
+
try {
|
|
119
|
+
targets = await deliveryService.listSessionSyncTargets(sessionId);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logFailure('lookup', null, error);
|
|
122
|
+
targets = [];
|
|
123
|
+
}
|
|
124
|
+
const successful = await deliver(
|
|
125
|
+
sessionId,
|
|
126
|
+
targets,
|
|
127
|
+
`${DSH_USER_PREFIX}${text}`,
|
|
128
|
+
'user delivery',
|
|
129
|
+
);
|
|
130
|
+
if (state.recipients === null) {
|
|
131
|
+
state.recipients = successful;
|
|
132
|
+
} else {
|
|
133
|
+
for (const key of [...state.recipients.keys()]) {
|
|
134
|
+
if (!successful.has(key)) state.recipients.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (event.type === 'assistant/message') {
|
|
141
|
+
if (state.origin !== 'dsh' || event.surfaceOp !== 'append'
|
|
142
|
+
|| event.data?.interrupted === true
|
|
143
|
+
|| (event.data?.turn !== undefined && event.data.turn !== state.turn)) return;
|
|
144
|
+
const text = textFromHarnessContent(event.data?.message?.content);
|
|
145
|
+
const step = Number.isSafeInteger(event.data?.step) ? event.data.step : state.step;
|
|
146
|
+
state.assistant.setCanonical(step, text);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (event.type !== 'turn/end' || event.data?.turn !== state.turn) return;
|
|
151
|
+
turns.delete(sessionId);
|
|
152
|
+
if (state.origin !== 'dsh' || !completedTurn(event.data?.reason)
|
|
153
|
+
|| !state.recipients?.size || !state.assistant.text) return;
|
|
154
|
+
await deliver(
|
|
155
|
+
sessionId,
|
|
156
|
+
state.recipients.values(),
|
|
157
|
+
`${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
|
|
158
|
+
'assistant delivery',
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const enqueue = (sessionId, event, origin = 'other') => {
|
|
163
|
+
if (closed || typeof sessionId !== 'string' || !sessionId) return Promise.resolve();
|
|
164
|
+
const previous = tails.get(sessionId) ?? Promise.resolve();
|
|
165
|
+
const task = previous.then(
|
|
166
|
+
() => processEvent(sessionId, event, origin),
|
|
167
|
+
() => processEvent(sessionId, event, origin),
|
|
168
|
+
);
|
|
169
|
+
const tail = task.catch((error) => logFailure('event handling', null, error)).finally(() => {
|
|
170
|
+
if (tails.get(sessionId) === tail) tails.delete(sessionId);
|
|
171
|
+
});
|
|
172
|
+
tails.set(sessionId, tail);
|
|
173
|
+
return tail;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
enqueue,
|
|
178
|
+
async whenIdle() {
|
|
179
|
+
while (tails.size > 0) await Promise.allSettled([...tails.values()]);
|
|
180
|
+
},
|
|
181
|
+
close() {
|
|
182
|
+
closed = true;
|
|
183
|
+
turns.clear();
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function installSessionSyncCoordinator(ctx, deliveryService, {
|
|
189
|
+
logger = console,
|
|
190
|
+
inputScope = ctx?.root ?? ctx,
|
|
191
|
+
} = {}) {
|
|
192
|
+
if (typeof ctx?.on !== 'function') {
|
|
193
|
+
throw new TypeError('Session sync requires Host session events');
|
|
194
|
+
}
|
|
195
|
+
const coordinator = createSessionSyncCoordinator({ deliveryService, logger });
|
|
196
|
+
const disposeEvent = ctx.on('session/event', (session, event) => {
|
|
197
|
+
const sessionId = sessionIdOf(session);
|
|
198
|
+
if (!sessionId) return;
|
|
199
|
+
// Origin lookup must be synchronous: the originating ask may clean its id
|
|
200
|
+
// immediately after this event callback returns.
|
|
201
|
+
const origin = event?.type === 'user/message'
|
|
202
|
+
? userInputOrigin(inputScope, event)
|
|
203
|
+
: 'other';
|
|
204
|
+
void coordinator.enqueue(sessionId, event, origin);
|
|
205
|
+
}, { global: true });
|
|
206
|
+
let disposed = false;
|
|
207
|
+
const close = () => {
|
|
208
|
+
if (disposed) return;
|
|
209
|
+
disposed = true;
|
|
210
|
+
disposeEvent?.();
|
|
211
|
+
coordinator.close();
|
|
212
|
+
};
|
|
213
|
+
if (typeof ctx.effect === 'function') {
|
|
214
|
+
ctx.effect(() => close, 'dsh-im: Session sync coordinator');
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze({ ...coordinator, close });
|
|
217
|
+
}
|
|
@@ -141,15 +141,21 @@ if ((client.match(/\.slots\.inject\(\s*["']settings\.section["']/gu) ?? []).leng
|
|
|
141
141
|
if (client.includes('settings.plugins.tab') || clientSources.includes('settings.plugins.tab')) {
|
|
142
142
|
throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
|
|
143
143
|
}
|
|
144
|
-
// Connections still have no channel-enable toggle.
|
|
145
|
-
//
|
|
146
|
-
// switch template and one mapped field-input
|
|
144
|
+
// Connections still have no channel-enable toggle. Checkable inputs are owned
|
|
145
|
+
// only by the shared context editor and the saved-target Session sync row.
|
|
146
|
+
// The context editor contains one switch template and one mapped field-input
|
|
147
|
+
// template; the delivery target adds one ordinary checkbox template.
|
|
147
148
|
const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
|
|
148
|
-
const
|
|
149
|
+
const deliverySettingsSource = await readFile(resolve(root, 'plugin-src/client/delivery-settings.js'), 'utf8');
|
|
150
|
+
const otherClientSources = clientSources
|
|
151
|
+
.replace(contextEditorSource, '')
|
|
152
|
+
.replace(deliverySettingsSource, '');
|
|
149
153
|
if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
|
|
154
|
+
|| (deliverySettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
|
|
155
|
+
|| /role:\s*["']switch["']/u.test(deliverySettingsSource)
|
|
150
156
|
|| (client.match(/role:\s*["']switch["']/g) ?? []).length !== 1
|
|
151
|
-
|| (client.match(/type:\s*["']checkbox["']/g) ?? []).length !==
|
|
152
|
-
throw new Error('checkable inputs must be limited to
|
|
157
|
+
|| (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 3) {
|
|
158
|
+
throw new Error('checkable inputs must be limited to context enhancement and Session sync');
|
|
153
159
|
}
|
|
154
160
|
for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
|
|
155
161
|
if (!host.includes(marker) || !client.includes(marker)) {
|