mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
fix: surface actionable channel message errors
This commit is contained in:
parent
b42fc411c2
commit
7cbed3aefb
59 changed files with 2405 additions and 607 deletions
|
|
@ -1,6 +1,20 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { h } from './i18n.js';
|
||||
import { h, isEnglish } from './i18n.js';
|
||||
|
||||
function messageErrorTime(value) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat(isEnglish() ? 'en-US' : 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(value));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function ChannelListHeading({ className = '', id, title, connectionLabel }) {
|
||||
const helpId = React.useId();
|
||||
|
|
@ -46,3 +60,24 @@ export function BotStatusMeta({
|
|||
h('span', null, '最近检查'),
|
||||
h('span', null, formatCheckedTime(lastCheckedAt))));
|
||||
}
|
||||
|
||||
export function LastMessageErrorSummary({ className = '', error }) {
|
||||
if (!error) return null;
|
||||
const occurredAt = messageErrorTime(error.at);
|
||||
return h('div', {
|
||||
className: `${className} dim-cardSummary`.trim(),
|
||||
role: 'status',
|
||||
},
|
||||
h('strong', null, '最近一条消息处理失败'),
|
||||
':',
|
||||
h('span', null, error.message),
|
||||
'(',
|
||||
h('span', null, '错误码'),
|
||||
` ${error.code} · `,
|
||||
h('span', null, '参考号'),
|
||||
` ${error.referenceId}`,
|
||||
occurredAt ? h(React.Fragment, null,
|
||||
' · ',
|
||||
h('time', { dateTime: new Date(error.at).toISOString() }, occurredAt)) : null,
|
||||
')');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 连接尚未就绪'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}连接尚未就绪`),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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, '企业微信连接尚未就绪'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 连接尚未就绪'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -114,6 +114,9 @@ const EN = Object.freeze({
|
|||
'消息通道': 'Message channel',
|
||||
'查看消息通道说明': 'View message channel details',
|
||||
'最近检查': 'Last checked',
|
||||
'最近一条消息处理失败': 'Latest message failed',
|
||||
'错误码': 'Code',
|
||||
'参考号': 'Reference',
|
||||
'当前工作区': 'Current workspace',
|
||||
'选择目录': 'Choose folder',
|
||||
'选择机器人工作区目录': 'Select bot workspace folder',
|
||||
|
|
|
|||
17
plugin-src/client/last-message-error.js
Normal file
17
plugin-src/client/last-message-error.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
function text(value, maxLength) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed.slice(0, maxLength) : null;
|
||||
}
|
||||
|
||||
export function normalizeLastMessageError(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
const code = text(value.code, 64);
|
||||
const reason = text(value.reason, 64);
|
||||
const message = text(value.message, 500);
|
||||
const referenceId = text(value.referenceId, 40);
|
||||
const at = Number.isFinite(value.at) ? value.at : null;
|
||||
return code && reason && message && referenceId && at !== null
|
||||
? { code, reason, message, referenceId, at }
|
||||
: null;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
normalizeAgentPresetId,
|
||||
} from '../../../../src/channels/shared/agent-preset.mjs';
|
||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||
import { publicMessageFailure } from '../../../../src/channels/shared/message-failure.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||
|
|
@ -281,6 +282,8 @@ function publicBotEntry(entry) {
|
|||
health: publicHealth(source, connected),
|
||||
};
|
||||
if (typeof source.workspace === 'string' && source.workspace) result.workspace = source.workspace;
|
||||
const lastMessageError = publicMessageFailure(source.lastMessageError);
|
||||
if (lastMessageError) result.lastMessageError = lastMessageError;
|
||||
const error = publicError(source.error);
|
||||
if (error) result.error = error;
|
||||
return result;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue