fix(wecomcs): handle service events and prevent duplicate replies

This commit is contained in:
Hyu
2026-10-10 16:25:36 +08:00
parent 36a167cd16
commit e8da99be63
19 changed files with 387 additions and 67 deletions
@@ -90,6 +90,26 @@ const DEBUG_EVENT_DEFINITIONS: Record<string, DebugEventDefinition> = {
messageField: 'text',
},
'message.deleted': deletion,
'wecomcs.enter_session': {
fields: [user],
defaults: { open_kfid: 'debug-kf', external_userid: 'debug-user', chat_id: 'debug-user|debug-kf', scene: 'debug', scene_param: '', welcome_code: '', wechat_channels: {} },
},
'wecomcs.msg_send_fail': {
fields: [user],
defaults: { open_kfid: 'debug-kf', external_userid: 'debug-user', chat_id: 'debug-user|debug-kf', fail_msgid: 'debug-message', fail_type: 4 },
},
'wecomcs.servicer_status_change': {
fields: [],
defaults: { open_kfid: 'debug-kf', servicer_userid: 'debug-servicer', status: 2, stop_type: 0 },
},
'wecomcs.session_status_change': {
fields: [user],
defaults: { open_kfid: 'debug-kf', external_userid: 'debug-user', chat_id: 'debug-user|debug-kf', change_type: 2, old_servicer_userid: 'debug-servicer-1', new_servicer_userid: 'debug-servicer-2', msg_code: '' },
},
'wecomcs.reject_customer_msg_switch_change': {
fields: [user],
defaults: { open_kfid: 'debug-kf', external_userid: 'debug-user', chat_id: 'debug-user|debug-kf', servicer_userid: 'debug-servicer', reject_switch: 1 },
},
'message.recalled': deletion,
'message.reaction': {
fields: [
@@ -49,7 +49,7 @@ function messageBody(value: unknown): unknown {
if (!value || typeof value !== 'object') return null;
const data = value as Record<string, unknown>;
if (['Image', 'File', 'Voice', 'At', 'AtAll', 'Quote'].includes(String(data.type))) return value;
for (const key of ['text', 'content', 'message_chain', 'message']) {
for (const key of ['text', 'content', 'message_chain', 'message', 'root']) {
if (data[key] != null) return messageBody(data[key]);
}
return null;
@@ -84,13 +84,19 @@ export default function ExecutionDetailSheet({
const [refresh, setRefresh] = useState(0);
const request = useRef(0);
const identity = `${workspace}:${row?.source}:${row?.id}`;
const requestedIdentity = useRef('');
const [loadedIdentity, setLoadedIdentity] = useState('');
const visible = loadedIdentity === identity ? detail : null;
const id = row?.id;
const source = row?.source;
useEffect(() => {
const version = ++request.current;
setDetail(null);
// Refresh the current trace in place so its scroll position and expanded
// disclosures survive polling. Only a different selection starts empty.
if (requestedIdentity.current !== identity) {
setDetail(null);
requestedIdentity.current = identity;
}
setError(false);
setSectionError(null);
setBusy(null);
@@ -188,11 +194,7 @@ export default function ExecutionDetailSheet({
'deliveries',
'conversation',
].includes(section);
const body = section === 'inputs'
? typeof item.content === 'string' ? item.content
: item.content && typeof item.content === 'object' && 'text' in item.content
? item.content.text : null
: messageBody(item.content);
const body = messageBody(item.content);
const title = content
? [
item.actor_name || item.actor_id,
@@ -367,13 +369,14 @@ export default function ExecutionDetailSheet({
size="icon"
disabled={loading}
aria-label={t('monitoring.refreshData')}
title={error ? t('monitoring.execution.detail.loadError') : t('monitoring.refreshData')}
onClick={() => setRefresh((value) => value + 1)}
>
<RefreshCw className="h-4 w-4" />
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''} ${error ? 'text-destructive' : ''}`} />
</Button>
</div>
{loading && <Skeleton className="h-36 w-full" />}
{error && (
{loading && !visible && <Skeleton className="h-36 w-full" />}
{error && !visible && (
<LoadErrorState title={t('monitoring.execution.detail.loadError')} onRetry={() => setRefresh((value) => value + 1)} />
)}
{record && (
@@ -421,7 +424,10 @@ export default function ExecutionDetailSheet({
</div>
{record.status_reason && !['stop', 'completed', 'success'].includes(record.status_reason) && (
<p className="rounded-md bg-muted p-3 text-sm">
{record.status_reason}
{record.status_reason.split('; ').map((reason) =>
reason === 'failure_reason_not_recorded'
? t('common.failureReasonNotRecorded') : reason,
).join('; ')}
</p>
)}
{mainSections.map((section) => renderSection(section, true))}
@@ -171,7 +171,7 @@ export default function ExecutionTable({
</TableCell>
<TableCell className="max-w-xs">
<div className="line-clamp-2 break-words text-sm">
{row.input_preview || '—'}
{row.input_preview || executionTargetLabel(row, t) || '—'}
</div>
<div className="mt-1 truncate text-xs text-muted-foreground">
{[row.bot_name, row.user_name || row.user_id]
+2 -2
View File
@@ -394,7 +394,7 @@ function MonitoringPageContent() {
)
}
>
{t('common.previous')}
{t('operationTrace.previousPage')}
</Button>
<span className="text-xs text-muted-foreground">
{executionOffset + 1}–
@@ -414,7 +414,7 @@ function MonitoringPageContent() {
)
}
>
{t('common.next')}
{t('operationTrace.nextPage')}
</Button>
</div>
)}
+12
View File
@@ -340,6 +340,7 @@ const enUS = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'The failure reason was not recorded. Check the bot logs or retry to capture more details.',
loadFailedTitle: 'Unable to load this content',
loadFailedHint: 'Please try again in a moment. If this continues, check your connection and service status.',
customValue: 'Custom',
@@ -896,6 +897,7 @@ const enUS = {
advancedEventValues: 'View all',
eventGroup: 'Group',
eventGroupNames: {
wecomcs: "WeCom Customer Service",
bot: 'Bot status',
feedback: 'Feedback',
friend: 'Friends',
@@ -997,6 +999,11 @@ const enUS = {
eventWildcard: 'All events',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "Customer entered conversation",
wecomcs_msg_send_fail: "Customer service message failed",
wecomcs_servicer_status_change: "Representative availability changed",
wecomcs_session_status_change: "Customer service conversation changed",
wecomcs_reject_customer_msg_switch_change: "Customer message rejection changed",
message_received: 'Message received',
message_edited: 'Message edited',
message_deleted: 'Message deleted',
@@ -1014,6 +1021,11 @@ const enUS = {
platform_specific: 'Platform-specific event',
},
eventDescriptions: {
wecomcs_enter_session: "A customer opened a service link; includes entry source and welcome code.",
wecomcs_msg_send_fail: "WeChat reported an outgoing message failure, with message ID and failure type.",
wecomcs_servicer_status_change: "A representative started, stopped or suspended service.",
wecomcs_session_status_change: "A representative accepted, transferred, closed or reopened a conversation.",
wecomcs_reject_customer_msg_switch_change: "A representative enabled or disabled rejection of customer messages.",
all: 'Matches every event received by this adapter.',
namespace: 'Matches all {{group}} events.',
namespace_bot:
+12
View File
@@ -343,6 +343,7 @@ const esES = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'No se registró el motivo del fallo. Consulta los registros del bot o vuelve a enviar el mensaje para obtener más detalles.',
loadFailedTitle: 'No se pudo cargar el contenido',
loadFailedHint: 'Inténtalo de nuevo. Si el problema persiste, comprueba la conexión y el estado del servicio.',
customValue: 'Personalizado',
@@ -914,6 +915,7 @@ const esES = {
advancedEventValues: 'Ver todos',
eventGroup: 'Grupo',
eventGroupNames: {
wecomcs: "Atención al cliente de WeCom",
bot: 'Estado del bot',
feedback: 'Comentarios',
friend: 'Amigos',
@@ -1016,6 +1018,11 @@ const esES = {
eventWildcard: 'Todos los eventos',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "Cliente entró en la conversación",
wecomcs_msg_send_fail: "Fallo de envío de atención al cliente",
wecomcs_servicer_status_change: "Disponibilidad del agente modificada",
wecomcs_session_status_change: "Estado de conversación modificado",
wecomcs_reject_customer_msg_switch_change: "Rechazo de mensajes modificado",
message_received: 'Mensaje recibido',
message_edited: 'Mensaje editado',
message_deleted: 'Mensaje eliminado',
@@ -1033,6 +1040,11 @@ const esES = {
platform_specific: 'Evento específico de la plataforma',
},
eventDescriptions: {
wecomcs_enter_session: "Un cliente abrió el enlace de atención; incluye origen y código de bienvenida.",
wecomcs_msg_send_fail: "WeChat notificó un fallo de envío con el ID y el tipo de error.",
wecomcs_servicer_status_change: "Un agente inició, detuvo o suspendió la atención.",
wecomcs_session_status_change: "Un agente aceptó, transfirió, cerró o reabrió la conversación.",
wecomcs_reject_customer_msg_switch_change: "Un agente activó o desactivó el rechazo de mensajes del cliente.",
all: 'Coincide con todos los eventos recibidos por este adaptador.',
namespace: 'Coincide con todos los eventos de {{group}}.',
namespace_bot:
+12
View File
@@ -339,6 +339,7 @@ const jaJP = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'この記録には失敗の理由が保存されていません。ボットのログを確認するか、再送信して詳細を確認してください。',
loadFailedTitle: '読み込めませんでした',
loadFailedHint: 'しばらくしてから再試行してください。解決しない場合は、接続とサービスの状態を確認してください。',
customValue: 'カスタム',
@@ -901,6 +902,7 @@ const jaJP = {
advancedEventValues: 'すべて表示',
eventGroup: 'グループ',
eventGroupNames: {
wecomcs: "WeCom カスタマーサービス",
bot: 'ボットの状態',
feedback: 'フィードバック',
friend: '友だち',
@@ -1002,6 +1004,11 @@ const jaJP = {
eventWildcard: 'すべてのイベント',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "顧客が会話に参加",
wecomcs_msg_send_fail: "カスタマーサービスの送信失敗",
wecomcs_servicer_status_change: "担当者の受付状態変更",
wecomcs_session_status_change: "カスタマーサービスの会話状態変更",
wecomcs_reject_customer_msg_switch_change: "顧客メッセージの受信拒否設定変更",
message_received: 'メッセージ受信',
message_edited: 'メッセージ編集',
message_deleted: 'メッセージ削除',
@@ -1019,6 +1026,11 @@ const jaJP = {
platform_specific: 'プラットフォーム固有イベント',
},
eventDescriptions: {
wecomcs_enter_session: "顧客がリンクから会話を開きました。流入元と歓迎メッセージ用コードを含みます。",
wecomcs_msg_send_fail: "WeChat が送信失敗を通知しました。メッセージ ID と失敗種別を含みます。",
wecomcs_servicer_status_change: "担当者が受付を開始、停止、または一時停止しました。",
wecomcs_session_status_change: "担当者が会話を受付、転送、終了、または再開しました。",
wecomcs_reject_customer_msg_switch_change: "担当者が顧客メッセージの受信拒否を有効または無効にしました。",
all: 'このアダプターが受信するすべてのイベントに一致します。',
namespace: 'すべての{{group}}イベントに一致します。',
namespace_bot:
+12
View File
@@ -342,6 +342,7 @@ const ruRU = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'Причина сбоя не сохранена. Проверьте журнал бота или отправьте сообщение повторно для получения подробностей.',
loadFailedTitle: 'Не удалось загрузить данные',
loadFailedHint: 'Повторите попытку позже. Если проблема остаётся, проверьте подключение и состояние сервиса.',
customValue: 'Свой вариант',
@@ -909,6 +910,7 @@ const ruRU = {
advancedEventValues: 'Показать все',
eventGroup: 'Группа',
eventGroupNames: {
wecomcs: "Поддержка WeCom",
bot: 'Состояние бота',
feedback: 'Обратная связь',
friend: 'Друзья',
@@ -1012,6 +1014,11 @@ const ruRU = {
eventWildcard: 'Все события',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "Клиент вошёл в диалог",
wecomcs_msg_send_fail: "Ошибка отправки сообщения поддержки",
wecomcs_servicer_status_change: "Статус оператора изменён",
wecomcs_session_status_change: "Статус диалога поддержки изменён",
wecomcs_reject_customer_msg_switch_change: "Приём сообщений клиента изменён",
message_received: 'Получено сообщение',
message_edited: 'Сообщение изменено',
message_deleted: 'Сообщение удалено',
@@ -1029,6 +1036,11 @@ const ruRU = {
platform_specific: 'Событие конкретной платформы',
},
eventDescriptions: {
wecomcs_enter_session: "Клиент открыл ссылку поддержки; событие содержит источник и код приветствия.",
wecomcs_msg_send_fail: "WeChat сообщил об ошибке отправки с ID сообщения и типом ошибки.",
wecomcs_servicer_status_change: "Оператор начал, остановил или приостановил приём обращений.",
wecomcs_session_status_change: "Оператор принял, передал, завершил или возобновил диалог.",
wecomcs_reject_customer_msg_switch_change: "Оператор включил или отключил отказ в приёме сообщений клиента.",
all: 'Соответствует всем событиям, полученным адаптером.',
namespace: 'Соответствует всем событиям {{group}}.',
namespace_bot:
+12
View File
@@ -335,6 +335,7 @@ const thTH = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'ไม่ได้บันทึกสาเหตุของข้อผิดพลาด โปรดตรวจสอบบันทึกของบอตหรือส่งข้อความอีกครั้งเพื่อดูรายละเอียด',
loadFailedTitle: 'ไม่สามารถโหลดเนื้อหาได้',
loadFailedHint: 'โปรดลองอีกครั้ง หากยังโหลดไม่ได้ ให้ตรวจสอบการเชื่อมต่อและสถานะบริการ',
customValue: 'กำหนดเอง',
@@ -883,6 +884,7 @@ const thTH = {
advancedEventValues: 'ดูทั้งหมด',
eventGroup: 'กลุ่ม',
eventGroupNames: {
wecomcs: "บริการลูกค้า WeCom",
bot: 'สถานะบอท',
feedback: 'ข้อเสนอแนะ',
friend: 'เพื่อน',
@@ -980,6 +982,11 @@ const thTH = {
eventWildcard: 'ทุกเหตุการณ์',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "ลูกค้าเข้าสู่การสนทนา",
wecomcs_msg_send_fail: "ส่งข้อความบริการลูกค้าไม่สำเร็จ",
wecomcs_servicer_status_change: "สถานะเจ้าหน้าที่เปลี่ยนแปลง",
wecomcs_session_status_change: "สถานะการสนทนาบริการลูกค้าเปลี่ยนแปลง",
wecomcs_reject_customer_msg_switch_change: "การปฏิเสธข้อความลูกค้าเปลี่ยนแปลง",
message_received: 'ได้รับข้อความ',
message_edited: 'แก้ไขข้อความ',
message_deleted: 'ลบข้อความ',
@@ -997,6 +1004,11 @@ const thTH = {
platform_specific: 'เหตุการณ์เฉพาะแพลตฟอร์ม',
},
eventDescriptions: {
wecomcs_enter_session: "ลูกค้าเปิดลิงก์บริการ พร้อมข้อมูลแหล่งที่มาและรหัสต้อนรับ",
wecomcs_msg_send_fail: "WeChat แจ้งการส่งล้มเหลว พร้อม ID ข้อความและประเภทข้อผิดพลาด",
wecomcs_servicer_status_change: "เจ้าหน้าที่เริ่ม หยุด หรือพักการให้บริการ",
wecomcs_session_status_change: "เจ้าหน้าที่รับ โอน ปิด หรือเปิดการสนทนาอีกครั้ง",
wecomcs_reject_customer_msg_switch_change: "เจ้าหน้าที่เปิดหรือปิดการปฏิเสธข้อความลูกค้า",
all: 'ตรงกับทุกเหตุการณ์ที่อะแดปเตอร์นี้ได้รับ',
namespace: 'ตรงกับทุกเหตุการณ์ของ {{group}}',
namespace_bot:
+12
View File
@@ -339,6 +339,7 @@ const viVN = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'Chưa lưu nguyên nhân lỗi. Hãy kiểm tra nhật ký bot hoặc gửi lại tin nhắn để biết thêm chi tiết.',
loadFailedTitle: 'Không thể tải nội dung',
loadFailedHint: 'Vui lòng thử lại sau. Nếu vẫn không tải được, hãy kiểm tra kết nối và trạng thái dịch vụ.',
customValue: 'Tùy chỉnh',
@@ -898,6 +899,7 @@ const viVN = {
advancedEventValues: 'Xem tất cả',
eventGroup: 'Nhóm',
eventGroupNames: {
wecomcs: "Dịch vụ khách hàng WeCom",
bot: 'Trạng thái bot',
feedback: 'Phản hồi',
friend: 'Bạn bè',
@@ -997,6 +999,11 @@ const viVN = {
eventWildcard: 'Tất cả sự kiện',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "Khách hàng vào hội thoại",
wecomcs_msg_send_fail: "Gửi tin nhắn hỗ trợ thất bại",
wecomcs_servicer_status_change: "Trạng thái nhân viên thay đổi",
wecomcs_session_status_change: "Trạng thái hội thoại hỗ trợ thay đổi",
wecomcs_reject_customer_msg_switch_change: "Cài đặt từ chối tin nhắn thay đổi",
message_received: 'Nhận tin nhắn',
message_edited: 'Tin nhắn được chỉnh sửa',
message_deleted: 'Tin nhắn bị xóa',
@@ -1014,6 +1021,11 @@ const viVN = {
platform_specific: 'Sự kiện riêng của nền tảng',
},
eventDescriptions: {
wecomcs_enter_session: "Khách hàng mở liên kết hỗ trợ; gồm nguồn truy cập và mã chào mừng.",
wecomcs_msg_send_fail: "WeChat thông báo gửi thất bại, gồm ID tin nhắn và loại lỗi.",
wecomcs_servicer_status_change: "Nhân viên bắt đầu, dừng hoặc tạm dừng tiếp nhận.",
wecomcs_session_status_change: "Nhân viên tiếp nhận, chuyển, kết thúc hoặc mở lại hội thoại.",
wecomcs_reject_customer_msg_switch_change: "Nhân viên bật hoặc tắt từ chối tin nhắn khách hàng.",
all: 'Khớp mọi sự kiện bộ chuyển đổi này nhận được.',
namespace: 'Khớp mọi sự kiện {{group}}.',
namespace_bot:
+12
View File
@@ -319,6 +319,7 @@ const zhHans = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: '这条历史记录未保存失败原因,请查看机器人日志,或重新发送消息获取详细错误。',
loadFailedTitle: '暂时无法加载',
loadFailedHint: '请稍后重试。如果仍无法加载,请检查网络连接与服务状态。',
customValue: '自定义',
@@ -843,6 +844,7 @@ const zhHans = {
advancedEventValues: '查看全部',
eventGroup: '事件组',
eventGroupNames: {
wecomcs: "企业微信客服",
bot: '机器人状态',
feedback: '反馈',
friend: '好友',
@@ -940,6 +942,11 @@ const zhHans = {
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "客户进入会话",
wecomcs_msg_send_fail: "客服消息发送失败",
wecomcs_servicer_status_change: "接待人员状态变更",
wecomcs_session_status_change: "客服会话状态变更",
wecomcs_reject_customer_msg_switch_change: "拒收客户消息设置变更",
message_received: '收到消息',
message_edited: '消息被编辑',
message_deleted: '消息被删除',
@@ -957,6 +964,11 @@ const zhHans = {
platform_specific: '平台特定事件',
},
eventDescriptions: {
wecomcs_enter_session: "客户通过客服链接进入会话,可获取来源和欢迎语凭据。",
wecomcs_msg_send_fail: "微信通知客服消息发送失败,包含消息 ID 和失败类型。",
wecomcs_servicer_status_change: "接待人员开始、停止接待或暂时挂起。",
wecomcs_session_status_change: "人工客服接入、转接、结束或重新接入会话。",
wecomcs_reject_customer_msg_switch_change: "接待人员开启或取消拒收客户消息。",
all: '匹配此适配器收到的全部事件。',
namespace: '匹配所有{{group}}事件。',
namespace_bot: '匹配机器人入群、退群、禁言和解除禁言等状态事件。',
+12
View File
@@ -316,6 +316,7 @@ const zhHant = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: '這筆歷史記錄未儲存失敗原因,請查看機器人日誌,或重新傳送訊息取得詳細錯誤。',
loadFailedTitle: '暫時無法載入',
loadFailedHint: '請稍後重試。如果仍無法載入,請檢查網路連線與服務狀態。',
customValue: '自訂',
@@ -842,6 +843,7 @@ const zhHant = {
advancedEventValues: '查看全部',
eventGroup: '事件組',
eventGroupNames: {
wecomcs: "企業微信客服",
bot: '機器人狀態',
feedback: '反饋',
friend: '好友',
@@ -939,6 +941,11 @@ const zhHant = {
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecomcs_enter_session: "客戶進入會話",
wecomcs_msg_send_fail: "客服訊息傳送失敗",
wecomcs_servicer_status_change: "接待人員狀態變更",
wecomcs_session_status_change: "客服會話狀態變更",
wecomcs_reject_customer_msg_switch_change: "拒收客戶訊息設定變更",
message_received: '收到訊息',
message_edited: '訊息已編輯',
message_deleted: '訊息已刪除',
@@ -956,6 +963,11 @@ const zhHant = {
platform_specific: '平台特定事件',
},
eventDescriptions: {
wecomcs_enter_session: "客戶透過客服連結進入會話,可取得來源和歡迎語憑證。",
wecomcs_msg_send_fail: "微信通知客服訊息傳送失敗,包含訊息 ID 和失敗類型。",
wecomcs_servicer_status_change: "接待人員開始、停止接待或暫時掛起。",
wecomcs_session_status_change: "人工客服接入、轉接、結束或重新接入會話。",
wecomcs_reject_customer_msg_switch_change: "接待人員開啟或取消拒收客戶訊息。",
all: '匹配此適配器收到的全部事件。',
namespace: '匹配所有{{group}}事件。',
namespace_bot: '匹配機器人入羣、退羣、禁言和解除禁言等狀態事件。',