feat: send test messages during bot connection checks

This commit is contained in:
xmanrui 2026-08-19 03:52:52 +08:00
parent 94da24538f
commit fb61c4235f
78 changed files with 3460 additions and 210 deletions

View file

@ -246,6 +246,13 @@ function normalizeError(value, fallbackCode, fallbackMessage) {
message: sanitizeMessage(value.message, fallbackMessage)
};
}
function normalizeTestMessage(value) {
if (!isRecord(value)) return null;
if (value.sent === true) return { sent: true };
if (value.sent !== false) return null;
const code = value.code === "test-target-unavailable" ? "test-target-unavailable" : "test-message-failed";
return { sent: false, code };
}
function unwrapRpcResult(result) {
if (!isRecord(result) || typeof result.ok !== "boolean") {
throw new Error("\u9489\u9489\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
@ -340,9 +347,17 @@ function normalizeSnapshot(value) {
configured: bots.length,
connected: bots.filter((bot) => bot.connected).length
},
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
testMessage: normalizeTestMessage(source.testMessage)
};
}
function connectionTestFeedback(result) {
if (result?.sent === true) return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
if (result?.code === "test-target-unavailable") {
return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
}
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"),
@ -436,6 +451,23 @@ var EN = Object.freeze({
"\u521A\u521A": "Just now",
"\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.",
"\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\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "Connection check completed. Send /status to the bot in a direct message, then check again.",
"\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.",
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the Feishu conversation.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6D4B\u8BD5\u6D88\u606F\u63A5\u6536\u4EBA\u3002": "Connection check completed, but no test-message recipient is available.",
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230 WhatsApp \u81EA\u804A\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the WhatsApp self-chat.",
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684 WhatsApp \u81EA\u804A\u76EE\u6807\u3002": "Connection check completed, but no WhatsApp self-chat target is available.",
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "DingTalk connection check completed and the test message was sent.",
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "DingTalk connection check completed. Send /status to the bot in a direct message, then check again.",
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "DingTalk connection check completed, but the test message could not be sent.",
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeChat connection check completed and the test message was sent.",
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "WeChat connection check completed. Send /status to the bot in a direct message, then check again.",
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeChat connection check completed, but the test message could not be sent.",
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeCom connection check completed and the test message was sent.",
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "WeCom connection check completed. Send /status to the bot in a direct message, then check again.",
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeCom connection check completed, but the test message could not be sent.",
"\u91CD\u8BD5\u8FDE\u63A5": "Reconnect",
"\u91CD\u8BD5\u4E2D\u2026": "Retrying\u2026",
"\u79FB\u9664\u63A5\u5165": "Remove connection",
@ -1735,6 +1767,7 @@ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
function AccountCard({
account,
busy,
feedback,
removing,
onReconnect,
onWorkspaceSave,
@ -1798,6 +1831,10 @@ function AccountCard({
"div",
{ className: "ddt-accountFooter dim-cardFooter" },
summary ? h2("div", { className: "ddt-summary dim-cardSummary" }, summary) : null,
feedback ? h2("div", {
className: "ddt-summary dim-cardSummary",
role: "status"
}, feedback) : null,
h2(
"div",
{ className: "ddt-actions dim-cardActions" },
@ -1837,6 +1874,7 @@ function AccountList(props) {
h2(AccountCard, {
account,
busy: props.busyByBot[account.botId],
feedback: props.feedbackByBot[account.botId]?.message,
removing: props.removeTarget === account.botId,
onReconnect: () => props.onReconnect(account),
onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
@ -1859,6 +1897,7 @@ function DingtalkSettingsTab({ rpcCall }) {
const [provision, setProvision] = React7.useState(null);
const [busy, setBusy] = React7.useState(false);
const [busyByBot, setBusyByBot] = React7.useState({});
const [feedbackByBot, setFeedbackByBot] = React7.useState({});
const [removeTarget, setRemoveTarget] = React7.useState(null);
const [credentialOpen, setCredentialOpen] = React7.useState(false);
const [credentialError, setCredentialError] = React7.useState(null);
@ -1900,6 +1939,21 @@ function DingtalkSettingsTab({ rpcCall }) {
});
}
}, []);
const discardStaleFeedback = React7.useCallback((snapshot) => {
const botsById = new Map(snapshot.bots.map((bot) => [bot.botId, bot]));
setFeedbackByBot((current) => {
let changed = false;
const next = { ...current };
for (const [botId, feedback] of Object.entries(next)) {
const bot = botsById.get(botId);
if (!bot || feedback.clearWhenDisconnected && (!bot.connected || bot.error)) {
delete next[botId];
changed = true;
}
}
return changed ? next : current;
});
}, []);
const focusAddButton = React7.useCallback(() => {
if (!mountedRef.current) return;
if (focusFrameRef.current !== null) window.cancelAnimationFrame(focusFrameRef.current);
@ -1936,6 +1990,7 @@ function DingtalkSettingsTab({ rpcCall }) {
revision: snapshot.revision,
error: null
});
discardStaleFeedback(snapshot);
if (restoreProvisioning && snapshot.provisioning) {
setProvision((current) => !current || current.attemptId === snapshot.provisioning.attemptId ? {
...current,
@ -1953,7 +2008,7 @@ function DingtalkSettingsTab({ rpcCall }) {
}));
return void 0;
}
}, [invoke, workspaceFence]);
}, [discardStaleFeedback, invoke, workspaceFence]);
React7.useEffect(() => {
const controller = new AbortController();
void loadStatus({ signal: controller.signal, restoreProvisioning: true });
@ -2042,6 +2097,7 @@ function DingtalkSettingsTab({ rpcCall }) {
revision: snapshot.revision,
error: null
});
discardStaleFeedback(snapshot);
}
setCredentialOpen(false);
announce("\u9489\u9489\u673A\u5668\u4EBA\u51ED\u636E\u5DF2\u7ED1\u5B9A\u3002");
@ -2052,7 +2108,7 @@ function DingtalkSettingsTab({ rpcCall }) {
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
if (mountedRef.current) setBusy(false);
}
}, [announce, invoke, loadStatus, workspaceFence]);
}, [announce, discardStaleFeedback, invoke, loadStatus, workspaceFence]);
const cancelProvisioning = React7.useCallback(async () => {
if (!mountedRef.current) return;
setBusy(true);
@ -2143,6 +2199,13 @@ function DingtalkSettingsTab({ rpcCall }) {
if (!mountedRef.current) return void 0;
const snapshotVersion = workspaceFence.beginMutation();
setBotBusy(account.botId, operation);
if (operation === "reconnect") {
setFeedbackByBot((current) => {
const next = { ...current };
delete next[account.botId];
return next;
});
}
try {
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
if (!mountedRef.current) return void 0;
@ -2154,25 +2217,47 @@ function DingtalkSettingsTab({ rpcCall }) {
revision: snapshot.revision,
error: null
});
discardStaleFeedback(snapshot);
}
announce(typeof success === "function" ? success(snapshot) : success);
const successMessage = typeof success === "function" ? success(snapshot) : success;
if (operation === "reconnect") {
setFeedbackByBot((current) => ({
...current,
[account.botId]: {
message: successMessage,
clearWhenDisconnected: snapshot.testMessage?.sent === true
}
}));
}
announce(successMessage);
return snapshot;
} catch (error) {
if (!mountedRef.current) return void 0;
announce(`\u64CD\u4F5C\u5931\u8D25\uFF1A${presentError(error).message}`);
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}`;
if (operation === "reconnect") {
setFeedbackByBot((current) => ({
...current,
[account.botId]: { message: failureMessage, clearWhenDisconnected: false }
}));
}
announce(failureMessage);
return void 0;
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true, restoreProvisioning: false });
if (mountedRef.current) setBotBusy(account.botId, null);
}
}, [announce, invoke, loadStatus, setBotBusy, workspaceFence]);
}, [announce, discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
const reconnect = React7.useCallback((account) => runBotAction({
account,
operation: "reconnect",
endpoint: DINGTALK_ENDPOINTS.reconnectBot,
payload: { botId: account.botId },
success: (snapshot) => snapshot?.bots.find((bot) => bot.botId === account.botId)?.connected ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002" : "\u9489\u9489\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002"
payload: { botId: account.botId, sendTest: true },
success: (snapshot) => {
const refreshed = snapshot?.bots.find((bot) => bot.botId === account.botId);
if (!refreshed?.connected) return "\u9489\u9489\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002";
return connectionTestFeedback(snapshot.testMessage) ?? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002";
}
}), [runBotAction]);
const saveWorkspace = React7.useCallback(async (account, workspace) => {
const workspaceVersion = workspaceFence.beginMutation();
@ -2190,13 +2275,14 @@ function DingtalkSettingsTab({ rpcCall }) {
revision: snapshot.revision,
error: null
});
discardStaleFeedback(snapshot);
}
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
if (mountedRef.current) setBotBusy(account.botId, null);
}
}, [invoke, loadStatus, setBotBusy, workspaceFence]);
}, [discardStaleFeedback, invoke, loadStatus, setBotBusy, workspaceFence]);
const remove = React7.useCallback(async (account) => {
const snapshot = await runBotAction({
account,
@ -2287,6 +2373,7 @@ function DingtalkSettingsTab({ rpcCall }) {
model.bots.length > 0 ? h2(AccountList, {
bots: model.bots,
busyByBot,
feedbackByBot,
removeTarget,
onReconnect: (account) => void reconnect(account),
onWorkspaceSave: saveWorkspace,
@ -2411,6 +2498,13 @@ function checkedTime2(value) {
return "\u521A\u521A";
}
}
function connectionTestNotice(value) {
if (value?.testMessage?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
if (value?.testMessage?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
}
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
function createTokenChannelSettings(definition) {
const {
channel: channel4,
@ -2433,7 +2527,7 @@ function createTokenChannelSettings(definition) {
credentialNoun = "Bot Token",
emptyActionLabel = "\u586B\u5199 Bot Token"
} = definition;
function AccountCard5({ account, busy, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
function AccountCard5({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
const state = busy === "reconnect" ? "connecting" : account.state;
const tone = account.connected ? "success" : state === "error" ? "error" : "warning";
const stateLabel = account.connected ? "\u8FD0\u884C\u6B63\u5E38" : state === "connecting" ? "\u6B63\u5728\u8FDE\u63A5" : "\u8FDE\u63A5\u672A\u5C31\u7EEA";
@ -2495,6 +2589,7 @@ function createTokenChannelSettings(definition) {
"div",
{ className: "ddt-accountFooter dim-cardFooter" },
summary ? h2("div", { className: "ddt-summary dim-cardSummary" }, summary) : null,
testNotice ? h2("div", { className: "ddt-summary dim-cardSummary", role: "status" }, testNotice) : null,
h2(
"div",
{ className: "ddt-actions dim-cardActions" },
@ -2541,6 +2636,7 @@ function createTokenChannelSettings(definition) {
const [credentialError, setCredentialError] = React8.useState(null);
const [busy, setBusy] = React8.useState(false);
const [busyByBot, setBusyByBot] = React8.useState({});
const [testNoticeByBot, setTestNoticeByBot] = React8.useState({});
const [removeTarget, setRemoveTarget] = React8.useState(null);
const mounted = React8.useRef(true);
const workspaceFence = useWorkspaceSnapshotFence();
@ -2619,10 +2715,25 @@ function createTokenChannelSettings(definition) {
const snapshotVersion = workspaceFence.beginMutation();
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
try {
const snapshot = api4.normalizeSnapshot(await invoke(endpoint, payload));
const value = await invoke(endpoint, payload);
const snapshot = api4.normalizeSnapshot(value);
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
}
if (mounted.current && operation === "reconnect") {
setTestNoticeByBot((current) => ({
...current,
[account.botId]: connectionTestNotice(value)
}));
}
} catch (error) {
if (operation !== "reconnect") throw error;
if (mounted.current) {
setTestNoticeByBot((current) => ({
...current,
[account.botId]: "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
}));
}
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
@ -2644,12 +2755,13 @@ function createTokenChannelSettings(definition) {
h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(AccountCard5, {
account,
busy: busyByBot[account.botId],
testNotice: testNoticeByBot[account.botId],
removing: removeTarget === account.botId,
onReconnect: () => void botAction(
account,
"reconnect",
endpoints.reconnectBot,
{ botId: account.botId }
{ botId: account.botId, sendTest: true }
),
onWorkspaceSave: (workspace) => botAction(
account,
@ -3946,6 +4058,15 @@ function formatCheckedTime(timestamp7) {
return "\u521A\u521A";
}
}
function connectionTestNotice2(value) {
if (value?.testMessage?.sent === true) {
return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
}
if (value?.testMessage?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6D4B\u8BD5\u6D88\u606F\u63A5\u6536\u4EBA\u3002";
}
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) {
const cancelRef = React10.useRef(null);
const idPart = bot.botId.replace(/[^a-zA-Z0-9_-]/g, "-");
@ -3988,6 +4109,7 @@ function BotCard({
connection,
busy,
actionError,
testNotice,
removing,
onReconnect,
onWorkspaceSave,
@ -4068,6 +4190,10 @@ function BotCard({
{ className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : void 0 },
summary
) : null,
testNotice ? h2("div", {
className: "bxf-healthSummary dim-cardSummary",
role: "status"
}, testNotice) : null,
h2(
"div",
{ className: "bxf-actions bxf-botActions dim-cardActions" },
@ -4116,6 +4242,7 @@ function BotList(props) {
connection: bot,
busy: props.busyByBot[bot.botId],
actionError: props.errorsByBot[bot.botId],
testNotice: props.testNoticesByBot[bot.botId],
removing: props.removeTargetId === bot.botId,
onReconnect: () => props.onReconnect(bot),
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
@ -4195,6 +4322,7 @@ function FeishuSettingsTab({ rpcCall }) {
const [credentialError, setCredentialError] = React10.useState(null);
const [busyByBot, setBusyByBot] = React10.useState({});
const [errorsByBot, setErrorsByBot] = React10.useState({});
const [testNoticesByBot, setTestNoticesByBot] = React10.useState({});
const [removeTargetId, setRemoveTargetId] = React10.useState(null);
const [announcement, setAnnouncement] = React10.useState("");
const [now, setNow] = React10.useState(() => Date.now());
@ -4460,8 +4588,14 @@ function FeishuSettingsTab({ rpcCall }) {
const snapshotVersion = workspaceFence.beginMutation();
setBotBusy(botId, "reconnect");
setBotError(botId, null);
setTestNoticesByBot((current) => {
const next = { ...current };
delete next[botId];
return next;
});
try {
const snapshot = normalizeBotsSnapshot(await invoke(FEISHU_ENDPOINTS.reconnectBot, { botId }));
const value = await invoke(FEISHU_ENDPOINTS.reconnectBot, { botId, sendTest: true });
const snapshot = normalizeBotsSnapshot(value);
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
mergeSnapshot(snapshot);
}
@ -4473,10 +4607,16 @@ function FeishuSettingsTab({ rpcCall }) {
error.code = refreshed?.error?.code ?? "FEISHU_BOT_OFFLINE";
throw error;
}
announce(connection.connected ? `${bot.name}\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002` : `${bot.name}\u5DF2\u91CD\u65B0\u8FDE\u63A5\u3002`);
const testNotice = connectionTestNotice2(value);
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setTestNoticesByBot((current) => ({ ...current, [botId]: testNotice }));
}
announce(testNotice ?? (connection.connected ? `${bot.name}\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002` : `${bot.name}\u5DF2\u91CD\u65B0\u8FDE\u63A5\u3002`));
} catch (error) {
setBotError(botId, error);
announce(`${bot.name}\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u673A\u5668\u4EBA\u72B6\u6001\u3002`);
const failure = new Error("\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
failure.code = error?.code;
setBotError(botId, failure);
announce(failure.message);
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
@ -4625,6 +4765,7 @@ function FeishuSettingsTab({ rpcCall }) {
bots: model.bots,
busyByBot,
errorsByBot,
testNoticesByBot,
removeTargetId,
onReconnect: (bot) => void reconnectOneBot(bot),
onWorkspaceSave: saveWorkspace,
@ -4652,6 +4793,7 @@ var QQ_ENDPOINTS = Object.freeze({
});
var PROVISION_STATES2 = /* @__PURE__ */ new Set(["starting", "pending", "refreshing", "connecting", "connected", "failed", "cancelled"]);
var ACCOUNT_STATES3 = /* @__PURE__ */ new Set(["connected", "connecting", "offline", "error"]);
var TEST_MESSAGE_CODES = /* @__PURE__ */ new Set(["test-target-unavailable", "test-message-failed"]);
var QR_DATA_URL2 = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i;
function isRecord4(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
@ -4725,17 +4867,35 @@ function normalizeBot3(value) {
} : null
};
}
function normalizeTestMessage2(value) {
if (!isRecord4(value) || typeof value.sent !== "boolean") return void 0;
if (value.sent) return { sent: true };
const code = text2(value.code, "test-message-failed", 80);
return {
sent: false,
code: TEST_MESSAGE_CODES.has(code) ? code : "test-message-failed"
};
}
function normalizeSnapshot3(value) {
const source = isRecord4(value?.snapshot) ? value.snapshot : value;
if (!isRecord4(source) || !Array.isArray(source.bots)) throw new Error("QQ \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u673A\u5668\u4EBA\u5217\u8868");
const bots = source.bots.map(normalizeBot3).filter(Boolean);
const testMessage = normalizeTestMessage2(source.testMessage);
return {
revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
bots,
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
provisioning: source.provisioning ? normalizeProvisioning3(source.provisioning) : null
provisioning: source.provisioning ? normalizeProvisioning3(source.provisioning) : null,
...testMessage ? { testMessage } : {}
};
}
function connectionTestFeedback2(result) {
if (result?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
if (result?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
}
return result ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
function presentError4(error) {
return {
code: text2(error?.code, "QQ_ERROR", 80),
@ -4983,6 +5143,7 @@ function RemoveConfirmation3({ account, busy, onConfirm, onCancel }) {
function AccountCard2({
account,
busy,
feedback,
removing,
onReconnect,
onWorkspaceSave,
@ -5035,6 +5196,11 @@ function AccountCard2({
"div",
{ className: "ddt-accountFooter dim-cardFooter" },
summary ? h2("div", { className: "ddt-summary dim-cardSummary" }, summary) : null,
feedback ? h2("div", {
className: "ddt-summary dim-cardSummary",
role: "status",
"aria-live": "polite"
}, feedback) : null,
h2(
"div",
{ className: "ddt-actions dim-cardActions" },
@ -5056,6 +5222,7 @@ function QqSettingsTab({ rpcCall }) {
const [provision, setProvision] = React11.useState(null);
const [busy, setBusy] = React11.useState(false);
const [busyByBot, setBusyByBot] = React11.useState({});
const [feedbackByBot, setFeedbackByBot] = React11.useState({});
const [removeTarget, setRemoveTarget] = React11.useState(null);
const [credentialOpen, setCredentialOpen] = React11.useState(false);
const [credentialError, setCredentialError] = React11.useState(null);
@ -5216,6 +5383,34 @@ function QqSettingsTab({ rpcCall }) {
});
}
}, [invoke, loadStatus, workspaceFence]);
const reconnect = React11.useCallback(async (account) => {
setFeedbackByBot((current) => {
const next = { ...current };
delete next[account.botId];
return next;
});
try {
const snapshot = await botAction(
account,
"reconnect",
QQ_ENDPOINTS.reconnectBot,
{ botId: account.botId, sendTest: true }
);
if (mounted.current) {
setFeedbackByBot((current) => ({
...current,
[account.botId]: connectionTestFeedback2(snapshot?.testMessage)
}));
}
} catch {
if (mounted.current) {
setFeedbackByBot((current) => ({
...current,
[account.botId]: "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
}));
}
}
}, [botAction]);
let provisionView = null;
if (provision?.status === "starting") provisionView = h2("div", { className: "ddt-card ddt-loading dim-surfaceCard" }, h2("div", { className: "ddt-spinner" }), "\u6B63\u5728\u7533\u8BF7 QQ \u4E8C\u7EF4\u7801\u2026");
else if (["pending", "refreshing"].includes(provision?.status)) provisionView = h2(QrPanel2, {
@ -5242,8 +5437,9 @@ function QqSettingsTab({ rpcCall }) {
h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(AccountCard2, {
account,
busy: busyByBot[account.botId],
feedback: feedbackByBot[account.botId],
removing: removeTarget === account.botId,
onReconnect: () => void botAction(account, "reconnect", QQ_ENDPOINTS.reconnectBot, { botId: account.botId }),
onReconnect: () => void reconnect(account),
onWorkspaceSave: (workspace) => botAction(
account,
"workspace",
@ -5597,6 +5793,13 @@ function timestamp4(value) {
const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
return Number.isNaN(parsed) ? void 0 : parsed;
}
function normalizeTestMessage3(value) {
if (!isRecord5(value)) return null;
if (value.sent === true) return { sent: true };
if (value.sent !== false) return null;
const code = value.code === "test-target-unavailable" ? "test-target-unavailable" : "test-message-failed";
return { sent: false, code };
}
function unwrapRpcResult7(result) {
if (!isRecord5(result) || typeof result.ok !== "boolean") throw new Error("\u4F01\u4E1A\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
if (!result.ok) {
@ -5662,7 +5865,8 @@ function normalizeSnapshot6(value) {
revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
bots,
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
provisioning: source.provisioning ? normalizeProvisioning4(source.provisioning) : null
provisioning: source.provisioning ? normalizeProvisioning4(source.provisioning) : null,
testMessage: normalizeTestMessage3(source.testMessage)
};
}
function presentError7(error) {
@ -5912,6 +6116,7 @@ function RemoveConfirmation4({ account, busy, onConfirm, onCancel }) {
function AccountCard3({
account,
busy,
feedback,
removing,
onReconnect,
onWorkspaceSave,
@ -5964,6 +6169,11 @@ function AccountCard3({
"div",
{ className: "ddt-accountFooter dim-cardFooter" },
summary ? h2("div", { className: "ddt-summary dim-cardSummary" }, summary) : null,
feedback ? h2("div", {
className: "ddt-summary dim-cardSummary",
role: "status",
"aria-live": "polite"
}, feedback) : null,
h2(
"div",
{ className: "ddt-actions dim-cardActions" },
@ -5985,19 +6195,40 @@ function WecomSettingsTab({ rpcCall }) {
const [provision, setProvision] = React13.useState(null);
const [busy, setBusy] = React13.useState(false);
const [busyByBot, setBusyByBot] = React13.useState({});
const [feedbackByBot, setFeedbackByBot] = React13.useState({});
const [removeTarget, setRemoveTarget] = React13.useState(null);
const [credentialOpen, setCredentialOpen] = React13.useState(false);
const [credentialError, setCredentialError] = React13.useState(null);
const [notice, setNotice] = React13.useState("");
const [now, setNow] = React13.useState(Date.now());
const mounted = React13.useRef(true);
const workspaceFence = useWorkspaceSnapshotFence();
const addButtonRef = React13.useRef(null);
const noticeFrameRef = React13.useRef(null);
const announce = React13.useCallback((message) => {
if (!mounted.current) return;
if (noticeFrameRef.current !== null) {
window.cancelAnimationFrame(noticeFrameRef.current);
noticeFrameRef.current = null;
}
setNotice("");
if (message) {
noticeFrameRef.current = window.requestAnimationFrame(() => {
noticeFrameRef.current = null;
if (mounted.current) setNotice(message);
});
}
}, []);
React13.useEffect(() => {
const disposeDingtalk = installDingtalkStyles();
const disposeWecom = installWecomStyles();
mounted.current = true;
return () => {
mounted.current = false;
if (noticeFrameRef.current !== null) {
window.cancelAnimationFrame(noticeFrameRef.current);
noticeFrameRef.current = null;
}
disposeWecom();
disposeDingtalk();
};
@ -6145,6 +6376,45 @@ function WecomSettingsTab({ rpcCall }) {
});
}
}, [invoke, loadStatus, workspaceFence]);
const reconnect = React13.useCallback(async (account) => {
setFeedbackByBot((current) => {
const next = { ...current };
delete next[account.botId];
return next;
});
try {
const snapshot = await botAction(
account,
"reconnect",
WECOM_ENDPOINTS.reconnectBot,
{ botId: account.botId, sendTest: true }
);
if (!snapshot) return;
const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
let feedback;
if (!refreshed?.connected) {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002";
} else if (snapshot.testMessage?.sent) {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
} else if (snapshot.testMessage) {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
} else {
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002";
}
if (mounted.current) {
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
}
announce(feedback);
} catch {
const feedback = "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
if (mounted.current) {
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
}
announce(feedback);
}
}, [announce, botAction]);
let provisionView = null;
if (provision?.status === "starting") provisionView = h2("div", { className: "ddt-card ddt-loading dim-surfaceCard" }, h2("div", { className: "ddt-spinner" }), "\u6B63\u5728\u7533\u8BF7\u4F01\u4E1A\u5FAE\u4FE1\u4E8C\u7EF4\u7801\u2026");
else if (["pending", "refreshing"].includes(provision?.status)) provisionView = h2(QrPanel3, {
@ -6171,8 +6441,9 @@ function WecomSettingsTab({ rpcCall }) {
h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(AccountCard3, {
account,
busy: busyByBot[account.botId],
feedback: feedbackByBot[account.botId],
removing: removeTarget === account.botId,
onReconnect: () => void botAction(account, "reconnect", WECOM_ENDPOINTS.reconnectBot, { botId: account.botId }),
onReconnect: () => void reconnect(account),
onWorkspaceSave: (workspace) => botAction(
account,
"workspace",
@ -6216,6 +6487,7 @@ function WecomSettingsTab({ rpcCall }) {
credentialOpen,
addButtonRef
}),
h2("div", { className: "ddt-visuallyHidden", role: "status", "aria-live": "polite" }, notice),
model.phase === "loading" ? h2(LoadingView4) : model.phase === "error" ? h2("div", { className: "ddt-card dim-surfaceCard" }, h2("div", { className: "ddt-inlineError dim-inlineError" }, h2("h3", null, "\u65E0\u6CD5\u8BFB\u53D6\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA\u72B6\u6001"), h2("p", null, model.error?.message), h2(Button9, { onClick: () => void loadStatus() }, "\u91CD\u65B0\u8BFB\u53D6"))) : h2(
React13.Fragment,
null,
@ -6263,6 +6535,13 @@ function string(value, fallback = "") {
function timestamp5(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function normalizeTestMessage4(value) {
if (!isRecord6(value)) return null;
if (value.sent === true) return { sent: true };
if (value.sent !== false) return null;
const code = value.code === "test-target-unavailable" ? "test-target-unavailable" : "test-message-failed";
return { sent: false, code };
}
function unwrapRpcResult8(result) {
if (!isRecord6(result) || typeof result.ok !== "boolean") {
throw new Error("\u5FAE\u4FE1\u670D\u52A1\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u54CD\u5E94");
@ -6356,7 +6635,8 @@ function normalizeSnapshot7(value) {
configured: bots.length,
connected: bots.filter((bot) => bot.connected).length
},
provisioning: value.provisioning ? normalizeProvisioning5(value.provisioning) : null
provisioning: value.provisioning ? normalizeProvisioning5(value.provisioning) : null,
testMessage: normalizeTestMessage4(value.testMessage)
};
}
function presentError8(error) {
@ -6736,6 +7016,7 @@ function checkedTime5(timestamp7) {
function AccountCard4({
account,
busy,
feedback,
removing,
onReconnect,
onWorkspaceSave,
@ -6793,6 +7074,11 @@ function AccountCard4({
"div",
{ className: "dxw-accountFooter dim-cardFooter" },
summary ? h2("div", { className: "dxw-summary dim-cardSummary" }, summary) : null,
feedback ? h2("div", {
className: "dxw-summary dim-cardSummary",
role: "status",
"aria-live": "polite"
}, feedback) : null,
h2(
"div",
{ className: "dxw-actions dim-cardActions" },
@ -6834,6 +7120,7 @@ function AccountList2(props) {
h2(AccountCard4, {
account,
busy: props.busyByBot[account.botId],
feedback: props.feedbackByBot[account.botId],
removing: props.removeTarget === account.botId,
onReconnect: () => props.onReconnect(account),
onWorkspaceSave: (workspace) => props.onWorkspaceSave(account, workspace),
@ -6865,6 +7152,7 @@ function WeixinSettingsTab({ rpcCall }) {
const [provision, setProvision] = React14.useState(null);
const [busy, setBusy] = React14.useState(false);
const [busyByBot, setBusyByBot] = React14.useState({});
const [feedbackByBot, setFeedbackByBot] = React14.useState({});
const [removeTarget, setRemoveTarget] = React14.useState(null);
const [notice, setNotice] = React14.useState("");
const [now, setNow] = React14.useState(() => Date.now());
@ -7063,15 +7351,42 @@ function WeixinSettingsTab({ rpcCall }) {
const reconnect = React14.useCallback(async (account) => {
const snapshotVersion = workspaceFence.beginMutation();
setBotBusy(account.botId, "reconnect");
setFeedbackByBot((current) => {
const next = { ...current };
delete next[account.botId];
return next;
});
try {
const snapshot = normalizeSnapshot7(await invoke(WEIXIN_ENDPOINTS.reconnectBot, { botId: account.botId }));
const snapshot = normalizeSnapshot7(await invoke(
WEIXIN_ENDPOINTS.reconnectBot,
{ botId: account.botId, sendTest: true }
));
if (mountedRef.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setModel((current) => ({ ...current, bots: snapshot.bots, totals: snapshot.totals, revision: snapshot.revision }));
}
const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
announce(refreshed?.connected ? "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002" : "\u5FAE\u4FE1\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002");
} catch (error) {
announce(`\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF1A${presentError8(error).message}`);
let feedback;
if (!refreshed?.connected) {
feedback = "\u5FAE\u4FE1\u4ECD\u672A\u8FDE\u63A5\uFF0C\u63D2\u4EF6\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5\u3002";
} else if (snapshot.testMessage?.sent) {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
} else if (snapshot.testMessage) {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
} else {
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002";
}
if (mountedRef.current) {
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
}
announce(feedback);
} catch {
const feedback = "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
if (mountedRef.current) {
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
}
announce(feedback);
} finally {
const shouldRefresh = workspaceFence.endMutation();
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
@ -7184,6 +7499,7 @@ function WeixinSettingsTab({ rpcCall }) {
model.bots.length > 0 ? h2(AccountList2, {
bots: model.bots,
busyByBot,
feedbackByBot,
removeTarget,
onReconnect: (account) => void reconnect(account),
onWorkspaceSave: saveWorkspace,
@ -7354,6 +7670,15 @@ function checkedTime6(value) {
return "\u521A\u521A";
}
}
function connectionTestNotice3(value) {
if (value?.testMessage?.sent === true) {
return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230 WhatsApp \u81EA\u804A\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
}
if (value?.testMessage?.code === "test-target-unavailable") {
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684 WhatsApp \u81EA\u804A\u76EE\u6807\u3002";
}
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
}
function Heading6({ totals, busy, onAdd, addButtonRef }) {
return h2(
"div",
@ -7542,6 +7867,7 @@ function RemoveConfirmation5({ account, busy, onConfirm, onCancel }) {
function WhatsappAccountCard({
account,
busy,
testNotice,
removing,
onReconnect,
onWorkspaceSave,
@ -7608,6 +7934,10 @@ function WhatsappAccountCard({
"div",
{ className: "ddt-accountFooter dim-cardFooter" },
summary ? h2("div", { className: "ddt-summary dim-cardSummary" }, summary) : null,
testNotice ? h2("div", {
className: "ddt-summary dim-cardSummary",
role: "status"
}, testNotice) : null,
h2(
"div",
{ className: "ddt-actions dim-cardActions" },
@ -7643,6 +7973,7 @@ function WhatsappSettingsTab({ rpcCall }) {
const [provision, setProvision] = React15.useState(null);
const [busy, setBusy] = React15.useState(false);
const [busyByBot, setBusyByBot] = React15.useState({});
const [testNoticeByBot, setTestNoticeByBot] = React15.useState({});
const [removeTarget, setRemoveTarget] = React15.useState(null);
const [now, setNow] = React15.useState(Date.now());
const mounted = React15.useRef(true);
@ -7781,10 +8112,32 @@ function WhatsappSettingsTab({ rpcCall }) {
const botAction = React15.useCallback(async (account, operation, endpoint, payload) => {
const snapshotVersion = workspaceFence.beginMutation();
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
if (operation === "reconnect") {
setTestNoticeByBot((current) => {
const next = { ...current };
delete next[account.botId];
return next;
});
}
try {
const snapshot = normalizeSnapshot8(await invoke(endpoint, payload));
const value = await invoke(endpoint, payload);
const snapshot = normalizeSnapshot8(value);
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setModel({ phase: "ready", bots: snapshot.bots, totals: snapshot.totals, error: null });
if (operation === "reconnect") {
setTestNoticeByBot((current) => ({
...current,
[account.botId]: connectionTestNotice3(value)
}));
}
}
} catch (error) {
if (operation !== "reconnect") throw error;
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
setTestNoticeByBot((current) => ({
...current,
[account.botId]: "\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
}));
}
} finally {
const shouldRefresh = workspaceFence.endMutation();
@ -7807,12 +8160,13 @@ function WhatsappSettingsTab({ rpcCall }) {
h2("ul", { className: "ddt-list dim-botList" }, model.bots.map((account) => h2("li", { key: account.botId }, h2(WhatsappAccountCard, {
account,
busy: busyByBot[account.botId],
testNotice: testNoticeByBot[account.botId],
removing: removeTarget === account.botId,
onReconnect: () => void botAction(
account,
"reconnect",
WHATSAPP_ENDPOINTS.reconnectBot,
{ botId: account.botId }
{ botId: account.botId, sendTest: true }
),
onWorkspaceSave: (workspace) => botAction(
account,

File diff suppressed because one or more lines are too long