fix: surface actionable channel message errors

This commit is contained in:
xmanrui 2026-08-25 14:54:50 +08:00
parent b42fc411c2
commit 7cbed3aefb
59 changed files with 2405 additions and 607 deletions

View file

@ -1,4 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
export const DINGTALK_RPC_CHANNEL = '/dingtalk';
@ -178,6 +179,7 @@ function normalizeBot(value) {
messagesReceived: nonNegativeInteger(stats.messagesReceived),
messagesReplied: nonNegativeInteger(stats.messagesReplied),
},
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: normalizeError(value.error, 'DINGTALK_ACCOUNT_ERROR', '钉钉连接尚未就绪') ?? null,
};
}

View file

@ -9,7 +9,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from '../../channel-card-meta.js';
import {
DINGTALK_ENDPOINTS,
DINGTALK_RPC_CHANNEL,
@ -262,6 +266,10 @@ 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.lastMessageError ? h(LastMessageErrorSummary, {
className: 'ddt-summary',
error: account.lastMessageError,
}) : null,
feedback ? h('div', {
className: 'ddt-summary dim-cardFeedback',
role: 'status',

View file

@ -7,6 +7,7 @@
*/
import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
import { normalizeLastMessageError } from "../../last-message-error.js";
export const FEISHU_RPC_CHANNEL = "/feishu";
@ -206,6 +207,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
bot: normalizeBot(value.bot),
health: normalizeHealth(value.health, connected),
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: normalizeError(value.error),
};
}
@ -226,6 +228,7 @@ export function normalizeBotsSnapshot(value) {
configured: true,
bot: value.bot,
health: value.health,
lastMessageError: value.lastMessageError,
error: value.error,
}];
}

View file

@ -23,7 +23,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from "../../agent-preset.js";
import { useWorkspaceSnapshotFence } from "../../workspace-snapshot-fence.js";
import { BotStatusMeta, ChannelListHeading } from "../../channel-card-meta.js";
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from "../../channel-card-meta.js";
import { installFeishuStyles } from "./styles.js";
export const name = "feishu-settings";
@ -661,6 +665,10 @@ export function BotCard({
}, "移除接入")),
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
summary) : null,
connection.lastMessageError ? h(LastMessageErrorSummary, {
className: "bxf-healthSummary",
error: connection.lastMessageError,
}) : null,
testNotice ? h("div", {
className: "bxf-healthSummary dim-cardFeedback",
role: "status",

View file

@ -1,4 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
export const QQ_RPC_CHANNEL = '/qq';
@ -94,6 +95,7 @@ function normalizeBot(value) {
summary: text(value.health?.summary, connected ? 'QQ WebSocket 长连接运行正常' : 'QQ 连接尚未就绪'),
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
},
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: isRecord(value.error) ? {
code: text(value.error.code, 'QQ_ACCOUNT_ERROR', 80),
message: text(value.error.message, 'QQ 连接尚未就绪'),

View file

@ -10,7 +10,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from '../../channel-card-meta.js';
import { installDingtalkStyles } from '../dingtalk/styles.js';
import {
QQ_ENDPOINTS,
@ -201,6 +205,10 @@ export function AccountCard({
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
account.lastMessageError ? h(LastMessageErrorSummary, {
className: 'ddt-summary',
error: account.lastMessageError,
}) : null,
feedback ? h('div', {
className: 'ddt-summary dim-cardFeedback',
role: 'status',

View file

@ -1,4 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
@ -68,6 +69,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
),
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
},
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: isRecord(value.error) ? {
code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
message: text(value.error.message, `${channel}连接尚未就绪`),

View file

@ -10,7 +10,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from '../../channel-card-meta.js';
const Button = React.forwardRef(function Button(
{ children, kind = 'secondary', className = '', ...props },
@ -121,6 +125,10 @@ export function createTokenChannelSettings(definition) {
disabled: Boolean(busy),
}, '移除接入')),
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
account.lastMessageError ? h(LastMessageErrorSummary, {
className: 'ddt-summary',
error: account.lastMessageError,
}) : null,
testNotice ? h('div', {
className: 'ddt-summary dim-cardFeedback',
role: 'status',

View file

@ -1,4 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
export const WECOM_RPC_CHANNEL = '/wecom';
@ -103,6 +104,7 @@ function normalizeBot(value) {
summary: text(value.health?.summary, connected ? '企业微信 WebSocket 长连接运行正常' : '企业微信连接尚未就绪'),
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
},
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: isRecord(value.error) ? {
code: text(value.error.code, 'WECOM_ACCOUNT_ERROR', 80),
message: text(value.error.message, '企业微信连接尚未就绪'),

View file

@ -10,7 +10,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from '../../channel-card-meta.js';
import { installDingtalkStyles } from '../dingtalk/styles.js';
import {
WECOM_ENDPOINTS,
@ -200,6 +204,10 @@ export function AccountCard({
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
account.lastMessageError ? h(LastMessageErrorSummary, {
className: 'ddt-summary',
error: account.lastMessageError,
}) : null,
feedback ? h('div', {
className: 'ddt-summary dim-cardFeedback',
role: 'status',

View file

@ -1,4 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
export const WEIXIN_RPC_CHANNEL = '/weixin';
export const WEIXIN_ENDPOINTS = Object.freeze({
@ -48,15 +49,6 @@ function normalizeTestMessage(value) {
return { sent: false, code };
}
function normalizeMessageError(value) {
if (!isRecord(value)) return null;
const code = string(value.code).slice(0, 64);
const reason = string(value.reason).slice(0, 128);
const message = string(value.message).slice(0, 500);
const at = timestamp(value.at);
return code && reason && message && at !== null ? { code, reason, message, at } : null;
}
export function unwrapRpcResult(result) {
if (!isRecord(result) || typeof result.ok !== 'boolean') {
throw new Error('微信服务返回了无法识别的响应');
@ -141,7 +133,7 @@ function normalizeBot(value) {
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
},
lastMessageError: normalizeMessageError(value.lastMessageError),
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: isRecord(value.error)
? {
code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),

View file

@ -22,7 +22,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from '../../channel-card-meta.js';
import { installWeixinStyles } from './styles.js';
export const name = 'weixin-settings';
@ -247,10 +251,10 @@ export function AccountCard({
busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
summary ? h('div', { className: 'dxw-summary dim-cardSummary' }, summary) : null,
account.lastMessageError ? h('div', {
className: 'dxw-summary dim-cardSummary',
role: 'status',
}, `最近一条消息处理失败:${account.lastMessageError.message}`) : null,
account.lastMessageError ? h(LastMessageErrorSummary, {
className: 'dxw-summary',
error: account.lastMessageError,
}) : null,
feedback ? h('div', {
className: 'dxw-summary dim-cardFeedback',
role: 'status',

View file

@ -1,4 +1,5 @@
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
import { normalizeLastMessageError } from '../../last-message-error.js';
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
@ -106,6 +107,7 @@ function normalizeBot(value) {
? 'WhatsApp Web 关联设备运行正常' : 'WhatsApp 连接尚未就绪'),
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
},
lastMessageError: normalizeLastMessageError(value.lastMessageError),
error: isRecord(value.error) ? {
code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
message: text(value.error.message, 'WhatsApp 连接尚未就绪'),

View file

@ -10,7 +10,11 @@ import {
EMPTY_AGENT_PRESET_CATALOG,
} from '../../agent-preset.js';
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
import {
BotStatusMeta,
ChannelListHeading,
LastMessageErrorSummary,
} from '../../channel-card-meta.js';
import { installDingtalkStyles } from '../dingtalk/styles.js';
import {
WHATSAPP_ENDPOINTS,
@ -345,6 +349,10 @@ export function WhatsappAccountCard({
className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
}, '移除接入')),
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
account.lastMessageError ? h(LastMessageErrorSummary, {
className: 'ddt-summary',
error: account.lastMessageError,
}) : null,
testNotice ? h('div', {
className: 'ddt-summary dim-cardFeedback',
role: 'status',