fix: add actionable DingTalk connection diagnostics

This commit is contained in:
xmanrui 2026-09-04 02:18:18 +08:00
parent 3010535409
commit 1b05fa8d32
18 changed files with 1038 additions and 299 deletions

View file

@ -85,11 +85,20 @@ function sanitizeMessage(value, fallback) {
return message.replace(/([=:]\s*)[^\s,;,。]+/g, '$1••••••').slice(0, 240);
}
function safeReferenceId(value) {
const referenceId = optionalString(value, 40);
return referenceId && /^DT-CONN-[A-F0-9]{8}$/.test(referenceId) ? referenceId : undefined;
}
function normalizeError(value, fallbackCode, fallbackMessage) {
if (!isRecord(value)) return undefined;
const hint = sanitizeMessage(value.hint, '');
const referenceId = safeReferenceId(value.referenceId);
return {
code: safeErrorCode(value.code, fallbackCode),
message: sanitizeMessage(value.message, fallbackMessage),
...(hint ? { hint } : {}),
...(referenceId ? { referenceId } : {}),
};
}
@ -108,8 +117,15 @@ export function unwrapRpcResult(result) {
throw new Error('钉钉服务返回了无法识别的响应');
}
if (!result.ok) {
const error = new Error(sanitizeMessage(result.error?.message, '钉钉操作失败'));
error.code = safeErrorCode(result.error?.code, 'DINGTALK_RPC_ERROR');
const visible = normalizeError(
result.error,
'DINGTALK_RPC_ERROR',
'钉钉操作失败',
) ?? { code: 'DINGTALK_RPC_ERROR', message: '钉钉操作失败' };
const error = new Error(visible.message);
error.code = visible.code;
if (visible.hint) error.hint = visible.hint;
if (visible.referenceId) error.referenceId = visible.referenceId;
throw error;
}
return result.value;
@ -227,10 +243,11 @@ export function connectionTestFeedback(result) {
}
export function presentError(error) {
return {
code: safeErrorCode(error?.code, 'DINGTALK_ERROR'),
message: sanitizeMessage(error?.message, '钉钉操作失败,请稍后重试'),
};
return normalizeError(
error,
'DINGTALK_ERROR',
'钉钉操作失败,请稍后重试',
) ?? { code: 'DINGTALK_ERROR', message: '钉钉操作失败,请稍后重试' };
}
export function formatRemaining(milliseconds) {

View file

@ -169,19 +169,36 @@ function ProgressPanel({ status, busy, onCancel }) {
h(Button, { onClick: onCancel, disabled: busy }, '取消接入')));
}
function ConnectionErrorDiagnostic({ error }) {
if (!error) return null;
return h('div', { className: 'ddt-errorDiagnostic' },
error.hint ? h('p', { className: 'ddt-errorHint' }, error.hint) : null,
h('span', { className: 'ddt-errorCode' },
h('span', null, '错误码'), `: ${error.code}`,
error.referenceId
? h(React.Fragment, null, ' · ', h('span', null, '参考号'), `: ${error.referenceId}`)
: null));
}
function ProvisionError({ provision, busy, onRetry, onClose }) {
const error = provision.error ?? {
code: 'DINGTALK_PROVISION_FAILED',
message: '钉钉机器人没有接入完成',
};
const connectionFailed = Boolean(error.referenceId);
return h('div', { className: 'ddt-card dim-surfaceCard' },
h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
h('h3', null, provision.status === 'expired' ? '二维码已过期' : '钉钉机器人没有接入完成'),
h('h3', null, provision.status === 'expired'
? '二维码已过期'
: connectionFailed ? '机器人已保存,但连接未就绪' : '钉钉机器人没有接入完成'),
h('p', null, error.message),
h('span', { className: 'ddt-errorCode' }, error.code),
h(ConnectionErrorDiagnostic, { error }),
h('div', { className: 'ddt-actions dim-viewActions' },
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
connectionFailed
? h(Button, { kind: 'primary', onClick: onClose, disabled: busy }, '查看已保存的机器人')
: h(React.Fragment, null,
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
h(Button, { onClick: onClose, disabled: busy }, '关闭')))));
}
function checkedTime(value) {
@ -278,6 +295,7 @@ export function AccountCard({
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) },
'移除接入')),
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
account.error ? h(ConnectionErrorDiagnostic, { error: account.error }) : null,
account.lastMessageError ? h(LastMessageErrorSummary, {
className: 'ddt-summary',
error: account.lastMessageError,
@ -614,6 +632,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
? snapshot?.bots.find((bot) => bot.botId === result.botId)
: snapshot?.bots.find((bot) => bot.connected);
if (!account?.connected) {
if (account?.error) {
setProvision((current) => current?.attemptId === attemptId
? { ...current, ...result, status: 'failed', error: account.error }
: current);
return;
}
setProvision((current) => current?.attemptId === attemptId
? { ...current, ...result, status: 'connecting' }
: current);
@ -698,9 +722,12 @@ export function DingtalkSettingsTab({ rpcCall }) {
return snapshot;
} catch (error) {
if (!mountedRef.current) return undefined;
const visibleError = presentError(error);
const failureMessage = operation === 'reconnect'
? '连接检查失败,请稍后重试。'
: `操作失败:${presentError(error).message}`;
? visibleError.referenceId
? `连接检查失败:${visibleError.message}(参考号:${visibleError.referenceId})`
: '连接检查失败,请稍后重试。'
: `操作失败:${visibleError.message}`;
if (operation === 'reconnect') {
setFeedbackByBot((current) => ({
...current,

View file

@ -77,6 +77,8 @@ const CSS = String.raw`
.ddt-inlineError h3 { font-size: 17px; }
.ddt-inlineError p { line-height: 1.55; }
.ddt-errorCode { font: 11px ui-monospace, SFMono-Regular, monospace; opacity: .8; }
.ddt-errorDiagnostic { display: grid; gap: 5px; color: var(--ddt-error); }
.ddt-errorHint { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.55; }
.ddt-listHeading { display: flex; align-items: center; justify-content: space-between; margin: 2px 0 9px; }
.ddt-listHeading h3 { margin: 0; font-size: 14px; }
.ddt-list { display: grid; gap: 12px; margin: 0; padding: 0; list-style: none; }

View file

@ -351,6 +351,8 @@ const EN = Object.freeze({
'检查连接': 'Check connection',
'检查中…': 'Checking…',
'连接检查失败,请稍后重试。': 'Connection check failed. Try again later.',
'机器人已保存,但连接未就绪': 'The bot was saved, but the connection is not ready',
'查看已保存的机器人': 'View saved bot',
'测试消息已发送,请到对应机器人会话中确认。': 'Test message sent. Check the matching bot conversation.',
'连接检查完成。机器人尚未收到可用于测试的私聊消息。': 'Connection check completed. The bot has not received a direct message it can use for testing.',
'连接检查完成,但测试消息发送失败。': 'Connection check completed, but the test message could not be sent.',
@ -921,6 +923,8 @@ function translateDynamic(text) {
if (match) return `Automatic status refresh failed: ${match[1]}`;
match = /^操作失败:(.+)$/.exec(text);
if (match) return `Operation failed: ${match[1]}`;
match = /^连接检查失败:(.+)(参考号:(.+))$/.exec(text);
if (match) return `Connection check failed: ${localizeText(match[1])} (reference: ${match[2]})`;
match = /^连接检查失败:(.+)$/.exec(text);
if (match) return `Connection check failed: ${match[1]}`;
match = /^移除失败:(.+)$/.exec(text);