@xmanrui/dsh-im 4.38.0 → 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/PROACTIVE_DELIVERY.en.md +18 -2
- package/PROACTIVE_DELIVERY.md +18 -2
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/lib/client.js +49 -5
- package/lib/index.js +320 -315
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- 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/delivery-adapter.mjs +9 -1
- package/plugin-src/host/delivery-service.mjs +4 -3
- package/plugin-src/host/modern-harness-api.mjs +2 -1
- package/plugin-src/host/session-sync-coordinator.mjs +9 -5
- package/src/channels/dingtalk/dingtalk-api.mjs +41 -24
- package/src/channels/dingtalk/dingtalk-runtime.mjs +3 -2
- package/src/channels/discord/discord-runtime.mjs +13 -2
- package/src/channels/feishu/bridge.mjs +95 -24
- package/src/channels/matrix/matrix-runtime.mjs +16 -6
- package/src/channels/qq/markdown-reply.mjs +7 -340
- package/src/channels/qq/qq-runtime.mjs +11 -3
- 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 +138 -19
- package/src/channels/shared/harness-mux.mjs +6 -4
- package/src/channels/shared/i18n-en/diagnostics.mjs +24 -0
- package/src/channels/shared/i18n-en/wecom.mjs +3 -0
- package/src/channels/shared/markdown-chunks.mjs +338 -0
- package/src/channels/shared/message-failure.mjs +31 -11
- package/src/channels/shared/text-harness-bridge.mjs +4 -1
- package/src/channels/shared/workspace-session.mjs +10 -1
- package/src/channels/slack/slack-api.mjs +2 -1
- package/src/channels/slack/slack-runtime.mjs +30 -0
- package/src/channels/telegram/telegram-runtime.mjs +1 -0
- package/src/channels/wecom/wecom-bridge.mjs +45 -10
- package/src/channels/wecom/wecom-runtime.mjs +11 -5
- package/src/channels/weixin/weixin-runtime.mjs +17 -10
- package/src/channels/whatsapp/whatsapp-markdown.mjs +59 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +26 -1
|
@@ -1469,9 +1469,9 @@ function resolveAgentPresetCatalog(catalog) {
|
|
|
1469
1469
|
: normalizeAgentPresetCatalog(value);
|
|
1470
1470
|
}
|
|
1471
1471
|
|
|
1472
|
-
function resolveModelCatalog(catalog) {
|
|
1472
|
+
function resolveModelCatalog(catalog, options) {
|
|
1473
1473
|
if (!catalog) return null;
|
|
1474
|
-
const value = typeof catalog === 'function' ? catalog() : catalog;
|
|
1474
|
+
const value = typeof catalog === 'function' ? catalog(options) : catalog;
|
|
1475
1475
|
return value && typeof value.then === 'function'
|
|
1476
1476
|
? value.then(normalizeModelCatalog)
|
|
1477
1477
|
: normalizeModelCatalog(value);
|
|
@@ -2264,7 +2264,7 @@ export function createWorkspaceAwareController(controller, {
|
|
|
2264
2264
|
throw error;
|
|
2265
2265
|
}
|
|
2266
2266
|
const catalog = model && modelCatalog
|
|
2267
|
-
? await resolveModelCatalog(modelCatalog)
|
|
2267
|
+
? await resolveModelCatalog(modelCatalog, { fresh: true })
|
|
2268
2268
|
: null;
|
|
2269
2269
|
const entry = modelCatalogEntry(catalog, model);
|
|
2270
2270
|
if (model && (!modelCatalog || !entry)) {
|
|
@@ -6,7 +6,7 @@ import { t } from './i18n.mjs';
|
|
|
6
6
|
const reported = new WeakMap();
|
|
7
7
|
const lastReport = new WeakMap();
|
|
8
8
|
const stages = new WeakMap();
|
|
9
|
-
const OWNED_REASONS = { 'connect-timeout': 'timeout', 'telegram-timeout': 'timeout', 'cli-timeout': 'timeout', 'provider-timeout': 'timeout', 'telegram-response-invalid': 'invalid-response', 'office-protocol-mismatch': 'invalid-response' };
|
|
9
|
+
const OWNED_REASONS = { 'harness-interaction-timeout': 'timeout', 'harness-response-invalid': 'invalid-response', 'connect-timeout': 'timeout', 'telegram-timeout': 'timeout', 'cli-timeout': 'timeout', 'provider-timeout': 'timeout', 'telegram-response-invalid': 'invalid-response', 'office-protocol-mismatch': 'invalid-response' };
|
|
10
10
|
const STANDARD_NAMES = new Set(['Error', 'TypeError', 'SyntaxError', 'AggregateError', 'DOMException', 'AxiosError', 'TimeoutError', 'AbortError']);
|
|
11
11
|
const KNOWN_MESSAGES = {
|
|
12
12
|
'discord-401': 'Discord Bot Token 无效,请重新填写。',
|
|
@@ -54,20 +54,45 @@ export function extractConnectionEvidence(error) {
|
|
|
54
54
|
const allReasons = [...new Set(errors.flatMap(e => e.reason === 'multiple-causes' ? e.reasons ?? [] : [e.reason]).filter(r => r && !['unknown', 'multiple-causes'].includes(r)))];
|
|
55
55
|
const precise = allReasons.filter(r => !['timeout', 'http-error', 'provider-rejected', 'invalid-response'].includes(r));
|
|
56
56
|
const reasons = precise.length ? precise : allReasons;
|
|
57
|
-
const first = field => chain.map(e => normalizeDiagnosticDetails({ [field]: e[field] ?? e.details?.[field] })[field]).find(v => v !== undefined);
|
|
57
|
+
const first = field => chain.map(e => normalizeDiagnosticDetails({ [field]: stages.get(e)?.[field] ?? e[field] ?? e.details?.[field] })[field]).find(v => v !== undefined);
|
|
58
|
+
const correlation = {};
|
|
59
|
+
for (const field of ['sessionId', 'promptRpcId']) {
|
|
60
|
+
// Only locally attached identifiers belong in logs, never arbitrary RPC details.
|
|
61
|
+
const value = chain.map(e => stages.get(e)?.[field]).find(v => v !== undefined);
|
|
62
|
+
if (typeof value === 'string' && /^[A-Za-z0-9_-]{1,256}$/u.test(value)) correlation[field] = value;
|
|
63
|
+
}
|
|
58
64
|
return {
|
|
59
|
-
chain, errors,
|
|
65
|
+
chain, errors, correlation,
|
|
60
66
|
details: normalizeDiagnosticDetails({
|
|
61
67
|
reason: reasons.length > 1 ? 'multiple-causes' : reasons[0] ?? 'unknown',
|
|
62
68
|
...(reasons.length > 1 ? { reasons } : {}),
|
|
63
69
|
httpStatus: errors.find(e => e.httpStatus !== undefined)?.httpStatus,
|
|
64
70
|
providerCode: errors.find(e => e.providerCode !== undefined)?.providerCode,
|
|
65
71
|
method: first('method'), transport: first('transport'),
|
|
72
|
+
stage: first('stage'), submissionState: first('submissionState'),
|
|
73
|
+
attempts: first('attempts'), lastFailureReason: first('lastFailureReason'),
|
|
66
74
|
durationMs: first('durationMs'), timeoutMs: first('timeoutMs'), host: first('host'), truncated,
|
|
67
75
|
}),
|
|
68
76
|
};
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
/** Keep request context off shared errors (for example a rejected model-catalog promise).
|
|
80
|
+
* Preserve the original prototype, properties and cause for existing business handlers. */
|
|
81
|
+
export function messageDiagnosticError(cause, context) {
|
|
82
|
+
if (cause?.name === 'AbortError') return cause;
|
|
83
|
+
const source = cause && typeof cause === 'object' ? cause : new Error('Message processing failed');
|
|
84
|
+
const error = source instanceof DOMException
|
|
85
|
+
? Object.defineProperties(new DOMException(source.message, source.name), Object.getOwnPropertyDescriptors(source))
|
|
86
|
+
: Object.defineProperties(Object.setPrototypeOf(source instanceof Error ? new Error() : {}, Object.getPrototypeOf(source)),
|
|
87
|
+
Object.getOwnPropertyDescriptors(source));
|
|
88
|
+
stages.set(error, { ...context, ...stages.get(source) });
|
|
89
|
+
return error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function runtimeDiagnosticDetails() {
|
|
93
|
+
return { pluginVersion: packageInfo.version, nodeVersion: process.versions.node, platform: process.platform };
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
export async function atConnectionStage(stage, operation, resource) {
|
|
72
97
|
try { return await operation(); }
|
|
73
98
|
catch (error) { throw connectionStageError(error, stage, resource); }
|
|
@@ -145,7 +170,7 @@ export function createConnectionDiagnostics({ channel, logger = console, now = D
|
|
|
145
170
|
const details = normalizeDiagnosticDetails({
|
|
146
171
|
...evidence.details, ...context, ...safe.details, ...described.details, ...owned,
|
|
147
172
|
channel, stage: owned.stage ?? described.details?.stage ?? safe.details?.stage ?? context.stage ?? operationStage(context.operation),
|
|
148
|
-
|
|
173
|
+
...runtimeDiagnosticDetails(),
|
|
149
174
|
});
|
|
150
175
|
const code = safe.code ?? context.code ?? `${channel}-operation-failed`;
|
|
151
176
|
const hint = described.hint ?? safe.hint ?? safe.details?.hint ?? connectionHint(details);
|
|
@@ -7,11 +7,13 @@ const OPERATIONS = new Set([
|
|
|
7
7
|
'bot.workspace.set', 'bot.model.set', 'bot.agent-preset.set', 'bot.context-enhancement.set', 'bot.access-policy.set', 'bot.alias.set',
|
|
8
8
|
]);
|
|
9
9
|
const STAGES = new Set([
|
|
10
|
+
'session.prepare', 'prompt.prepare', 'interaction.open', 'prompt.submit', 'reply.wait',
|
|
10
11
|
'credential.verify', 'permission.check', 'sdk.load', 'operation', 'message.receive', 'message.send', 'connection.test', 'startup.load', 'qr.begin', 'qr.encode', 'qr.poll', 'qr.verify', 'qr.cancel', 'credential.read', 'credential.save', 'credential.remove',
|
|
11
12
|
'account.save', 'account.remove', 'state.load', 'state.write', 'state.cleanup', 'workspace.write', 'workspace.cleanup',
|
|
12
13
|
'runtime.prepare', 'activation', 'harness.check', 'connection.start', 'connection.poll', 'connection.stop', 'status.read', 'rollback', 'management.request',
|
|
13
14
|
]);
|
|
14
15
|
const REASONS = new Set([
|
|
16
|
+
'stream-closed-before-open', 'stream-failed-before-open', 'stream-failed',
|
|
15
17
|
'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'ETIMEDOUT', 'EPIPE',
|
|
16
18
|
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET',
|
|
17
19
|
'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
|
|
@@ -25,7 +27,8 @@ const RPC_METHODS = new Set([
|
|
|
25
27
|
'session.history', 'session.prompt', 'session.rename', 'session.cancel', 'session.models',
|
|
26
28
|
'session.permissions', 'session.selectModel', 'llm.models',
|
|
27
29
|
]);
|
|
28
|
-
const TRANSPORTS = new Set(['host-api', 'http']);
|
|
30
|
+
const TRANSPORTS = new Set(['host-api', 'http', 'websocket']);
|
|
31
|
+
const SUBMISSION_STATES = new Set(['not-submitted', 'accepted', 'unknown']);
|
|
29
32
|
const CONFIG_FILES = new Set(['config.json', 'workspaces.json']);
|
|
30
33
|
export const CONFIG_ISSUE_LABELS = Object.freeze({
|
|
31
34
|
'expected-object': '应为 JSON 对象。',
|
|
@@ -50,7 +53,7 @@ const CONFIG_FIELD = /^(?:\$|version|accounts(?:\[\d{1,10}\](?:\.(?:accountId|ow
|
|
|
50
53
|
export function normalizeDiagnosticDetails(value) {
|
|
51
54
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
52
55
|
const result = {};
|
|
53
|
-
for (const [field, allowed] of [['operation', OPERATIONS], ['stage', STAGES], ['reason', REASONS], ['resource', RESOURCES], ['method', RPC_METHODS], ['transport', TRANSPORTS]]) {
|
|
56
|
+
for (const [field, allowed] of [['operation', OPERATIONS], ['stage', STAGES], ['reason', REASONS], ['resource', RESOURCES], ['method', RPC_METHODS], ['transport', TRANSPORTS], ['submissionState', SUBMISSION_STATES], ['lastFailureReason', REASONS]]) {
|
|
54
57
|
if (allowed.has(value[field])) result[field] = value[field];
|
|
55
58
|
}
|
|
56
59
|
if (DIAGNOSTIC_CHANNELS.includes(value.channel)) result.channel = value.channel;
|
|
@@ -59,6 +62,7 @@ export function normalizeDiagnosticDetails(value) {
|
|
|
59
62
|
if (reasons.length) result.reasons = reasons;
|
|
60
63
|
}
|
|
61
64
|
for (const field of ['durationMs', 'timeoutMs']) if (Number.isFinite(value[field]) && value[field] >= 0) result[field] = Math.round(value[field]);
|
|
65
|
+
if (Number.isSafeInteger(value.attempts) && value.attempts >= 0) result.attempts = value.attempts;
|
|
62
66
|
for (const field of ['nodeVersion', 'dshVersion']) if (typeof value[field] === 'string' && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value[field]) && value[field].length < 80) result[field] = value[field];
|
|
63
67
|
if (['darwin', 'linux', 'win32', 'freebsd'].includes(value.platform)) result.platform = value.platform;
|
|
64
68
|
if (['ilinkai.weixin.qq.com', 'oapi.dingtalk.com', 'api.dingtalk.com', 'api.telegram.org', 'discord.com', 'slack.com', 'open.feishu.cn', 'qyapi.weixin.qq.com'].includes(value.host)) result.host = value.host;
|
|
@@ -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,40 @@ 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
|
+
|
|
55
|
+
function clientTimeZoneFields() {
|
|
56
|
+
try {
|
|
57
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
58
|
+
return typeof timeZone === 'string' && timeZone.trim()
|
|
59
|
+
? { clientTimeZone: timeZone }
|
|
60
|
+
: {};
|
|
61
|
+
} catch {
|
|
62
|
+
// Time-zone metadata is optional; unresolved values are not JSON-safe.
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
async function smallResponseText(response) {
|
|
37
68
|
const stream = response?.body;
|
|
38
69
|
if (!stream || typeof stream.getReader !== 'function') return null;
|
|
@@ -1175,9 +1206,46 @@ export class HarnessClient {
|
|
|
1175
1206
|
}
|
|
1176
1207
|
|
|
1177
1208
|
async listModels(options = {}) {
|
|
1209
|
+
options.signal?.throwIfAborted();
|
|
1178
1210
|
await this.ensureRunning(options);
|
|
1179
|
-
|
|
1180
|
-
|
|
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);
|
|
1181
1249
|
}
|
|
1182
1250
|
|
|
1183
1251
|
async getSessionModels(sessionId, options = {}) {
|
|
@@ -1212,13 +1280,18 @@ export class HarnessClient {
|
|
|
1212
1280
|
: { reasoningEffort: selection.reasoningEffort }),
|
|
1213
1281
|
}, 30_000, signal ? { ...options, signal } : options);
|
|
1214
1282
|
};
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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 });
|
|
1220
1294
|
}
|
|
1221
|
-
return value;
|
|
1222
1295
|
}
|
|
1223
1296
|
|
|
1224
1297
|
async isSessionRunning(sessionId, options = {}) {
|
|
@@ -1267,6 +1340,9 @@ export class HarnessClient {
|
|
|
1267
1340
|
const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
|
|
1268
1341
|
if (agentPreset != null) payload.agentPreset = agentPreset;
|
|
1269
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
|
+
}
|
|
1270
1346
|
return created.sessionId;
|
|
1271
1347
|
}
|
|
1272
1348
|
|
|
@@ -1397,6 +1473,7 @@ export class HarnessClient {
|
|
|
1397
1473
|
onSessionEvent,
|
|
1398
1474
|
onOpen,
|
|
1399
1475
|
ownership,
|
|
1476
|
+
onDiagnostic,
|
|
1400
1477
|
} = {}) {
|
|
1401
1478
|
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
1402
1479
|
if (!signal || typeof signal.addEventListener !== 'function') {
|
|
@@ -1414,8 +1491,17 @@ export class HarnessClient {
|
|
|
1414
1491
|
if (onOpen !== undefined && typeof onOpen !== 'function') {
|
|
1415
1492
|
throw new TypeError('onOpen must be a function');
|
|
1416
1493
|
}
|
|
1417
|
-
|
|
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
|
+
};
|
|
1418
1502
|
while (!signal.aborted) {
|
|
1503
|
+
attempts += 1;
|
|
1504
|
+
report();
|
|
1419
1505
|
try {
|
|
1420
1506
|
await this.#watchInteractionStream(sessionId, {
|
|
1421
1507
|
signal,
|
|
@@ -1427,6 +1513,8 @@ export class HarnessClient {
|
|
|
1427
1513
|
});
|
|
1428
1514
|
} catch (error) {
|
|
1429
1515
|
if (signal.aborted) return;
|
|
1516
|
+
lastFailureReason = extractConnectionEvidence(error).details.reason;
|
|
1517
|
+
report();
|
|
1430
1518
|
console.warn('[dsh-im] Harness interaction stream disconnected:', this.#logPrefix, error.message);
|
|
1431
1519
|
}
|
|
1432
1520
|
if (signal.aborted) return;
|
|
@@ -1621,7 +1709,7 @@ export class HarnessClient {
|
|
|
1621
1709
|
sessionId,
|
|
1622
1710
|
mode: 'steer',
|
|
1623
1711
|
content: [{ type: 'text', text }],
|
|
1624
|
-
|
|
1712
|
+
...clientTimeZoneFields(),
|
|
1625
1713
|
}, 30_000, { ...options, rpcId: inputRpcId });
|
|
1626
1714
|
return true;
|
|
1627
1715
|
} catch (error) {
|
|
@@ -1699,6 +1787,16 @@ export class HarnessClient {
|
|
|
1699
1787
|
|
|
1700
1788
|
async ask(sessionId, prompt, options = {}) {
|
|
1701
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) {
|
|
1702
1800
|
const timeoutMs = options.timeoutMs ?? 600_000;
|
|
1703
1801
|
const signal = options.signal;
|
|
1704
1802
|
const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
|
|
@@ -1728,6 +1826,7 @@ export class HarnessClient {
|
|
|
1728
1826
|
);
|
|
1729
1827
|
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
1730
1828
|
const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1829
|
+
diagnostic.promptRpcId = promptRpcId;
|
|
1731
1830
|
const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
|
|
1732
1831
|
const tracker = new HarnessReplyTracker({
|
|
1733
1832
|
promptRpcId, afterSeq: baselineSeq, reasoning, finalAnswerOnly: options.finalAnswerOnly,
|
|
@@ -1857,6 +1956,9 @@ export class HarnessClient {
|
|
|
1857
1956
|
});
|
|
1858
1957
|
}
|
|
1859
1958
|
if (interactionSignal) {
|
|
1959
|
+
diagnostic.stage = 'interaction.open';
|
|
1960
|
+
const startedAt = performance.now();
|
|
1961
|
+
let streamDiagnostic = { attempts: 0 };
|
|
1860
1962
|
let markOpen;
|
|
1861
1963
|
const opened = new Promise((resolve) => { markOpen = resolve; });
|
|
1862
1964
|
interactionTask = this.watchInteractions(sessionId, {
|
|
@@ -1868,16 +1970,25 @@ export class HarnessClient {
|
|
|
1868
1970
|
: undefined,
|
|
1869
1971
|
onOpen: markOpen,
|
|
1870
1972
|
ownership,
|
|
1973
|
+
onDiagnostic: (value) => { streamDiagnostic = value; },
|
|
1871
1974
|
});
|
|
1872
1975
|
void interactionTask.catch(() => undefined);
|
|
1873
1976
|
await Promise.race([
|
|
1874
1977
|
opened,
|
|
1875
1978
|
sleep(30_000, interactionSignal).then(() => {
|
|
1876
|
-
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
|
+
});
|
|
1877
1988
|
}),
|
|
1878
1989
|
]);
|
|
1879
1990
|
}
|
|
1880
|
-
|
|
1991
|
+
diagnostic.stage = 'prompt.prepare';
|
|
1881
1992
|
const content = typeof prompt === 'string'
|
|
1882
1993
|
? [{ type: 'text', text: prompt }]
|
|
1883
1994
|
: prompt;
|
|
@@ -1890,13 +2001,17 @@ export class HarnessClient {
|
|
|
1890
2001
|
// `content`: the prompt also carries whatever the user typed, and a
|
|
1891
2002
|
// message that merely looks like a guidance block is not configuration.
|
|
1892
2003
|
imSourceGuidance.publish(sessionId, options.sourceGuidance);
|
|
1893
|
-
const
|
|
1894
|
-
const sendPrompt = (promptContent) =>
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2004
|
+
const timeZoneFields = clientTimeZoneFields();
|
|
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
|
+
};
|
|
1900
2015
|
try {
|
|
1901
2016
|
await sendPrompt(content);
|
|
1902
2017
|
} catch (error) {
|
|
@@ -1936,6 +2051,8 @@ export class HarnessClient {
|
|
|
1936
2051
|
await sendPrompt(fallbackPrompt);
|
|
1937
2052
|
}
|
|
1938
2053
|
promptAccepted = true;
|
|
2054
|
+
diagnostic.submissionState = 'accepted';
|
|
2055
|
+
diagnostic.stage = 'reply.wait';
|
|
1939
2056
|
|
|
1940
2057
|
try {
|
|
1941
2058
|
// Treat timeoutMs as a stall window rather than a hard runtime limit.
|
|
@@ -2020,6 +2137,8 @@ export class HarnessClient {
|
|
|
2020
2137
|
continue;
|
|
2021
2138
|
}
|
|
2022
2139
|
const timeoutError = new HarnessTurnError('harness-reply-timeout');
|
|
2140
|
+
timeoutError.durationMs = Date.now() - lastProgressAt;
|
|
2141
|
+
timeoutError.timeoutMs = timeoutMs;
|
|
2023
2142
|
// Data-only context for deferred delivery; timeout semantics unchanged.
|
|
2024
2143
|
timeoutError.details = { sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq };
|
|
2025
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",
|
|
@@ -30,6 +30,9 @@ export default {
|
|
|
30
30
|
// Bot replies and status text (wecom-bridge.mjs)
|
|
31
31
|
|
|
32
32
|
'当前待处理图片较多,请稍后重新发送。': 'Too many images are pending. Please send them again later.',
|
|
33
|
+
'图片 {indexes} 来自当前消息。': 'Images {indexes} are from the current message.',
|
|
34
|
+
'图片 {indexes} 来自被引用消息。图片序号与本轮附件清单一致。': 'Images {indexes} are from the quoted message. Image numbers match the attachment list for this turn.',
|
|
35
|
+
'有 {count} 张引用图片未提供可下载资源,未附上这些图片;需要看图时请用户重新发送。': '{count} quoted images have no downloadable resource and are not attached. Ask the user to send them again if needed.',
|
|
33
36
|
|
|
34
37
|
'结果文件「{name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。': 'The result file "{name}" was generated, but the Enterprise WeChat bot lacks media upload or file message capability. Please check the bot permissions.',
|
|
35
38
|
'结果文件「{name}」超过当前企业微信机器人可发送的文件大小,未发送。': 'The result file "{name}" exceeds the file size this Enterprise WeChat bot can send and was not sent.',
|