@xmanrui/dsh-im 4.38.1 → 4.38.2
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 +49 -5
- package/lib/index.js +293 -293
- package/package.json +1 -1
- package/plugin-src/client/connection-error.js +12 -2
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +1 -1
- 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/modern-harness-api.mjs +2 -1
- package/src/channels/feishu/bridge.mjs +87 -23
- package/src/channels/shared/bot-workspace-store.mjs +3 -3
- package/src/channels/shared/connection-error.mjs +29 -4
- package/src/channels/shared/diagnostic-details.mjs +6 -2
- package/src/channels/shared/harness-client.mjs +124 -17
- package/src/channels/shared/harness-mux.mjs +6 -4
- package/src/channels/shared/i18n-en/diagnostics.mjs +24 -0
- package/src/channels/shared/message-failure.mjs +30 -10
- package/src/channels/shared/workspace-session.mjs +10 -1
|
@@ -22,6 +22,7 @@ import { imSourceGuidance } from './im-source-guidance.mjs';
|
|
|
22
22
|
import { outboundArtifactRegistry } from './semantic/artifact.mjs';
|
|
23
23
|
import { t } from './i18n.mjs';
|
|
24
24
|
import { watchHarnessMux } from './harness-mux.mjs';
|
|
25
|
+
import { extractConnectionEvidence, messageDiagnosticError } from './connection-error.mjs';
|
|
25
26
|
|
|
26
27
|
// Every channel plugin runs in the same Host process. Sharing ownership by
|
|
27
28
|
// Host identity (or an explicitly configured HTTP origin) prevents clients
|
|
@@ -29,10 +30,28 @@ import { watchHarnessMux } from './harness-mux.mjs';
|
|
|
29
30
|
// from claiming or cancelling each other's interactions.
|
|
30
31
|
const interactionRegistries = new Map();
|
|
31
32
|
const hostInteractionRegistries = new WeakMap();
|
|
33
|
+
const modelCatalogCaches = new WeakMap();
|
|
34
|
+
const MODEL_CATALOG_TTL_MS = 30_000;
|
|
32
35
|
const MAX_ERROR_CLASSIFICATION_BYTES = 64;
|
|
33
36
|
const IM_INPUT_ORIGIN_TTL_MS = 30 * 60 * 1000;
|
|
34
37
|
const MAX_IM_INPUT_ORIGINS = 4096;
|
|
35
38
|
|
|
39
|
+
function invalidateModelCatalog(entry) {
|
|
40
|
+
if (!entry) return;
|
|
41
|
+
entry.value = null;
|
|
42
|
+
entry.expiresAt = 0;
|
|
43
|
+
// Old readers may finish, but must not repopulate the cache after a fresh read.
|
|
44
|
+
entry.pending = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearModelCatalogCache(apiProxy) {
|
|
48
|
+
const entry = modelCatalogCaches.get(apiProxy);
|
|
49
|
+
if (!entry) return;
|
|
50
|
+
modelCatalogCaches.delete(apiProxy);
|
|
51
|
+
invalidateModelCatalog(entry);
|
|
52
|
+
entry.controller.abort(new DOMException('Harness model catalog disposed', 'AbortError'));
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
function clientTimeZoneFields() {
|
|
37
56
|
try {
|
|
38
57
|
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -1187,9 +1206,46 @@ export class HarnessClient {
|
|
|
1187
1206
|
}
|
|
1188
1207
|
|
|
1189
1208
|
async listModels(options = {}) {
|
|
1209
|
+
options.signal?.throwIfAborted();
|
|
1190
1210
|
await this.ensureRunning(options);
|
|
1191
|
-
|
|
1192
|
-
|
|
1211
|
+
options.signal?.throwIfAborted();
|
|
1212
|
+
if (!this.#apiProxy || options.fresh === true) {
|
|
1213
|
+
try {
|
|
1214
|
+
const value = await this.rpc('llm.models', {}, 30_000, options);
|
|
1215
|
+
return validateModelCatalog(value, 'llm.models');
|
|
1216
|
+
} finally {
|
|
1217
|
+
if (this.#apiProxy) invalidateModelCatalog(modelCatalogCaches.get(this.#apiProxy));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
let entry = modelCatalogCaches.get(this.#apiProxy);
|
|
1222
|
+
if (!entry) {
|
|
1223
|
+
entry = { value: null, expiresAt: 0, pending: null, controller: new AbortController() };
|
|
1224
|
+
modelCatalogCaches.set(this.#apiProxy, entry);
|
|
1225
|
+
}
|
|
1226
|
+
if (entry.value && Date.now() < entry.expiresAt) return structuredClone(entry.value);
|
|
1227
|
+
if (!entry.pending) {
|
|
1228
|
+
// The Host owns this read; one caller cancelling must not cancel its peers.
|
|
1229
|
+
const pending = this.rpc('llm.models', {}, 30_000, {
|
|
1230
|
+
...options, signal: entry.controller.signal,
|
|
1231
|
+
}).then((value) => {
|
|
1232
|
+
const catalog = structuredClone(validateModelCatalog(value, 'llm.models'));
|
|
1233
|
+
if (modelCatalogCaches.get(this.#apiProxy) === entry && entry.pending === pending) {
|
|
1234
|
+
entry.value = catalog;
|
|
1235
|
+
entry.expiresAt = Date.now() + MODEL_CATALOG_TTL_MS;
|
|
1236
|
+
}
|
|
1237
|
+
return catalog;
|
|
1238
|
+
}).finally(() => {
|
|
1239
|
+
if (entry.pending === pending) entry.pending = null;
|
|
1240
|
+
});
|
|
1241
|
+
entry.pending = pending;
|
|
1242
|
+
}
|
|
1243
|
+
const pending = entry.pending;
|
|
1244
|
+
const value = await callWithSignal(
|
|
1245
|
+
() => pending,
|
|
1246
|
+
options.signal ?? entry.controller.signal,
|
|
1247
|
+
);
|
|
1248
|
+
return structuredClone(value);
|
|
1193
1249
|
}
|
|
1194
1250
|
|
|
1195
1251
|
async getSessionModels(sessionId, options = {}) {
|
|
@@ -1224,13 +1280,18 @@ export class HarnessClient {
|
|
|
1224
1280
|
: { reasoningEffort: selection.reasoningEffort }),
|
|
1225
1281
|
}, 30_000, signal ? { ...options, signal } : options);
|
|
1226
1282
|
};
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1283
|
+
try {
|
|
1284
|
+
const value = this.#sessionMaintenanceExecutor
|
|
1285
|
+
? await this.#sessionMaintenanceExecutor({ sessionId, operation })
|
|
1286
|
+
: await operation();
|
|
1287
|
+
if (!value || typeof value !== 'object' || !validModelSelection(value.selected)) {
|
|
1288
|
+
throw new HarnessTransportError('harness-response-invalid', 'session.selectModel');
|
|
1289
|
+
}
|
|
1290
|
+
return value;
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
if (options.signal?.aborted) throw error;
|
|
1293
|
+
throw messageDiagnosticError(error, { stage: 'session.prepare', sessionId });
|
|
1232
1294
|
}
|
|
1233
|
-
return value;
|
|
1234
1295
|
}
|
|
1235
1296
|
|
|
1236
1297
|
async isSessionRunning(sessionId, options = {}) {
|
|
@@ -1279,6 +1340,9 @@ export class HarnessClient {
|
|
|
1279
1340
|
const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
|
|
1280
1341
|
if (agentPreset != null) payload.agentPreset = agentPreset;
|
|
1281
1342
|
const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
|
|
1343
|
+
if (typeof created?.sessionId !== 'string' || !created.sessionId) {
|
|
1344
|
+
throw new HarnessTransportError('harness-response-invalid', 'session.create');
|
|
1345
|
+
}
|
|
1282
1346
|
return created.sessionId;
|
|
1283
1347
|
}
|
|
1284
1348
|
|
|
@@ -1409,6 +1473,7 @@ export class HarnessClient {
|
|
|
1409
1473
|
onSessionEvent,
|
|
1410
1474
|
onOpen,
|
|
1411
1475
|
ownership,
|
|
1476
|
+
onDiagnostic,
|
|
1412
1477
|
} = {}) {
|
|
1413
1478
|
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
1414
1479
|
if (!signal || typeof signal.addEventListener !== 'function') {
|
|
@@ -1426,8 +1491,17 @@ export class HarnessClient {
|
|
|
1426
1491
|
if (onOpen !== undefined && typeof onOpen !== 'function') {
|
|
1427
1492
|
throw new TypeError('onOpen must be a function');
|
|
1428
1493
|
}
|
|
1429
|
-
|
|
1494
|
+
let attempts = 0;
|
|
1495
|
+
let lastFailureReason;
|
|
1496
|
+
const report = () => {
|
|
1497
|
+
try {
|
|
1498
|
+
Promise.resolve(onDiagnostic?.({ attempts, ...(lastFailureReason ? { lastFailureReason } : {}) }))
|
|
1499
|
+
.catch(() => undefined);
|
|
1500
|
+
} catch { /* Diagnostics cannot change stream/reconnect behavior. */ }
|
|
1501
|
+
};
|
|
1430
1502
|
while (!signal.aborted) {
|
|
1503
|
+
attempts += 1;
|
|
1504
|
+
report();
|
|
1431
1505
|
try {
|
|
1432
1506
|
await this.#watchInteractionStream(sessionId, {
|
|
1433
1507
|
signal,
|
|
@@ -1439,6 +1513,8 @@ export class HarnessClient {
|
|
|
1439
1513
|
});
|
|
1440
1514
|
} catch (error) {
|
|
1441
1515
|
if (signal.aborted) return;
|
|
1516
|
+
lastFailureReason = extractConnectionEvidence(error).details.reason;
|
|
1517
|
+
report();
|
|
1442
1518
|
console.warn('[dsh-im] Harness interaction stream disconnected:', this.#logPrefix, error.message);
|
|
1443
1519
|
}
|
|
1444
1520
|
if (signal.aborted) return;
|
|
@@ -1711,6 +1787,16 @@ export class HarnessClient {
|
|
|
1711
1787
|
|
|
1712
1788
|
async ask(sessionId, prompt, options = {}) {
|
|
1713
1789
|
if (typeof options === 'number') options = { timeoutMs: options };
|
|
1790
|
+
const diagnostic = { stage: 'prompt.prepare', submissionState: 'not-submitted', sessionId };
|
|
1791
|
+
try {
|
|
1792
|
+
return await this.#ask(sessionId, prompt, options, diagnostic);
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
if (options.signal?.aborted) throw error;
|
|
1795
|
+
throw messageDiagnosticError(error, diagnostic);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
async #ask(sessionId, prompt, options, diagnostic) {
|
|
1714
1800
|
const timeoutMs = options.timeoutMs ?? 600_000;
|
|
1715
1801
|
const signal = options.signal;
|
|
1716
1802
|
const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
|
|
@@ -1740,6 +1826,7 @@ export class HarnessClient {
|
|
|
1740
1826
|
);
|
|
1741
1827
|
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
1742
1828
|
const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1829
|
+
diagnostic.promptRpcId = promptRpcId;
|
|
1743
1830
|
const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
|
|
1744
1831
|
const tracker = new HarnessReplyTracker({
|
|
1745
1832
|
promptRpcId, afterSeq: baselineSeq, reasoning, finalAnswerOnly: options.finalAnswerOnly,
|
|
@@ -1869,6 +1956,9 @@ export class HarnessClient {
|
|
|
1869
1956
|
});
|
|
1870
1957
|
}
|
|
1871
1958
|
if (interactionSignal) {
|
|
1959
|
+
diagnostic.stage = 'interaction.open';
|
|
1960
|
+
const startedAt = performance.now();
|
|
1961
|
+
let streamDiagnostic = { attempts: 0 };
|
|
1872
1962
|
let markOpen;
|
|
1873
1963
|
const opened = new Promise((resolve) => { markOpen = resolve; });
|
|
1874
1964
|
interactionTask = this.watchInteractions(sessionId, {
|
|
@@ -1880,16 +1970,25 @@ export class HarnessClient {
|
|
|
1880
1970
|
: undefined,
|
|
1881
1971
|
onOpen: markOpen,
|
|
1882
1972
|
ownership,
|
|
1973
|
+
onDiagnostic: (value) => { streamDiagnostic = value; },
|
|
1883
1974
|
});
|
|
1884
1975
|
void interactionTask.catch(() => undefined);
|
|
1885
1976
|
await Promise.race([
|
|
1886
1977
|
opened,
|
|
1887
1978
|
sleep(30_000, interactionSignal).then(() => {
|
|
1888
|
-
throw new
|
|
1979
|
+
throw Object.assign(new HarnessInteractionError(
|
|
1980
|
+
'harness-interaction-timeout',
|
|
1981
|
+
'Harness interaction stream did not open within 30 seconds',
|
|
1982
|
+
), {
|
|
1983
|
+
transport: this.#apiProxy ? 'host-api' : 'websocket',
|
|
1984
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
1985
|
+
timeoutMs: 30_000,
|
|
1986
|
+
...streamDiagnostic,
|
|
1987
|
+
});
|
|
1889
1988
|
}),
|
|
1890
1989
|
]);
|
|
1891
1990
|
}
|
|
1892
|
-
|
|
1991
|
+
diagnostic.stage = 'prompt.prepare';
|
|
1893
1992
|
const content = typeof prompt === 'string'
|
|
1894
1993
|
? [{ type: 'text', text: prompt }]
|
|
1895
1994
|
: prompt;
|
|
@@ -1903,12 +2002,16 @@ export class HarnessClient {
|
|
|
1903
2002
|
// message that merely looks like a guidance block is not configuration.
|
|
1904
2003
|
imSourceGuidance.publish(sessionId, options.sourceGuidance);
|
|
1905
2004
|
const timeZoneFields = clientTimeZoneFields();
|
|
1906
|
-
const sendPrompt = (promptContent) =>
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2005
|
+
const sendPrompt = (promptContent) => {
|
|
2006
|
+
diagnostic.stage = 'prompt.submit';
|
|
2007
|
+
diagnostic.submissionState = 'unknown';
|
|
2008
|
+
return this.#sessionRpc('session.prompt', {
|
|
2009
|
+
sessionId,
|
|
2010
|
+
mode: 'queue',
|
|
2011
|
+
content: promptContent,
|
|
2012
|
+
...timeZoneFields,
|
|
2013
|
+
}, 30_000, { rpcId: promptRpcId, signal });
|
|
2014
|
+
};
|
|
1912
2015
|
try {
|
|
1913
2016
|
await sendPrompt(content);
|
|
1914
2017
|
} catch (error) {
|
|
@@ -1948,6 +2051,8 @@ export class HarnessClient {
|
|
|
1948
2051
|
await sendPrompt(fallbackPrompt);
|
|
1949
2052
|
}
|
|
1950
2053
|
promptAccepted = true;
|
|
2054
|
+
diagnostic.submissionState = 'accepted';
|
|
2055
|
+
diagnostic.stage = 'reply.wait';
|
|
1951
2056
|
|
|
1952
2057
|
try {
|
|
1953
2058
|
// Treat timeoutMs as a stall window rather than a hard runtime limit.
|
|
@@ -2032,6 +2137,8 @@ export class HarnessClient {
|
|
|
2032
2137
|
continue;
|
|
2033
2138
|
}
|
|
2034
2139
|
const timeoutError = new HarnessTurnError('harness-reply-timeout');
|
|
2140
|
+
timeoutError.durationMs = Date.now() - lastProgressAt;
|
|
2141
|
+
timeoutError.timeoutMs = timeoutMs;
|
|
2035
2142
|
// Data-only context for deferred delivery; timeout semantics unchanged.
|
|
2036
2143
|
timeoutError.details = { sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq };
|
|
2037
2144
|
throw timeoutError;
|
|
@@ -97,12 +97,14 @@ function watchWebSocketMux({ baseUrl, createWebSocket, signal, onOpen, onEnvelop
|
|
|
97
97
|
}
|
|
98
98
|
invoke(onEnvelope, envelope, finish);
|
|
99
99
|
};
|
|
100
|
-
const handleClose = () => finish(opened ? undefined : new Error(
|
|
100
|
+
const handleClose = () => finish(opened ? undefined : Object.assign(new Error(
|
|
101
101
|
'Harness event mux WebSocket closed before opening',
|
|
102
|
-
));
|
|
103
|
-
const handleError = () => finish(new Error(opened
|
|
102
|
+
), { code: 'stream-closed-before-open' }));
|
|
103
|
+
const handleError = () => finish(Object.assign(new Error(opened
|
|
104
104
|
? 'Harness event mux WebSocket failed'
|
|
105
|
-
: 'Harness event mux WebSocket failed before opening')
|
|
105
|
+
: 'Harness event mux WebSocket failed before opening'), {
|
|
106
|
+
code: opened ? 'stream-failed' : 'stream-failed-before-open',
|
|
107
|
+
}));
|
|
106
108
|
const handleAbort = () => finish();
|
|
107
109
|
socket.addEventListener('open', handleOpen);
|
|
108
110
|
socket.addEventListener('message', handleMessage);
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
|
+
'准备会话': 'Prepare session',
|
|
3
|
+
'准备消息': 'Prepare message',
|
|
4
|
+
'建立交互通道': 'Open interaction channel',
|
|
5
|
+
'提交消息': 'Submit message',
|
|
6
|
+
'等待处理结果': 'Wait for result',
|
|
7
|
+
'消息提交状态': 'Message submission status',
|
|
8
|
+
'未提交': 'Not submitted',
|
|
9
|
+
'已确认接收': 'Acceptance confirmed',
|
|
10
|
+
'提交结果未知': 'Submission outcome unknown',
|
|
11
|
+
'连接尝试次数': 'Connection attempts',
|
|
12
|
+
'最近一次连接失败': 'Last observed connection failure',
|
|
13
|
+
'打开前关闭': 'Closed before opening',
|
|
14
|
+
'打开前失败': 'Failed before opening',
|
|
15
|
+
'通道连接失败': 'Stream connection failed',
|
|
16
|
+
'与 DSH 的交互通道在 30 秒内未就绪,本条消息尚未提交执行。请稍后重试。':
|
|
17
|
+
'The DSH interaction channel was not ready within 30 seconds. This message has not been submitted for execution. Please try again later.',
|
|
18
|
+
'消息提交前的操作超时,本条消息尚未提交执行。请稍后重试。':
|
|
19
|
+
'An operation timed out before submission. This message has not been submitted for execution. Please try again later.',
|
|
20
|
+
'消息提交前的网络请求失败,本条消息尚未提交执行。请检查连接后重试。':
|
|
21
|
+
'A network request failed before submission. This message has not been submitted for execution. Check the connection and try again.',
|
|
22
|
+
'消息在提交前处理失败,尚未提交执行。请重试;若持续发生,请将参考号提供给管理员。':
|
|
23
|
+
'Message preparation failed before submission. It has not been submitted for execution. Try again; if this persists, share the reference with your administrator.',
|
|
24
|
+
'消息已尝试提交,但任务结果尚未确认。请先到 DSH 查看状态,不要立即重复提交;若持续发生,请将参考号提供给管理员。':
|
|
25
|
+
'Message submission was attempted, but the task outcome is unconfirmed. Check its status in DSH before resubmitting; if this persists, share the reference with your administrator.',
|
|
2
26
|
"DSH 版本": "DSH version",
|
|
3
27
|
"依赖版本": "Dependency versions",
|
|
4
28
|
"代理已配置": "Proxy configured",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectionHint, extractConnectionEvidence } from './connection-error.mjs';
|
|
1
|
+
import { connectionHint, extractConnectionEvidence, messageDiagnosticError, runtimeDiagnosticDetails } from './connection-error.mjs';
|
|
2
2
|
import { diagnosticFields } from './diagnostic-details.mjs';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
@@ -26,6 +26,8 @@ const PROVIDER_FAILURES = Object.freeze({
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
const FAILURE_MESSAGES = Object.freeze({
|
|
29
|
+
HARNESS_INTERACTION_TIMEOUT:
|
|
30
|
+
'与 DSH 的交互通道在 30 秒内未就绪,本条消息尚未提交执行。请稍后重试。',
|
|
29
31
|
HARNESS_CONNECT:
|
|
30
32
|
'无法连接处理服务,消息尚未提交。请确认 DeepSeek Harness 正在运行后重试。',
|
|
31
33
|
HARNESS_TIMEOUT:
|
|
@@ -107,18 +109,23 @@ function providerFailureCode(error) {
|
|
|
107
109
|
return PROVIDER_FAILURES[value.trim().toUpperCase()] ?? null;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
function failureCode(error) {
|
|
112
|
+
function failureCode(error, details) {
|
|
111
113
|
const code = typeof error?.code === 'string' ? error.code : '';
|
|
112
114
|
const providerCode = providerFailureCode(error);
|
|
113
115
|
if (providerCode) return providerCode;
|
|
116
|
+
if (code === 'harness-interaction-timeout') return 'HARNESS_INTERACTION_TIMEOUT';
|
|
117
|
+
|
|
118
|
+
const uncertain = details.submissionState === undefined
|
|
119
|
+
? ['session.prompt', 'session.history'].includes(error?.method)
|
|
120
|
+
: details.submissionState !== 'not-submitted';
|
|
114
121
|
|
|
115
122
|
if (code === 'harness-connect-failed') {
|
|
116
|
-
return
|
|
123
|
+
return uncertain
|
|
117
124
|
? 'HARNESS_RESULT_UNCERTAIN'
|
|
118
125
|
: 'HARNESS_CONNECT';
|
|
119
126
|
}
|
|
120
127
|
if (code === 'harness-timeout') {
|
|
121
|
-
return
|
|
128
|
+
return uncertain
|
|
122
129
|
? 'HARNESS_RESULT_UNCERTAIN'
|
|
123
130
|
: 'HARNESS_TIMEOUT';
|
|
124
131
|
}
|
|
@@ -205,25 +212,38 @@ export function classifyMessageFailure(error, {
|
|
|
205
212
|
at = Date.now(),
|
|
206
213
|
} = {}) {
|
|
207
214
|
const safeReason = safeFailureReason(reason);
|
|
208
|
-
const
|
|
215
|
+
const details = extractConnectionEvidence(error).details;
|
|
216
|
+
const classifiedCode = failureCode(error, details);
|
|
209
217
|
const businessCode = classifiedCode === 'INTERNAL_UNKNOWN'
|
|
210
218
|
&& safeReason
|
|
211
219
|
&& typeof userMessage === 'string'
|
|
212
220
|
&& userMessage.trim()
|
|
213
221
|
? 'INPUT_INVALID'
|
|
214
222
|
: classifiedCode;
|
|
215
|
-
const details = extractConnectionEvidence(error).details;
|
|
216
223
|
const code = businessCode === 'INTERNAL_UNKNOWN'
|
|
217
224
|
? transportFailureCode(error, details)
|
|
218
225
|
: businessCode;
|
|
219
226
|
if (code === 'REQUEST_TIMEOUT' || code === 'NETWORK_ERROR') details.hint = connectionHint(details);
|
|
227
|
+
if (details.stage || details.submissionState) Object.assign(details, runtimeDiagnosticDetails());
|
|
228
|
+
let message = FAILURE_MESSAGES[code];
|
|
229
|
+
if (['INTERNAL_UNKNOWN', 'REQUEST_TIMEOUT', 'NETWORK_ERROR'].includes(code)) {
|
|
230
|
+
if (details.submissionState === 'not-submitted') {
|
|
231
|
+
message = code === 'REQUEST_TIMEOUT'
|
|
232
|
+
? '消息提交前的操作超时,本条消息尚未提交执行。请稍后重试。'
|
|
233
|
+
: code === 'NETWORK_ERROR'
|
|
234
|
+
? '消息提交前的网络请求失败,本条消息尚未提交执行。请检查连接后重试。'
|
|
235
|
+
: '消息在提交前处理失败,尚未提交执行。请重试;若持续发生,请将参考号提供给管理员。';
|
|
236
|
+
} else if (details.submissionState) {
|
|
237
|
+
message = '消息已尝试提交,但任务结果尚未确认。请先到 DSH 查看状态,不要立即重复提交;若持续发生,请将参考号提供给管理员。';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
220
240
|
return Object.freeze({
|
|
221
|
-
...(code === 'INTERNAL_UNKNOWN' || details.reason !== 'unknown' || details.httpStatus || details.providerCode || details.method ? { details } : {}),
|
|
241
|
+
...(code === 'INTERNAL_UNKNOWN' || details.reason !== 'unknown' || details.httpStatus || details.providerCode || details.method || details.stage || details.submissionState ? { details } : {}),
|
|
222
242
|
code,
|
|
223
243
|
reason: safeReason ?? safeFailureReason(error?.code) ?? code,
|
|
224
244
|
message: typeof userMessage === 'string' && userMessage.trim()
|
|
225
245
|
? userMessage.trim()
|
|
226
|
-
: t(
|
|
246
|
+
: t(message) + (code === 'SESSION_ARCHIVED' && error?.promptAccepted === true
|
|
227
247
|
? `\n${t('本条消息可能仍在会话队列中,请先到 DSH 查看状态,避免重复提交。')}` : ''),
|
|
228
248
|
referenceId: safeReferenceId(referenceId),
|
|
229
249
|
at: Number.isFinite(at) ? at : Date.now(),
|
|
@@ -237,7 +257,7 @@ export function messageFailureText(failure) {
|
|
|
237
257
|
export function messageFailureDiagnostic(error, failure) {
|
|
238
258
|
const evidence = extractConnectionEvidence(error);
|
|
239
259
|
return { code: failure.code, reason: failure.reason, referenceId: failure.referenceId,
|
|
240
|
-
details: evidence.details, errors: evidence.errors };
|
|
260
|
+
details: failure.details ?? evidence.details, errors: evidence.errors, ...evidence.correlation };
|
|
241
261
|
}
|
|
242
262
|
|
|
243
263
|
export function setLastMessageFailure(status, error, options) {
|
|
@@ -262,7 +282,7 @@ export function channelDeliveryFailure(error, { uncertain = true } = {}) {
|
|
|
262
282
|
? 'channel-delivery-uncertain'
|
|
263
283
|
: 'channel-delivery-failed';
|
|
264
284
|
if (Number.isInteger(status)) wrapped.status = status;
|
|
265
|
-
return wrapped;
|
|
285
|
+
return messageDiagnosticError(wrapped, { stage: 'message.send' });
|
|
266
286
|
}
|
|
267
287
|
|
|
268
288
|
export function publicMessageFailure(value) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withSessionBindingLock } from './session-binding-lock.mjs';
|
|
2
2
|
import { initialSessionTitle } from './session-title.mjs';
|
|
3
|
+
import { messageDiagnosticError } from './connection-error.mjs';
|
|
3
4
|
|
|
4
5
|
export const WORKSPACE_SESSION_STALE = 'workspace-session-stale';
|
|
5
6
|
|
|
@@ -114,16 +115,19 @@ export async function askInWorkspaceSession({
|
|
|
114
115
|
?? (typeof askOptions === 'object' ? askOptions?.signal : undefined);
|
|
115
116
|
const renameOptions = renameSignal ? { signal: renameSignal } : undefined;
|
|
116
117
|
while (true) {
|
|
118
|
+
const diagnostic = { stage: 'session.prepare', submissionState: 'not-submitted' };
|
|
117
119
|
try {
|
|
118
120
|
const binding = await withSessionBindingLock(state, key, async () => {
|
|
119
121
|
await awaitPendingConversationSwitch(harness, key);
|
|
120
122
|
let sessionId = state.sessionFor(key);
|
|
123
|
+
diagnostic.sessionId = sessionId;
|
|
121
124
|
let session = sessionId ? workspaceSession(harness, sessionId, key) : null;
|
|
122
125
|
if (!session || !(await sessionExists(session, existsOptions))) {
|
|
123
126
|
sessionId = await createSession(harness, {
|
|
124
127
|
conversationKey: key,
|
|
125
128
|
...(createOptions ?? {}),
|
|
126
129
|
});
|
|
130
|
+
diagnostic.sessionId = sessionId;
|
|
127
131
|
if (await state.setSession(key, sessionId) === false) return null;
|
|
128
132
|
// Binding committed: capture the conversation's effective-workspace
|
|
129
133
|
// generation together with the session, so the prompt below is fenced
|
|
@@ -150,6 +154,8 @@ export async function askInWorkspaceSession({
|
|
|
150
154
|
return { sessionId, session };
|
|
151
155
|
});
|
|
152
156
|
if (!binding) continue;
|
|
157
|
+
diagnostic.sessionId = binding.sessionId;
|
|
158
|
+
diagnostic.stage = 'prompt.prepare';
|
|
153
159
|
const artifacts = [];
|
|
154
160
|
const originalOnArtifact = typeof askOptions === 'object'
|
|
155
161
|
&& typeof askOptions?.onArtifact === 'function'
|
|
@@ -172,6 +178,9 @@ export async function askInWorkspaceSession({
|
|
|
172
178
|
? await prepareContent({ sessionId: binding.sessionId, content, text })
|
|
173
179
|
: content ?? text;
|
|
174
180
|
try {
|
|
181
|
+
// An alternate Harness adapter may not supply finer ask() diagnostics.
|
|
182
|
+
diagnostic.stage = 'operation';
|
|
183
|
+
diagnostic.submissionState = 'unknown';
|
|
175
184
|
answer = await binding.session.ask(prompt, artifactOptions);
|
|
176
185
|
} catch (error) {
|
|
177
186
|
if (error?.code === 'harness-reply-timeout' && deferredDelivery) {
|
|
@@ -190,7 +199,7 @@ export async function askInWorkspaceSession({
|
|
|
190
199
|
...(artifacts.length > 0 ? { artifacts } : {}),
|
|
191
200
|
};
|
|
192
201
|
} catch (error) {
|
|
193
|
-
if (error?.code !== WORKSPACE_SESSION_STALE) throw error;
|
|
202
|
+
if (error?.code !== WORKSPACE_SESSION_STALE) throw messageDiagnosticError(error, diagnostic);
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
}
|