@xmanrui/dsh-im 4.23.0 → 4.24.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 +7 -2
- package/README.md +7 -2
- package/lib/client.js +498 -347
- package/lib/index.js +284 -283
- package/package.json +11 -2
- package/plugin-src/client/channel-card-meta.js +3 -9
- package/plugin-src/client/channels/dingtalk/index.js +9 -8
- package/plugin-src/client/channels/feishu/index.js +21 -17
- package/plugin-src/client/channels/qq/index.js +9 -8
- package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -8
- package/plugin-src/client/channels/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- package/plugin-src/client/channels/wecom/index.js +9 -8
- package/plugin-src/client/channels/wecom-app/index.js +9 -8
- package/plugin-src/client/channels/weixin/index.js +9 -8
- package/plugin-src/client/channels/whatsapp/index.js +9 -8
- package/plugin-src/client/i18n.js +13 -1
- package/plugin-src/client/index.js +10 -2
- package/plugin-src/client/styles.js +12 -8
- package/plugin-src/client/update-panel.js +18 -8
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/host/update-service.mjs +19 -14
- package/scripts/verify-package.mjs +11 -5
- package/src/channels/email/email-runtime.mjs +9 -2
- package/src/channels/email/transports/agent-mail.mjs +14 -3
- package/src/channels/feishu/bridge.mjs +122 -46
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/shared/harness-client.mjs +224 -39
- package/src/channels/shared/text-harness-bridge.mjs +60 -14
- package/src/channels/shared/workspace-session.mjs +7 -1
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +198 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const FEISHU_STEP_PUSH_MODES = Object.freeze({
|
|
2
2
|
POST: 'post',
|
|
3
3
|
STREAMING_CARD: 'streaming_card',
|
|
4
|
+
LIVE_COT: 'live_cot',
|
|
4
5
|
});
|
|
5
6
|
|
|
6
7
|
/** New connections explicitly opt into the process-card presentation. */
|
|
@@ -8,12 +9,17 @@ export const DEFAULT_FEISHU_STEP_PUSH_MODE = FEISHU_STEP_PUSH_MODES.STREAMING_CA
|
|
|
8
9
|
|
|
9
10
|
export function normalizeFeishuStepPushMode(value) {
|
|
10
11
|
// Bots created before modes existed used posts when step push was enabled.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if (value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD) {
|
|
13
|
+
return FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
14
|
+
}
|
|
15
|
+
if (value === FEISHU_STEP_PUSH_MODES.LIVE_COT) {
|
|
16
|
+
return FEISHU_STEP_PUSH_MODES.LIVE_COT;
|
|
17
|
+
}
|
|
18
|
+
return FEISHU_STEP_PUSH_MODES.POST;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export function isFeishuStepPushMode(value) {
|
|
17
22
|
return value === FEISHU_STEP_PUSH_MODES.POST
|
|
18
|
-
|| value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
|
|
23
|
+
|| value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
|
|
24
|
+
|| value === FEISHU_STEP_PUSH_MODES.LIVE_COT;
|
|
19
25
|
}
|
|
@@ -356,10 +356,23 @@ export function textFromHarnessContent(content) {
|
|
|
356
356
|
.trim();
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
/** Join only reasoning blocks from one Harness message payload. */
|
|
360
|
+
export function reasoningFromHarnessContent(content) {
|
|
361
|
+
return (Array.isArray(content) ? content : [])
|
|
362
|
+
.filter((part) => part?.type === 'reasoning' && typeof part.text === 'string')
|
|
363
|
+
.map((part) => part.text)
|
|
364
|
+
.join('\n')
|
|
365
|
+
.trim();
|
|
366
|
+
}
|
|
367
|
+
|
|
359
368
|
function assistantMessageText(event) {
|
|
360
369
|
return textFromHarnessContent(event?.data?.message?.content);
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
function assistantReasoningText(event) {
|
|
373
|
+
return reasoningFromHarnessContent(event?.data?.message?.content);
|
|
374
|
+
}
|
|
375
|
+
|
|
363
376
|
/** Aggregate assistant text in stable step/index order for one Harness Turn. */
|
|
364
377
|
export class AssistantTextAccumulator {
|
|
365
378
|
#steps = new Map();
|
|
@@ -415,6 +428,29 @@ function toolResultErrorText(error) {
|
|
|
415
428
|
return null;
|
|
416
429
|
}
|
|
417
430
|
|
|
431
|
+
/** Join the visible text carried by a Harness tool result. */
|
|
432
|
+
function toolResultText(data) {
|
|
433
|
+
const blocks = Array.isArray(data?.message?.content) ? data.message.content : [];
|
|
434
|
+
const nested = blocks.flatMap((block) => (
|
|
435
|
+
Array.isArray(block?.content) ? block.content : [block]
|
|
436
|
+
));
|
|
437
|
+
const text = nested
|
|
438
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
439
|
+
.map((block) => block.text)
|
|
440
|
+
.join('');
|
|
441
|
+
return text || nonEmptyText(data?.text) || toolResultErrorText(data?.error) || '';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function toolResultCallId(data) {
|
|
445
|
+
const blocks = Array.isArray(data?.message?.content) ? data.message.content : [];
|
|
446
|
+
return blocks
|
|
447
|
+
.map((block) => nonEmptyText(block?.toolCallId))
|
|
448
|
+
.find(Boolean)
|
|
449
|
+
?? nonEmptyText(data?.message?.source?.callId)
|
|
450
|
+
?? nonEmptyText(data?.callId)
|
|
451
|
+
?? nonEmptyText(data?.subCallId);
|
|
452
|
+
}
|
|
453
|
+
|
|
418
454
|
function consumeInteractionOwnership(ownership, entries) {
|
|
419
455
|
const ordered = [...entries]
|
|
420
456
|
.map((entry) => entry?.event ?? entry)
|
|
@@ -505,10 +541,18 @@ export class HarnessReplyTracker {
|
|
|
505
541
|
#reason = null;
|
|
506
542
|
#toolNames = new Map();
|
|
507
543
|
#lastToolName = null;
|
|
544
|
+
#transientSeqs = new Set();
|
|
545
|
+
#pendingReasoning = new Map();
|
|
546
|
+
#pendingReasoningChars = 0;
|
|
547
|
+
#reasoning = false;
|
|
508
548
|
|
|
509
|
-
constructor({ promptRpcId, afterSeq = -1 }) {
|
|
549
|
+
constructor({ promptRpcId, afterSeq = -1, reasoning = false }) {
|
|
510
550
|
this.#promptRpcId = promptRpcId;
|
|
511
551
|
this.#lastSeq = afterSeq;
|
|
552
|
+
// Reasoning updates are opt-in per consumer: only channels that surface
|
|
553
|
+
// thinking traces (Telegram thinking mode) subscribe; every other channel
|
|
554
|
+
// keeps its pre-thinking-traces update stream untouched.
|
|
555
|
+
this.#reasoning = reasoning === true;
|
|
512
556
|
}
|
|
513
557
|
|
|
514
558
|
get finished() {
|
|
@@ -542,7 +586,35 @@ export class HarnessReplyTracker {
|
|
|
542
586
|
pushUpdate({ type: 'text', text });
|
|
543
587
|
}
|
|
544
588
|
|
|
545
|
-
|
|
589
|
+
#bufferReasoning(event) {
|
|
590
|
+
const text = event.data?.chunk?.text;
|
|
591
|
+
if (typeof text !== 'string' || !text || text.length > 65536
|
|
592
|
+
|| this.#pendingReasoning.has(event.seq)) return;
|
|
593
|
+
this.#pendingReasoning.set(event.seq, event);
|
|
594
|
+
this.#pendingReasoningChars += text.length;
|
|
595
|
+
while (this.#pendingReasoning.size > 256 || this.#pendingReasoningChars > 65536) {
|
|
596
|
+
const [seq, oldest] = this.#pendingReasoning.entries().next().value;
|
|
597
|
+
this.#pendingReasoning.delete(seq);
|
|
598
|
+
this.#pendingReasoningChars -= oldest.data.chunk.text.length;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#takePendingReasoning() {
|
|
603
|
+
const events = [...this.#pendingReasoning.values()];
|
|
604
|
+
this.#pendingReasoning.clear();
|
|
605
|
+
this.#pendingReasoningChars = 0;
|
|
606
|
+
return events;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#emitReasoning(event, pushUpdate) {
|
|
610
|
+
const text = event.data?.chunk?.text;
|
|
611
|
+
if (event.data?.turn !== this.#targetTurn || this.#transientSeqs.has(event.seq)
|
|
612
|
+
|| typeof text !== 'string' || !text) return;
|
|
613
|
+
this.#transientSeqs.add(event.seq);
|
|
614
|
+
pushUpdate({ type: 'reasoning', turn: this.#targetTurn, text });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
consumeAll(entries, { live = false, fromMux = false } = {}) {
|
|
546
618
|
const updates = [];
|
|
547
619
|
// 同一批轮询内的 text 帧只保留最新累积,其余事件逐帧透出,
|
|
548
620
|
// 让消费方能按顺序看到每个工具调用与结果。
|
|
@@ -556,20 +628,53 @@ export class HarnessReplyTracker {
|
|
|
556
628
|
}
|
|
557
629
|
updates.push(update);
|
|
558
630
|
};
|
|
559
|
-
const ordered = [...entries]
|
|
631
|
+
const ordered = [...(fromMux ? [] : this.#takePendingReasoning()), ...entries]
|
|
560
632
|
.map((entry) => entry?.event ?? entry)
|
|
561
633
|
.filter(Boolean)
|
|
562
634
|
.sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1));
|
|
563
635
|
|
|
564
636
|
for (const event of ordered) {
|
|
637
|
+
if (this.#finished) break;
|
|
565
638
|
const seq = event.seq ?? -1;
|
|
639
|
+
const isReasoning = event.type === 'assistant/chunk'
|
|
640
|
+
&& event.data?.chunk?.type === 'reasoning-delta';
|
|
641
|
+
// The mux is lossy across reconnects. Only history may advance the
|
|
642
|
+
// durable cursor or finish the reply; the mux supplements reasoning.
|
|
643
|
+
if (fromMux && !isReasoning) continue;
|
|
644
|
+
if (isReasoning) {
|
|
645
|
+
if (!live || !Number.isFinite(seq)) continue;
|
|
646
|
+
if (this.#targetTurn === null) this.#bufferReasoning(event);
|
|
647
|
+
else this.#emitReasoning(event, pushUpdate);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (this.#targetTurn === null) {
|
|
651
|
+
if (seq <= this.#lastSeq) continue;
|
|
652
|
+
if (event.type === 'turn/start') {
|
|
653
|
+
this.#openTurn = event.data?.turn ?? null;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (event.type === 'user/message' && event.data?.source?.rpcId === this.#promptRpcId) {
|
|
657
|
+
this.#lastSeq = seq;
|
|
658
|
+
this.#targetTurn = event.data?.turn ?? this.#openTurn;
|
|
659
|
+
if (live && this.#targetTurn !== null) {
|
|
660
|
+
pushUpdate({ type: 'turn-start', turn: this.#targetTurn });
|
|
661
|
+
// Only earlier fragments flush here. Later steps remain in the
|
|
662
|
+
// sorted batch, interleaved with their tool calls and results.
|
|
663
|
+
for (const pending of this.#takePendingReasoning()) {
|
|
664
|
+
this.#emitReasoning(pending, pushUpdate);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
566
670
|
if (seq <= this.#lastSeq) continue;
|
|
567
671
|
this.#lastSeq = seq;
|
|
568
672
|
|
|
569
673
|
if (event.type === 'turn/start') this.#openTurn = event.data?.turn ?? null;
|
|
570
674
|
|
|
571
675
|
if (event.type === 'user/message' && event.data?.source?.rpcId === this.#promptRpcId) {
|
|
572
|
-
this.#targetTurn = this.#openTurn;
|
|
676
|
+
this.#targetTurn = event.data?.turn ?? this.#openTurn;
|
|
677
|
+
if (live) pushUpdate({ type: 'turn-start', turn: this.#targetTurn });
|
|
573
678
|
continue;
|
|
574
679
|
}
|
|
575
680
|
if (this.#targetTurn === null) continue;
|
|
@@ -579,6 +684,13 @@ export class HarnessReplyTracker {
|
|
|
579
684
|
this.#finished = true;
|
|
580
685
|
this.#reason = event.data?.reason ?? null;
|
|
581
686
|
this.#openTurn = null;
|
|
687
|
+
if (live) {
|
|
688
|
+
pushUpdate({
|
|
689
|
+
type: 'turn-end',
|
|
690
|
+
turn: this.#targetTurn,
|
|
691
|
+
reason: this.#reason,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
582
694
|
continue;
|
|
583
695
|
}
|
|
584
696
|
if (event.data?.turn !== this.#targetTurn) continue;
|
|
@@ -597,7 +709,22 @@ export class HarnessReplyTracker {
|
|
|
597
709
|
this.#assistantText.setCanonical(step, text);
|
|
598
710
|
// canonical 定稿且非空时按 step 透出,供分步推送消费方使用;
|
|
599
711
|
// 先于 commitText 透出,保持 text 更新作为批次末尾的既有语义。
|
|
600
|
-
if (text)
|
|
712
|
+
if (text) {
|
|
713
|
+
pushUpdate({
|
|
714
|
+
type: 'assistant-message',
|
|
715
|
+
step,
|
|
716
|
+
text,
|
|
717
|
+
...(live ? { turn: this.#targetTurn } : {}),
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
// Thinking-trace channels consume this as the 💭 line that precedes the
|
|
721
|
+
// tool call it explains; only consumers that explicitly subscribed
|
|
722
|
+
// (reasoning: true) see these updates, so default-mode channels keep
|
|
723
|
+
// their pre-thinking-traces progress stream.
|
|
724
|
+
if (this.#reasoning) {
|
|
725
|
+
const reasoning = assistantReasoningText(event);
|
|
726
|
+
if (reasoning) pushUpdate({ type: 'reasoning', step, text: reasoning });
|
|
727
|
+
}
|
|
601
728
|
this.#commitText(this.#assistantText.text, pushUpdate);
|
|
602
729
|
continue;
|
|
603
730
|
}
|
|
@@ -620,20 +747,38 @@ export class HarnessReplyTracker {
|
|
|
620
747
|
}
|
|
621
748
|
}
|
|
622
749
|
}
|
|
623
|
-
pushUpdate({
|
|
750
|
+
pushUpdate({
|
|
751
|
+
type: 'tool',
|
|
752
|
+
name,
|
|
753
|
+
...(argsText ? { arguments: argsText } : {}),
|
|
754
|
+
...(callId ? { callId } : {}),
|
|
755
|
+
...(live ? { turn: this.#targetTurn } : {}),
|
|
756
|
+
});
|
|
624
757
|
} else if (event.type === 'tool/result') {
|
|
625
|
-
const callId =
|
|
626
|
-
?? nonEmptyText(event.data?.callId)
|
|
627
|
-
?? nonEmptyText(event.data?.subCallId);
|
|
758
|
+
const callId = toolResultCallId(event.data);
|
|
628
759
|
const toolName = (callId ? this.#toolNames.get(callId) : null)
|
|
629
760
|
?? this.#lastToolName;
|
|
630
761
|
const error = toolResultErrorText(event.data?.error);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
762
|
+
if (live) {
|
|
763
|
+
const providerErrorCode = event.data?.error?.code;
|
|
764
|
+
pushUpdate({
|
|
765
|
+
type: 'tool-result',
|
|
766
|
+
turn: this.#targetTurn,
|
|
767
|
+
...(callId ? { callId } : {}),
|
|
768
|
+
...(toolName ? { toolName } : {}),
|
|
769
|
+
text: toolResultText(event.data),
|
|
770
|
+
...(providerErrorCode !== undefined && providerErrorCode !== null
|
|
771
|
+
? { errorCode: String(providerErrorCode) }
|
|
772
|
+
: {}),
|
|
773
|
+
});
|
|
774
|
+
} else {
|
|
775
|
+
pushUpdate({
|
|
776
|
+
type: 'status',
|
|
777
|
+
text: t('正在整理结果…'),
|
|
778
|
+
...(toolName ? { toolName } : {}),
|
|
779
|
+
...(error ? { error } : {}),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
637
782
|
}
|
|
638
783
|
}
|
|
639
784
|
return updates;
|
|
@@ -644,6 +789,19 @@ export class HarnessReplyTracker {
|
|
|
644
789
|
}
|
|
645
790
|
}
|
|
646
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Progress updates an ask() consumer actually receives. latest 模式只投递一条
|
|
794
|
+
* 最新进展;assistant-message 是分步推送专用更新,且 canonical 去重后可能成为
|
|
795
|
+
* 批次唯一变化,绝不能冒充进度投给全部渠道。reasoning 同为思考留痕渠道专用,
|
|
796
|
+
* 默认模式下不得冒充进度(钉钉、企微等会展示其 text)。
|
|
797
|
+
*/
|
|
798
|
+
export function visibleProgressUpdates(updates, progressMode) {
|
|
799
|
+
if (progressMode === 'all') return updates;
|
|
800
|
+
return updates
|
|
801
|
+
.filter((update) => update.type !== 'assistant-message' && update.type !== 'reasoning')
|
|
802
|
+
.slice(-1);
|
|
803
|
+
}
|
|
804
|
+
|
|
647
805
|
export class HarnessRpcError extends Error {
|
|
648
806
|
constructor(method, error) {
|
|
649
807
|
super(`${method}: ${error?.message ?? 'unknown Harness RPC error'}`);
|
|
@@ -1146,6 +1304,7 @@ export class HarnessClient {
|
|
|
1146
1304
|
signal,
|
|
1147
1305
|
onInteraction,
|
|
1148
1306
|
onResolved,
|
|
1307
|
+
onSessionEvent,
|
|
1149
1308
|
onOpen,
|
|
1150
1309
|
ownership,
|
|
1151
1310
|
} = {}) {
|
|
@@ -1159,6 +1318,9 @@ export class HarnessClient {
|
|
|
1159
1318
|
if (onResolved !== undefined && typeof onResolved !== 'function') {
|
|
1160
1319
|
throw new TypeError('onResolved must be a function');
|
|
1161
1320
|
}
|
|
1321
|
+
if (onSessionEvent !== undefined && typeof onSessionEvent !== 'function') {
|
|
1322
|
+
throw new TypeError('onSessionEvent must be a function');
|
|
1323
|
+
}
|
|
1162
1324
|
if (onOpen !== undefined && typeof onOpen !== 'function') {
|
|
1163
1325
|
throw new TypeError('onOpen must be a function');
|
|
1164
1326
|
}
|
|
@@ -1169,6 +1331,7 @@ export class HarnessClient {
|
|
|
1169
1331
|
signal,
|
|
1170
1332
|
onInteraction,
|
|
1171
1333
|
onResolved,
|
|
1334
|
+
onSessionEvent,
|
|
1172
1335
|
onOpen,
|
|
1173
1336
|
ownership,
|
|
1174
1337
|
});
|
|
@@ -1449,7 +1612,12 @@ export class HarnessClient {
|
|
|
1449
1612
|
const timeoutMs = options.timeoutMs ?? 600_000;
|
|
1450
1613
|
const signal = options.signal;
|
|
1451
1614
|
const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
|
|
1452
|
-
const progressMode = options.progressMode === '
|
|
1615
|
+
const progressMode = options.progressMode === 'live'
|
|
1616
|
+
? 'live'
|
|
1617
|
+
: options.progressMode === 'all' ? 'all' : 'latest';
|
|
1618
|
+
// Reasoning updates only exist for consumers that opt in (thinking-trace
|
|
1619
|
+
// mode); default ask() consumers keep the pre-thinking-traces stream.
|
|
1620
|
+
const reasoning = options.reasoning === true;
|
|
1453
1621
|
const onArtifact = typeof options.onArtifact === 'function' ? options.onArtifact : null;
|
|
1454
1622
|
const onInteraction = typeof options.onInteraction === 'function'
|
|
1455
1623
|
? options.onInteraction
|
|
@@ -1470,8 +1638,39 @@ export class HarnessClient {
|
|
|
1470
1638
|
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
1471
1639
|
const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1472
1640
|
const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
|
|
1473
|
-
const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq });
|
|
1641
|
+
const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq, reasoning });
|
|
1642
|
+
let lastProgressAt = Date.now();
|
|
1643
|
+
let lastPollSeq = tracker.lastSeq;
|
|
1644
|
+
let progressTail = Promise.resolve();
|
|
1645
|
+
const consumeProgress = (entries, { fromMux = false } = {}) => {
|
|
1646
|
+
const updates = tracker.consumeAll(entries, {
|
|
1647
|
+
live: progressMode === 'live',
|
|
1648
|
+
fromMux,
|
|
1649
|
+
});
|
|
1650
|
+
const seqAdvanced = tracker.lastSeq > lastPollSeq;
|
|
1651
|
+
lastPollSeq = tracker.lastSeq;
|
|
1652
|
+
if (seqAdvanced) lastProgressAt = Date.now();
|
|
1653
|
+
if (!onUpdate) return progressTail;
|
|
1654
|
+
const visibleUpdates = progressMode === 'all' || progressMode === 'live'
|
|
1655
|
+
? updates
|
|
1656
|
+
: updates
|
|
1657
|
+
.filter((update) => update.type !== 'assistant-message' && update.type !== 'reasoning')
|
|
1658
|
+
.slice(-1);
|
|
1659
|
+
for (const update of visibleUpdates) {
|
|
1660
|
+
progressTail = progressTail
|
|
1661
|
+
.then(() => onUpdate(update))
|
|
1662
|
+
.catch((error) => {
|
|
1663
|
+
console.warn(
|
|
1664
|
+
'[dsh-im] ignored a progress update failure:',
|
|
1665
|
+
this.#logPrefix,
|
|
1666
|
+
error.message,
|
|
1667
|
+
);
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return progressTail;
|
|
1671
|
+
};
|
|
1474
1672
|
const interactionController = onInteraction || onInteractionResolved
|
|
1673
|
+
|| (progressMode === 'live' && onUpdate)
|
|
1475
1674
|
? new AbortController()
|
|
1476
1675
|
: null;
|
|
1477
1676
|
const interactionSignal = interactionController
|
|
@@ -1568,6 +1767,9 @@ export class HarnessClient {
|
|
|
1568
1767
|
signal: interactionSignal,
|
|
1569
1768
|
onInteraction,
|
|
1570
1769
|
onResolved: onInteractionResolved,
|
|
1770
|
+
onSessionEvent: progressMode === 'live'
|
|
1771
|
+
? (event) => { consumeProgress([event], { fromMux: true }); }
|
|
1772
|
+
: undefined,
|
|
1571
1773
|
onOpen: markOpen,
|
|
1572
1774
|
ownership,
|
|
1573
1775
|
});
|
|
@@ -1645,8 +1847,6 @@ export class HarnessClient {
|
|
|
1645
1847
|
// confirm the Session is still running before renewing the wait.
|
|
1646
1848
|
// Interaction ownership is intentionally not a liveness signal: it stays
|
|
1647
1849
|
// active until turn/end and can therefore outlive a stalled turn.
|
|
1648
|
-
let lastProgressAt = Date.now();
|
|
1649
|
-
let lastPollSeq = tracker.lastSeq;
|
|
1650
1850
|
while (true) {
|
|
1651
1851
|
await sleep(300, signal);
|
|
1652
1852
|
const history = await this.rpc(
|
|
@@ -1660,24 +1860,7 @@ export class HarnessClient {
|
|
|
1660
1860
|
this.#consumeInteractionOwnerships(sessionId, history.events ?? []);
|
|
1661
1861
|
if (!wasActive && ownership.active) ownership.reconnect?.();
|
|
1662
1862
|
}
|
|
1663
|
-
|
|
1664
|
-
const seqAdvanced = tracker.lastSeq > lastPollSeq;
|
|
1665
|
-
lastPollSeq = tracker.lastSeq;
|
|
1666
|
-
if (seqAdvanced) lastProgressAt = Date.now();
|
|
1667
|
-
if (onUpdate) {
|
|
1668
|
-
// latest 模式只投递一条最新进展;assistant-message 是分步推送专用更新,
|
|
1669
|
-
// 且 canonical 去重后可能成为批次唯一变化,绝不能冒充进度投给全部渠道。
|
|
1670
|
-
const visibleUpdates = progressMode === 'all'
|
|
1671
|
-
? updates
|
|
1672
|
-
: updates.filter((update) => update.type !== 'assistant-message').slice(-1);
|
|
1673
|
-
for (const update of visibleUpdates) {
|
|
1674
|
-
try {
|
|
1675
|
-
await onUpdate(update);
|
|
1676
|
-
} catch (error) {
|
|
1677
|
-
console.warn('[dsh-im] ignored a progress update failure:', this.#logPrefix, error.message);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1863
|
+
await consumeProgress(history.events ?? []);
|
|
1681
1864
|
if (tracker.finished) {
|
|
1682
1865
|
turnFinished = true;
|
|
1683
1866
|
if (!ownership?.stopRequested && !harnessTurnSucceeded(tracker.reason)) {
|
|
@@ -1761,6 +1944,7 @@ export class HarnessClient {
|
|
|
1761
1944
|
signal,
|
|
1762
1945
|
onInteraction,
|
|
1763
1946
|
onResolved,
|
|
1947
|
+
onSessionEvent,
|
|
1764
1948
|
onOpen,
|
|
1765
1949
|
ownership,
|
|
1766
1950
|
}) {
|
|
@@ -1800,8 +1984,9 @@ export class HarnessClient {
|
|
|
1800
1984
|
};
|
|
1801
1985
|
const processEnvelope = (envelope) => {
|
|
1802
1986
|
const payload = envelope.payload;
|
|
1803
|
-
if (
|
|
1804
|
-
this.#consumeInteractionOwnerships(sessionId, [payload.event]);
|
|
1987
|
+
if (payload.type === 'session/event') {
|
|
1988
|
+
if (ownership) this.#consumeInteractionOwnerships(sessionId, [payload.event]);
|
|
1989
|
+
dispatch(onSessionEvent, payload.event);
|
|
1805
1990
|
return;
|
|
1806
1991
|
}
|
|
1807
1992
|
if (payload.type === 'question/requested' || payload.type === 'approval/requested') {
|
|
@@ -168,6 +168,7 @@ export class TextHarnessBridge {
|
|
|
168
168
|
#deferred;
|
|
169
169
|
#contextEnhancement;
|
|
170
170
|
#accessPolicy;
|
|
171
|
+
#thinkingTraces;
|
|
171
172
|
#status;
|
|
172
173
|
#logger;
|
|
173
174
|
#replyTimeoutMs;
|
|
@@ -195,6 +196,7 @@ export class TextHarnessBridge {
|
|
|
195
196
|
state,
|
|
196
197
|
contextEnhancement,
|
|
197
198
|
accessPolicy,
|
|
199
|
+
thinkingTraces = false,
|
|
198
200
|
status = createTextBridgeStatus(),
|
|
199
201
|
logger = console,
|
|
200
202
|
replyTimeoutMs = 600_000,
|
|
@@ -211,6 +213,7 @@ export class TextHarnessBridge {
|
|
|
211
213
|
this.#state = state;
|
|
212
214
|
this.#contextEnhancement = contextEnhancement;
|
|
213
215
|
this.#accessPolicy = accessPolicy;
|
|
216
|
+
this.#thinkingTraces = thinkingTraces;
|
|
214
217
|
this.#status = status;
|
|
215
218
|
this.#logger = logger;
|
|
216
219
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
@@ -654,6 +657,10 @@ export class TextHarnessBridge {
|
|
|
654
657
|
const batchSubmission = message.batchSubmission;
|
|
655
658
|
let stream = null;
|
|
656
659
|
let semanticStream = false;
|
|
660
|
+
// Thinking streams accept the same delivery-block contract as semantic
|
|
661
|
+
// streams: finish() must receive { text, format } so a markdown answer
|
|
662
|
+
// renders rich instead of falling back to plain text.
|
|
663
|
+
let thinkingStream = false;
|
|
657
664
|
// A keepalive heartbeat keeps short-lived carriers (e.g. Telegram's
|
|
658
665
|
// private-chat Rich Draft) visible during long silent stretches such as a
|
|
659
666
|
// running tool call. Declared outside the try so every exit path (including
|
|
@@ -724,29 +731,51 @@ export class TextHarnessBridge {
|
|
|
724
731
|
this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
|
|
725
732
|
});
|
|
726
733
|
let streamFinished = false;
|
|
727
|
-
if (typeof this.#bot.
|
|
734
|
+
if (this.#thinkingTraces && typeof this.#bot.openThinkingStream === 'function') {
|
|
728
735
|
try {
|
|
729
|
-
stream = await this.#bot.
|
|
730
|
-
|
|
736
|
+
stream = await this.#bot.openThinkingStream(target);
|
|
737
|
+
thinkingStream = true;
|
|
731
738
|
} catch (error) {
|
|
739
|
+
stream = null;
|
|
732
740
|
this.#logger.warn?.(
|
|
733
|
-
`[dsh-im:${this.#descriptor.key}] unable to start a
|
|
741
|
+
`[dsh-im:${this.#descriptor.key}] unable to start a thinking stream; using final delivery:`,
|
|
734
742
|
error,
|
|
735
743
|
);
|
|
736
744
|
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
+
}
|
|
746
|
+
if (!stream) {
|
|
747
|
+
if (typeof this.#bot.openDeliveryStream === 'function') {
|
|
748
|
+
try {
|
|
749
|
+
stream = await this.#bot.openDeliveryStream(target);
|
|
750
|
+
semanticStream = true;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
this.#logger.warn?.(
|
|
753
|
+
`[dsh-im:${this.#descriptor.key}] unable to start a semantic reply stream; using final delivery:`,
|
|
754
|
+
error,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
} else if (typeof this.#bot.openStream === 'function') {
|
|
758
|
+
try {
|
|
759
|
+
stream = await this.#bot.openStream(target);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
this.#logger.warn?.(
|
|
762
|
+
`[dsh-im:${this.#descriptor.key}] unable to start a streamed reply; using text:`,
|
|
763
|
+
error,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
745
766
|
}
|
|
746
767
|
}
|
|
768
|
+
const thinkingMode = Boolean(stream
|
|
769
|
+
&& this.#thinkingTraces
|
|
770
|
+
&& typeof stream.sendToolTrace === 'function');
|
|
771
|
+
// What the model sees. A message may carry two texts: `controlText`, the
|
|
772
|
+
// plain body the parsers read, and `content`, the same body decorated for
|
|
773
|
+
// the model — the email channel prefixes the mail headers there, so a
|
|
774
|
+
// subject-only instruction is not silently dropped. Falling back to the
|
|
775
|
+
// parsed text kept commands working but lost that decoration entirely.
|
|
747
776
|
let content = hasImages || hasReply
|
|
748
777
|
? await promptContentForInboundMessage(message, { signal: this.#signal, deferImages: true })
|
|
749
|
-
: undefined;
|
|
778
|
+
: cleanText(message.content) || undefined;
|
|
750
779
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
751
780
|
let contextEnhanced = false;
|
|
752
781
|
if (snapshot) {
|
|
@@ -801,7 +830,24 @@ export class TextHarnessBridge {
|
|
|
801
830
|
timeoutMs: this.#replyTimeoutMs,
|
|
802
831
|
signal: this.#signal,
|
|
803
832
|
control: { owner: this, key: conversationKey },
|
|
833
|
+
// Thinking mode needs every update (reasoning + tool calls) in
|
|
834
|
+
// order; latest mode filters as before. Only thinking mode subscribes
|
|
835
|
+
// to reasoning updates, so default-mode consumers on other channels
|
|
836
|
+
// never see them (their progress handlers would render update.text).
|
|
837
|
+
progressMode: thinkingMode ? 'all' : undefined,
|
|
838
|
+
reasoning: thinkingMode,
|
|
804
839
|
onUpdate: stream ? async (update) => {
|
|
840
|
+
if (thinkingMode) {
|
|
841
|
+
// Only the 💭 and 🔧 lines are surfaced; status/text updates
|
|
842
|
+
// and the assistant-message canonical text are not.
|
|
843
|
+
if (update.type === 'reasoning') {
|
|
844
|
+
await stream.sendThinking(update.text);
|
|
845
|
+
} else if (update.type === 'tool') {
|
|
846
|
+
await stream.sendToolTrace(update.name, update.arguments);
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (update.type === 'reasoning') return;
|
|
805
851
|
const progress = update.type === 'text' ? update.text
|
|
806
852
|
: update.type === 'tool' ? t('正在使用{name}…', { name: update.name }) : update.text;
|
|
807
853
|
if (progress) {
|
|
@@ -841,7 +887,7 @@ export class TextHarnessBridge {
|
|
|
841
887
|
let textReceipt = null;
|
|
842
888
|
if (stream) {
|
|
843
889
|
try {
|
|
844
|
-
const result = await stream.finish(semanticStream
|
|
890
|
+
const result = await stream.finish(semanticStream || thinkingStream
|
|
845
891
|
? createTextDeliveryBlock(visibleAnswer, answerFormat)
|
|
846
892
|
: visibleAnswer);
|
|
847
893
|
streamFinished = true;
|
|
@@ -94,6 +94,7 @@ export async function askInWorkspaceSession({
|
|
|
94
94
|
key,
|
|
95
95
|
text,
|
|
96
96
|
content,
|
|
97
|
+
prepareContent,
|
|
97
98
|
titleText,
|
|
98
99
|
sourceGuidance,
|
|
99
100
|
contextEnhanced = false,
|
|
@@ -165,8 +166,13 @@ export async function askInWorkspaceSession({
|
|
|
165
166
|
await originalOnArtifact?.(artifact);
|
|
166
167
|
};
|
|
167
168
|
let answer;
|
|
169
|
+
// Prepare session-dependent context after binding, outside the binding
|
|
170
|
+
// lock. A stale-workspace retry must prepare it for the new session too.
|
|
171
|
+
const prompt = prepareContent
|
|
172
|
+
? await prepareContent({ sessionId: binding.sessionId, content, text })
|
|
173
|
+
: content ?? text;
|
|
168
174
|
try {
|
|
169
|
-
answer = await binding.session.ask(
|
|
175
|
+
answer = await binding.session.ask(prompt, artifactOptions);
|
|
170
176
|
} catch (error) {
|
|
171
177
|
if (error?.code === 'harness-reply-timeout' && deferredDelivery) {
|
|
172
178
|
try {
|
|
@@ -50,12 +50,15 @@ export function normalizeTelegramAccessPolicy(value = {}) {
|
|
|
50
50
|
function normalizeTelegramBotExtension(value) {
|
|
51
51
|
const hasAccessMode = Object.hasOwn(value, 'accessMode');
|
|
52
52
|
const hasAllowedUsers = Object.hasOwn(value, 'allowedUsers');
|
|
53
|
-
|
|
53
|
+
const hasThinkingTraces = Object.hasOwn(value, 'thinkingTraces');
|
|
54
|
+
if (!hasAccessMode && !hasAllowedUsers && !hasThinkingTraces) return {};
|
|
54
55
|
try {
|
|
55
56
|
const policy = normalizeTelegramAccessPolicy(value);
|
|
56
57
|
return {
|
|
57
58
|
...(hasAccessMode ? { accessMode: policy.accessMode } : {}),
|
|
58
59
|
...(hasAllowedUsers || hasAccessMode ? { allowedUsers: policy.allowedUsers } : {}),
|
|
60
|
+
// Explicit field is kept; an absent field stays absent.
|
|
61
|
+
...(hasThinkingTraces ? { thinkingTraces: value.thinkingTraces === true } : {}),
|
|
59
62
|
};
|
|
60
63
|
} catch {
|
|
61
64
|
return null;
|
|
@@ -28,7 +28,12 @@ export class TelegramController extends TokenBotController {
|
|
|
28
28
|
bots: snapshot.bots.map((bot) => {
|
|
29
29
|
const config = this.#configStore.get(bot.botId);
|
|
30
30
|
const accessPolicy = normalizeTelegramAccessPolicy(config ?? {});
|
|
31
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
...bot,
|
|
33
|
+
accessPolicy,
|
|
34
|
+
// Default ON; only an explicit false in the config opts out.
|
|
35
|
+
thinkingTraces: config?.thinkingTraces !== false,
|
|
36
|
+
};
|
|
32
37
|
}),
|
|
33
38
|
};
|
|
34
39
|
}
|
|
@@ -37,4 +42,11 @@ export class TelegramController extends TokenBotController {
|
|
|
37
42
|
const accessPolicy = normalizeTelegramAccessPolicy(value);
|
|
38
43
|
return this.updateBotConfig(botId, (config) => ({ ...config, ...accessPolicy }));
|
|
39
44
|
}
|
|
45
|
+
|
|
46
|
+
async setThinkingTraces(botId, value) {
|
|
47
|
+
return this.updateBotConfig(botId, (config) => ({
|
|
48
|
+
...config,
|
|
49
|
+
thinkingTraces: value === true,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
40
52
|
}
|