mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 00:32:43 +08:00
fix(weixin): classify Harness health failures
This commit is contained in:
parent
8eed60bbc9
commit
eb21b14f60
9 changed files with 387 additions and 171 deletions
|
|
@ -364,6 +364,26 @@ export class HarnessRpcError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class HarnessTransportError extends Error {
|
||||
constructor(code, method, { cause, status } = {}) {
|
||||
const statusDetail = Number.isInteger(status) ? `, HTTP ${status}` : '';
|
||||
super(`Harness ${method} transport failed (${code}${statusDetail})`, { cause });
|
||||
this.name = 'HarnessTransportError';
|
||||
this.code = code;
|
||||
this.method = method;
|
||||
if (Number.isInteger(status)) this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export class HarnessHealthError extends Error {
|
||||
constructor(cause) {
|
||||
super('Harness health RPC was rejected', { cause });
|
||||
this.name = 'HarnessHealthError';
|
||||
this.code = 'harness-rpc-rejected';
|
||||
this.method = 'host.describe';
|
||||
}
|
||||
}
|
||||
|
||||
export class HarnessInteractionError extends Error {
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
|
|
@ -455,24 +475,60 @@ export class HarnessClient {
|
|||
const signal = options.signal
|
||||
? AbortSignal.any([options.signal, timeoutSignal])
|
||||
: timeoutSignal;
|
||||
const response = await this.#fetch(new URL(`/api/${method}`, this.#baseUrl), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`Harness transport ${method} failed: HTTP ${response.status}`);
|
||||
const body = await response.json();
|
||||
let response;
|
||||
try {
|
||||
response = await this.#fetch(new URL(`/api/${method}`, this.#baseUrl), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
// Preserve an explicit caller cancellation; it is control flow, not a
|
||||
// Harness availability diagnosis.
|
||||
if (options.signal?.aborted) throw error;
|
||||
throw new HarnessTransportError(
|
||||
timeoutSignal.aborted ? 'harness-timeout' : 'harness-connect-failed',
|
||||
method,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const code = response.status === 401 || response.status === 403
|
||||
? 'harness-access-denied'
|
||||
: response.status === 404
|
||||
? 'harness-api-not-found'
|
||||
: 'harness-http-failed';
|
||||
throw new HarnessTransportError(code, method, { status: response.status });
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch (error) {
|
||||
throw new HarnessTransportError('harness-response-invalid', method, { cause: error });
|
||||
}
|
||||
if (body?.type !== 'server-response' || body?.rpcId !== rpcId) {
|
||||
throw new Error(`Harness returned an invalid response for ${method}`);
|
||||
throw new HarnessTransportError('harness-response-invalid', method, {
|
||||
cause: new Error(`Harness returned an invalid response for ${method}`),
|
||||
});
|
||||
}
|
||||
if (!body.result || typeof body.result !== 'object' || typeof body.result.ok !== 'boolean') {
|
||||
throw new HarnessTransportError('harness-response-invalid', method, {
|
||||
cause: new Error(`Harness returned an invalid result for ${method}`),
|
||||
});
|
||||
}
|
||||
if (!body.result?.ok) throw new HarnessRpcError(method, body.result?.error);
|
||||
return body.result.value;
|
||||
}
|
||||
|
||||
async health(options = {}) {
|
||||
await this.rpc('host.describe', {}, 5_000, options);
|
||||
return true;
|
||||
try {
|
||||
await this.rpc('host.describe', {}, 5_000, options);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof HarnessRpcError) throw new HarnessHealthError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async ensureRunning(options = {}) {
|
||||
|
|
@ -506,7 +562,8 @@ export class HarnessClient {
|
|||
lastError = error;
|
||||
}
|
||||
}
|
||||
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
||||
if (lastError) throw lastError;
|
||||
throw new HarnessTransportError('harness-timeout', 'host.describe');
|
||||
}
|
||||
|
||||
async listWorkspaces(options = {}) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,14 @@ const ACTIVATION_ERROR_MESSAGES = Object.freeze({
|
|||
'credential-save-failed': '微信已授权,但登录凭据无法写入 DSH 凭据存储。请检查凭据存储是否可写。',
|
||||
'account-config-save-failed': '微信已授权,但账号配置无法写入本机。请检查 DSH_HOME 目录权限。',
|
||||
'runtime-prepare-failed': '微信已授权,但无法初始化账号状态或工作区。请检查 DSH_HOME 和工作区目录。',
|
||||
'harness-unreachable': '微信已授权,但插件无法连接本机 Harness。请确认 dsh web 已正常启动。',
|
||||
'harness-connect-failed': '微信已授权,但插件无法连接本机 Harness。请检查 dsh web 地址和端口。',
|
||||
'harness-timeout': '微信已授权,但 Harness 健康检查超时。请确认 dsh web 未阻塞。',
|
||||
'harness-access-denied': '微信已授权,但 Harness 拒绝了本机健康检查。请检查 Host 信任配置。',
|
||||
'harness-api-not-found': '微信已授权,但找不到 Harness 健康检查接口。请确认 Harness 与插件版本兼容。',
|
||||
'harness-http-failed': '微信已授权,但 Harness 健康检查返回服务错误。请查看 dsh web 日志。',
|
||||
'harness-response-invalid': '微信已授权,但 Harness 返回了无法识别的响应。请确认 Harness 与插件版本兼容。',
|
||||
'harness-rpc-rejected': '微信已授权,但 Harness 拒绝了健康检查请求。请查看 dsh web 日志。',
|
||||
'harness-check-unknown-failed': '微信已授权,但 Harness 健康检查发生未知错误。请查看 dsh web 日志。',
|
||||
'connection-start-failed': '微信已授权,但消息连接初始化失败。请查看 dsh web 日志后重试。',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ import {
|
|||
} from '../shared/connection-test.mjs';
|
||||
|
||||
const DEFAULT_START_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000]);
|
||||
const HARNESS_HEALTH_ERROR_CODES = new Set([
|
||||
'harness-connect-failed',
|
||||
'harness-timeout',
|
||||
'harness-access-denied',
|
||||
'harness-api-not-found',
|
||||
'harness-http-failed',
|
||||
'harness-response-invalid',
|
||||
'harness-rpc-rejected',
|
||||
]);
|
||||
|
||||
function startRetryDelays(value) {
|
||||
if (value === undefined) return [...DEFAULT_START_RETRY_DELAYS_MS];
|
||||
|
|
@ -32,6 +41,13 @@ function runtimeStartError(code, cause) {
|
|||
return error;
|
||||
}
|
||||
|
||||
function harnessHealthError(cause) {
|
||||
const code = HARNESS_HEALTH_ERROR_CODES.has(cause?.code)
|
||||
? cause.code
|
||||
: 'harness-check-unknown-failed';
|
||||
return runtimeStartError(code, cause);
|
||||
}
|
||||
|
||||
function delay(ms, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
|
|
@ -127,7 +143,7 @@ export class WeixinRuntime {
|
|||
try {
|
||||
await this.#harness.ensureRunning();
|
||||
} catch (error) {
|
||||
throw runtimeStartError('harness-unreachable', error);
|
||||
throw harnessHealthError(error);
|
||||
}
|
||||
this.#status.harnessReachable = true;
|
||||
await this.#notifyStart();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue