@xmanrui/dsh-im 4.35.0 → 4.36.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 +50 -0
- package/PROACTIVE_DELIVERY.md +22 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/lib/client.js +4 -3
- package/lib/index.js +311 -310
- package/package.json +3 -3
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +4 -2
- package/plugin-src/host/channels/matrix/production.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom-app/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/plugin-src/host/delivery-adapter.mjs +60 -12
- package/plugin-src/host/delivery-service.mjs +127 -1
- package/plugin-src/host/delivery-suggestions.mjs +2 -1
- package/plugin-src/host/harness-connection.mjs +3 -7
- package/plugin-src/host/index.mjs +5 -2
- package/plugin-src/host/modern-harness-api.mjs +139 -4
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +14 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +10 -0
- package/src/channels/discord/discord-runtime.mjs +13 -0
- package/src/channels/feishu/bridge.mjs +30 -13
- package/src/channels/feishu/external-consumer.mjs +118 -0
- package/src/channels/feishu/feishu-runtime.mjs +72 -3
- package/src/channels/feishu/history-reader.mjs +92 -0
- package/src/channels/feishu/multi-bot-controller.mjs +130 -2
- package/src/channels/feishu/nearby-history.mjs +72 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/matrix/matrix-controller.mjs +9 -0
- package/src/channels/matrix/matrix-runtime.mjs +14 -0
- package/src/channels/qq/qq-bridge.mjs +8 -0
- package/src/channels/qq/qq-controller.mjs +14 -0
- package/src/channels/qq/qq-runtime.mjs +10 -0
- package/src/channels/shared/deferred-delivery-coordinator.mjs +2 -1
- package/src/channels/shared/deferred-delivery.mjs +8 -5
- package/src/channels/shared/harness-approval.mjs +84 -7
- package/src/channels/shared/harness-client.mjs +152 -42
- package/src/channels/shared/i18n-en/shared-a.mjs +6 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +3 -0
- package/src/channels/shared/message-failure.mjs +6 -2
- package/src/channels/shared/text-harness-bridge.mjs +6 -0
- package/src/channels/shared/token-bot-controller.mjs +16 -0
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +7 -0
- package/src/channels/slack/slack-controller.mjs +14 -0
- package/src/channels/slack/slack-runtime.mjs +12 -0
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +6 -0
- package/src/channels/wecom/wecom-controller.mjs +14 -0
- package/src/channels/wecom/wecom-runtime.mjs +10 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +6 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +14 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +10 -0
- package/src/channels/weixin/weixin-bridge.mjs +6 -0
- package/src/channels/weixin/weixin-controller.mjs +14 -0
- package/src/channels/weixin/weixin-runtime.mjs +10 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +14 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +10 -0
|
@@ -380,6 +380,11 @@ export class AssistantTextAccumulator {
|
|
|
380
380
|
#steps = new Map();
|
|
381
381
|
#legacyText = '';
|
|
382
382
|
|
|
383
|
+
clear() {
|
|
384
|
+
this.#steps.clear();
|
|
385
|
+
this.#legacyText = '';
|
|
386
|
+
}
|
|
387
|
+
|
|
383
388
|
appendDelta(step, index, text) {
|
|
384
389
|
if (typeof text !== 'string' || !text) return;
|
|
385
390
|
const stepNumber = Number.isSafeInteger(step) ? step : 0;
|
|
@@ -415,6 +420,22 @@ export class AssistantTextAccumulator {
|
|
|
415
420
|
}
|
|
416
421
|
}
|
|
417
422
|
|
|
423
|
+
/** Keep answer text after the last tool boundary, shared by live and recovered replies. */
|
|
424
|
+
export function accumulateFinalAssistantText(accumulator, event) {
|
|
425
|
+
if (event.type === 'tool/call') {
|
|
426
|
+
accumulator.clear();
|
|
427
|
+
} else if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
428
|
+
accumulator.appendDelta(event.data.step, event.data.chunk.index, event.data.chunk.text);
|
|
429
|
+
} else if (event.type === 'assistant/message') {
|
|
430
|
+
const content = event.data?.message?.content;
|
|
431
|
+
const text = textFromHarnessContent(content);
|
|
432
|
+
// Canonical tool messages can arrive before or after the separate call.
|
|
433
|
+
// An explicitly empty final message must not revive an earlier preamble.
|
|
434
|
+
if (!text || content.some((part) => part?.type === 'tool-call')) accumulator.clear();
|
|
435
|
+
else accumulator.setCanonical(event.data.step, text);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
418
439
|
function nonEmptyText(value) {
|
|
419
440
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
420
441
|
}
|
|
@@ -483,33 +504,9 @@ function consumeInteractionOwnership(ownership, entries) {
|
|
|
483
504
|
ownership.completed = true;
|
|
484
505
|
continue;
|
|
485
506
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
&& event.data?.turn === ownership.turn
|
|
490
|
-
&& typeof event.data?.callId === 'string'
|
|
491
|
-
&& event.data.callId) {
|
|
492
|
-
toolCall = {
|
|
493
|
-
callId: event.data.callId,
|
|
494
|
-
name: event.data?.name,
|
|
495
|
-
arguments: event.data?.arguments,
|
|
496
|
-
};
|
|
497
|
-
} else if (event.type === 'tool/code-dispatch-start'
|
|
498
|
-
&& ownership.active
|
|
499
|
-
&& typeof event.data?.subCallId === 'string'
|
|
500
|
-
&& event.data.subCallId) {
|
|
501
|
-
let argumentsText;
|
|
502
|
-
try {
|
|
503
|
-
argumentsText = JSON.stringify(event.data?.arguments);
|
|
504
|
-
} catch {
|
|
505
|
-
argumentsText = undefined;
|
|
506
|
-
}
|
|
507
|
-
toolCall = {
|
|
508
|
-
callId: event.data.subCallId,
|
|
509
|
-
name: event.data?.name,
|
|
510
|
-
arguments: argumentsText,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
507
|
+
const toolCall = ownership.active
|
|
508
|
+
&& (event.type !== 'tool/call' || event.data?.turn === ownership.turn)
|
|
509
|
+
? harnessToolCallFromEvent(event) : null;
|
|
513
510
|
if (toolCall) ownership.toolCalls.set(toolCall.callId, Object.freeze(toolCall));
|
|
514
511
|
}
|
|
515
512
|
}
|
|
@@ -519,17 +516,36 @@ function consumeInteractionOwnership(ownership, entries) {
|
|
|
519
516
|
* this Session. The modern Harness adapter uses the same ownership registry
|
|
520
517
|
* as HarnessClient so it never steals a browser-owned question or approval.
|
|
521
518
|
*/
|
|
522
|
-
export function
|
|
519
|
+
export function activeHarnessInteractionOwner(scope, sessionId, entries = []) {
|
|
523
520
|
if (!scope || !['object', 'function', 'string'].includes(typeof scope)
|
|
524
|
-
|| typeof sessionId !== 'string' || !sessionId) return
|
|
521
|
+
|| typeof sessionId !== 'string' || !sessionId) return null;
|
|
525
522
|
const owners = interactionRegistry(scope).ownerships.get(sessionId);
|
|
526
|
-
if (!owners || owners.size === 0) return
|
|
523
|
+
if (!owners || owners.size === 0) return null;
|
|
527
524
|
for (const ownership of owners) consumeInteractionOwnership(ownership, entries);
|
|
528
|
-
return [...owners].
|
|
525
|
+
return [...owners].filter((ownership) => (
|
|
529
526
|
ownership.active
|
|
530
527
|
&& !ownership.completed
|
|
531
528
|
&& typeof ownership.reconnect === 'function'
|
|
532
|
-
));
|
|
529
|
+
)).sort((left, right) => left.order - right.order)[0] ?? null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Whether a live IM consumer owns this turn. */
|
|
533
|
+
export function hasActiveHarnessInteractionOwner(scope, sessionId, entries = []) {
|
|
534
|
+
return activeHarnessInteractionOwner(scope, sessionId, entries) !== null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Read the operation attached to a normal or PTC tool-call event. */
|
|
538
|
+
export function harnessToolCallFromEvent(event) {
|
|
539
|
+
const data = event?.data;
|
|
540
|
+
if (event?.type === 'tool/call' && typeof data?.callId === 'string') {
|
|
541
|
+
return { callId: data.callId, name: data.name, arguments: data.arguments };
|
|
542
|
+
}
|
|
543
|
+
if (event?.type === 'tool/code-dispatch-start' && typeof data?.subCallId === 'string') {
|
|
544
|
+
let argumentsText;
|
|
545
|
+
try { argumentsText = JSON.stringify(data.arguments); } catch { /* Unserializable details cannot be approved. */ }
|
|
546
|
+
return { callId: data.subCallId, name: data.name, arguments: argumentsText };
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
533
549
|
}
|
|
534
550
|
|
|
535
551
|
export class HarnessReplyTracker {
|
|
@@ -538,6 +554,7 @@ export class HarnessReplyTracker {
|
|
|
538
554
|
#openTurn = null;
|
|
539
555
|
#targetTurn = null;
|
|
540
556
|
#assistantText = new AssistantTextAccumulator();
|
|
557
|
+
#finalAssistantText = null;
|
|
541
558
|
#latestText = '';
|
|
542
559
|
#finished = false;
|
|
543
560
|
#reason = null;
|
|
@@ -548,13 +565,14 @@ export class HarnessReplyTracker {
|
|
|
548
565
|
#pendingReasoningChars = 0;
|
|
549
566
|
#reasoning = false;
|
|
550
567
|
|
|
551
|
-
constructor({ promptRpcId, afterSeq = -1, reasoning = false }) {
|
|
568
|
+
constructor({ promptRpcId, afterSeq = -1, reasoning = false, finalAnswerOnly = false }) {
|
|
552
569
|
this.#promptRpcId = promptRpcId;
|
|
553
570
|
this.#lastSeq = afterSeq;
|
|
554
571
|
// Reasoning updates are opt-in per consumer: only channels that surface
|
|
555
572
|
// thinking traces (Telegram thinking mode) subscribe; every other channel
|
|
556
573
|
// keeps its pre-thinking-traces update stream untouched.
|
|
557
574
|
this.#reasoning = reasoning === true;
|
|
575
|
+
if (finalAnswerOnly === true) this.#finalAssistantText = new AssistantTextAccumulator();
|
|
558
576
|
}
|
|
559
577
|
|
|
560
578
|
get finished() {
|
|
@@ -570,6 +588,10 @@ export class HarnessReplyTracker {
|
|
|
570
588
|
return this.#latestText.trim();
|
|
571
589
|
}
|
|
572
590
|
|
|
591
|
+
get finalAnswer() {
|
|
592
|
+
return this.#finalAssistantText ? this.#finalAssistantText.text : this.answer;
|
|
593
|
+
}
|
|
594
|
+
|
|
573
595
|
get reason() {
|
|
574
596
|
return this.#reason;
|
|
575
597
|
}
|
|
@@ -697,6 +719,8 @@ export class HarnessReplyTracker {
|
|
|
697
719
|
}
|
|
698
720
|
if (event.data?.turn !== this.#targetTurn) continue;
|
|
699
721
|
|
|
722
|
+
if (this.#finalAssistantText) accumulateFinalAssistantText(this.#finalAssistantText, event);
|
|
723
|
+
|
|
700
724
|
if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
701
725
|
const step = event.data?.step ?? 0;
|
|
702
726
|
const index = event.data.chunk.index ?? 0;
|
|
@@ -876,6 +900,14 @@ function harnessTurnSucceeded(reason) {
|
|
|
876
900
|
return (nonEmptyText(reason?.kind) ?? nonEmptyText(reason)) === 'completed';
|
|
877
901
|
}
|
|
878
902
|
|
|
903
|
+
function sessionArchivedError(sessionId, cause) {
|
|
904
|
+
const error = new Error('The Session is archived and cannot process this message.', { cause });
|
|
905
|
+
error.code = 'session-archived';
|
|
906
|
+
error.promptAccepted = false;
|
|
907
|
+
error.details = { sessionId };
|
|
908
|
+
return error;
|
|
909
|
+
}
|
|
910
|
+
|
|
879
911
|
export class HarnessClient {
|
|
880
912
|
#baseUrl;
|
|
881
913
|
#apiProxy;
|
|
@@ -897,6 +929,7 @@ export class HarnessClient {
|
|
|
897
929
|
#managedProcess = null;
|
|
898
930
|
#interactionRegistry;
|
|
899
931
|
#interactionOwnerships;
|
|
932
|
+
#competitiveApprovals;
|
|
900
933
|
#interactionClaims;
|
|
901
934
|
#controlOwnerships;
|
|
902
935
|
|
|
@@ -904,6 +937,7 @@ export class HarnessClient {
|
|
|
904
937
|
baseUrl,
|
|
905
938
|
apiProxy,
|
|
906
939
|
interactionScope = apiProxy,
|
|
940
|
+
competitiveApprovals = false,
|
|
907
941
|
workspace,
|
|
908
942
|
ungroupedWorkspace,
|
|
909
943
|
agentPreset,
|
|
@@ -954,6 +988,7 @@ export class HarnessClient {
|
|
|
954
988
|
|| !['object', 'function'].includes(typeof interactionScope))) {
|
|
955
989
|
throw new TypeError('interactionScope must identify the current Host');
|
|
956
990
|
}
|
|
991
|
+
this.#competitiveApprovals = competitiveApprovals === true;
|
|
957
992
|
this.#workspace = workspace;
|
|
958
993
|
// Only the reserved IM directory bypasses grouping, even after /ws switches.
|
|
959
994
|
this.#ungroupedWorkspace = ungroupedWorkspace;
|
|
@@ -1265,11 +1300,47 @@ export class HarnessClient {
|
|
|
1265
1300
|
}, timeoutMs, options);
|
|
1266
1301
|
}
|
|
1267
1302
|
|
|
1303
|
+
async #isSessionArchived(sessionId, options = {}) {
|
|
1304
|
+
const value = await this.rpc('workspace.list', {}, 30_000, options);
|
|
1305
|
+
if (!Array.isArray(value?.archivedSessionIds)
|
|
1306
|
+
|| value.archivedSessionIds.some((id) => typeof id !== 'string' || !id)) {
|
|
1307
|
+
throw new HarnessTransportError('harness-response-invalid', 'workspace.list');
|
|
1308
|
+
}
|
|
1309
|
+
return value.archivedSessionIds.includes(sessionId);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// A diagnostic recheck must not hide the original failure or cancellation.
|
|
1313
|
+
async #confirmedArchiveError(sessionId, options, cause) {
|
|
1314
|
+
try {
|
|
1315
|
+
if (await this.#isSessionArchived(sessionId, options)) return sessionArchivedError(sessionId, cause);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (options?.signal?.aborted) throw options.signal.reason ?? error;
|
|
1318
|
+
}
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
async #sessionRpc(method, payload, timeoutMs, options) {
|
|
1323
|
+
try {
|
|
1324
|
+
return await this.rpc(method, payload, timeoutMs, options);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (options?.signal?.aborted) throw options.signal.reason ?? error;
|
|
1327
|
+
if (error instanceof HarnessRpcError
|
|
1328
|
+
&& ['internal', 'gateway/internal', 'session-not-found', 'session/not-found'].includes(error.code)) {
|
|
1329
|
+
throw await this.#confirmedArchiveError(payload.sessionId, { signal: options?.signal }, error) ?? error;
|
|
1330
|
+
}
|
|
1331
|
+
throw error;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1268
1335
|
async sessionExists(sessionId, options = {}) {
|
|
1336
|
+
// Archived Sessions still exist. Returning false here would silently bind
|
|
1337
|
+
// the conversation to a new Session; reading their active log can also fail.
|
|
1338
|
+
if (await this.#isSessionArchived(sessionId, options)) return true;
|
|
1269
1339
|
try {
|
|
1270
|
-
await this
|
|
1340
|
+
await this.#sessionRpc('session.history', { sessionId, maxMessages: 1 }, 30_000, options);
|
|
1271
1341
|
return true;
|
|
1272
1342
|
} catch (error) {
|
|
1343
|
+
if (error.code === 'session-archived') return true;
|
|
1273
1344
|
if (error instanceof HarnessRpcError && error.code === 'session-not-found') return false;
|
|
1274
1345
|
throw error;
|
|
1275
1346
|
}
|
|
@@ -1641,7 +1712,8 @@ export class HarnessClient {
|
|
|
1641
1712
|
const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
|
|
1642
1713
|
const inboundImages = Array.isArray(options.images) ? options.images.filter(Boolean) : [];
|
|
1643
1714
|
await this.ensureRunning({ signal });
|
|
1644
|
-
|
|
1715
|
+
if (await this.#isSessionArchived(sessionId, { signal })) throw sessionArchivedError(sessionId);
|
|
1716
|
+
const before = await this.#sessionRpc(
|
|
1645
1717
|
'session.history',
|
|
1646
1718
|
{ sessionId, maxMessages: 1 },
|
|
1647
1719
|
30_000,
|
|
@@ -1650,7 +1722,10 @@ export class HarnessClient {
|
|
|
1650
1722
|
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
1651
1723
|
const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1652
1724
|
const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
|
|
1653
|
-
const tracker = new HarnessReplyTracker({
|
|
1725
|
+
const tracker = new HarnessReplyTracker({
|
|
1726
|
+
promptRpcId, afterSeq: baselineSeq, reasoning, finalAnswerOnly: options.finalAnswerOnly,
|
|
1727
|
+
});
|
|
1728
|
+
let checkedBlockedSeq = baselineSeq;
|
|
1654
1729
|
let lastProgressAt = Date.now();
|
|
1655
1730
|
let lastPollSeq = tracker.lastSeq;
|
|
1656
1731
|
let progressTail = Promise.resolve();
|
|
@@ -1706,6 +1781,7 @@ export class HarnessClient {
|
|
|
1706
1781
|
lastSeq: baselineSeq,
|
|
1707
1782
|
reconnect: null,
|
|
1708
1783
|
order: -1,
|
|
1784
|
+
competitiveApprovals: this.#competitiveApprovals,
|
|
1709
1785
|
toolCalls: new Map(),
|
|
1710
1786
|
control,
|
|
1711
1787
|
}
|
|
@@ -1808,7 +1884,7 @@ export class HarnessClient {
|
|
|
1808
1884
|
// message that merely looks like a guidance block is not configuration.
|
|
1809
1885
|
imSourceGuidance.publish(sessionId, options.sourceGuidance);
|
|
1810
1886
|
const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1811
|
-
const sendPrompt = (promptContent) => this
|
|
1887
|
+
const sendPrompt = (promptContent) => this.#sessionRpc('session.prompt', {
|
|
1812
1888
|
sessionId,
|
|
1813
1889
|
mode: 'queue',
|
|
1814
1890
|
content: promptContent,
|
|
@@ -1862,7 +1938,7 @@ export class HarnessClient {
|
|
|
1862
1938
|
// active until turn/end and can therefore outlive a stalled turn.
|
|
1863
1939
|
while (true) {
|
|
1864
1940
|
await sleep(300, signal);
|
|
1865
|
-
const history = await this
|
|
1941
|
+
const history = await this.#sessionRpc(
|
|
1866
1942
|
'session.history',
|
|
1867
1943
|
{ sessionId, maxMessages: 50 },
|
|
1868
1944
|
30_000,
|
|
@@ -1877,16 +1953,19 @@ export class HarnessClient {
|
|
|
1877
1953
|
if (tracker.finished) {
|
|
1878
1954
|
turnFinished = true;
|
|
1879
1955
|
if (!ownership?.stopRequested && !harnessTurnSucceeded(tracker.reason)) {
|
|
1880
|
-
|
|
1956
|
+
const failure = harnessTurnError(tracker.reason);
|
|
1957
|
+
if (failure.code === 'turn-blocked') {
|
|
1958
|
+
throw await this.#confirmedArchiveError(sessionId, { signal }, failure) ?? failure;
|
|
1959
|
+
}
|
|
1960
|
+
throw failure;
|
|
1881
1961
|
}
|
|
1882
1962
|
// An accepted /stop revokes attachment delivery even when Harness
|
|
1883
1963
|
// preserved a useful partial text answer for the existing UX.
|
|
1884
1964
|
const artifactCount = ownership?.stopRequested
|
|
1885
1965
|
? 0
|
|
1886
1966
|
: await deliverArtifacts();
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
}
|
|
1967
|
+
const answer = ownership?.stopRequested ? tracker.answer : tracker.finalAnswer;
|
|
1968
|
+
if (answer) return answer;
|
|
1890
1969
|
if (artifactCount > 0) return '';
|
|
1891
1970
|
if (ownership?.stopRequested) throw turnStoppedError();
|
|
1892
1971
|
if (artifactHandoffError) throw artifactHandoffError;
|
|
@@ -1898,8 +1977,30 @@ export class HarnessClient {
|
|
|
1898
1977
|
throw harnessTurnError(tracker.reason);
|
|
1899
1978
|
}
|
|
1900
1979
|
|
|
1980
|
+
if (tracker.turn === null && !ownership?.stopRequested) {
|
|
1981
|
+
// An admission gate can block before user/message supplies rpcId.
|
|
1982
|
+
// This event only prompts a Session-level archive check; it never
|
|
1983
|
+
// grants ownership of another caller's turn, artifacts or approvals.
|
|
1984
|
+
const blockedSeq = Math.max(checkedBlockedSeq, ...(history.events ?? [])
|
|
1985
|
+
.map((entry) => entry?.event ?? entry)
|
|
1986
|
+
.filter((event) => event?.type === 'turn/end'
|
|
1987
|
+
&& (event.data?.reason?.kind ?? event.data?.reason) === 'blocked'
|
|
1988
|
+
&& Number.isSafeInteger(event.seq))
|
|
1989
|
+
.map((event) => event.seq));
|
|
1990
|
+
if (blockedSeq > checkedBlockedSeq) {
|
|
1991
|
+
checkedBlockedSeq = blockedSeq;
|
|
1992
|
+
const archived = await this.#confirmedArchiveError(sessionId, { signal });
|
|
1993
|
+
if (archived) throw archived;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1901
1997
|
if (Date.now() - lastProgressAt < timeoutMs) continue;
|
|
1902
1998
|
|
|
1999
|
+
if (!ownership?.stopRequested) {
|
|
2000
|
+
const archived = await this.#confirmedArchiveError(sessionId, { signal });
|
|
2001
|
+
if (archived) throw archived;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1903
2004
|
let running = false;
|
|
1904
2005
|
try {
|
|
1905
2006
|
running = await this.isSessionRunning(sessionId, { signal });
|
|
@@ -1917,6 +2018,10 @@ export class HarnessClient {
|
|
|
1917
2018
|
throw timeoutError;
|
|
1918
2019
|
}
|
|
1919
2020
|
} catch (error) {
|
|
2021
|
+
if (error.code === 'session-archived') {
|
|
2022
|
+
error.promptAccepted = promptAccepted;
|
|
2023
|
+
error.details = { ...error.details, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq };
|
|
2024
|
+
}
|
|
1920
2025
|
if (error instanceof HarnessTurnError) {
|
|
1921
2026
|
error.details ??= {
|
|
1922
2027
|
sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq,
|
|
@@ -2024,6 +2129,11 @@ export class HarnessClient {
|
|
|
2024
2129
|
? this.#interactionClaims.get(claimKey)?.recovered === true
|
|
2025
2130
|
: false,
|
|
2026
2131
|
...(toolCall ? { toolCall } : {}),
|
|
2132
|
+
...(kind === 'approval' && payload.competitive === true ? {
|
|
2133
|
+
withdraw: () => this.respondInteraction(envelope.rpcId, {
|
|
2134
|
+
ok: false, error: { code: 'unavailable', message: 'IM cannot answer this approval', details: {} },
|
|
2135
|
+
}, { signal: AbortSignal.timeout(5_000) }),
|
|
2136
|
+
} : {}),
|
|
2027
2137
|
reconnect: close,
|
|
2028
2138
|
respond: (result, options = {}) => this.respondInteraction(
|
|
2029
2139
|
envelope.rpcId,
|
|
@@ -78,8 +78,12 @@ export default {
|
|
|
78
78
|
'The model rejected the current content. Revise the request and try again.',
|
|
79
79
|
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。':
|
|
80
80
|
'The model reached its output limit without producing a displayable result. Reduce the task scope and try again.',
|
|
81
|
-
'
|
|
82
|
-
'
|
|
81
|
+
'本轮处理被阻止,未能完成。请在 DSH 中查看会话状态和相关提示。':
|
|
82
|
+
'This turn was blocked and did not finish. Check the Session status and related notices in DSH.',
|
|
83
|
+
'当前会话已归档,无法继续处理。请在 DSH 中取消归档,或发送 /new 创建新会话。':
|
|
84
|
+
'The current Session is archived and cannot continue. Unarchive it in DSH, or send /new to create a new Session.',
|
|
85
|
+
'本条消息可能仍在会话队列中,请先到 DSH 查看状态,避免重复提交。':
|
|
86
|
+
'This message may still be queued in the Session. Check its status in DSH before submitting it again.',
|
|
83
87
|
'任务被意外中断,本次未完成。请重试。':
|
|
84
88
|
'The task was unexpectedly interrupted and did not finish. Please try again.',
|
|
85
89
|
'当前会话已不存在。请发送 /new 创建新会话后重试。':
|
|
@@ -70,6 +70,9 @@ export default {
|
|
|
70
70
|
'已批准,仅对本次操作有效。':
|
|
71
71
|
'Approved — valid for this operation only.',
|
|
72
72
|
'已拒绝此次操作。': 'This operation was rejected.',
|
|
73
|
+
'机器人端无法处理这次审批,请到 Web 查看。': 'The bot cannot handle this approval. Please check the Web interface.',
|
|
74
|
+
'未找到可处理端,本次操作未获批准。': 'No approval interface is available. This operation was not approved.',
|
|
75
|
+
'有操作在等待审批:{tool},请到 Web 处理。': 'An operation is awaiting approval: {tool}. Please handle it in the Web interface.',
|
|
73
76
|
'无法完整展示这次操作,已安全拒绝此次审批。':
|
|
74
77
|
'The operation could not be displayed in full, so this approval was safely rejected.',
|
|
75
78
|
'审批提交失败,请重新回复「批准」或「拒绝」。':
|
|
@@ -67,11 +67,13 @@ const FAILURE_MESSAGES = Object.freeze({
|
|
|
67
67
|
MODEL_OUTPUT_LIMIT:
|
|
68
68
|
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。',
|
|
69
69
|
TURN_BLOCKED:
|
|
70
|
-
'
|
|
70
|
+
'本轮处理被阻止,未能完成。请在 DSH 中查看会话状态和相关提示。',
|
|
71
71
|
TURN_INTERRUPTED:
|
|
72
72
|
'任务被意外中断,本次未完成。请重试。',
|
|
73
73
|
SESSION_NOT_FOUND:
|
|
74
74
|
'当前会话已不存在。请发送 /new 创建新会话后重试。',
|
|
75
|
+
SESSION_ARCHIVED:
|
|
76
|
+
'当前会话已归档,无法继续处理。请在 DSH 中取消归档,或发送 /new 创建新会话。',
|
|
75
77
|
SESSION_BUSY:
|
|
76
78
|
'当前会话仍在处理上一项任务。请等待完成,或发送 /stop 后重试。',
|
|
77
79
|
SESSION_STALE:
|
|
@@ -137,6 +139,7 @@ function failureCode(error) {
|
|
|
137
139
|
if (code === 'turn-blocked') return 'TURN_BLOCKED';
|
|
138
140
|
if (['turn-interrupted', 'turn-aborted'].includes(code)) return 'TURN_INTERRUPTED';
|
|
139
141
|
if (code === 'session-not-found') return 'SESSION_NOT_FOUND';
|
|
142
|
+
if (code === 'session-archived') return 'SESSION_ARCHIVED';
|
|
140
143
|
if (code === 'agent-busy') return 'SESSION_BUSY';
|
|
141
144
|
if (code === 'workspace-session-stale') return 'SESSION_STALE';
|
|
142
145
|
if (code.startsWith('workspace-')) return 'WORKSPACE_UNAVAILABLE';
|
|
@@ -220,7 +223,8 @@ export function classifyMessageFailure(error, {
|
|
|
220
223
|
reason: safeReason ?? safeFailureReason(error?.code) ?? code,
|
|
221
224
|
message: typeof userMessage === 'string' && userMessage.trim()
|
|
222
225
|
? userMessage.trim()
|
|
223
|
-
: t(FAILURE_MESSAGES[code])
|
|
226
|
+
: t(FAILURE_MESSAGES[code]) + (code === 'SESSION_ARCHIVED' && error?.promptAccepted === true
|
|
227
|
+
? `\n${t('本条消息可能仍在会话队列中,请先到 DSH 查看状态,避免重复提交。')}` : ''),
|
|
224
228
|
referenceId: safeReferenceId(referenceId),
|
|
225
229
|
at: Number.isFinite(at) ? at : Date.now(),
|
|
226
230
|
});
|
|
@@ -1381,6 +1381,12 @@ export class TextHarnessBridge {
|
|
|
1381
1381
|
});
|
|
1382
1382
|
}
|
|
1383
1383
|
|
|
1384
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
1385
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
1386
|
+
...options, runtimeSignal: this.#signal ?? options.runtimeSignal, accessPolicy: this.#accessPolicy,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1384
1390
|
async #handleInteraction(interaction, {
|
|
1385
1391
|
key,
|
|
1386
1392
|
actor,
|
|
@@ -247,6 +247,22 @@ export class TokenBotController {
|
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
251
|
+
const config = this.#configStore.get(botId);
|
|
252
|
+
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
|
253
|
+
return this.#withBotTransition(botId, async () => {
|
|
254
|
+
const runtime = this.#runtimes.get(botId);
|
|
255
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
256
|
+
const error = new Error(t('{label}机器人尚未连接', {
|
|
257
|
+
label: this.#descriptor.label,
|
|
258
|
+
}));
|
|
259
|
+
error.code = 'bot-not-connected';
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
async deleteBot(botId) {
|
|
251
267
|
const warnings = [];
|
|
252
268
|
const config = this.#configStore.get(botId);
|
|
@@ -234,6 +234,13 @@ export class SlackApi {
|
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
async conversationInfo({ channelId, signal } = {}) {
|
|
238
|
+
const value = await this.#request('conversations.info', {
|
|
239
|
+
tokenKind: 'bot', signal, body: { channel: slackId(channelId, 'channel id') },
|
|
240
|
+
});
|
|
241
|
+
return value.channel;
|
|
242
|
+
}
|
|
243
|
+
|
|
237
244
|
async fileInfo({ fileId, signal } = {}) {
|
|
238
245
|
const value = await this.#request('files.info', {
|
|
239
246
|
tokenKind: 'bot',
|
|
@@ -207,6 +207,20 @@ export class SlackController {
|
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
211
|
+
const config = this.#configStore.get(botId);
|
|
212
|
+
if (!config) throw new Error('Unknown Slack bot');
|
|
213
|
+
return this.#withBotTransition(botId, async () => {
|
|
214
|
+
const runtime = this.#runtimes.get(botId);
|
|
215
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
216
|
+
const error = new Error(t('Slack机器人尚未连接'));
|
|
217
|
+
error.code = 'bot-not-connected';
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
async deleteBot(botId) {
|
|
211
225
|
const warnings = [];
|
|
212
226
|
const config = this.#configStore.get(botId);
|
|
@@ -473,6 +473,18 @@ export class SlackRuntime {
|
|
|
473
473
|
return this.#bridge.sendConnectionTest(text);
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
477
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
478
|
+
const channelId = target?.route?.channelId;
|
|
479
|
+
const channel = await this.#api.conversationInfo({ channelId, signal: options.signal });
|
|
480
|
+
const actor = channel?.is_im === true && typeof channel.user === 'string' ? channel.user : '';
|
|
481
|
+
if (!actor) return false;
|
|
482
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
483
|
+
key: options.key, actor, validate: options.validate,
|
|
484
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
485
|
+
}, options);
|
|
486
|
+
}
|
|
487
|
+
|
|
476
488
|
async sendProactiveText(target, text, options = {}) {
|
|
477
489
|
if (!this.#status.ready || !this.#bridge) {
|
|
478
490
|
const error = new Error('Slack bot is not connected');
|
|
@@ -1171,6 +1171,16 @@ export class TelegramRuntime {
|
|
|
1171
1171
|
return this.#bridge.sendConnectionTest(text);
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
1175
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
1176
|
+
const actor = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
1177
|
+
if (!actor) return false;
|
|
1178
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
1179
|
+
key: options.key, actor, validate: options.validate,
|
|
1180
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
1181
|
+
}, options);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1174
1184
|
async sendProactiveText(target, text, options = {}) {
|
|
1175
1185
|
if (!this.#status.ready || !this.#bridge) {
|
|
1176
1186
|
const error = new Error('Telegram bot is not connected');
|
|
@@ -1668,6 +1668,12 @@ export class WecomHarnessBridge {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
1672
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
1673
|
+
...options, runtimeSignal: this.#signal, accessPolicy: this.#accessPolicy,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1671
1677
|
async #handleInteraction(interaction, {
|
|
1672
1678
|
key,
|
|
1673
1679
|
actor,
|
|
@@ -281,6 +281,20 @@ export class WecomController {
|
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
285
|
+
const config = this.#configStore.get(botId);
|
|
286
|
+
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
|
287
|
+
return this.#withBotTransition(botId, async () => {
|
|
288
|
+
const runtime = this.#runtimes.get(botId);
|
|
289
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
290
|
+
const error = new Error(t('企业微信连接当前离线'));
|
|
291
|
+
error.code = 'bot-not-connected';
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
284
298
|
async deleteBot(botId) {
|
|
285
299
|
const warnings = [];
|
|
286
300
|
const config = this.#configStore.get(botId);
|
|
@@ -230,6 +230,16 @@ export class WecomRuntime {
|
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
234
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
235
|
+
const actor = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
236
|
+
if (!actor) return false;
|
|
237
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
238
|
+
key: options.key, actor, validate: options.validate,
|
|
239
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
240
|
+
}, options);
|
|
241
|
+
}
|
|
242
|
+
|
|
233
243
|
async sendProactiveText(target, text, { signal } = {}) {
|
|
234
244
|
const chatId = typeof target?.route?.chatId === 'string'
|
|
235
245
|
? target.route.chatId.trim() : '';
|
|
@@ -855,6 +855,12 @@ export class WecomAppBridge {
|
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
859
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
860
|
+
...options, runtimeSignal: this.#signal, accessPolicy: this.#accessPolicy,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
858
864
|
async #handleInteraction(interaction, { key, actor }) {
|
|
859
865
|
if (interaction?.kind === 'approval') {
|
|
860
866
|
return this.#approvals.handleRequested(interaction, {
|
|
@@ -290,6 +290,20 @@ export class WecomAppController {
|
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
294
|
+
const config = this.#configStore.get(botId);
|
|
295
|
+
if (!config) throw new Error('Unknown Enterprise WeChat app bot');
|
|
296
|
+
return this.#withBotTransition(botId, async () => {
|
|
297
|
+
const runtime = this.#runtimes.get(botId);
|
|
298
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
299
|
+
const error = new Error(t('企业微信应用当前离线'));
|
|
300
|
+
error.code = 'bot-not-connected';
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
293
307
|
async deleteBot(botId) {
|
|
294
308
|
const warnings = [];
|
|
295
309
|
const config = this.#configStore.get(botId);
|
|
@@ -193,6 +193,16 @@ export class WecomAppRuntime {
|
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
197
|
+
if (!this.#started || !this.#bridge) return false;
|
|
198
|
+
const actor = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
199
|
+
if (!actor) return false;
|
|
200
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
201
|
+
key: options.key, actor, validate: options.validate,
|
|
202
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
203
|
+
}, options);
|
|
204
|
+
}
|
|
205
|
+
|
|
196
206
|
async sendProactiveText(target, text, { signal } = {}) {
|
|
197
207
|
const chatId = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
198
208
|
if ((target?.kind !== 'user') || !chatId) {
|
|
@@ -1102,6 +1102,12 @@ export class WeixinHarnessBridge {
|
|
|
1102
1102
|
}
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
1106
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
1107
|
+
...options, runtimeSignal: this.#signal, accessPolicy: this.#accessPolicy,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1105
1111
|
async #handleInteraction(interaction, {
|
|
1106
1112
|
key,
|
|
1107
1113
|
actor,
|