mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 05:20:46 +08:00
fix: add actionable DingTalk connection diagnostics
This commit is contained in:
parent
3010535409
commit
1b05fa8d32
18 changed files with 1038 additions and 299 deletions
299
src/channels/dingtalk/connection-error.mjs
Normal file
299
src/channels/dingtalk/connection-error.mjs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const STAGE_CODES = new Set([
|
||||
'dingtalk-harness-connect-failed',
|
||||
'dingtalk-runtime-prepare-failed',
|
||||
'dingtalk-stream-client-load-failed',
|
||||
'dingtalk-stream-listener-failed',
|
||||
'dingtalk-stream-connect-failed',
|
||||
]);
|
||||
const PROXY_VARIABLES = Object.freeze([
|
||||
'HTTPS_PROXY',
|
||||
'https_proxy',
|
||||
'HTTP_PROXY',
|
||||
'http_proxy',
|
||||
'ALL_PROXY',
|
||||
'all_proxy',
|
||||
]);
|
||||
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
|
||||
const PUBLIC_REFERENCE_PATTERN = /^DT-CONN-[A-F0-9]{8}$/;
|
||||
let installedDependencyVersions;
|
||||
|
||||
function nonEmptyString(value, maxLength = 500) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const text = value.trim();
|
||||
return text ? text.slice(0, maxLength) : null;
|
||||
}
|
||||
|
||||
function safeVersion(value) {
|
||||
const version = nonEmptyString(value, 80);
|
||||
return version && VERSION_PATTERN.test(version) ? version : null;
|
||||
}
|
||||
|
||||
function resolvedPackage(name, from = require) {
|
||||
try {
|
||||
const packagePath = from.resolve(`${name}/package.json`);
|
||||
return {
|
||||
version: safeVersion(from(packagePath)?.version),
|
||||
require: createRequire(packagePath),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns only non-sensitive versions involved in the external DingTalk Stream dependency chain. */
|
||||
export function installedDingtalkConnectionDependencies() {
|
||||
if (installedDependencyVersions) return installedDependencyVersions;
|
||||
const dingtalkStream = resolvedPackage('dingtalk-stream');
|
||||
const axios = dingtalkStream ? resolvedPackage('axios', dingtalkStream.require) : null;
|
||||
const httpsProxyAgent = axios ? resolvedPackage('https-proxy-agent', axios.require) : null;
|
||||
const agentBase = httpsProxyAgent ? resolvedPackage('agent-base', httpsProxyAgent.require) : null;
|
||||
installedDependencyVersions = Object.freeze({
|
||||
dingtalkStream: dingtalkStream?.version ?? null,
|
||||
axios: axios?.version ?? null,
|
||||
httpsProxyAgent: httpsProxyAgent?.version ?? null,
|
||||
agentBase: agentBase?.version ?? null,
|
||||
});
|
||||
return installedDependencyVersions;
|
||||
}
|
||||
|
||||
function errorChain(error) {
|
||||
const chain = [];
|
||||
const seen = new Set();
|
||||
let current = error;
|
||||
while (current && typeof current === 'object' && chain.length < 4 && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
chain.push(current);
|
||||
current = current.cause;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function statusFrom(error) {
|
||||
if (Number.isInteger(error?.status)) return error.status;
|
||||
if (Number.isInteger(error?.statusCode)) return error.statusCode;
|
||||
if (Number.isInteger(error?.response?.status)) return error.response.status;
|
||||
return null;
|
||||
}
|
||||
|
||||
function providerCodeFrom(error) {
|
||||
const value = nonEmptyString(
|
||||
error?.providerCode
|
||||
?? error?.response?.data?.code
|
||||
?? error?.response?.data?.errorCode
|
||||
?? error?.response?.data?.errcode,
|
||||
100,
|
||||
);
|
||||
return value && /^[A-Za-z0-9_.:-]+$/.test(value) ? value : null;
|
||||
}
|
||||
|
||||
function redactMessage(value, sensitiveValues) {
|
||||
let message = nonEmptyString(value);
|
||||
if (!message) return null;
|
||||
for (const sensitive of sensitiveValues) {
|
||||
const text = nonEmptyString(sensitive, 2_048);
|
||||
if (text && text.length >= 4) message = message.replaceAll(text, '••••');
|
||||
}
|
||||
return message
|
||||
.replace(/(https?:\/\/)[^/\s@]+@/giu, '$1••••@')
|
||||
.replace(/([?&](?:appsecret|client_secret|clientsecret|access_token|token|password)=)[^&\s]*/giu, '$1••••')
|
||||
.replace(/((?:app|client|access)[_-]?secret|authorization|password|token)\s*[=:]\s*[^\s,;,。]+/giu, '$1=••••')
|
||||
.replace(/\b[A-Za-z0-9_.-]*secret[A-Za-z0-9_.-]*\b/giu, '••••')
|
||||
.slice(0, 500);
|
||||
}
|
||||
|
||||
function safeDependencyVersions(value) {
|
||||
const source = value && typeof value === 'object' ? value : {};
|
||||
return {
|
||||
dingtalkStream: safeVersion(source.dingtalkStream),
|
||||
axios: safeVersion(source.axios),
|
||||
httpsProxyAgent: safeVersion(source.httpsProxyAgent),
|
||||
agentBase: safeVersion(source.agentBase),
|
||||
};
|
||||
}
|
||||
|
||||
function diagnosticErrors(chain, sensitiveValues) {
|
||||
return chain.map((error) => {
|
||||
const name = nonEmptyString(error?.name, 80);
|
||||
const code = nonEmptyString(error?.code, 100);
|
||||
const status = statusFrom(error);
|
||||
const providerCode = providerCodeFrom(error);
|
||||
const message = redactMessage(error?.message, sensitiveValues);
|
||||
return {
|
||||
...(name ? { name } : {}),
|
||||
...(code ? { code } : {}),
|
||||
...(status !== null ? { status } : {}),
|
||||
...(providerCode ? { providerCode } : {}),
|
||||
...(message ? { message } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function publicReference(value) {
|
||||
return PUBLIC_REFERENCE_PATTERN.test(value ?? '')
|
||||
? value
|
||||
: `DT-CONN-${randomUUID().replaceAll('-', '').slice(0, 8).toUpperCase()}`;
|
||||
}
|
||||
|
||||
function fixedPublicError(code, message, hint, referenceId) {
|
||||
return Object.freeze({ code, message: t(message), hint: t(hint), referenceId });
|
||||
}
|
||||
|
||||
/** Adds a stable startup stage without discarding the original exception as `cause`. */
|
||||
export function dingtalkRuntimeStartError(code, cause) {
|
||||
if (cause?.name === 'AbortError' || STAGE_CODES.has(cause?.code)) return cause;
|
||||
const error = new Error(
|
||||
nonEmptyString(cause?.message) ?? 'DingTalk runtime startup failed',
|
||||
{ cause },
|
||||
);
|
||||
error.name = 'DingtalkRuntimeStartError';
|
||||
error.code = STAGE_CODES.has(code) ? code : 'dingtalk-runtime-prepare-failed';
|
||||
return error;
|
||||
}
|
||||
|
||||
/** Creates browser-safe guidance plus a redacted Host-log diagnostic for one connection failure. */
|
||||
export function describeDingtalkConnectionFailure(error, {
|
||||
fallbackMessage = '钉钉连接未就绪,请稍后重试。',
|
||||
clientId,
|
||||
clientSecret,
|
||||
environment = process.env,
|
||||
dependencies = installedDingtalkConnectionDependencies(),
|
||||
nodeVersion = process.versions.node,
|
||||
referenceId: suppliedReferenceId,
|
||||
} = {}) {
|
||||
const referenceId = publicReference(suppliedReferenceId);
|
||||
const chain = errorChain(error);
|
||||
const codes = new Set(chain
|
||||
.map((entry) => nonEmptyString(entry?.code, 100)?.toUpperCase())
|
||||
.filter(Boolean));
|
||||
const messages = chain
|
||||
.map((entry) => nonEmptyString(entry?.message)?.toLowerCase())
|
||||
.filter(Boolean);
|
||||
const statuses = chain.map(statusFrom).filter((status) => status !== null);
|
||||
const stage = chain.map((entry) => entry?.code).find((code) => STAGE_CODES.has(code)) ?? null;
|
||||
const proxyVariables = PROXY_VARIABLES.filter((name) => nonEmptyString(environment?.[name], 4_096));
|
||||
const proxyConfigured = proxyVariables.length > 0;
|
||||
const versions = safeDependencyVersions(dependencies);
|
||||
const messageContains = (pattern) => messages.some((message) => pattern.test(message));
|
||||
const codeContains = (pattern) => [...codes].some((code) => pattern.test(code));
|
||||
const proxyFailureSignal = statuses.includes(407)
|
||||
|| codeContains(/(?:PROXY|ERR_INVALID_PROTOCOL)/u)
|
||||
|| messageContains(/proxy|tunneling socket/u);
|
||||
|
||||
let publicError;
|
||||
if (stage === 'dingtalk-stream-connect-failed'
|
||||
&& proxyConfigured
|
||||
&& versions.agentBase === '6.0.0') {
|
||||
publicError = fixedPublicError(
|
||||
'stream-proxy-dependency-incompatible',
|
||||
'钉钉 Stream 连接失败:检测到代理依赖 agent-base 6.0.0。',
|
||||
'请将 DSH profile 中的 agent-base@6 固定为 6.0.2 后重新安装依赖,或升级 pnpm 后重新解析 lockfile。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (stage === 'dingtalk-harness-connect-failed') {
|
||||
publicError = fixedPublicError(
|
||||
'harness-unavailable',
|
||||
'插件无法连接本机 Harness。',
|
||||
'请确认 dsh web 正常运行,并查看 dsh web 日志中相同参考号对应的诊断信息。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (stage === 'dingtalk-stream-client-load-failed') {
|
||||
publicError = fixedPublicError(
|
||||
'stream-sdk-load-failed',
|
||||
'钉钉 Stream SDK 加载失败。',
|
||||
'请重新安装当前 DSH profile 的插件依赖,并查看 dsh web 日志中相同参考号对应的诊断信息。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (statuses.some((status) => status === 401 || status === 403)) {
|
||||
publicError = fixedPublicError(
|
||||
'stream-credentials-rejected',
|
||||
'钉钉拒绝了当前应用凭据。',
|
||||
'请核对 Client ID、Client Secret 和机器人权限后重试。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (codeContains(/(?:TIMEOUT|ETIMEDOUT)/u) || messageContains(/timed? out|timeout/u)) {
|
||||
publicError = fixedPublicError(
|
||||
'stream-handshake-timeout',
|
||||
'连接钉钉 Stream 超时。',
|
||||
'请检查网络、代理和防火墙后重试;详细原因可在 dsh web 日志中按参考号查找。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (codeContains(/^(?:ENOTFOUND|EAI_AGAIN)$/u)) {
|
||||
publicError = fixedPublicError(
|
||||
'stream-dns-failed',
|
||||
'无法解析钉钉服务地址。',
|
||||
'请检查 DNS、网络和代理设置;详细原因可在 dsh web 日志中按参考号查找。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (codeContains(/(?:CERT|TLS|SSL|UNABLE_TO_VERIFY)/u)) {
|
||||
publicError = fixedPublicError(
|
||||
'stream-tls-failed',
|
||||
'钉钉 Stream 的 TLS 连接校验失败。',
|
||||
'请检查系统证书、代理证书或 HTTPS 中间代理;详细原因可在 dsh web 日志中按参考号查找。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (stage === 'dingtalk-stream-connect-failed'
|
||||
&& proxyFailureSignal) {
|
||||
publicError = fixedPublicError(
|
||||
'stream-proxy-failed',
|
||||
'钉钉 Stream 无法通过当前代理建立连接。',
|
||||
'请检查 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 和代理连通性;详细原因可在 dsh web 日志中按参考号查找。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (stage === 'dingtalk-stream-connect-failed') {
|
||||
publicError = fixedPublicError(
|
||||
'stream-connect-failed',
|
||||
'钉钉 Stream 消息连接建立失败。',
|
||||
'请检查网络和机器人配置;详细原因可在 dsh web 日志中按参考号查找。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (stage === 'dingtalk-stream-listener-failed') {
|
||||
publicError = fixedPublicError(
|
||||
'stream-listener-failed',
|
||||
'钉钉 Stream 消息监听初始化失败。',
|
||||
'请确认 dsh-im 与 dingtalk-stream 版本兼容,并按参考号查看 dsh web 日志。',
|
||||
referenceId,
|
||||
);
|
||||
} else if (stage === 'dingtalk-runtime-prepare-failed') {
|
||||
publicError = fixedPublicError(
|
||||
'runtime-prepare-failed',
|
||||
'钉钉机器人运行环境初始化失败。',
|
||||
'请检查 DSH 数据目录、工作区和插件依赖,并按参考号查看 dsh web 日志。',
|
||||
referenceId,
|
||||
);
|
||||
} else {
|
||||
publicError = fixedPublicError(
|
||||
'connection-failed',
|
||||
fallbackMessage,
|
||||
'请在 dsh web 日志中查找相同参考号,以获取已脱敏的具体错误。',
|
||||
referenceId,
|
||||
);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
publicError,
|
||||
diagnostic: Object.freeze({
|
||||
referenceId,
|
||||
category: publicError.code,
|
||||
stage,
|
||||
runtime: { node: safeVersion(nodeVersion) },
|
||||
proxy: { configured: proxyConfigured, variables: proxyVariables },
|
||||
dependencies: versions,
|
||||
errors: diagnosticErrors(chain, [clientId, clientSecret]),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Carries only a pre-built public projection across the Host RPC boundary. */
|
||||
export function dingtalkPublicConnectionError(publicError, cause) {
|
||||
const error = new Error(publicError.message, { cause });
|
||||
error.name = 'DingtalkPublicConnectionError';
|
||||
error.code = publicError.code;
|
||||
error.publicError = structuredClone(publicError);
|
||||
return error;
|
||||
}
|
||||
|
|
@ -12,6 +12,11 @@ import {
|
|||
} from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
||||
import {
|
||||
describeDingtalkConnectionFailure,
|
||||
dingtalkPublicConnectionError,
|
||||
dingtalkRuntimeStartError,
|
||||
} from './connection-error.mjs';
|
||||
|
||||
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
||||
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
||||
|
|
@ -216,12 +221,14 @@ export class DingtalkController {
|
|||
try {
|
||||
await this.#startRuntime(latest, clientSecret);
|
||||
this.#errors.delete(latest.botId);
|
||||
} catch {
|
||||
this.#errors.set(
|
||||
latest.botId,
|
||||
safeError('connection-failed', t('钉钉连接未就绪,请稍后重试。')),
|
||||
);
|
||||
this.#logger.warn?.(`[dsh-dingtalk] bot ${latest.botId} failed to initialize`);
|
||||
} catch (error) {
|
||||
this.#rememberConnectionFailure({
|
||||
config: latest,
|
||||
clientSecret,
|
||||
error,
|
||||
fallbackMessage: '钉钉连接未就绪,请稍后重试。',
|
||||
context: `bot ${latest.botId} failed to initialize`,
|
||||
});
|
||||
}
|
||||
this.#touch();
|
||||
});
|
||||
|
|
@ -318,12 +325,14 @@ export class DingtalkController {
|
|||
try {
|
||||
await this.#startRuntime(config, normalizedSecret);
|
||||
this.#errors.delete(identity.botId);
|
||||
} catch {
|
||||
this.#errors.set(
|
||||
identity.botId,
|
||||
safeError('connection-failed', t('钉钉已接入,但消息连接暂未就绪,请稍后重试。')),
|
||||
);
|
||||
this.#logger.warn?.('[dsh-dingtalk] credential-bound bot saved but its connection is not ready');
|
||||
} catch (error) {
|
||||
this.#rememberConnectionFailure({
|
||||
config,
|
||||
clientSecret: normalizedSecret,
|
||||
error,
|
||||
fallbackMessage: '钉钉已接入,但消息连接暂未就绪,请稍后重试。',
|
||||
context: `credential-bound bot ${identity.botId} is not ready`,
|
||||
});
|
||||
}
|
||||
this.#touch();
|
||||
});
|
||||
|
|
@ -380,11 +389,14 @@ export class DingtalkController {
|
|||
await this.#startRuntime(config, clientSecret);
|
||||
this.#errors.delete(botId);
|
||||
} catch (error) {
|
||||
this.#errors.set(
|
||||
botId,
|
||||
safeError('connection-failed', t('钉钉连接仍未就绪,请稍后重试。')),
|
||||
);
|
||||
throw error;
|
||||
const publicError = this.#rememberConnectionFailure({
|
||||
config,
|
||||
clientSecret,
|
||||
error,
|
||||
fallbackMessage: '钉钉连接仍未就绪,请稍后重试。',
|
||||
context: `bot ${botId} failed to reconnect`,
|
||||
});
|
||||
throw dingtalkPublicConnectionError(publicError, error);
|
||||
} finally {
|
||||
this.#touch();
|
||||
}
|
||||
|
|
@ -675,11 +687,13 @@ export class DingtalkController {
|
|||
await rollback();
|
||||
throw abortError();
|
||||
}
|
||||
this.#errors.set(
|
||||
identity.botId,
|
||||
safeError('connection-failed', t('钉钉已接入,但消息连接暂未就绪,请稍后重试。')),
|
||||
);
|
||||
this.#logger.warn?.('[dsh-dingtalk] authorized bot saved but its connection is not ready');
|
||||
this.#rememberConnectionFailure({
|
||||
config,
|
||||
clientSecret,
|
||||
error,
|
||||
fallbackMessage: '钉钉已接入,但消息连接暂未就绪,请稍后重试。',
|
||||
context: `authorized bot ${identity.botId} is not ready`,
|
||||
});
|
||||
}
|
||||
return { botId: identity.botId, alreadyConnected: Boolean(previousConfig) };
|
||||
});
|
||||
|
|
@ -696,11 +710,14 @@ export class DingtalkController {
|
|||
} catch (error) {
|
||||
await this.#configStore.save(previousConfig).catch(() => undefined);
|
||||
await this.#startRuntime(previousConfig, clientSecret).catch(() => undefined);
|
||||
this.#errors.set(
|
||||
previousConfig.botId,
|
||||
safeError('connection-failed', t('钉钉连接未就绪,请稍后重试。')),
|
||||
);
|
||||
throw error;
|
||||
const publicError = this.#rememberConnectionFailure({
|
||||
config: previousConfig,
|
||||
clientSecret,
|
||||
error,
|
||||
fallbackMessage: '钉钉连接未就绪,请稍后重试。',
|
||||
context: `bot ${previousConfig.botId} failed to apply updated settings`,
|
||||
});
|
||||
throw dingtalkPublicConnectionError(publicError, error);
|
||||
} finally {
|
||||
this.#touch();
|
||||
}
|
||||
|
|
@ -711,13 +728,21 @@ export class DingtalkController {
|
|||
if (this.#closed) throw abortError();
|
||||
await this.#stopRuntime(config.botId);
|
||||
if (this.#closed) throw abortError();
|
||||
const runtime = await this.#createRuntime({
|
||||
botId: config.botId,
|
||||
config: structuredClone(config),
|
||||
clientSecret,
|
||||
});
|
||||
let runtime;
|
||||
try {
|
||||
runtime = await this.#createRuntime({
|
||||
botId: config.botId,
|
||||
config: structuredClone(config),
|
||||
clientSecret,
|
||||
});
|
||||
} catch (error) {
|
||||
throw dingtalkRuntimeStartError('dingtalk-runtime-prepare-failed', error);
|
||||
}
|
||||
if (!runtime || typeof runtime.start !== 'function' || typeof runtime.stop !== 'function') {
|
||||
throw new TypeError('createRuntime returned an invalid DingTalk runtime');
|
||||
throw dingtalkRuntimeStartError(
|
||||
'dingtalk-runtime-prepare-failed',
|
||||
new TypeError('createRuntime returned an invalid DingTalk runtime'),
|
||||
);
|
||||
}
|
||||
if (this.#closed) {
|
||||
await runtime.stop().catch(() => undefined);
|
||||
|
|
@ -733,10 +758,30 @@ export class DingtalkController {
|
|||
} catch (error) {
|
||||
if (this.#runtimes.get(config.botId) === runtime) this.#runtimes.delete(config.botId);
|
||||
await runtime.stop().catch(() => undefined);
|
||||
throw error;
|
||||
throw dingtalkRuntimeStartError('dingtalk-stream-connect-failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
#rememberConnectionFailure({
|
||||
config,
|
||||
clientSecret,
|
||||
error,
|
||||
fallbackMessage,
|
||||
context,
|
||||
}) {
|
||||
const failure = describeDingtalkConnectionFailure(error, {
|
||||
fallbackMessage,
|
||||
clientId: config.clientId,
|
||||
clientSecret,
|
||||
});
|
||||
this.#errors.set(config.botId, failure.publicError);
|
||||
this.#logger.error?.(
|
||||
`[dsh-dingtalk] ${context} [${failure.publicError.referenceId}]`,
|
||||
failure.diagnostic,
|
||||
);
|
||||
return failure.publicError;
|
||||
}
|
||||
|
||||
async #stopRuntime(botId) {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
this.#runtimes.delete(botId);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
||||
import { t } from '../shared/i18n.mjs';
|
||||
import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
|
||||
import { dingtalkRuntimeStartError } from './connection-error.mjs';
|
||||
|
||||
function nonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
|
|
@ -223,10 +224,12 @@ export class DingtalkRuntime {
|
|||
this.#status.startedAt = new Date().toISOString();
|
||||
this.#status.dingtalkStreamState = 'connecting';
|
||||
this.#status.lastError = null;
|
||||
let startStage = 'dingtalk-harness-connect-failed';
|
||||
|
||||
try {
|
||||
await this.#harness.ensureRunning({ signal });
|
||||
this.#status.harnessReachable = true;
|
||||
startStage = 'dingtalk-runtime-prepare-failed';
|
||||
if (typeof this.#state.removePendingSenderByStaffId === 'function') {
|
||||
for (const staffId of approvedSenderIds(this.#config)) {
|
||||
await this.#state.removePendingSenderByStaffId(staffId);
|
||||
|
|
@ -249,6 +252,7 @@ export class DingtalkRuntime {
|
|||
signal,
|
||||
});
|
||||
|
||||
startStage = 'dingtalk-stream-client-load-failed';
|
||||
const created = await this.#streamFactory({
|
||||
clientId: this.#config.clientId,
|
||||
clientSecret: this.#clientSecret,
|
||||
|
|
@ -266,6 +270,7 @@ export class DingtalkRuntime {
|
|||
|
||||
const client = this.#client;
|
||||
const bridge = this.#bridge;
|
||||
startStage = 'dingtalk-stream-listener-failed';
|
||||
client.registerCallbackListener(this.#topic, (response) => {
|
||||
if (this.#client !== client || this.#bridge !== bridge) return;
|
||||
const callbackMessageId = nonEmptyString(response?.headers?.messageId);
|
||||
|
|
@ -314,6 +319,7 @@ export class DingtalkRuntime {
|
|||
this.#callbackTasks.add(task);
|
||||
});
|
||||
|
||||
startStage = 'dingtalk-stream-connect-failed';
|
||||
await connectStream(
|
||||
client,
|
||||
this.#connectTimeoutMs,
|
||||
|
|
@ -336,11 +342,12 @@ export class DingtalkRuntime {
|
|||
return this.status;
|
||||
} catch (error) {
|
||||
const aborted = signal.aborted;
|
||||
const failure = aborted ? error : dingtalkRuntimeStartError(startStage, error);
|
||||
this.#status.ready = false;
|
||||
this.#status.dingtalkStreamState = aborted ? 'idle' : 'failed';
|
||||
this.#status.lastError = aborted ? null : (error?.message ?? String(error));
|
||||
this.#status.lastError = aborted ? null : (failure?.message ?? String(failure));
|
||||
await this.stop({ preserveError: !aborted });
|
||||
throw error;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,29 @@ export default {
|
|||
'钉钉机器人凭据缺失,请移除后重新扫码。': 'The DingTalk bot credentials are missing. Remove the bot and scan the QR code again.',
|
||||
'钉钉连接未就绪,请稍后重试。': 'The DingTalk connection is not ready. Please try again later.',
|
||||
'钉钉连接仍未就绪,请稍后重试。': 'The DingTalk connection is still not ready. Please try again later.',
|
||||
'钉钉 Stream 连接失败:检测到代理依赖 agent-base 6.0.0。': 'The DingTalk Stream connection failed because proxy dependency agent-base 6.0.0 was detected.',
|
||||
'请将 DSH profile 中的 agent-base@6 固定为 6.0.2 后重新安装依赖,或升级 pnpm 后重新解析 lockfile。': 'Pin agent-base@6 to 6.0.2 in the DSH profile and reinstall dependencies, or upgrade pnpm and re-resolve the lockfile.',
|
||||
'插件无法连接本机 Harness。': 'The plugin could not connect to the local Harness.',
|
||||
'请确认 dsh web 正常运行,并查看 dsh web 日志中相同参考号对应的诊断信息。': 'Make sure dsh web is running, then find the matching reference in the dsh web log.',
|
||||
'钉钉 Stream SDK 加载失败。': 'The DingTalk Stream SDK could not be loaded.',
|
||||
'请重新安装当前 DSH profile 的插件依赖,并查看 dsh web 日志中相同参考号对应的诊断信息。': 'Reinstall plugin dependencies in the current DSH profile, then find the matching reference in the dsh web log.',
|
||||
'钉钉拒绝了当前应用凭据。': 'DingTalk rejected the current app credentials.',
|
||||
'请核对 Client ID、Client Secret 和机器人权限后重试。': 'Verify the app credentials and bot permissions, then try again.',
|
||||
'连接钉钉 Stream 超时。': 'The DingTalk Stream connection timed out.',
|
||||
'请检查网络、代理和防火墙后重试;详细原因可在 dsh web 日志中按参考号查找。': 'Check the network, proxy, and firewall, then try again. Find details in the dsh web log using the reference.',
|
||||
'无法解析钉钉服务地址。': 'The DingTalk service address could not be resolved.',
|
||||
'请检查 DNS、网络和代理设置;详细原因可在 dsh web 日志中按参考号查找。': 'Check DNS, network, and proxy settings. Find details in the dsh web log using the reference.',
|
||||
'钉钉 Stream 的 TLS 连接校验失败。': 'TLS validation failed for the DingTalk Stream connection.',
|
||||
'请检查系统证书、代理证书或 HTTPS 中间代理;详细原因可在 dsh web 日志中按参考号查找。': 'Check system certificates, proxy certificates, or the HTTPS interception proxy. Find details in the dsh web log using the reference.',
|
||||
'钉钉 Stream 无法通过当前代理建立连接。': 'DingTalk Stream could not connect through the current proxy.',
|
||||
'请检查 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 和代理连通性;详细原因可在 dsh web 日志中按参考号查找。': 'Check HTTP_PROXY, HTTPS_PROXY, NO_PROXY, and proxy connectivity. Find details in the dsh web log using the reference.',
|
||||
'钉钉 Stream 消息连接建立失败。': 'The DingTalk Stream message connection could not be established.',
|
||||
'请检查网络和机器人配置;详细原因可在 dsh web 日志中按参考号查找。': 'Check the network and bot configuration. Find details in the dsh web log using the reference.',
|
||||
'钉钉 Stream 消息监听初始化失败。': 'DingTalk Stream message-listener initialization failed.',
|
||||
'请确认 dsh-im 与 dingtalk-stream 版本兼容,并按参考号查看 dsh web 日志。': 'Make sure dsh-im and dingtalk-stream are compatible, then inspect the dsh web log using the reference.',
|
||||
'钉钉机器人运行环境初始化失败。': 'The DingTalk bot runtime could not be initialized.',
|
||||
'请检查 DSH 数据目录、工作区和插件依赖,并按参考号查看 dsh web 日志。': 'Check the DSH data directory, workspace, and plugin dependencies, then inspect the dsh web log using the reference.',
|
||||
'请在 dsh web 日志中查找相同参考号,以获取已脱敏的具体错误。': 'Find the matching reference in the dsh web log for the redacted error details.',
|
||||
'扫码接入已取消。': 'QR code setup has been cancelled.',
|
||||
'无法生成钉钉二维码,请稍后重试。': 'Could not generate the DingTalk QR code. Please try again later.',
|
||||
'二维码已过期,请重新生成。': 'The QR code has expired. Generate a new one.',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue