@xmanrui/dsh-im 4.17.1 → 4.18.1
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/README.en.md +24 -16
- package/README.md +23 -18
- package/lib/client.js +428 -246
- package/lib/index.js +280 -258
- package/package.json +11 -2
- package/plugin-src/client/access-policy-settings.js +5 -0
- package/plugin-src/client/channel-logos.js +10 -0
- package/plugin-src/client/channels/imessage/api.js +12 -0
- package/plugin-src/client/channels/imessage/index.js +45 -0
- package/plugin-src/client/channels/imessage/styles.js +19 -0
- package/plugin-src/client/channels/shared/token-api.js +1 -0
- package/plugin-src/client/channels/shared/token-channel.js +5 -1
- package/plugin-src/client/delivery-settings.js +5 -0
- package/plugin-src/client/i18n.js +23 -1
- package/plugin-src/client/index.js +20 -0
- package/plugin-src/client/session-channel-logos.js +2 -0
- package/plugin-src/client/styles.js +1 -0
- package/plugin-src/host/channels/imessage/index.mjs +21 -0
- package/plugin-src/host/channels/imessage/production.mjs +13 -0
- package/plugin-src/host/channels/imessage/rpc.mjs +70 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/modern-harness-api.mjs +1 -1
- package/plugin-src/host/rpc-authority.mjs +3 -2
- package/scripts/verify-lan-management.mjs +176 -0
- package/src/channels/imessage/config-store.mjs +24 -0
- package/src/channels/imessage/controller.mjs +37 -0
- package/src/channels/imessage/harness-client.mjs +7 -0
- package/src/channels/imessage/imessage-api.mjs +194 -0
- package/src/channels/imessage/imessage-bridge.mjs +16 -0
- package/src/channels/imessage/runtime.mjs +98 -0
- package/src/channels/imessage/state-store.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
- package/src/channels/shared/message-failure.mjs +5 -5
- package/src/channels/shared/session-channel-labels.mjs +1 -0
- package/src/channels/shared/text-harness-bridge.mjs +38 -0
- package/src/channels/telegram/telegram-runtime.mjs +77 -25
|
@@ -144,6 +144,7 @@ export class TextHarnessBridge {
|
|
|
144
144
|
#logger;
|
|
145
145
|
#replyTimeoutMs;
|
|
146
146
|
#signal;
|
|
147
|
+
#keepaliveIntervalMs;
|
|
147
148
|
#queues = new Map();
|
|
148
149
|
#pendingInteractions = new Map();
|
|
149
150
|
#interactionKeys = new Map();
|
|
@@ -165,6 +166,7 @@ export class TextHarnessBridge {
|
|
|
165
166
|
logger = console,
|
|
166
167
|
replyTimeoutMs = 600_000,
|
|
167
168
|
signal,
|
|
169
|
+
keepaliveIntervalMs = 4_000,
|
|
168
170
|
}) {
|
|
169
171
|
if (!descriptor?.key || !descriptor?.label) throw new TypeError('A channel descriptor is required');
|
|
170
172
|
if (!bot || typeof bot.sendText !== 'function') throw new TypeError('A bot client is required');
|
|
@@ -179,6 +181,7 @@ export class TextHarnessBridge {
|
|
|
179
181
|
this.#logger = logger;
|
|
180
182
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
181
183
|
this.#signal = signal;
|
|
184
|
+
this.#keepaliveIntervalMs = keepaliveIntervalMs;
|
|
182
185
|
this.#deferred = createDeferredDeliveryCoordinator({ harness, state, signal, logger,
|
|
183
186
|
deliver: (entry, outcome) => this.#deliverDeferredOutcome(entry, outcome),
|
|
184
187
|
});
|
|
@@ -586,6 +589,15 @@ export class TextHarnessBridge {
|
|
|
586
589
|
const batchSubmission = message.batchSubmission;
|
|
587
590
|
let stream = null;
|
|
588
591
|
let semanticStream = false;
|
|
592
|
+
// A keepalive heartbeat keeps short-lived carriers (e.g. Telegram's
|
|
593
|
+
// private-chat Rich Draft) visible during long silent stretches such as a
|
|
594
|
+
// running tool call. Declared outside the try so every exit path (including
|
|
595
|
+
// pre-prompt failures like image parsing) clears the timer.
|
|
596
|
+
let keepaliveTimer = null;
|
|
597
|
+
const stopKeepalive = () => {
|
|
598
|
+
if (keepaliveTimer !== null) clearInterval(keepaliveTimer);
|
|
599
|
+
keepaliveTimer = null;
|
|
600
|
+
};
|
|
589
601
|
try {
|
|
590
602
|
this.#signal?.throwIfAborted();
|
|
591
603
|
if (message.kind === 'group' && message.addressed !== true) {
|
|
@@ -685,6 +697,29 @@ export class TextHarnessBridge {
|
|
|
685
697
|
}));
|
|
686
698
|
contextEnhanced = content !== originalContent;
|
|
687
699
|
}
|
|
700
|
+
// Start the keepalive only after the inbound payload is ready, so a
|
|
701
|
+
// pre-prompt failure (image parsing, context building) cannot leave the
|
|
702
|
+
// timer running; the outermost finally below clears it on every path.
|
|
703
|
+
if (stream && stream.keepalive === true && typeof stream.refresh === 'function') {
|
|
704
|
+
let refreshing = false;
|
|
705
|
+
keepaliveTimer = setInterval(async () => {
|
|
706
|
+
// Skip ticks while the previous heartbeat is pending so redundant
|
|
707
|
+
// refreshes cannot queue ahead of the final answer on a slow network.
|
|
708
|
+
if (refreshing) return;
|
|
709
|
+
refreshing = true;
|
|
710
|
+
try {
|
|
711
|
+
await Promise.allSettled([
|
|
712
|
+
this.#bot.sendTyping?.(target),
|
|
713
|
+
stream.refresh(),
|
|
714
|
+
]);
|
|
715
|
+
} catch {
|
|
716
|
+
// Keepalive is best-effort, including synchronous adapter failures.
|
|
717
|
+
} finally {
|
|
718
|
+
refreshing = false;
|
|
719
|
+
}
|
|
720
|
+
}, this.#keepaliveIntervalMs);
|
|
721
|
+
keepaliveTimer.unref?.();
|
|
722
|
+
}
|
|
688
723
|
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
689
724
|
deferredDelivery: () => ({ coordinator: this.#deferred, target: this.#descriptor.key === 'whatsapp' ? { jid: target.jid, selfChat: target.selfChat } : target }),
|
|
690
725
|
harness: this.#harness,
|
|
@@ -719,6 +754,7 @@ export class TextHarnessBridge {
|
|
|
719
754
|
files: message.files,
|
|
720
755
|
},
|
|
721
756
|
});
|
|
757
|
+
stopKeepalive();
|
|
722
758
|
if (batchSubmission) {
|
|
723
759
|
this.#batches.complete(conversationKey, batchSubmission.token);
|
|
724
760
|
}
|
|
@@ -805,6 +841,7 @@ export class TextHarnessBridge {
|
|
|
805
841
|
}
|
|
806
842
|
return delivery.receipt;
|
|
807
843
|
} catch (error) {
|
|
844
|
+
stopKeepalive();
|
|
808
845
|
const turnStopped = error?.code === 'turn-stopped';
|
|
809
846
|
if (batchSubmission && turnStopped) {
|
|
810
847
|
this.#batches.complete(conversationKey, batchSubmission.token);
|
|
@@ -875,6 +912,7 @@ export class TextHarnessBridge {
|
|
|
875
912
|
}
|
|
876
913
|
return error.deliveryReceipt;
|
|
877
914
|
} finally {
|
|
915
|
+
stopKeepalive();
|
|
878
916
|
await Promise.allSettled([
|
|
879
917
|
this.#cancelPendingInteraction(conversationKey),
|
|
880
918
|
this.#approvals.closeRoute(conversationKey),
|
|
@@ -325,48 +325,99 @@ class TelegramDeliveryStream {
|
|
|
325
325
|
#providerMessageIds;
|
|
326
326
|
#closed = false;
|
|
327
327
|
#lastUpdate = null;
|
|
328
|
+
#lastBlock = null;
|
|
329
|
+
#keepalive = false;
|
|
330
|
+
#chain = Promise.resolve();
|
|
328
331
|
|
|
329
|
-
constructor({
|
|
332
|
+
constructor({
|
|
333
|
+
update,
|
|
334
|
+
finish,
|
|
335
|
+
fail,
|
|
336
|
+
providerMessageIds = [],
|
|
337
|
+
presentation,
|
|
338
|
+
logger,
|
|
339
|
+
keepalive = false,
|
|
340
|
+
}) {
|
|
330
341
|
this.#update = update;
|
|
331
342
|
this.#finish = finish;
|
|
332
343
|
this.#fail = fail;
|
|
333
344
|
this.#providerMessageIds = providerMessageIds;
|
|
334
345
|
this.presentation = presentation;
|
|
335
346
|
this.#logger = logger;
|
|
347
|
+
// Only short-lived carriers (e.g. the private-chat Rich Draft) need a
|
|
348
|
+
// keepalive refresh; editing a real placeholder message with identical
|
|
349
|
+
// content would be rejected by the platform.
|
|
350
|
+
this.#keepalive = keepalive === true;
|
|
336
351
|
}
|
|
337
352
|
|
|
338
353
|
get providerMessageIds() {
|
|
339
354
|
return [...this.#providerMessageIds];
|
|
340
355
|
}
|
|
341
356
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const key = `${block.format}:${block.text}`;
|
|
346
|
-
if (key === this.#lastUpdate) return undefined;
|
|
347
|
-
this.#lastUpdate = key;
|
|
348
|
-
try {
|
|
349
|
-
return await this.#update(block);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', error);
|
|
352
|
-
return undefined;
|
|
353
|
-
}
|
|
357
|
+
/** Whether this carrier is short-lived and wants a keepalive heartbeat. */
|
|
358
|
+
get keepalive() {
|
|
359
|
+
return this.#keepalive;
|
|
354
360
|
}
|
|
355
361
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
this.#
|
|
361
|
-
return
|
|
362
|
+
/** Serialize every write so an in-flight keepalive refresh can never land
|
|
363
|
+
* after the final frame: finish()/fail() queue behind refresh()/update(). */
|
|
364
|
+
#enqueue(task) {
|
|
365
|
+
const run = this.#chain.then(task);
|
|
366
|
+
this.#chain = run.catch(() => undefined);
|
|
367
|
+
return run;
|
|
362
368
|
}
|
|
363
369
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
+
update(value) {
|
|
371
|
+
return this.#enqueue(async () => {
|
|
372
|
+
if (this.#closed) return undefined;
|
|
373
|
+
const block = createTextDeliveryBlock(value);
|
|
374
|
+
this.#lastBlock = block;
|
|
375
|
+
const key = `${block.format}:${block.text}`;
|
|
376
|
+
if (key === this.#lastUpdate) return undefined;
|
|
377
|
+
this.#lastUpdate = key;
|
|
378
|
+
try {
|
|
379
|
+
return await this.#update(block);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.#logger.warn?.('[dsh-im:telegram] rich stream update failed:', error);
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Re-send the most recent frame even when unchanged, to keep a short-lived
|
|
388
|
+
* carrier (the private-chat Rich Draft) visible during long silent
|
|
389
|
+
* stretches such as a running tool call. Serialized like update() so it
|
|
390
|
+
* never overtakes a later finish(). No-op for carriers without keepalive. */
|
|
391
|
+
refresh() {
|
|
392
|
+
return this.#enqueue(async () => {
|
|
393
|
+
if (this.#closed || !this.#keepalive || !this.#lastBlock) return undefined;
|
|
394
|
+
try {
|
|
395
|
+
return await this.#update(this.#lastBlock);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
this.#logger.warn?.('[dsh-im:telegram] rich stream refresh failed:', error);
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
finish(value) {
|
|
404
|
+
return this.#enqueue(async () => {
|
|
405
|
+
if (this.#closed) throw new Error('Message stream is already closed');
|
|
406
|
+
this.#closed = true;
|
|
407
|
+
const result = await this.#finish(createTextDeliveryBlock(value));
|
|
408
|
+
this.#providerMessageIds.push(...(result?.providerMessageIds ?? []));
|
|
409
|
+
return result;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fail(text) {
|
|
414
|
+
return this.#enqueue(async () => {
|
|
415
|
+
if (this.#closed) return undefined;
|
|
416
|
+
this.#closed = true;
|
|
417
|
+
const result = await this.#fail(createTextDeliveryBlock(text, 'plain'));
|
|
418
|
+
this.#providerMessageIds.push(...(result?.providerMessageIds ?? []));
|
|
419
|
+
return result;
|
|
420
|
+
});
|
|
370
421
|
}
|
|
371
422
|
|
|
372
423
|
cancel() {
|
|
@@ -663,6 +714,7 @@ export class TelegramBotClient {
|
|
|
663
714
|
finish: (block) => this.#sendRich(target, block),
|
|
664
715
|
fail: (block) => this.#sendPlain(target, block.text),
|
|
665
716
|
presentation: 'telegram-rich-draft',
|
|
717
|
+
keepalive: true,
|
|
666
718
|
logger: this.#logger,
|
|
667
719
|
});
|
|
668
720
|
await stream.update(createTextDeliveryBlock('正在处理…', 'plain'));
|