mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 07:55:18 +08:00
fix(wecomcs): handle service events and prevent duplicate replies
This commit is contained in:
@@ -5,6 +5,7 @@ import base64
|
||||
import binascii
|
||||
import contextvars
|
||||
import functools
|
||||
import hashlib
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
@@ -119,6 +120,10 @@ class WecomCSClient:
|
||||
'example': [],
|
||||
}
|
||||
self._http_client: httpx.AsyncClient | None = None
|
||||
self._delivery_tasks: set[asyncio.Task] = set()
|
||||
self._received_messages: dict[str, float] = {}
|
||||
self._sync_lock = asyncio.Lock()
|
||||
self._sync_cursors: dict[str, str] = {}
|
||||
|
||||
@asynccontextmanager
|
||||
async def _http_client_context(self):
|
||||
@@ -127,6 +132,10 @@ class WecomCSClient:
|
||||
yield self._http_client
|
||||
|
||||
async def close(self) -> None:
|
||||
for task in self._delivery_tasks:
|
||||
task.cancel()
|
||||
if self._delivery_tasks:
|
||||
await asyncio.gather(*self._delivery_tasks, return_exceptions=True)
|
||||
if self._http_client is not None:
|
||||
await self._http_client.aclose()
|
||||
self._http_client = None
|
||||
@@ -170,47 +179,44 @@ class WecomCSClient:
|
||||
else:
|
||||
raise Exception(f'未获取access token: {data}')
|
||||
|
||||
@_bounded_token_retry
|
||||
async def get_detailed_message_list(self, xml_msg: str):
|
||||
# 在本方法中解析消息,并且获得消息的具体内容
|
||||
if isinstance(xml_msg, bytes):
|
||||
xml_msg = xml_msg.decode('utf-8')
|
||||
"""Drain all callback pages, including non-message events and empty pages."""
|
||||
root = await asyncio.to_thread(ET.fromstring, xml_msg)
|
||||
token = root.find('Token').text
|
||||
open_kfid = root.find('OpenKfId').text
|
||||
|
||||
# if open_kfid in self.openkfid_list:
|
||||
# return None
|
||||
# else:
|
||||
# self.openkfid_list.append(open_kfid)
|
||||
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
|
||||
url = self.base_url + '/kf/sync_msg?access_token=' + self.access_token
|
||||
async with self._http_client_context() as client:
|
||||
params = {
|
||||
'token': token,
|
||||
'voice_format': 0,
|
||||
'open_kfid': open_kfid,
|
||||
}
|
||||
response = await client.post(url, json=params)
|
||||
data = await httpclient.parse_json_response(response)
|
||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.get_detailed_message_list(xml_msg)
|
||||
if data['errcode'] != 0:
|
||||
raise Exception('Failed to get message')
|
||||
|
||||
last_msg_data = data['msg_list'][-1]
|
||||
open_kfid = last_msg_data.get('open_kfid')
|
||||
# 进行获取图片操作
|
||||
if last_msg_data.get('msgtype') == 'image':
|
||||
media_id = last_msg_data.get('image').get('media_id')
|
||||
picurl = await self.get_pic_url(media_id)
|
||||
last_msg_data['picurl'] = picurl
|
||||
# await self.change_service_status(userid=external_userid,openkfid=open_kfid,servicer=servicer)
|
||||
return last_msg_data
|
||||
token = root.findtext('Token')
|
||||
open_kfid = root.findtext('OpenKfId')
|
||||
if not token or not open_kfid:
|
||||
raise ValueError('WeCom customer-service callback is missing Token or OpenKfId')
|
||||
async with self._sync_lock:
|
||||
cursor = self._sync_cursors.get(open_kfid)
|
||||
messages = []
|
||||
refreshed = False
|
||||
while True:
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
params = {'token': token, 'voice_format': 0, 'open_kfid': open_kfid, 'limit': 1000}
|
||||
if cursor:
|
||||
params['cursor'] = cursor
|
||||
async with self._http_client_context() as client:
|
||||
response = await client.post(
|
||||
self.base_url + '/kf/sync_msg', params={'access_token': self.access_token}, json=params,
|
||||
)
|
||||
data = await httpclient.parse_json_response(response)
|
||||
if data.get('errcode') in (40014, 42001) and not refreshed:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
refreshed = True
|
||||
continue
|
||||
if data.get('errcode') != 0:
|
||||
raise RuntimeError(f"WeCom sync_msg failed (errcode={data.get('errcode')})")
|
||||
messages.extend(data.get('msg_list') or [])
|
||||
next_cursor = data.get('next_cursor')
|
||||
if data.get('has_more'):
|
||||
if not next_cursor or next_cursor == cursor:
|
||||
raise RuntimeError('WeCom sync_msg returned a non-advancing cursor')
|
||||
cursor = next_cursor
|
||||
continue
|
||||
if next_cursor:
|
||||
self._sync_cursors[open_kfid] = next_cursor
|
||||
return messages
|
||||
|
||||
@_bounded_token_retry
|
||||
async def change_service_status(self, userid: str, openkfid: str, servicer: str):
|
||||
@@ -292,9 +298,36 @@ class WecomCSClient:
|
||||
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
||||
if data['errcode'] != 0:
|
||||
await self.logger.error(f'发送消息失败:{data}')
|
||||
if data['errcode'] == 95002:
|
||||
raise ValueError(
|
||||
'WeCom CS rejected the reply (95002): the customer must send a message first, '
|
||||
'and ordinary replies must be sent within 48 hours. Entering a session alone '
|
||||
'does not open this window; welcome replies require the event reply API.'
|
||||
)
|
||||
raise Exception(f'Failed to send message: {data}')
|
||||
return data
|
||||
|
||||
@_bounded_token_retry
|
||||
async def send_event_text(self, code: str, msgid: str, content: str):
|
||||
"""Send a single event response using its short-lived, one-time code."""
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
url = f'{self.base_url}/kf/send_msg_on_event?access_token={self.access_token}'
|
||||
payload = {'code': code, 'msgid': msgid, 'msgtype': 'text', 'text': {'content': content}}
|
||||
async with self._http_client_context() as client:
|
||||
response = await client.post(url, json=payload)
|
||||
data = await httpclient.parse_json_response(response)
|
||||
if data.get('errcode') in (40014, 42001):
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.send_event_text(code, msgid, content)
|
||||
if data.get('errcode') != 0:
|
||||
raise ValueError(
|
||||
f'WeCom CS event reply failed (errcode={data.get("errcode")}): {data.get("errmsg")}. '
|
||||
'The event code is single-use and must match the current session state; '
|
||||
'welcome and session-end codes expire after 20 seconds. Do not retry an expired or used code.'
|
||||
)
|
||||
return data
|
||||
|
||||
@_bounded_token_retry
|
||||
async def send_image_msg(self, open_kfid: str, external_userid: str, msgid: str, media_id: str):
|
||||
if not await self.check_access_token():
|
||||
@@ -375,12 +408,11 @@ class WecomCSClient:
|
||||
if ret != 0:
|
||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||
|
||||
# 解析消息并处理
|
||||
message_data = await self.get_detailed_message_list(xml_msg)
|
||||
if message_data is not None:
|
||||
event = WecomCSEvent.from_payload(message_data)
|
||||
if event:
|
||||
await self._handle_message(event)
|
||||
# Acknowledge after verification; API fetching and processing must
|
||||
# survive a callback disconnect.
|
||||
task = asyncio.create_task(self._process_callback(xml_msg))
|
||||
self._delivery_tasks.add(task)
|
||||
task.add_done_callback(self._delivery_tasks.discard)
|
||||
|
||||
return 'success'
|
||||
except Exception as e:
|
||||
@@ -390,6 +422,46 @@ class WecomCSClient:
|
||||
traceback.print_exc()
|
||||
return f'Error processing request: {str(e)}', 400
|
||||
|
||||
async def _process_callback(self, xml_msg):
|
||||
try:
|
||||
messages = await self.get_detailed_message_list(xml_msg)
|
||||
for message_data in messages:
|
||||
try:
|
||||
await self._process_synced_message(message_data)
|
||||
except Exception:
|
||||
if self.logger:
|
||||
await self.logger.error(f'WeCom customer-service event failed: {traceback.format_exc()}')
|
||||
|
||||
except Exception:
|
||||
if self.logger:
|
||||
await self.logger.error(f'WeCom customer-service delivery failed: {traceback.format_exc()}')
|
||||
|
||||
async def _process_synced_message(self, message_data):
|
||||
# Agent/API and human replies must not become new customer input.
|
||||
if message_data.get('msgtype') != 'event' and message_data.get('origin', 3) != 3:
|
||||
return
|
||||
if message_data.get('msgtype') == 'image':
|
||||
media_id = (message_data.get('image') or {}).get('media_id')
|
||||
if media_id:
|
||||
message_data['picurl'] = await self.get_pic_url(media_id)
|
||||
event = WecomCSEvent.from_payload(message_data)
|
||||
if event:
|
||||
# A callback disconnect must not cancel processor execution.
|
||||
# Retries can return the same message from sync_msg.
|
||||
now = time.monotonic()
|
||||
self._received_messages = {
|
||||
key: seen for key, seen in self._received_messages.items() if now - seen < 4 * 86400
|
||||
}
|
||||
message_id = message_data.get('msgid') or hashlib.sha256(
|
||||
json.dumps(message_data, sort_keys=True, ensure_ascii=False, separators=(',', ':')).encode()
|
||||
).hexdigest()
|
||||
if not message_id or message_id not in self._received_messages:
|
||||
if message_id:
|
||||
if len(self._received_messages) >= 4096:
|
||||
self._received_messages.pop(next(iter(self._received_messages)))
|
||||
self._received_messages[message_id] = now
|
||||
await self._handle_message(event)
|
||||
|
||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||
"""
|
||||
启动 Quart 应用。
|
||||
|
||||
@@ -749,6 +749,14 @@ async def execute_platform_tool(
|
||||
message_id=delivery['reply_target']['message_id'],
|
||||
)
|
||||
return await bot.adapter.reply_message(source, message)
|
||||
# Some platforms require a one-time event credential instead of an
|
||||
# ordinary conversation send. Only pass the Host-frozen event context.
|
||||
event_reply_handler = getattr(bot.adapter, 'reply_to_event', None)
|
||||
if definition.name == 'event_reply' and callable(event_reply_handler):
|
||||
async def api_func(target_type, target_id, message):
|
||||
return await event_reply_handler(
|
||||
context=context, target_type=target_type, target_id=target_id, message=message
|
||||
)
|
||||
if not callable(api_func):
|
||||
raise ValueError(f'Platform API {definition.api} is declared but not implemented')
|
||||
if definition.api == 'send_message':
|
||||
|
||||
@@ -37,6 +37,22 @@ def event_content(content, event_data):
|
||||
return {'text': event_summary(event_data), 'event': event_data, 'input': content}
|
||||
|
||||
|
||||
def event_preview(payload, fallback='', event_type=''):
|
||||
"""Never use serialized event metadata as a human-readable list preview."""
|
||||
if event_type.startswith('wecomcs.'):
|
||||
return ''
|
||||
if isinstance(payload, dict):
|
||||
if payload.get('type'):
|
||||
if str(payload['type']).startswith('wecomcs.'):
|
||||
return ''
|
||||
return event_summary(payload)[:1000]
|
||||
if isinstance(payload.get('text'), str):
|
||||
return payload['text'][:1000]
|
||||
if fallback and not isinstance(parsed(fallback), (dict, list)):
|
||||
return fallback[:1000]
|
||||
return ''
|
||||
|
||||
|
||||
def event_item(row):
|
||||
metadata = parsed(row.metadata_json, {})
|
||||
return {
|
||||
@@ -240,14 +256,13 @@ class ExecutionDetailsMixin:
|
||||
):
|
||||
"""Persist entry facts even when routing never creates a processor run."""
|
||||
from ....agent.runner.event_log_store import EventLogStore
|
||||
from .monitoring import _message_preview
|
||||
|
||||
workspace = self._require_write_context(context)
|
||||
store = EventLogStore(self.ap.persistence_mgr.get_db_engine())
|
||||
# Reuse the message sanitizer (including inline-media handling) instead
|
||||
# of storing an unbounded adapter object in the event journal.
|
||||
content = parsed(self._sanitize_message_content(json.dumps(payload, ensure_ascii=False, default=str)), {})
|
||||
summary = _message_preview(json.dumps(content, ensure_ascii=False), 1000) if content else event_type
|
||||
summary = event_preview(content, event_type=event_type)
|
||||
await store.append_event(
|
||||
event_id=event_id,
|
||||
event_type=event_type,
|
||||
@@ -266,7 +281,7 @@ class ExecutionDetailsMixin:
|
||||
safe_routes = []
|
||||
for route in routes:
|
||||
if isinstance(route, BaseException):
|
||||
safe_routes.append({'status': 'failed', 'reason': str(route)})
|
||||
safe_routes.append({'status': 'failed', 'reason': str(route) or type(route).__name__})
|
||||
elif isinstance(route, dict):
|
||||
safe_routes.append(
|
||||
{
|
||||
@@ -362,7 +377,7 @@ class ExecutionDetailsMixin:
|
||||
'status': status,
|
||||
'status_group': status,
|
||||
'title': row.event_type,
|
||||
'input_preview': row.input_summary or '',
|
||||
'input_preview': event_preview(parsed(row.input_json), row.input_summary, row.event_type),
|
||||
'target_kind': 'event',
|
||||
'target_id': None,
|
||||
'target_name': None,
|
||||
@@ -385,7 +400,11 @@ class ExecutionDetailsMixin:
|
||||
'runner_id': None,
|
||||
'debug': row.source == 'debug',
|
||||
'has_error': status == 'failed',
|
||||
'status_reason': metadata.get('reason'),
|
||||
'status_reason': metadata.get('reason') or '; '.join(
|
||||
route.get('reason') or route.get('failure_code') or 'failure_reason_not_recorded'
|
||||
for route in metadata.get('routes', [])
|
||||
if isinstance(route, dict) and route.get('status') == 'failed'
|
||||
) or None,
|
||||
}
|
||||
|
||||
async def enrich_execution_rows(self, workspace, items):
|
||||
@@ -449,7 +468,7 @@ class ExecutionDetailsMixin:
|
||||
meta = parsed(event.metadata_json, {})
|
||||
item.update(
|
||||
event_type=event.event_type,
|
||||
input_preview=event.input_summary or item.get('input_preview') or '',
|
||||
input_preview=event_preview(parsed(event.input_json), event.input_summary, event.event_type),
|
||||
user_id=event.actor_id,
|
||||
user_name=event.actor_name,
|
||||
platform=meta.get('platform') or event.source,
|
||||
|
||||
@@ -79,6 +79,12 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
def get_supported_events(self) -> list[str]:
|
||||
return [
|
||||
'message.received',
|
||||
'message.deleted',
|
||||
'wecomcs.enter_session',
|
||||
'wecomcs.msg_send_fail',
|
||||
'wecomcs.servicer_status_change',
|
||||
'wecomcs.session_status_change',
|
||||
'wecomcs.reject_customer_msg_switch_change',
|
||||
'platform.specific',
|
||||
]
|
||||
|
||||
@@ -110,6 +116,36 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
)
|
||||
return platform_events.MessageResult(raw={'results': raw_results})
|
||||
|
||||
async def reply_to_event(
|
||||
self, *, context: dict, target_type: str, target_id: str, message: platform_message.MessageChain
|
||||
) -> platform_events.MessageResult:
|
||||
"""Use event credentials for welcome and session-status replies."""
|
||||
event_type = context.get('event_type')
|
||||
if event_type not in ('wecomcs.enter_session', 'wecomcs.session_status_change'):
|
||||
return await self.send_message(target_type, target_id, message)
|
||||
data = context.get('data') or {}
|
||||
code_key = 'welcome_code' if event_type == 'wecomcs.enter_session' else 'msg_code'
|
||||
code = data.get(code_key) or (data.get('data') or {}).get(code_key)
|
||||
if not code:
|
||||
raise ValueError(
|
||||
f'WeCom CS did not provide {code_key} for this event; an event response is not available. '
|
||||
'Entering a session does not open the ordinary messaging window. '
|
||||
'Ask the customer to send a message before sending an ordinary reply.'
|
||||
)
|
||||
timestamp = data.get('timestamp')
|
||||
if event_type == 'wecomcs.enter_session' and timestamp and time.time() - float(timestamp) >= 20:
|
||||
raise ValueError(
|
||||
'WeCom CS welcome reply expired: welcome_code is valid for only 20 seconds after entry. '
|
||||
'Use a fast, fixed welcome response; do not wait for a long AI run or retry this event.'
|
||||
)
|
||||
if not message.root or any(not isinstance(item, platform_message.Plain) for item in message.root):
|
||||
raise ValueError('WeCom CS event replies currently support a single text message only')
|
||||
text = ''.join(item.text for item in message.root)
|
||||
if not text.strip():
|
||||
raise ValueError('WeCom CS event reply text must not be empty')
|
||||
result = await self.bot.send_event_text(str(code), self._make_outbound_msgid(), text)
|
||||
return platform_events.MessageResult(message_id=result.get('msgid'), raw=result)
|
||||
|
||||
async def reply_message(
|
||||
self,
|
||||
message_source: platform_events.MessageEvent,
|
||||
@@ -183,7 +219,7 @@ class WecomCSAdapter(WecomCSAPIMixin, abstract_platform_adapter.AbstractPlatform
|
||||
async def on_message(event: WecomCSEvent):
|
||||
await self._handle_native_event(event)
|
||||
|
||||
for msg_type in ('text', 'image', 'file', 'voice'):
|
||||
for msg_type in ('text', 'image', 'file', 'voice', 'event'):
|
||||
self.bot.on_message(msg_type)(on_message)
|
||||
|
||||
async def _handle_native_event(self, event: WecomCSEvent):
|
||||
|
||||
@@ -29,6 +29,39 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
async def target2yiri(event: WecomCSEvent, bot: WecomCSClient | None = None) -> platform_events.Event | None:
|
||||
if event.type in {'text', 'image', 'file', 'voice'}:
|
||||
return await WecomCSEventConverter.message_to_eba(event, bot)
|
||||
if event.type == 'event':
|
||||
data = event.get('event') or {}
|
||||
event_type = data.get('event_type', 'unknown')
|
||||
customer_id = data.get('external_userid') or ''
|
||||
common = {
|
||||
'adapter_name': ADAPTER_NAME,
|
||||
'timestamp': float(event.timestamp or 0),
|
||||
'source_platform_object': event,
|
||||
'chat_id': make_private_chat_id(customer_id, data.get('open_kfid')) if customer_id else '',
|
||||
}
|
||||
if event_type in {'user_recall_msg', 'servicer_recall_msg'}:
|
||||
operator_id = data.get('servicer_userid') if event_type == 'servicer_recall_msg' else customer_id
|
||||
return platform_events.MessageDeletedEvent(
|
||||
message_id=data.get('recall_msgid') or '',
|
||||
operator=platform_entities.User(id=operator_id) if operator_id else None,
|
||||
**common,
|
||||
)
|
||||
event_classes = {
|
||||
'enter_session': platform_events.WecomCSEnterSessionEvent,
|
||||
'msg_send_fail': platform_events.WecomCSMessageSendFailedEvent,
|
||||
'servicer_status_change': platform_events.WecomCSServicerStatusChangedEvent,
|
||||
'session_status_change': platform_events.WecomCSSessionStatusChangedEvent,
|
||||
'reject_customer_msg_switch_change': platform_events.WecomCSRejectCustomerMessageChangedEvent,
|
||||
}
|
||||
event_class = event_classes.get(event_type)
|
||||
if event_class:
|
||||
fields = {key: value for key, value in data.items() if key in event_class.model_fields and value is not None}
|
||||
fields.update(common)
|
||||
fields['data'] = dict(data)
|
||||
actor_id = data.get('servicer_userid') or customer_id
|
||||
fields['user'] = platform_entities.User(id=actor_id) if actor_id else None
|
||||
return event_class(**fields)
|
||||
return WecomCSEventConverter.platform_specific(event, f'wecomcs.{event_type}')
|
||||
return WecomCSEventConverter.platform_specific(event, f'wecomcs.{event.type or "unknown"}')
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -93,6 +93,12 @@ spec:
|
||||
|
||||
supported_events:
|
||||
- message.received
|
||||
- message.deleted
|
||||
- wecomcs.enter_session
|
||||
- wecomcs.msg_send_fail
|
||||
- wecomcs.servicer_status_change
|
||||
- wecomcs.session_status_change
|
||||
- wecomcs.reject_customer_msg_switch_change
|
||||
- platform.specific
|
||||
|
||||
supported_apis:
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
@@ -85,6 +86,7 @@ class RuntimeBot:
|
||||
):
|
||||
if not isinstance(execution_context, ExecutionContext):
|
||||
raise WorkspaceRequiredError('RuntimeBot requires an ExecutionContext')
|
||||
self._ingress_dedup_lock = asyncio.Lock()
|
||||
if not execution_context.instance_uuid.strip() or not execution_context.workspace_uuid.strip():
|
||||
raise WorkspaceRequiredError('RuntimeBot requires an instance and Workspace')
|
||||
if execution_context.placement_generation <= 0:
|
||||
@@ -888,6 +890,18 @@ class RuntimeBot:
|
||||
@staticmethod
|
||||
def _platform_event_raw_id(event: platform_events.EBAEvent) -> str:
|
||||
event_type = getattr(event, 'type', None) or event.__class__.__name__
|
||||
if isinstance(event, platform_events.MessageDeletedEvent) and event.message_id:
|
||||
# A recall is distinct from the original message.
|
||||
return f'{event_type}:{event.message_id}'
|
||||
if getattr(event, 'adapter_name', None) == 'wecomcs-omni':
|
||||
source = getattr(event, 'source_platform_object', None)
|
||||
if isinstance(source, dict):
|
||||
if source.get('msgid'):
|
||||
return str(source['msgid'])
|
||||
# Native service events can lack msgid. Preserve their identity
|
||||
# across sync retries and process restarts instead of using UUIDs.
|
||||
payload = json.dumps(source, sort_keys=True, ensure_ascii=False, separators=(',', ':'))
|
||||
return f'{event_type}:{hashlib.sha256(payload.encode()).hexdigest()}'
|
||||
return str(
|
||||
getattr(event, 'message_id', None) or getattr(event, 'feedback_id', None) or f'{event_type}:{uuid.uuid4()}'
|
||||
)
|
||||
@@ -904,6 +918,16 @@ class RuntimeBot:
|
||||
|
||||
event.bot_uuid = self.bot_entity.uuid
|
||||
execution_id = self._platform_event_execution_id(event)
|
||||
if getattr(event, 'adapter_name', None) == 'wecomcs-omni':
|
||||
from ..agent.runner.event_log_store import EventLogStore
|
||||
|
||||
# The sync API can replay history after an adapter restart. Reuse
|
||||
# the durable ingress journal, including records from earlier runs.
|
||||
async with self._ingress_dedup_lock:
|
||||
store = EventLogStore(self.ap.persistence_mgr.get_db_engine())
|
||||
if await store.get_event(execution_id):
|
||||
return
|
||||
await self._persist_monitoring_ingress(event, adapter, execution_id)
|
||||
with ingress(
|
||||
getattr(self, 'ap', None),
|
||||
'event_done',
|
||||
@@ -945,7 +969,7 @@ class RuntimeBot:
|
||||
if service is None or not execution_id:
|
||||
return
|
||||
try:
|
||||
sender = getattr(event, 'sender', None)
|
||||
sender = getattr(event, 'sender', None) or getattr(event, 'user', None) or getattr(event, 'operator', None)
|
||||
target_type, target_id, _ = self._infer_reply_target(event)
|
||||
await service.record_ingress_event(
|
||||
self.execution_context,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: '匹配机器人入群、退群、禁言和解除禁言等状态事件。',
|
||||
|
||||
@@ -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: '匹配機器人入羣、退羣、禁言和解除禁言等狀態事件。',
|
||||
|
||||
Reference in New Issue
Block a user