@xmanrui/dsh-im 4.35.0 → 4.35.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/lib/client.js +1 -1
- package/lib/index.js +290 -289
- package/package.json +1 -1
- package/src/channels/qq/qq-bridge.mjs +2 -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-client.mjs +116 -10
- package/src/channels/shared/i18n-en/shared-a.mjs +6 -2
- package/src/channels/shared/message-failure.mjs +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.35.
|
|
3
|
+
"version": "4.35.1",
|
|
4
4
|
"icon": "./assets/logo-plugin-message-link-gradient.webp",
|
|
5
5
|
"description": "把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.",
|
|
6
6
|
"keywords": [
|
|
@@ -488,6 +488,7 @@ export class QqHarnessBridge {
|
|
|
488
488
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
489
489
|
this.#signal = signal;
|
|
490
490
|
this.#deferred = createDeferredDeliveryCoordinator({ harness, state, signal, logger,
|
|
491
|
+
finalAnswerOnly: true,
|
|
491
492
|
deliver: (entry, outcome) => this.#deliverDeferredOutcome(entry, outcome),
|
|
492
493
|
});
|
|
493
494
|
this.#fetchImpl = fetchImpl;
|
|
@@ -1085,6 +1086,7 @@ export class QqHarnessBridge {
|
|
|
1085
1086
|
signal: this.#signal,
|
|
1086
1087
|
control: { owner: this, key },
|
|
1087
1088
|
progressMode: 'all',
|
|
1089
|
+
finalAnswerOnly: true,
|
|
1088
1090
|
onUpdate: (update) => {
|
|
1089
1091
|
if (update.error) {
|
|
1090
1092
|
const name = (nonEmptyString(update.toolName) ?? t('工具'))
|
|
@@ -24,6 +24,7 @@ function orderedEvents(history) {
|
|
|
24
24
|
export function createDeferredDeliveryCoordinator({
|
|
25
25
|
harness, state, deliver, signal, logger = console,
|
|
26
26
|
retryDelayMs = 1_000, pollDelayMs = 30_000, watch = true,
|
|
27
|
+
finalAnswerOnly = false,
|
|
27
28
|
} = {}) {
|
|
28
29
|
const enabled = ['deferredEntries', 'putDeferred', 'patchDeferred', 'removeDeferred']
|
|
29
30
|
.every((name) => typeof state?.[name] === 'function');
|
|
@@ -100,7 +101,7 @@ export function createDeferredDeliveryCoordinator({
|
|
|
100
101
|
}
|
|
101
102
|
const outcome = targetTurn === null
|
|
102
103
|
? { found: false, endSeq: -1, turn: null }
|
|
103
|
-
: extractCompletedTurnAnswer(ordered, { turn: targetTurn });
|
|
104
|
+
: extractCompletedTurnAnswer(ordered, { turn: targetTurn, finalAnswerOnly });
|
|
104
105
|
const hasStart = ordered.some((event) => event.type === 'turn/start' && event.data?.turn === targetTurn);
|
|
105
106
|
if (!history.hasMore || hasStart) return { ...outcome, turn: targetTurn };
|
|
106
107
|
const oldest = batch[0]?.seq;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AssistantTextAccumulator } from './harness-client.mjs';
|
|
1
|
+
import { AssistantTextAccumulator, accumulateFinalAssistantText } from './harness-client.mjs';
|
|
2
2
|
|
|
3
3
|
export const MAX_DEFERRED_PER_KEY = 4;
|
|
4
4
|
|
|
@@ -35,12 +35,13 @@ function textFromContent(content) {
|
|
|
35
35
|
*
|
|
36
36
|
* Reuses HarnessReplyTracker text accumulation: chunk deltas accumulate per
|
|
37
37
|
* (step, part index); a canonical assistant/message replaces its step; steps
|
|
38
|
-
* join with a blank line.
|
|
38
|
+
* join with a blank line. finalAnswerOnly discards text before tool calls.
|
|
39
|
+
* `turn` omitted → prefer the latest completed answer.
|
|
39
40
|
* Returns `{ found, turn, text, reason, endSeq }`; `found: false` when the
|
|
40
41
|
* turn ended without a completed final answer (stopped/failed/empty/
|
|
41
42
|
* interrupted) — `reason`/`endSeq` still identify the confirmed terminal.
|
|
42
43
|
*/
|
|
43
|
-
export function extractCompletedTurnAnswer(entries, { turn } = {}) {
|
|
44
|
+
export function extractCompletedTurnAnswer(entries, { turn, finalAnswerOnly = false } = {}) {
|
|
44
45
|
const events = orderedEvents(entries);
|
|
45
46
|
const wantTurn = Number.isSafeInteger(turn) ? turn : null;
|
|
46
47
|
function turnAnswer(turnEnd) {
|
|
@@ -48,12 +49,14 @@ export function extractCompletedTurnAnswer(entries, { turn } = {}) {
|
|
|
48
49
|
let interrupted = false;
|
|
49
50
|
for (const event of events) {
|
|
50
51
|
if (event.seq > turnEnd.seq || event.data?.turn !== turnEnd.data.turn) continue;
|
|
51
|
-
if (
|
|
52
|
+
if (finalAnswerOnly === true) {
|
|
53
|
+
accumulateFinalAssistantText(accumulator, event);
|
|
54
|
+
} else if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
52
55
|
accumulator.appendDelta(event.data.step, event.data.chunk.index, event.data.chunk.text);
|
|
53
56
|
} else if (event.type === 'assistant/message') {
|
|
54
57
|
accumulator.setCanonical(event.data.step, textFromContent(event.data.message?.content));
|
|
55
|
-
interrupted = event.data.interrupted === true;
|
|
56
58
|
}
|
|
59
|
+
if (event.type === 'assistant/message') interrupted = event.data.interrupted === true;
|
|
57
60
|
}
|
|
58
61
|
return { text: accumulator.text, interrupted };
|
|
59
62
|
}
|
|
@@ -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
|
}
|
|
@@ -538,6 +559,7 @@ export class HarnessReplyTracker {
|
|
|
538
559
|
#openTurn = null;
|
|
539
560
|
#targetTurn = null;
|
|
540
561
|
#assistantText = new AssistantTextAccumulator();
|
|
562
|
+
#finalAssistantText = null;
|
|
541
563
|
#latestText = '';
|
|
542
564
|
#finished = false;
|
|
543
565
|
#reason = null;
|
|
@@ -548,13 +570,14 @@ export class HarnessReplyTracker {
|
|
|
548
570
|
#pendingReasoningChars = 0;
|
|
549
571
|
#reasoning = false;
|
|
550
572
|
|
|
551
|
-
constructor({ promptRpcId, afterSeq = -1, reasoning = false }) {
|
|
573
|
+
constructor({ promptRpcId, afterSeq = -1, reasoning = false, finalAnswerOnly = false }) {
|
|
552
574
|
this.#promptRpcId = promptRpcId;
|
|
553
575
|
this.#lastSeq = afterSeq;
|
|
554
576
|
// Reasoning updates are opt-in per consumer: only channels that surface
|
|
555
577
|
// thinking traces (Telegram thinking mode) subscribe; every other channel
|
|
556
578
|
// keeps its pre-thinking-traces update stream untouched.
|
|
557
579
|
this.#reasoning = reasoning === true;
|
|
580
|
+
if (finalAnswerOnly === true) this.#finalAssistantText = new AssistantTextAccumulator();
|
|
558
581
|
}
|
|
559
582
|
|
|
560
583
|
get finished() {
|
|
@@ -570,6 +593,10 @@ export class HarnessReplyTracker {
|
|
|
570
593
|
return this.#latestText.trim();
|
|
571
594
|
}
|
|
572
595
|
|
|
596
|
+
get finalAnswer() {
|
|
597
|
+
return this.#finalAssistantText ? this.#finalAssistantText.text : this.answer;
|
|
598
|
+
}
|
|
599
|
+
|
|
573
600
|
get reason() {
|
|
574
601
|
return this.#reason;
|
|
575
602
|
}
|
|
@@ -697,6 +724,8 @@ export class HarnessReplyTracker {
|
|
|
697
724
|
}
|
|
698
725
|
if (event.data?.turn !== this.#targetTurn) continue;
|
|
699
726
|
|
|
727
|
+
if (this.#finalAssistantText) accumulateFinalAssistantText(this.#finalAssistantText, event);
|
|
728
|
+
|
|
700
729
|
if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
701
730
|
const step = event.data?.step ?? 0;
|
|
702
731
|
const index = event.data.chunk.index ?? 0;
|
|
@@ -876,6 +905,14 @@ function harnessTurnSucceeded(reason) {
|
|
|
876
905
|
return (nonEmptyText(reason?.kind) ?? nonEmptyText(reason)) === 'completed';
|
|
877
906
|
}
|
|
878
907
|
|
|
908
|
+
function sessionArchivedError(sessionId, cause) {
|
|
909
|
+
const error = new Error('The Session is archived and cannot process this message.', { cause });
|
|
910
|
+
error.code = 'session-archived';
|
|
911
|
+
error.promptAccepted = false;
|
|
912
|
+
error.details = { sessionId };
|
|
913
|
+
return error;
|
|
914
|
+
}
|
|
915
|
+
|
|
879
916
|
export class HarnessClient {
|
|
880
917
|
#baseUrl;
|
|
881
918
|
#apiProxy;
|
|
@@ -1265,11 +1302,47 @@ export class HarnessClient {
|
|
|
1265
1302
|
}, timeoutMs, options);
|
|
1266
1303
|
}
|
|
1267
1304
|
|
|
1305
|
+
async #isSessionArchived(sessionId, options = {}) {
|
|
1306
|
+
const value = await this.rpc('workspace.list', {}, 30_000, options);
|
|
1307
|
+
if (!Array.isArray(value?.archivedSessionIds)
|
|
1308
|
+
|| value.archivedSessionIds.some((id) => typeof id !== 'string' || !id)) {
|
|
1309
|
+
throw new HarnessTransportError('harness-response-invalid', 'workspace.list');
|
|
1310
|
+
}
|
|
1311
|
+
return value.archivedSessionIds.includes(sessionId);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// A diagnostic recheck must not hide the original failure or cancellation.
|
|
1315
|
+
async #confirmedArchiveError(sessionId, options, cause) {
|
|
1316
|
+
try {
|
|
1317
|
+
if (await this.#isSessionArchived(sessionId, options)) return sessionArchivedError(sessionId, cause);
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (options?.signal?.aborted) throw options.signal.reason ?? error;
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async #sessionRpc(method, payload, timeoutMs, options) {
|
|
1325
|
+
try {
|
|
1326
|
+
return await this.rpc(method, payload, timeoutMs, options);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
if (options?.signal?.aborted) throw options.signal.reason ?? error;
|
|
1329
|
+
if (error instanceof HarnessRpcError
|
|
1330
|
+
&& ['internal', 'gateway/internal', 'session-not-found', 'session/not-found'].includes(error.code)) {
|
|
1331
|
+
throw await this.#confirmedArchiveError(payload.sessionId, { signal: options?.signal }, error) ?? error;
|
|
1332
|
+
}
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1268
1337
|
async sessionExists(sessionId, options = {}) {
|
|
1338
|
+
// Archived Sessions still exist. Returning false here would silently bind
|
|
1339
|
+
// the conversation to a new Session; reading their active log can also fail.
|
|
1340
|
+
if (await this.#isSessionArchived(sessionId, options)) return true;
|
|
1269
1341
|
try {
|
|
1270
|
-
await this
|
|
1342
|
+
await this.#sessionRpc('session.history', { sessionId, maxMessages: 1 }, 30_000, options);
|
|
1271
1343
|
return true;
|
|
1272
1344
|
} catch (error) {
|
|
1345
|
+
if (error.code === 'session-archived') return true;
|
|
1273
1346
|
if (error instanceof HarnessRpcError && error.code === 'session-not-found') return false;
|
|
1274
1347
|
throw error;
|
|
1275
1348
|
}
|
|
@@ -1641,7 +1714,8 @@ export class HarnessClient {
|
|
|
1641
1714
|
const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
|
|
1642
1715
|
const inboundImages = Array.isArray(options.images) ? options.images.filter(Boolean) : [];
|
|
1643
1716
|
await this.ensureRunning({ signal });
|
|
1644
|
-
|
|
1717
|
+
if (await this.#isSessionArchived(sessionId, { signal })) throw sessionArchivedError(sessionId);
|
|
1718
|
+
const before = await this.#sessionRpc(
|
|
1645
1719
|
'session.history',
|
|
1646
1720
|
{ sessionId, maxMessages: 1 },
|
|
1647
1721
|
30_000,
|
|
@@ -1650,7 +1724,10 @@ export class HarnessClient {
|
|
|
1650
1724
|
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
1651
1725
|
const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1652
1726
|
const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
|
|
1653
|
-
const tracker = new HarnessReplyTracker({
|
|
1727
|
+
const tracker = new HarnessReplyTracker({
|
|
1728
|
+
promptRpcId, afterSeq: baselineSeq, reasoning, finalAnswerOnly: options.finalAnswerOnly,
|
|
1729
|
+
});
|
|
1730
|
+
let checkedBlockedSeq = baselineSeq;
|
|
1654
1731
|
let lastProgressAt = Date.now();
|
|
1655
1732
|
let lastPollSeq = tracker.lastSeq;
|
|
1656
1733
|
let progressTail = Promise.resolve();
|
|
@@ -1808,7 +1885,7 @@ export class HarnessClient {
|
|
|
1808
1885
|
// message that merely looks like a guidance block is not configuration.
|
|
1809
1886
|
imSourceGuidance.publish(sessionId, options.sourceGuidance);
|
|
1810
1887
|
const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1811
|
-
const sendPrompt = (promptContent) => this
|
|
1888
|
+
const sendPrompt = (promptContent) => this.#sessionRpc('session.prompt', {
|
|
1812
1889
|
sessionId,
|
|
1813
1890
|
mode: 'queue',
|
|
1814
1891
|
content: promptContent,
|
|
@@ -1862,7 +1939,7 @@ export class HarnessClient {
|
|
|
1862
1939
|
// active until turn/end and can therefore outlive a stalled turn.
|
|
1863
1940
|
while (true) {
|
|
1864
1941
|
await sleep(300, signal);
|
|
1865
|
-
const history = await this
|
|
1942
|
+
const history = await this.#sessionRpc(
|
|
1866
1943
|
'session.history',
|
|
1867
1944
|
{ sessionId, maxMessages: 50 },
|
|
1868
1945
|
30_000,
|
|
@@ -1877,16 +1954,19 @@ export class HarnessClient {
|
|
|
1877
1954
|
if (tracker.finished) {
|
|
1878
1955
|
turnFinished = true;
|
|
1879
1956
|
if (!ownership?.stopRequested && !harnessTurnSucceeded(tracker.reason)) {
|
|
1880
|
-
|
|
1957
|
+
const failure = harnessTurnError(tracker.reason);
|
|
1958
|
+
if (failure.code === 'turn-blocked') {
|
|
1959
|
+
throw await this.#confirmedArchiveError(sessionId, { signal }, failure) ?? failure;
|
|
1960
|
+
}
|
|
1961
|
+
throw failure;
|
|
1881
1962
|
}
|
|
1882
1963
|
// An accepted /stop revokes attachment delivery even when Harness
|
|
1883
1964
|
// preserved a useful partial text answer for the existing UX.
|
|
1884
1965
|
const artifactCount = ownership?.stopRequested
|
|
1885
1966
|
? 0
|
|
1886
1967
|
: await deliverArtifacts();
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
}
|
|
1968
|
+
const answer = ownership?.stopRequested ? tracker.answer : tracker.finalAnswer;
|
|
1969
|
+
if (answer) return answer;
|
|
1890
1970
|
if (artifactCount > 0) return '';
|
|
1891
1971
|
if (ownership?.stopRequested) throw turnStoppedError();
|
|
1892
1972
|
if (artifactHandoffError) throw artifactHandoffError;
|
|
@@ -1898,8 +1978,30 @@ export class HarnessClient {
|
|
|
1898
1978
|
throw harnessTurnError(tracker.reason);
|
|
1899
1979
|
}
|
|
1900
1980
|
|
|
1981
|
+
if (tracker.turn === null && !ownership?.stopRequested) {
|
|
1982
|
+
// An admission gate can block before user/message supplies rpcId.
|
|
1983
|
+
// This event only prompts a Session-level archive check; it never
|
|
1984
|
+
// grants ownership of another caller's turn, artifacts or approvals.
|
|
1985
|
+
const blockedSeq = Math.max(checkedBlockedSeq, ...(history.events ?? [])
|
|
1986
|
+
.map((entry) => entry?.event ?? entry)
|
|
1987
|
+
.filter((event) => event?.type === 'turn/end'
|
|
1988
|
+
&& (event.data?.reason?.kind ?? event.data?.reason) === 'blocked'
|
|
1989
|
+
&& Number.isSafeInteger(event.seq))
|
|
1990
|
+
.map((event) => event.seq));
|
|
1991
|
+
if (blockedSeq > checkedBlockedSeq) {
|
|
1992
|
+
checkedBlockedSeq = blockedSeq;
|
|
1993
|
+
const archived = await this.#confirmedArchiveError(sessionId, { signal });
|
|
1994
|
+
if (archived) throw archived;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1901
1998
|
if (Date.now() - lastProgressAt < timeoutMs) continue;
|
|
1902
1999
|
|
|
2000
|
+
if (!ownership?.stopRequested) {
|
|
2001
|
+
const archived = await this.#confirmedArchiveError(sessionId, { signal });
|
|
2002
|
+
if (archived) throw archived;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1903
2005
|
let running = false;
|
|
1904
2006
|
try {
|
|
1905
2007
|
running = await this.isSessionRunning(sessionId, { signal });
|
|
@@ -1917,6 +2019,10 @@ export class HarnessClient {
|
|
|
1917
2019
|
throw timeoutError;
|
|
1918
2020
|
}
|
|
1919
2021
|
} catch (error) {
|
|
2022
|
+
if (error.code === 'session-archived') {
|
|
2023
|
+
error.promptAccepted = promptAccepted;
|
|
2024
|
+
error.details = { ...error.details, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq };
|
|
2025
|
+
}
|
|
1920
2026
|
if (error instanceof HarnessTurnError) {
|
|
1921
2027
|
error.details ??= {
|
|
1922
2028
|
sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq,
|
|
@@ -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 创建新会话后重试。':
|
|
@@ -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
|
});
|