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

@ -593,10 +593,7 @@ var package_default = {
"whatsapp",
"ai-office"
],
author: {
name: "xmanrui",
url: "https://github.com/xmanrui"
},
author: "xmanrui (https://github.com/xmanrui)",
contributors: [
{
name: "C3H3-AI",
@ -618,7 +615,9 @@ var package_default = {
url: "git+https://github.com/xmanrui/dsh-im.git"
},
homepage: "https://github.com/xmanrui/dsh-im#readme",
bugs: "https://github.com/xmanrui/dsh-im/issues",
bugs: {
url: "https://github.com/xmanrui/dsh-im/issues"
},
publishConfig: {
access: "public"
},
@ -687,6 +686,11 @@ var package_default = {
"react-dom": "18.3.1",
"react-test-renderer": "18.3.1",
semver: "7.8.5"
},
directories: {
doc: "docs",
lib: "lib",
test: "test"
}
};
@ -1189,6 +1193,8 @@ var EN = Object.freeze({
"\u68C0\u67E5\u8FDE\u63A5": "Check connection",
"\u68C0\u67E5\u4E2D\u2026": "Checking\u2026",
"\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "Connection check failed. Try again later.",
"\u673A\u5668\u4EBA\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u8FDE\u63A5\u672A\u5C31\u7EEA": "The bot was saved, but the connection is not ready",
"\u67E5\u770B\u5DF2\u4FDD\u5B58\u7684\u673A\u5668\u4EBA": "View saved bot",
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the matching bot conversation.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002": "Connection check completed. The bot has not received a direct message it can use for testing.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "Connection check completed, but the test message could not be sent.",
@ -1753,6 +1759,8 @@ function translateDynamic(text6) {
if (match) return `Automatic status refresh failed: ${match[1]}`;
match = /^操作失败:(.+)$/.exec(text6);
if (match) return `Operation failed: ${match[1]}`;
match = /^连接检查失败:(.+)(参考号:(.+))$/.exec(text6);
if (match) return `Connection check failed: ${localizeText(match[1])} (reference: ${match[2]})`;
match = /^连接检查失败:(.+)$/.exec(text6);
if (match) return `Connection check failed: ${match[1]}`;
match = /^移除失败:(.+)$/.exec(text6);
@ -2252,11 +2260,19 @@ function sanitizeMessage(value, fallback) {
if (FORBIDDEN_ERROR_FIELDS.test(message)) return fallback;
return message.replace(/([=:]\s*)[^\s,;,。]+/g, "$1\u2022\u2022\u2022\u2022\u2022\u2022").slice(0, 240);
}
function safeReferenceId(value) {
const referenceId = optionalString(value, 40);
return referenceId && /^DT-CONN-[A-F0-9]{8}$/.test(referenceId) ? referenceId : void 0;
}
function normalizeError(value, fallbackCode, fallbackMessage) {
if (!isRecord(value)) return void 0;
const hint = sanitizeMessage(value.hint, "");
const referenceId = safeReferenceId(value.referenceId);
return {
code: safeErrorCode(value.code, fallbackCode),
message: sanitizeMessage(value.message, fallbackMessage)
message: sanitizeMessage(value.message, fallbackMessage),
...hint ? { hint } : {},
...referenceId ? { referenceId } : {}
};
}
function normalizeTestMessage(value) {
@ -2271,8 +2287,15 @@ function unwrapRpcResult(result) {
throw new Error("\u9489\u9489\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
}
if (!result.ok) {
const error = new Error(sanitizeMessage(result.error?.message, "\u9489\u9489\u64CD\u4F5C\u5931\u8D25"));
error.code = safeErrorCode(result.error?.code, "DINGTALK_RPC_ERROR");
const visible = normalizeError(
result.error,
"DINGTALK_RPC_ERROR",
"\u9489\u9489\u64CD\u4F5C\u5931\u8D25"
) ?? { code: "DINGTALK_RPC_ERROR", message: "\u9489\u9489\u64CD\u4F5C\u5931\u8D25" };
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;
@ -2377,10 +2400,11 @@ function connectionTestFeedback(result) {
return result ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
function presentError(error) {
return {
code: safeErrorCode(error?.code, "DINGTALK_ERROR"),
message: sanitizeMessage(error?.message, "\u9489\u9489\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5")
};
return normalizeError(
error,
"DINGTALK_ERROR",
"\u9489\u9489\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"
) ?? { code: "DINGTALK_ERROR", message: "\u9489\u9489\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" };
}
function formatRemaining(milliseconds) {
const seconds = Math.max(0, Math.ceil(Number(milliseconds) / 1e3) || 0);
@ -3680,6 +3704,8 @@ var 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; }
@ -3936,25 +3962,45 @@ function ProgressPanel({ status, busy, onCancel }) {
)
);
}
function ConnectionErrorDiagnostic({ error }) {
if (!error) return null;
return h2(
"div",
{ className: "ddt-errorDiagnostic" },
error.hint ? h2("p", { className: "ddt-errorHint" }, error.hint) : null,
h2(
"span",
{ className: "ddt-errorCode" },
h2("span", null, "\u9519\u8BEF\u7801"),
`: ${error.code}`,
error.referenceId ? h2(React10.Fragment, null, " \xB7 ", h2("span", null, "\u53C2\u8003\u53F7"), `: ${error.referenceId}`) : null
)
);
}
function ProvisionError({ provision, busy, onRetry, onClose }) {
const error = provision.error ?? {
code: "DINGTALK_PROVISION_FAILED",
message: "\u9489\u9489\u673A\u5668\u4EBA\u6CA1\u6709\u63A5\u5165\u5B8C\u6210"
};
const connectionFailed = Boolean(error.referenceId);
return h2(
"div",
{ className: "ddt-card dim-surfaceCard" },
h2(
"div",
{ className: "ddt-inlineError dim-inlineError", role: "alert" },
h2("h3", null, provision.status === "expired" ? "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F" : "\u9489\u9489\u673A\u5668\u4EBA\u6CA1\u6709\u63A5\u5165\u5B8C\u6210"),
h2("h3", null, provision.status === "expired" ? "\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F" : connectionFailed ? "\u673A\u5668\u4EBA\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u8FDE\u63A5\u672A\u5C31\u7EEA" : "\u9489\u9489\u673A\u5668\u4EBA\u6CA1\u6709\u63A5\u5165\u5B8C\u6210"),
h2("p", null, error.message),
h2("span", { className: "ddt-errorCode" }, error.code),
h2(ConnectionErrorDiagnostic, { error }),
h2(
"div",
{ className: "ddt-actions dim-viewActions" },
h2(Button, { kind: "primary", onClick: onRetry, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801"),
h2(Button, { onClick: onClose, disabled: busy }, "\u5173\u95ED")
connectionFailed ? h2(Button, { kind: "primary", onClick: onClose, disabled: busy }, "\u67E5\u770B\u5DF2\u4FDD\u5B58\u7684\u673A\u5668\u4EBA") : h2(
React10.Fragment,
null,
h2(Button, { kind: "primary", onClick: onRetry, disabled: busy }, "\u91CD\u65B0\u751F\u6210\u4E8C\u7EF4\u7801"),
h2(Button, { onClick: onClose, disabled: busy }, "\u5173\u95ED")
)
)
)
);
@ -4091,6 +4137,7 @@ function AccountCard({
)
),
summary2 ? h2("div", { className: "ddt-summary dim-cardSummary" }, summary2) : null,
account.error ? h2(ConnectionErrorDiagnostic, { error: account.error }) : null,
account.lastMessageError ? h2(LastMessageErrorSummary, {
className: "ddt-summary",
error: account.lastMessageError
@ -4416,6 +4463,10 @@ function DingtalkSettingsTab({ rpcCall }) {
if (!canCommit()) return;
const account = result.botId ? 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);
schedule(result.pollIntervalMs);
return;
@ -4490,7 +4541,8 @@ function DingtalkSettingsTab({ rpcCall }) {
return snapshot;
} catch (error) {
if (!mountedRef.current) return void 0;
const failureMessage = operation === "reconnect" ? "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : `\u64CD\u4F5C\u5931\u8D25\uFF1A${presentError(error).message}`;
const visibleError = presentError(error);
const failureMessage = operation === "reconnect" ? visibleError.referenceId ? `\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF1A${visibleError.message}\uFF08\u53C2\u8003\u53F7\uFF1A${visibleError.referenceId}\uFF09` : "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : `\u64CD\u4F5C\u5931\u8D25\uFF1A${visibleError.message}`;
if (operation === "reconnect") {
setFeedbackByBot((current) => ({
...current,