fix(ci): align SDK dependency and validate acceptance migrations

This commit is contained in:
Hyu
2026-10-11 00:59:51 +08:00
parent e3e5f88809
commit 353b452453
81 changed files with 3527 additions and 1818 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ dependencies = [
"langchain-text-splitters>=1.1.2",
"chromadb>=1.0.0,<2.0.0",
"qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.7.11rc1",
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@8c29bc0787be3b514fb379c5154dfc5eaa561cbd",
"asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0",
"matrix-nio>=0.25.2",
+23 -10
View File
@@ -80,7 +80,9 @@ def is_stream_card_configured(config: dict) -> bool:
return False
template_id = config.get('card_template_id')
# Older manifests stored instructional placeholder text as the default.
return isinstance(template_id, str) and bool(template_id.strip()) and template_id.strip() != '填写你的卡片template_id'
return (
isinstance(template_id, str) and bool(template_id.strip()) and template_id.strip() != '填写你的卡片template_id'
)
class DingTalkClient:
@@ -283,9 +285,9 @@ class DingTalkClient:
data = response.json()
if response.status_code != 200 or data.get('errcode', 0) != 0 or data.get('code'):
raise RuntimeError(
f"DingTalk delivery failed (HTTP {response.status_code}, "
f"code={data.get('errcode', data.get('code', 'unknown'))}): "
f"{data.get('errmsg', data.get('message', 'Unknown error'))}"
f'DingTalk delivery failed (HTTP {response.status_code}, '
f'code={data.get("errcode", data.get("code", "unknown"))}): '
f'{data.get("errmsg", data.get("message", "Unknown error"))}'
)
return data
@@ -333,7 +335,8 @@ class DingTalkClient:
response = await client.post(
'https://oapi.dingtalk.com/media/upload',
params={'access_token': token, 'type': 'image' if is_image else 'file'},
files={'media': (name, body)}, timeout=30.0,
files={'media': (name, body)},
timeout=30.0,
)
uploaded = self._check_send_result(response)
media_id = uploaded.get('media_id')
@@ -341,9 +344,11 @@ class DingTalkClient:
raise RuntimeError('DingTalk media upload returned no media ID')
if not await self.check_access_token():
await self.get_access_token()
params = {'photoURL': media_id} if is_image else {
'mediaId': media_id, 'fileName': name, 'fileType': os.path.splitext(name)[1].lstrip('.')
}
params = (
{'photoURL': media_id}
if is_image
else {'mediaId': media_id, 'fileName': name, 'fileType': os.path.splitext(name)[1].lstrip('.')}
)
payload = {
'robotCode': self.robot_code or self.key,
'msgKey': 'sampleImageMsg' if is_image else 'sampleFile',
@@ -359,7 +364,8 @@ class DingTalkClient:
response = await client.post(
f'{DINGTALK_OPENAPI_BASE}/v1.0/robot/{endpoint}',
headers={'x-acs-dingtalk-access-token': self.access_token},
json=payload, timeout=30.0,
json=payload,
timeout=30.0,
)
return self._check_send_result(response)
@@ -724,7 +730,14 @@ class DingTalkClient:
else:
open_space_id = f'dtv1.card//IM_ROBOT.{incoming_message.sender_staff_id}'
card_param_map = {'content': '', 'answer': '', 'reasoning': '', 'flowStatus': '1', 'brand': '', 'hasReasoning': ''}
card_param_map = {
'content': '',
'answer': '',
'reasoning': '',
'flowStatus': '1',
'brand': '',
'hasReasoning': '',
}
if incoming_message.message_type == 'text':
card_param_map['query'] = incoming_message.get_text_list()[0]
else:
+12 -12
View File
@@ -2,17 +2,17 @@
import re
_MARKER = re.compile(r"</?think>", re.IGNORECASE)
_MARKER = re.compile(r'</?think>', re.IGNORECASE)
def visible_think_markers(content: str, markdown: bool = True) -> str:
def replace(match):
tag = match.group(1).lower()
separator = "\n\n" if markdown else "\n"
return separator + (f"`{tag}`" if markdown else tag) + separator
separator = '\n\n' if markdown else '\n'
return separator + (f'`{tag}`' if markdown else tag) + separator
# Normalize only marker boundaries; preserve paragraph spacing in the body.
return re.sub(r"[ \t\r\n]*(</?think>)[ \t\r\n]*", replace, content, flags=re.IGNORECASE).strip()
return re.sub(r'[ \t\r\n]*(</?think>)[ \t\r\n]*', replace, content, flags=re.IGNORECASE).strip()
def card_text_fields(content: str) -> dict[str, str]:
@@ -20,20 +20,20 @@ def card_text_fields(content: str) -> dict[str, str]:
inside = False
offset = 0
for match in _MARKER.finditer(content):
(thinking if inside else answer).append(content[offset:match.start()])
inside = match.group(0).lower() == "<think>"
(thinking if inside else answer).append(content[offset : match.start()])
inside = match.group(0).lower() == '<think>'
offset = match.end()
tail = content[offset:]
# A stream can stop in the middle of an opening or closing marker.
for size in range(min(len(tail), 7), 0, -1):
if any(marker.startswith(tail[-size:].lower()) for marker in ("<think>", "</think>")):
if any(marker.startswith(tail[-size:].lower()) for marker in ('<think>', '</think>')):
tail = tail[:-size]
break
(thinking if inside else answer).append(tail)
reasoning = "\n".join(part.strip() for part in thinking if part.strip())
response = "\n".join(part.strip() for part in answer if part.strip())
reasoning = '\n'.join(part.strip() for part in thinking if part.strip())
response = '\n'.join(part.strip() for part in answer if part.strip())
return {
"reasoning": "> " + reasoning.replace("\n", "\n> ") if reasoning else "",
"answer": response,
"hasReasoning": "true" if reasoning else "",
'reasoning': '> ' + reasoning.replace('\n', '\n> ') if reasoning else '',
'answer': response,
'hasReasoning': 'true' if reasoning else '',
}
+39 -14
View File
@@ -33,15 +33,26 @@ async def _read_callback_xml(client, req):
if root.find('Encrypt') is not None:
crypt = WXBizMsgCrypt(client.token, client.aes, client.appid)
code, xml = await asyncio.to_thread(
crypt.DecryptMsg, body, req.args.get('msg_signature', ''),
req.args.get('timestamp', ''), req.args.get('nonce', ''),
crypt.DecryptMsg,
body,
req.args.get('msg_signature', ''),
req.args.get('timestamp', ''),
req.args.get('nonce', ''),
)
if code != 0:
raise ValueError('Official Account callback decryption failed')
return xml.decode('utf-8') if isinstance(xml, bytes) else xml
digest = hashlib.sha1(''.join(sorted([
client.token, req.args.get('timestamp', ''), req.args.get('nonce', ''),
])).encode('utf-8')).hexdigest()
digest = hashlib.sha1(
''.join(
sorted(
[
client.token,
req.args.get('timestamp', ''),
req.args.get('nonce', ''),
]
)
).encode('utf-8')
).hexdigest()
if not hmac.compare_digest(digest, req.args.get('signature', '')):
raise ValueError('Official Account callback signature verification failed')
return body.decode('utf-8')
@@ -232,9 +243,15 @@ class OAClient:
if message_data.get('MsgId'):
message_data['MsgId'] = int(message_data['MsgId'])
else:
message_data['MsgId'] = ':'.join(str(message_data.get(key) or '') for key in (
'FromUserName', 'CreateTime', 'Event', 'EventKey',
))
message_data['MsgId'] = ':'.join(
str(message_data.get(key) or '')
for key in (
'FromUserName',
'CreateTime',
'Event',
'EventKey',
)
)
return message_data
@@ -423,9 +440,11 @@ class OAClientForLongerResponse:
root = await asyncio.to_thread(ET.fromstring, xml_msg)
from_user = root.find('FromUserName').text
to_user = root.find('ToUserName').text
message_key = (from_user, root.findtext('MsgId') or (
root.findtext('CreateTime'), root.findtext('Event'), root.findtext('EventKey')
))
message_key = (
from_user,
root.findtext('MsgId')
or (root.findtext('CreateTime'), root.findtext('Event'), root.findtext('EventKey')),
)
cached = self._callback_responses.get(message_key)
if cached and time.monotonic() - cached[0] < 600:
return cached[1]
@@ -502,9 +521,15 @@ class OAClientForLongerResponse:
if message_data.get('MsgId'):
message_data['MsgId'] = int(message_data['MsgId'])
else:
message_data['MsgId'] = ':'.join(str(message_data.get(key) or '') for key in (
'FromUserName', 'CreateTime', 'Event', 'EventKey',
))
message_data['MsgId'] = ':'.join(
str(message_data.get(key) or '')
for key in (
'FromUserName',
'CreateTime',
'Event',
'EventKey',
)
)
return message_data
@@ -198,7 +198,9 @@ class WecomCSClient:
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,
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:
@@ -206,7 +208,7 @@ class WecomCSClient:
refreshed = True
continue
if data.get('errcode') != 0:
raise RuntimeError(f"WeCom sync_msg failed (errcode={data.get('errcode')})")
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'):
@@ -452,9 +454,12 @@ class WecomCSClient:
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()
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:
@@ -192,7 +192,10 @@ class PersistentStateStore:
if self._check_scope_enabled('conversation', binding) and event.conversation_id:
if 'external.conversation_id' not in snapshot['conversation']:
from .context_reset import get_reset_generation
generation = await get_reset_generation(self._db_engine, event.workspace_id, event.bot_id, event.conversation_id)
generation = await get_reset_generation(
self._db_engine, event.workspace_id, event.bot_id, event.conversation_id
)
snapshot['conversation']['external.conversation_id'] = generation or event.conversation_id
return snapshot
@@ -742,6 +742,7 @@ async def execute_platform_tool(
raise ValueError(f'Platform API {definition.api} is no longer supported by bot {bot_id}')
api_func = getattr(bot.adapter, definition.api, None)
if reply_via_source:
async def api_func(target_type, target_id, message):
source = await bot.adapter.get_message(
chat_type=target_type,
@@ -749,14 +750,17 @@ 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':
@@ -336,6 +336,7 @@ class TranscriptStore:
strict_thread: bool,
) -> typing.Any:
from .context_reset import active_history_condition
query = query.where(active_history_condition(Transcript))
if bot_id is not None:
query = query.where(Transcript.bot_id == bot_id)
@@ -32,6 +32,7 @@ def validate_monitoring_window(start_time: datetime.datetime | None, end_time: d
if start_time >= end or end - start_time > datetime.timedelta(days=365):
quart.abort(400, description='Monitoring time range must be positive and no longer than 365 days')
@group.group_class('monitoring', '/api/v1/monitoring')
class MonitoringRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@@ -545,15 +546,21 @@ class MonitoringRouterGroup(group.RouterGroup):
}
)
@self.route('/sessions/reset-context', methods=['POST'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY, permission=Permission.RESOURCE_MANAGE)
@self.route(
'/sessions/reset-context',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
permission=Permission.RESOURCE_MANAGE,
)
async def reset_session_context(request_context: RequestContext):
body = await quart.request.get_json()
if not isinstance(body, dict):
return self.http_status(400, 'invalid_request', 'Expected a JSON object')
try:
result = await self.ap.monitoring_service.reset_session_context(
request_context, body.get('bot_id'), body.get('session_id'),
request_context,
body.get('bot_id'),
body.get('session_id'),
)
except ValueError as exc:
return self.http_status(400, 'invalid_request', str(exc))
@@ -162,23 +162,43 @@ class AdaptersRouterGroup(group.RouterGroup):
slack_setup = get_slack_setup(self.ap)
@self.route('/slack/setup', methods=['POST'], permission=Permission.RESOURCE_MANAGE,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
@self.route(
'/slack/setup',
methods=['POST'],
permission=Permission.RESOURCE_MANAGE,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def slack_start(request_context: RequestContext):
data = await quart.request.get_json()
if not isinstance(data, dict):
return self.http_status(400, -1, 'invalid_input')
try:
result = await slack_setup.start(request_context, **{
key: data[key] for key in ('bot_uuid', 'access_token', 'refresh_token', 'webhook_url',
'name', 'socket_mode', 'redirect_url') if key in data
})
result = await slack_setup.start(
request_context,
**{
key: data[key]
for key in (
'bot_uuid',
'access_token',
'refresh_token',
'webhook_url',
'name',
'socket_mode',
'redirect_url',
)
if key in data
},
)
return self.success(data=result)
except (SlackSetupError, TypeError) as exc:
return self.http_status(400, -1, str(exc) if isinstance(exc, SlackSetupError) else 'invalid_input')
@self.route('/slack/setup/<session_id>', methods=['GET', 'DELETE'],
permission=Permission.RESOURCE_MANAGE, auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
@self.route(
'/slack/setup/<session_id>',
methods=['GET', 'DELETE'],
permission=Permission.RESOURCE_MANAGE,
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
)
async def slack_status(session_id: str, request_context: RequestContext):
try:
slack_setup.owned(request_context, session_id)
@@ -194,15 +214,22 @@ class AdaptersRouterGroup(group.RouterGroup):
@self.route('/slack/setup/callback', methods=['GET'], auth_type=group.AuthType.NONE)
async def slack_callback():
try:
await slack_setup.finish(quart.request.args.get('state', ''), quart.request.args.get('code', ''),
quart.request.args.get('error'))
await slack_setup.finish(
quart.request.args.get('state', ''),
quart.request.args.get('code', ''),
quart.request.args.get('error'),
)
message = 'Slack authorization completed. Return to LangBot to finish setup. / Slack 授权完成,请返回 LangBot 完成配置。'
status = 200
except SlackSetupError:
message = 'Slack authorization failed or expired. Return to LangBot for details. / Slack 授权失败或已过期,请返回 LangBot 查看详情。'
status = 400
return quart.Response(message, status=status, content_type='text/plain; charset=utf-8',
headers={'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer'})
return quart.Response(
message,
status=status,
content_type='text/plain; charset=utf-8',
headers={'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer'},
)
@self.route('', methods=['GET'])
async def _() -> str:
+6 -2
View File
@@ -452,7 +452,9 @@ class AgentService:
if runner_id:
descriptor = await self.ap.runner_registry.get(context, runner_id)
config['runner_config'][runner_id] = await self.ap.llm_model_service.apply_default_model(
context, descriptor.config_schema, config['runner_config'][runner_id],
context,
descriptor.config_schema,
config['runner_config'][runner_id],
)
patterns = agent_data.get('supported_event_patterns', AGENT_DEFAULT_EVENT_PATTERNS)
new_uuid = str(uuid.uuid4())
@@ -566,7 +568,9 @@ class AgentService:
if parameters is None:
parameters = self.ap.pipeline_service._get_default_values_from_schema(descriptor.config_schema)
if draft:
parameters = await self.ap.llm_model_service.apply_default_model(context, descriptor.config_schema, parameters)
parameters = await self.ap.llm_model_service.apply_default_model(
context, descriptor.config_schema, parameters
)
if not isinstance(parameters, dict):
raise ValueError('Processor parameters must be an object')
for field in descriptor.config_schema:
+24 -7
View File
@@ -198,6 +198,7 @@ class LLMModelsService:
async def get_starred_model(self, context: TenantContext) -> str | None:
from ....entity.persistence.metadata import WorkspaceMetadata
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(WorkspaceMetadata.value).where(
WorkspaceMetadata.workspace_uuid == require_workspace_uuid(context),
@@ -212,6 +213,7 @@ class LLMModelsService:
async def set_starred_model(self, context: TenantContext, model_uuid: str | None) -> None:
from ....entity.persistence.metadata import WorkspaceMetadata
workspace_uuid = require_workspace_uuid(context)
if model_uuid and await self.get_llm_model(context, model_uuid) is None:
raise ValueError('Model not found in this Workspace')
@@ -222,11 +224,16 @@ class LLMModelsService:
else:
from sqlalchemy.dialects.sqlite import insert
statement = insert(WorkspaceMetadata).values(
workspace_uuid=workspace_uuid, key='starred_llm_model', value=model_uuid or '',
workspace_uuid=workspace_uuid,
key='starred_llm_model',
value=model_uuid or '',
)
await self.ap.persistence_mgr.execute_async(
statement.on_conflict_do_update(
index_elements=['workspace_uuid', 'key'],
set_={'value': model_uuid or ''},
)
)
await self.ap.persistence_mgr.execute_async(statement.on_conflict_do_update(
index_elements=['workspace_uuid', 'key'], set_={'value': model_uuid or ''},
))
async def get_default_model(self, context: TenantContext) -> str | None:
starred = await self.get_starred_model(context)
@@ -245,9 +252,18 @@ class LLMModelsService:
async def apply_default_model(self, context: TenantContext, schema: list[dict], parameters: dict) -> dict:
import copy
fields = [field for field in schema if field.get('type') in (
'llm-model-selector', 'select-llm-model', 'model-fallback-selector',
) and field.get('name')]
fields = [
field
for field in schema
if field.get('type')
in (
'llm-model-selector',
'select-llm-model',
'model-fallback-selector',
)
and field.get('name')
]
if not fields:
return parameters
model_uuid = await self.get_default_model(context)
@@ -264,6 +280,7 @@ class LLMModelsService:
elif not result.get(name):
result[name] = model_uuid
return result
async def get_llm_models(self, context: TenantContext, include_secret: bool = False) -> list[dict]:
"""Get all LLM models with provider info"""
result = await self.ap.persistence_mgr.execute_async(
+144 -53
View File
@@ -132,6 +132,7 @@ def _workspace_transaction(method):
result = await method(self, context, *args, **kwargs)
if method.__name__ in {'record_message', 'update_message_status'}:
from ....utils.inflight import inflight_hub
inflight_hub.notify(workspace_uuid)
return result
@@ -1430,10 +1431,13 @@ class MonitoringService(ExecutionDetailsMixin):
pipeline_timing = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(AgentRun.started_at, AgentRun.finished_at)
.select_from(Message)
.join(AgentRun, sqlalchemy.and_(
AgentRun.workspace_id == workspace_uuid,
AgentRun.run_id == Message.run_id,
))
.join(
AgentRun,
sqlalchemy.and_(
AgentRun.workspace_id == workspace_uuid,
AgentRun.run_id == Message.run_id,
),
)
.where(
*pipeline_conditions,
Message.status == 'success',
@@ -1443,10 +1447,7 @@ class MonitoringService(ExecutionDetailsMixin):
.order_by(Message.timestamp.desc())
.limit(self._detail_limit())
)
durations.extend(
(row[1] - row[0]).total_seconds() * 1000
for row in pipeline_timing.all() if row[0] and row[1]
)
durations.extend((row[1] - row[0]).total_seconds() * 1000 for row in pipeline_timing.all() if row[0] and row[1])
durations.sort()
pipeline_status_result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(
@@ -1679,21 +1680,39 @@ class MonitoringService(ExecutionDetailsMixin):
workspace = require_workspace_uuid(context)
active = await self.get_executions(context, statuses=['running', 'queued'], limit=100, include_summary=False)
recent = await self.get_executions(
context, start_time=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=30),
limit=100, include_summary=False,
context,
start_time=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=30),
limit=100,
include_summary=False,
)
items = {(r['source'], r['id']): r for r in recent['items'] + active['items'] if r['source'] != 'event'}
# Keep the actual terminal state of older runs that just left the active set.
for kind, model, column, scope in [
('agent', persistence_agent_run.AgentRun, persistence_agent_run.AgentRun.run_id, persistence_agent_run.AgentRun.workspace_id),
('pipeline', persistence_monitoring.MonitoringMessage, persistence_monitoring.MonitoringMessage.id, persistence_monitoring.MonitoringMessage.workspace_uuid),
(
'agent',
persistence_agent_run.AgentRun,
persistence_agent_run.AgentRun.run_id,
persistence_agent_run.AgentRun.workspace_id,
),
(
'pipeline',
persistence_monitoring.MonitoringMessage,
persistence_monitoring.MonitoringMessage.id,
persistence_monitoring.MonitoringMessage.workspace_uuid,
),
]:
missing = [r['id'] for r in (previous or [])[:100] if r['source'] == kind and (kind, r['id']) not in items]
if missing:
records = await self._execution_rows(workspace, sqlalchemy.select(model).where(scope == workspace, column.in_(missing)))
records = await self._execution_rows(
workspace, sqlalchemy.select(model).where(scope == workspace, column.in_(missing))
)
names = await self._agent_names(workspace, records) if kind == 'agent' else {}
for record in records:
row = self._serialize_agent_execution(record, names) if kind == 'agent' else self._serialize_pipeline_execution(record)
row = (
self._serialize_agent_execution(record, names)
if kind == 'agent'
else self._serialize_pipeline_execution(record)
)
items[(kind, row['id'])] = row
rows = list(items.values())
Run = persistence_agent_run.AgentRun
@@ -1701,18 +1720,28 @@ class MonitoringService(ExecutionDetailsMixin):
agent_ids = [r['id'] for r in rows if r['source'] == 'agent']
pipeline_ids = [r['id'] for r in rows if r['source'] == 'pipeline']
linked = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(Run.run_id, Message.id.label('message_id')).outerjoin(
Message, sqlalchemy.and_(Message.workspace_uuid == workspace, Message.run_id == Run.run_id)
).where(Run.workspace_id == workspace, sqlalchemy.or_(Run.run_id.in_(agent_ids), Message.id.in_(pipeline_ids)))
sqlalchemy.select(Run.run_id, Message.id.label('message_id'))
.outerjoin(Message, sqlalchemy.and_(Message.workspace_uuid == workspace, Message.run_id == Run.run_id))
.where(
Run.workspace_id == workspace, sqlalchemy.or_(Run.run_id.in_(agent_ids), Message.id.in_(pipeline_ids))
)
)
identities = {r.run_id: ('pipeline', r.message_id) if r.message_id in pipeline_ids else ('agent', r.run_id) for r in linked.all()}
identities = {
r.run_id: ('pipeline', r.message_id) if r.message_id in pipeline_ids else ('agent', r.run_id)
for r in linked.all()
}
inflight_hub.track(workspace, identities)
Event = persistence_agent_run.AgentRunEvent
latest = sqlalchemy.select(Event.run_id, sqlalchemy.func.max(Event.sequence).label('seq')).where(
Event.run_id.in_(identities)
).group_by(Event.run_id).subquery()
latest = (
sqlalchemy.select(Event.run_id, sqlalchemy.func.max(Event.sequence).label('seq'))
.where(Event.run_id.in_(identities))
.group_by(Event.run_id)
.subquery()
)
phases = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(Event.run_id, Event.type).join(latest, sqlalchemy.and_(Event.run_id == latest.c.run_id, Event.sequence == latest.c.seq))
sqlalchemy.select(Event.run_id, Event.type).join(
latest, sqlalchemy.and_(Event.run_id == latest.c.run_id, Event.sequence == latest.c.seq)
)
)
for run_id, event_type in phases.all():
row = items.get(identities[run_id])
@@ -1951,7 +1980,10 @@ class MonitoringService(ExecutionDetailsMixin):
if end_time:
query = query.where(source.c.timestamp <= end_time)
return await self._session_projection_page(
workspace_uuid, query.order_by(source.c.timestamp.desc(), source.c.id.desc()), limit, offset,
workspace_uuid,
query.order_by(source.c.timestamp.desc(), source.c.id.desc()),
limit,
offset,
)
conditions = [persistence_monitoring.MonitoringMessage.workspace_uuid == workspace_uuid]
@@ -2204,47 +2236,99 @@ class MonitoringService(ExecutionDetailsMixin):
workspace = require_workspace_uuid(context)
execute = self.ap.persistence_mgr.execute_async
source = self._session_message_source(workspace, [bot_id])
exists = (await execute(sqlalchemy.select(source.c.session_id).where(
source.c.session_id == session_id,
).limit(1))).first()
exists = (
await execute(
sqlalchemy.select(source.c.session_id)
.where(
source.c.session_id == session_id,
)
.limit(1)
)
).first()
if exists is None:
session = persistence_monitoring.MonitoringSession
exists = (await execute(sqlalchemy.select(session.session_id).where(
session.workspace_uuid == workspace, session.bot_id == bot_id, session.session_id == session_id,
).limit(1))).first()
exists = (
await execute(
sqlalchemy.select(session.session_id)
.where(
session.workspace_uuid == workspace,
session.bot_id == bot_id,
session.session_id == session_id,
)
.limit(1)
)
).first()
if exists is None:
raise LookupError('Session not found')
# Legacy sessions use uppercase PERSON/GROUP; envelope IDs use lowercase.
conversation = re.sub(r'^(PERSON|GROUP)_', lambda match: match[1].lower() + '_', session_id)
run = persistence_agent_run.AgentRun
busy = (await execute(sqlalchemy.select(run.run_id).where(
run.workspace_id == workspace, run.bot_id == bot_id, run.conversation_id == conversation,
run.status.in_(['created', 'queued', 'claimed', 'running']),
).limit(1))).first()
busy = (
await execute(
sqlalchemy.select(run.run_id)
.where(
run.workspace_id == workspace,
run.bot_id == bot_id,
run.conversation_id == conversation,
run.status.in_(['created', 'queued', 'claimed', 'running']),
)
.limit(1)
)
).first()
message = persistence_monitoring.MonitoringMessage
pending = (await execute(sqlalchemy.select(message.id).where(
message.workspace_uuid == workspace, message.bot_id == bot_id,
message.session_id.in_([session_id, conversation]), message.status == 'pending',
).limit(1))).first()
pending = (
await execute(
sqlalchemy.select(message.id)
.where(
message.workspace_uuid == workspace,
message.bot_id == bot_id,
message.session_id.in_([session_id, conversation]),
message.status == 'pending',
)
.limit(1)
)
).first()
interaction = persistence_agent_interaction.AgentInteraction
waiting = (await execute(sqlalchemy.select(interaction.id).where(
interaction.workspace_id == workspace, interaction.bot_id == bot_id,
interaction.conversation_id == conversation, interaction.status.in_(['pending', 'submitted']),
sqlalchemy.or_(interaction.expires_at.is_(None), interaction.expires_at > datetime.datetime.utcnow()),
).limit(1))).first()
waiting = (
await execute(
sqlalchemy.select(interaction.id)
.where(
interaction.workspace_id == workspace,
interaction.bot_id == bot_id,
interaction.conversation_id == conversation,
interaction.status.in_(['pending', 'submitted']),
sqlalchemy.or_(
interaction.expires_at.is_(None), interaction.expires_at > datetime.datetime.utcnow()
),
)
.limit(1)
)
).first()
if busy or pending or waiting:
raise RuntimeError('Session is processing a task; wait until it finishes')
await execute(sqlalchemy.delete(RunnerState).where(
RunnerState.workspace_id == workspace, RunnerState.bot_id == bot_id,
RunnerState.conversation_id == conversation, RunnerState.scope == 'conversation',
))
await execute(
sqlalchemy.delete(RunnerState).where(
RunnerState.workspace_id == workspace,
RunnerState.bot_id == bot_id,
RunnerState.conversation_id == conversation,
RunnerState.scope == 'conversation',
)
)
generation = str(uuid.uuid4())
await execute(sqlalchemy.insert(RunnerState).values(
runner_id=HOST_RESET_RUNNER, binding_identity=HOST_RESET_RUNNER, scope='conversation',
scope_key=reset_scope(workspace, bot_id, conversation), state_key='generation',
value_json=json.dumps(generation), workspace_id=workspace, bot_id=bot_id,
conversation_id=conversation, created_at=datetime.datetime.utcnow(),
))
await execute(
sqlalchemy.insert(RunnerState).values(
runner_id=HOST_RESET_RUNNER,
binding_identity=HOST_RESET_RUNNER,
scope='conversation',
scope_key=reset_scope(workspace, bot_id, conversation),
state_key='generation',
value_json=json.dumps(generation),
workspace_id=workspace,
bot_id=bot_id,
conversation_id=conversation,
created_at=datetime.datetime.utcnow(),
)
)
return {'reset': True, 'session_id': session_id}
async def get_sessions(
@@ -2266,7 +2350,14 @@ class MonitoringService(ExecutionDetailsMixin):
workspace_uuid = require_workspace_uuid(context)
if bot_ids and not pipeline_ids and not execution_statuses and mode == 'all':
return await self.get_bot_conversation_sessions(
workspace_uuid, bot_ids, start_time, end_time, user_query, is_active, limit, offset,
workspace_uuid,
bot_ids,
start_time,
end_time,
user_query,
is_active,
limit,
offset,
)
conditions = [persistence_monitoring.MonitoringSession.workspace_uuid == workspace_uuid]
@@ -104,16 +104,24 @@ class ExecutionDetailsMixin:
message = models.MonitoringMessage
conversation = sa.func.coalesce(
EventLog.conversation_id,
sa.select(AgentRun.conversation_id).where(
sa.select(AgentRun.conversation_id)
.where(
AgentRun.workspace_id == workspace,
AgentRun.bot_id == EventLog.bot_id,
AgentRun.event_id == EventLog.event_id,
).order_by(AgentRun.id).limit(1).scalar_subquery(),
)
.order_by(AgentRun.id)
.limit(1)
.scalar_subquery(),
sa.case(
(self._execution_json_text(EventLog.input_json, 'chat_type') == 'group',
sa.literal('group_') + self._execution_json_text(EventLog.input_json, 'chat_id')),
(self._execution_json_text(EventLog.input_json, 'chat_type') == 'private',
sa.literal('person_') + self._execution_json_text(EventLog.input_json, ('sender', 'id'))),
(
self._execution_json_text(EventLog.input_json, 'chat_type') == 'group',
sa.literal('group_') + self._execution_json_text(EventLog.input_json, 'chat_id'),
),
(
self._execution_json_text(EventLog.input_json, 'chat_type') == 'private',
sa.literal('person_') + self._execution_json_text(EventLog.input_json, ('sender', 'id')),
),
),
)
values = {
@@ -122,8 +130,12 @@ class ExecutionDetailsMixin:
'timestamp': sa.func.coalesce(EventLog.event_time, EventLog.created_at),
'bot_id': EventLog.bot_id,
'bot_name': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, 'bot_name'), ''),
'pipeline_id': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_uuid')), ''),
'pipeline_name': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), ''),
'pipeline_id': sa.func.coalesce(
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_uuid')), ''
),
'pipeline_name': sa.func.coalesce(
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), ''
),
'message_content': sa.func.coalesce(EventLog.input_json, EventLog.input_summary, ''),
'session_id': conversation,
'status': sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, 'status'), 'success'),
@@ -137,45 +149,62 @@ class ExecutionDetailsMixin:
}
columns = list(message.__table__.columns)
stored = sa.select(*columns).where(message.workspace_uuid == workspace, message.bot_id.in_(bot_ids))
incoming = sa.select(*[
values.get(column.name, sa.literal(None)).label(column.name) for column in columns
]).where(
incoming = sa.select(
*[values.get(column.name, sa.literal(None)).label(column.name) for column in columns]
).where(
EventLog.workspace_id == workspace,
EventLog.bot_id.in_(bot_ids),
EventLog.source == 'platform',
EventLog.event_type == 'message.received',
conversation.is_not(None),
conversation != '',
~sa.exists(sa.select(message.id).where(
message.workspace_uuid == workspace,
message.bot_id == EventLog.bot_id,
sa.func.coalesce(message.role, 'user') != 'assistant',
sa.or_(message.event_id == EventLog.event_id, message.id == EventLog.event_id),
)),
~sa.exists(
sa.select(message.id).where(
message.workspace_uuid == workspace,
message.bot_id == EventLog.bot_id,
sa.func.coalesce(message.role, 'user') != 'assistant',
sa.or_(message.event_id == EventLog.event_id, message.id == EventLog.event_id),
)
),
)
return sa.union_all(stored, incoming).subquery()
async def _session_projection_page(self, workspace, statement, limit, offset):
count = (await self._execution_query(workspace, sa.select(sa.func.count()).select_from(statement.subquery()))).scalar_one()
count = (
await self._execution_query(workspace, sa.select(sa.func.count()).select_from(statement.subquery()))
).scalar_one()
rows = (await self._execution_query(workspace, statement.limit(limit).offset(offset))).mappings().all()
return [
{key: value.isoformat() if isinstance(value, datetime.datetime) else value for key, value in row.items()}
for row in rows
], count
async def get_bot_conversation_sessions(self, workspace, bot_ids, start_time, end_time, user_query, is_active, limit, offset):
async def get_bot_conversation_sessions(
self, workspace, bot_ids, start_time, end_time, user_query, is_active, limit, offset
):
"""Read session summaries across processor types without mutating history."""
messages = self._session_message_source(workspace, bot_ids)
key = [messages.c.workspace_uuid, messages.c.bot_id, messages.c.session_id]
ranked = sa.select(
*messages.c,
sa.func.row_number().over(partition_by=key, order_by=[
sa.case((messages.c.role == 'user', 0), else_=1), messages.c.timestamp.desc(), messages.c.id.desc(),
]).label('position'),
sa.func.min(messages.c.timestamp).over(partition_by=key).label('first_seen'),
sa.func.max(messages.c.timestamp).over(partition_by=key).label('last_seen'),
sa.func.count().over(partition_by=key).label('count'),
).where(messages.c.session_id != '').subquery()
ranked = (
sa.select(
*messages.c,
sa.func.row_number()
.over(
partition_by=key,
order_by=[
sa.case((messages.c.role == 'user', 0), else_=1),
messages.c.timestamp.desc(),
messages.c.id.desc(),
],
)
.label('position'),
sa.func.min(messages.c.timestamp).over(partition_by=key).label('first_seen'),
sa.func.max(messages.c.timestamp).over(partition_by=key).label('last_seen'),
sa.func.count().over(partition_by=key).label('count'),
)
.where(messages.c.session_id != '')
.subquery()
)
session = models.MonitoringSession
fields = list(session.__table__.columns)
projected = {
@@ -184,15 +213,21 @@ class ExecutionDetailsMixin:
'message_count': ranked.c['count'],
'is_active': sa.literal(True),
}
derived = sa.select(*[
(projected[field.name] if field.name in projected else ranked.c[field.name]).label(field.name)
for field in fields
]).where(ranked.c.position == 1)
derived = sa.select(
*[
(projected[field.name] if field.name in projected else ranked.c[field.name]).label(field.name)
for field in fields
]
).where(ranked.c.position == 1)
existing = sa.select(*fields).where(session.workspace_uuid == workspace, session.bot_id.in_(bot_ids))
combined = sa.union_all(existing, derived).subquery()
group = [combined.c.workspace_uuid, combined.c.bot_id, combined.c.session_id]
merged = sa.select(
*[combined.c[field.name] for field in fields if field.name not in {'message_count', 'start_time', 'last_activity'}],
*[
combined.c[field.name]
for field in fields
if field.name not in {'message_count', 'start_time', 'last_activity'}
],
sa.func.max(combined.c.message_count).over(partition_by=group).label('message_count'),
sa.func.min(combined.c.start_time).over(partition_by=group).label('start_time'),
sa.func.max(combined.c.last_activity).over(partition_by=group).label('last_activity'),
@@ -209,7 +244,10 @@ class ExecutionDetailsMixin:
if is_active is not None:
query = query.where(merged.c.is_active == is_active)
return await self._session_projection_page(
workspace, query.order_by(merged.c.last_activity.desc(), merged.c.session_id), limit, offset,
workspace,
query.order_by(merged.c.last_activity.desc(), merged.c.session_id),
limit,
offset,
)
def _execution_json_text(self, column, key):
@@ -335,12 +373,10 @@ class ExecutionDetailsMixin:
# Delivered pipeline ingress is not an unhandled execution, including
# older queued/aggregated messages whose trace identity was lost.
~sa.and_(
sa.func.coalesce(
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), ''
) == 'pipeline',
sa.func.coalesce(
self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'status')), ''
) == 'delivered',
sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'target_type')), '')
== 'pipeline',
sa.func.coalesce(self._execution_json_text(EventLog.metadata_json, ('routes', 0, 'status')), '')
== 'delivered',
),
~sa.exists(
sa.select(AgentRun.id).where(
@@ -400,11 +436,13 @@ class ExecutionDetailsMixin:
'runner_id': None,
'debug': row.source == 'debug',
'has_error': status == 'failed',
'status_reason': metadata.get('reason') or '; '.join(
'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,
)
or None,
}
async def enrich_execution_rows(self, workspace, items):
@@ -415,10 +453,13 @@ class ExecutionDetailsMixin:
linked = await self._execution_query(
workspace,
sa.select(models.MonitoringMessage.id, AgentRun)
.join(AgentRun, sa.and_(
AgentRun.workspace_id == workspace,
AgentRun.run_id == models.MonitoringMessage.run_id,
))
.join(
AgentRun,
sa.and_(
AgentRun.workspace_id == workspace,
AgentRun.run_id == models.MonitoringMessage.run_id,
),
)
.where(
models.MonitoringMessage.workspace_uuid == workspace,
models.MonitoringMessage.id.in_(pipeline_items),
@@ -426,10 +467,9 @@ class ExecutionDetailsMixin:
)
for message_id, run in linked.all():
metrics = self._serialize_agent_execution(run)
pipeline_items[message_id].update({
key: metrics[key]
for key in ('started_at_ms', 'finished_at_ms', 'duration_ms', 'usage', 'cost')
})
pipeline_items[message_id].update(
{key: metrics[key] for key in ('started_at_ms', 'finished_at_ms', 'duration_ms', 'usage', 'cost')}
)
calls = models.MonitoringLLMCall
usage_rows = await self._execution_query(
@@ -439,10 +479,12 @@ class ExecutionDetailsMixin:
sa.func.sum(calls.input_tokens),
sa.func.sum(calls.output_tokens),
sa.func.sum(calls.total_tokens),
).where(
)
.where(
calls.workspace_uuid == workspace,
calls.message_id.in_(pipeline_items),
).group_by(calls.message_id),
)
.group_by(calls.message_id),
)
for message_id, input_tokens, output_tokens, total_tokens in usage_rows.all():
if not pipeline_items[message_id].get('usage'):
+3 -1
View File
@@ -69,7 +69,9 @@ class PipelineService:
runner_config.setdefault('expire-time', 0)
ai_config['runner_config'] = {selected.id: self._get_default_values_from_schema(selected.config_schema)}
ai_config['runner_config'][selected.id] = await self.ap.llm_model_service.apply_default_model(
context, selected.config_schema, ai_config['runner_config'][selected.id],
context,
selected.config_schema,
ai_config['runner_config'][selected.id],
)
return config
+52 -18
View File
@@ -207,35 +207,65 @@ class LangBotMCPServer:
return _dump(await ap.monitoring_service.get_message_details(context, identifier))
return _dump(await ap.monitoring_service.get_session_analysis(context, identifier))
@mcp.tool(description='Read a bounded snapshot of active and recently started Workspace executions, including the latest progress event. No percentage is estimated.')
@mcp.tool(
description='Read a bounded snapshot of active and recently started Workspace executions, including the latest progress event. No percentage is estimated.'
)
async def get_inflight_executions() -> str:
return _dump(await ap.monitoring_service.get_inflight_snapshot(_authorized(Permission.RESOURCE_VIEW)))
@mcp.tool(description='Reset a bot session model context, preserving monitoring history. Refuses busy sessions.')
@mcp.tool(
description='Reset a bot session model context, preserving monitoring history. Refuses busy sessions.'
)
async def reset_session_context(bot_id: str, session_id: str) -> str:
return _dump(await ap.monitoring_service.reset_session_context(
_authorized(Permission.RESOURCE_MANAGE), bot_id, session_id,
))
return _dump(
await ap.monitoring_service.reset_session_context(
_authorized(Permission.RESOURCE_MANAGE),
bot_id,
session_id,
)
)
@mcp.tool(description='Create/configure a Slack app for an existing Workspace bot draft. Returns a setup session; poll it for the installation authorization URL. The user must approve Slack installation. Configuration tokens stay in memory only.')
async def start_slack_setup(bot_uuid: str, access_token: str, refresh_token: str,
webhook_url: str = '', name: str = 'LangBot',
socket_mode: bool = False, redirect_url: str = '') -> str:
@mcp.tool(
description='Create/configure a Slack app for an existing Workspace bot draft. Returns a setup session; poll it for the installation authorization URL. The user must approve Slack installation. Configuration tokens stay in memory only.'
)
async def start_slack_setup(
bot_uuid: str,
access_token: str,
refresh_token: str,
webhook_url: str = '',
name: str = 'LangBot',
socket_mode: bool = False,
redirect_url: str = '',
) -> str:
from langbot.pkg.api.http.service.slack_setup import get_slack_setup
return _dump(await get_slack_setup(ap).start(
_authorized(Permission.RESOURCE_MANAGE), bot_uuid=bot_uuid, access_token=access_token,
refresh_token=refresh_token, webhook_url=webhook_url, name=name,
socket_mode=socket_mode, redirect_url=redirect_url,
))
@mcp.tool(description='Read an owned Slack setup session. Success includes adapter credentials; save them with the existing bot update tool. Socket Mode additionally needs an App-Level Token with connections:write.')
return _dump(
await get_slack_setup(ap).start(
_authorized(Permission.RESOURCE_MANAGE),
bot_uuid=bot_uuid,
access_token=access_token,
refresh_token=refresh_token,
webhook_url=webhook_url,
name=name,
socket_mode=socket_mode,
redirect_url=redirect_url,
)
)
@mcp.tool(
description='Read an owned Slack setup session. Success includes adapter credentials; save them with the existing bot update tool. Socket Mode additionally needs an App-Level Token with connections:write.'
)
async def get_slack_setup_status(session_id: str) -> str:
from langbot.pkg.api.http.service.slack_setup import get_slack_setup
return _dump(get_slack_setup(ap).status(_authorized(Permission.RESOURCE_MANAGE), session_id))
@mcp.tool(description='Discard an owned Slack setup session and its temporary credentials. Does not delete the Slack app.')
@mcp.tool(
description='Discard an owned Slack setup session and its temporary credentials. Does not delete the Slack app.'
)
async def cancel_slack_setup(session_id: str) -> str:
from langbot.pkg.api.http.service.slack_setup import get_slack_setup
service = get_slack_setup(ap)
service.owned(_authorized(Permission.RESOURCE_MANAGE), session_id)
service.discard(session_id)
@@ -463,13 +493,17 @@ class LangBotMCPServer:
context = _authorized(Permission.RESOURCE_VIEW)
return _dump({'uuid': await ap.llm_model_service.get_starred_model(context)})
@mcp.tool(description='Star one LLM model for this Workspace, replacing the previous choice. Pass null to clear.')
@mcp.tool(
description='Star one LLM model for this Workspace, replacing the previous choice. Pass null to clear.'
)
async def set_starred_model(model_uuid: str | None = None) -> str:
context = _authorized(Permission.PROVIDER_SECRET_MANAGE)
await ap.llm_model_service.set_starred_model(context, model_uuid)
return _dump({'uuid': model_uuid})
@mcp.tool(description='Resolve the default LLM model: starred model, then the available Space wizard recommendation.')
@mcp.tool(
description='Resolve the default LLM model: starred model, then the available Space wizard recommendation.'
)
async def get_default_model() -> str:
context = _authorized(Permission.RESOURCE_MANAGE)
return _dump({'uuid': await ap.llm_model_service.get_default_model(context)})
+1
View File
@@ -359,6 +359,7 @@ class Application:
async def run(self):
self.event_loop_monitor.start()
from ..utils.execution_lease import maintain_execution_leases
self.task_mgr.create_task(
maintain_execution_leases(self),
name='execution-lease-maintenance',
@@ -10,15 +10,32 @@ depends_on = None
def upgrade():
inspector = sa.inspect(op.get_bind())
for table in ('agent_run', 'monitoring_messages'):
op.add_column(table, sa.Column('execution_owner_id', sa.String(36), nullable=True))
op.add_column(table, sa.Column('execution_lease_expires_at', sa.DateTime(), nullable=True))
for column in ('execution_owner_id', 'execution_lease_expires_at'):
op.create_index(f'ix_{table}_{column}', table, [column])
if not inspector.has_table(table):
continue
columns = {c['name'] for c in inspector.get_columns(table)}
indexes = {i['name'] for i in inspector.get_indexes(table)}
for column, column_type in (
('execution_owner_id', sa.String(36)),
('execution_lease_expires_at', sa.DateTime()),
):
if column not in columns:
op.add_column(table, sa.Column(column, column_type, nullable=True))
index = f'ix_{table}_{column}'
if index not in indexes:
op.create_index(index, table, [column])
def downgrade():
inspector = sa.inspect(op.get_bind())
for table in ('agent_run', 'monitoring_messages'):
if not inspector.has_table(table):
continue
columns = {c['name'] for c in inspector.get_columns(table)}
indexes = {i['name'] for i in inspector.get_indexes(table)}
for column in ('execution_owner_id', 'execution_lease_expires_at'):
op.drop_index(f'ix_{table}_{column}', table_name=table)
op.drop_column(table, column)
if f'ix_{table}_{column}' in indexes:
op.drop_index(f'ix_{table}_{column}', table_name=table)
if column in columns:
op.drop_column(table, column)
+1 -2
View File
@@ -489,8 +489,7 @@ def _validate_scoped_statement_call(args: tuple[typing.Any, ...], kwargs: dict[s
and type(source.type) is sqlalchemy.Text
and getattr(getattr(source, 'table', None), 'name', None) in {'event_log', 'agent_run'}
and (
source.name == 'metadata_json'
or (source.table.name == 'event_log' and source.name == 'input_json')
source.name == 'metadata_json' or (source.table.name == 'event_log' and source.name == 'input_json')
)
)
if not (pipeline_json_cas or monitoring_metadata):
+3 -1
View File
@@ -449,7 +449,9 @@ class RuntimePipeline:
try:
from ..platform.processing_indicator import processing_indicator
async with processing_indicator(query.adapter, query.launcher_type.value.lower(), query.launcher_id):
async with processing_indicator(
query.adapter, query.launcher_type.value.lower(), query.launcher_id
):
return await self._process_query(query)
except asyncio.CancelledError:
lane_outcome = 'cancelled'
@@ -166,9 +166,9 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
assert isinstance(message_source.source_platform_object, DingTalkEvent)
incoming_message = message_source.source_platform_object.incoming_message
markdown_enabled = self.config.get('markdown_card', False)
text_message = platform_message.MessageChain([
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
])
text_message = platform_message.MessageChain(
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
)
content, at = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
raw = await self.bot.send_message(content, incoming_message, at)
await self._send_attachments(message, incoming_message)
@@ -206,9 +206,9 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
last_updates[message_id] = now
markdown_enabled = self.config.get('markdown_card', False)
text_message = platform_message.MessageChain([
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
])
text_message = platform_message.MessageChain(
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
)
content, _ = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
card_instance, card_instance_id = self.card_instance_id_dict[message_id]
if not content and bot_message.content:
@@ -14,14 +14,23 @@ def subscription_events(message):
headers = message.headers
action = headers.event_type or data.get('EventType') or data.get('eventType') or ''
raw = {'headers': vars(headers).copy(), 'data': data}
stamp = headers.event_born_time or data.get('TimeStamp') or data.get('EventTime') or data.get('timestamp') or headers.time or 0
stamp = (
headers.event_born_time
or data.get('TimeStamp')
or data.get('EventTime')
or data.get('timestamp')
or headers.time
or 0
)
stamp = float(stamp)
common = dict(
adapter_name=ADAPTER_NAME,
timestamp=stamp / 1000 if stamp > 10_000_000_000 else stamp,
source_platform_object=raw,
)
group_id = data.get('OpenConversationId') or data.get('openConversationId') or data.get('chatId') or data.get('ChatId')
group_id = (
data.get('OpenConversationId') or data.get('openConversationId') or data.get('chatId') or data.get('ChatId')
)
group = entities.UserGroup(id=str(group_id or ''), name=str(data.get('Title') or data.get('title') or ''))
owner_id = data.get('Owner') or data.get('owner')
if owner_id:
@@ -38,13 +47,18 @@ def subscription_events(message):
yield events.MemberJoinedEvent(group=group, member=member, inviter=operator, **common)
else:
yield events.MemberLeftEvent(
group=group, member=member, operator=operator,
is_kicked=action == 'chat_remove_member', **common,
group=group,
member=member,
operator=operator,
is_kicked=action == 'chat_remove_member',
**common,
)
elif group_id and action in {'chat_update_title', 'chat_update_owner'}:
yield events.GroupInfoUpdatedEvent(
group=group, operator=operator,
changed_fields=['name' if action == 'chat_update_title' else 'owner_id'], **common,
group=group,
operator=operator,
changed_fields=['name' if action == 'chat_update_title' else 'owner_id'],
**common,
)
else:
yield events.PlatformSpecificEvent(action=action or 'unknown', data=raw, **common)
@@ -570,7 +570,9 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
ContentCardElementRequest.builder()
.card_id(card_id)
.element_id('streaming_txt')
.request_body(ContentCardElementRequestBody.builder().content(streaming_text(content)).sequence(sequence).build())
.request_body(
ContentCardElementRequestBody.builder().content(streaming_text(content)).sequence(sequence).build()
)
.build()
)
response: ContentCardElementResponse = await self.api_client.cardkit.v1.card_element.acontent(
@@ -578,7 +580,9 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
)
if not response.success():
if self._streaming_mode_closed(response):
await self._replace_streaming_card(card_id, content, finished=is_final and not bot_message.tool_calls)
await self._replace_streaming_card(
card_id, content, finished=is_final and not bot_message.tool_calls
)
else:
raise RuntimeError(f'Lark card_element update failed: {response.code} {response.msg}')
self.card_last_update_dict[card_id] = now
@@ -46,9 +46,7 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
operator = cls._event_user(data.get('operator_id'))
if event_type == 'im.chat.member.bot.deleted_v1':
return [platform_events.BotRemovedFromGroupEvent(group=group, operator=operator, **common)]
if event_type in {
'im.chat.member.user.added_v1', 'im.chat.member.user.deleted_v1'
}:
if event_type in {'im.chat.member.user.added_v1', 'im.chat.member.user.deleted_v1'}:
events = []
for user in data.get('users') or []:
member = cls._event_user(user.get('user_id'), user.get('name') or '')
@@ -56,15 +54,25 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
continue
if event_type == 'im.chat.member.user.added_v1':
inviter = operator if operator and operator.id != member.id else None
events.append(platform_events.MemberJoinedEvent(
group=group, member=member, inviter=inviter,
join_type='invite' if inviter else None, **common,
))
events.append(
platform_events.MemberJoinedEvent(
group=group,
member=member,
inviter=inviter,
join_type='invite' if inviter else None,
**common,
)
)
else:
events.append(platform_events.MemberLeftEvent(
group=group, member=member, operator=operator,
is_kicked=operator is not None and operator.id != member.id, **common,
))
events.append(
platform_events.MemberLeftEvent(
group=group,
member=member,
operator=operator,
is_kicked=operator is not None and operator.id != member.id,
**common,
)
)
return events
if event_type == 'im.chat.updated_v1':
after = data.get('after_change') or {}
@@ -77,9 +85,14 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
group.avatar_url = after.get('avatar')
owner = cls._event_user(after.get('owner_id'))
group.owner_id = owner.id if owner else None
return [platform_events.GroupInfoUpdatedEvent(
group=group, operator=operator, changed_fields=sorted(changed), **common,
)]
return [
platform_events.GroupInfoUpdatedEvent(
group=group,
operator=operator,
changed_fields=sorted(changed),
**common,
)
]
context = message_context or {}
if event_type == 'im.message.recalled_v1':
return [platform_events.MessageDeletedEvent(message_id=data['message_id'], **context, **common)]
@@ -87,11 +100,16 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
user = cls._event_user(data.get('user_id'))
if user is None:
user = platform_entities.User(id=data.get('app_id') or '', is_bot=data.get('operator_type') == 'app')
return [platform_events.MessageReactionEvent(
message_id=data['message_id'], user=user,
reaction=(data.get('reaction_type') or {}).get('emoji_type') or '',
is_add=event_type == 'im.message.reaction.created_v1', **context, **common,
)]
return [
platform_events.MessageReactionEvent(
message_id=data['message_id'],
user=user,
reaction=(data.get('reaction_type') or {}).get('emoji_type') or '',
is_add=event_type == 'im.message.reaction.created_v1',
**context,
**common,
)
]
return []
_processed_thread_quote_cache: typing.ClassVar[dict[str, float]] = {}
@@ -7,7 +7,7 @@ def split_content(content: str) -> tuple[str, str]:
reasoning, answer = [], []
inside, offset = False, 0
for marker in re.finditer(r'</?think>', content, re.IGNORECASE):
(reasoning if inside else answer).append(content[offset:marker.start()])
(reasoning if inside else answer).append(content[offset : marker.start()])
inside = marker.group().lower() == '<think>'
offset = marker.end()
tail = content[offset:]
@@ -38,32 +38,62 @@ def build_card(content: str = '', *, streaming: bool = False, finished: bool = T
elements.append({'tag': 'markdown', 'content': streaming_text(content), 'element_id': 'streaming_txt'})
else:
if reasoning:
elements.append({
'tag': 'collapsible_panel', 'expanded': not finished,
'header': {'title': {'tag': 'plain_text', 'content': '思考过程 / Thinking'}},
'elements': [{'tag': 'markdown', 'content': reasoning}],
})
elements.append(
{
'tag': 'collapsible_panel',
'expanded': not finished,
'header': {'title': {'tag': 'plain_text', 'content': '思考过程 / Thinking'}},
'elements': [{'tag': 'markdown', 'content': reasoning}],
}
)
if answer:
elements.append({'tag': 'markdown', 'content': answer})
elements.append({
'tag': 'column_set', 'horizontal_spacing': '8px',
'columns': [
{'tag': 'column', 'width': 'weighted', 'weight': 1, 'vertical_align': 'center',
'elements': [{'tag': 'markdown', 'content': '内容由 AI 生成 / AI-generated',
'text_size': 'notation'}]},
{'tag': 'column', 'width': 'auto', 'vertical_align': 'center',
'elements': [{'tag': 'markdown', 'content': '[LangBot](https://langbot.app?utm_source=feishu&utm_medium=bot_card&utm_campaign=langbot)', 'text_size': 'notation',
'text_align': 'right',
'icon': {'tag': 'custom_icon',
'img_key': 'img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg'}}]},
],
})
elements.append(
{
'tag': 'column_set',
'horizontal_spacing': '8px',
'columns': [
{
'tag': 'column',
'width': 'weighted',
'weight': 1,
'vertical_align': 'center',
'elements': [
{'tag': 'markdown', 'content': '内容由 AI 生成 / AI-generated', 'text_size': 'notation'}
],
},
{
'tag': 'column',
'width': 'auto',
'vertical_align': 'center',
'elements': [
{
'tag': 'markdown',
'content': '[LangBot](https://langbot.app?utm_source=feishu&utm_medium=bot_card&utm_campaign=langbot)',
'text_size': 'notation',
'text_align': 'right',
'icon': {
'tag': 'custom_icon',
'img_key': 'img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg',
},
}
],
},
],
}
)
config = {'update_multi': True}
if streaming:
config.update(streaming_mode=True, streaming_config={
'print_step': {'default': 3}, 'print_frequency_ms': {'default': 40}, 'print_strategy': 'fast',
})
config.update(
streaming_mode=True,
streaming_config={
'print_step': {'default': 3},
'print_frequency_ms': {'default': 40},
'print_strategy': 'fast',
},
)
return {
'schema': '2.0', 'config': config,
'schema': '2.0',
'config': config,
'body': {'direction': 'vertical', 'padding': '12px 12px 12px 12px', 'elements': elements},
}
@@ -55,7 +55,9 @@ class WecomCSEventConverter(abstract_platform_adapter.AbstractEventConverter):
}
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 = {
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
+6 -6
View File
@@ -613,9 +613,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
incoming_message = event.incoming_message
markdown_enabled = self.config.get('markdown_card', False)
text_message = platform_message.MessageChain([
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
])
text_message = platform_message.MessageChain(
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
)
content, at = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
await self.bot.send_message(content, incoming_message, at)
for part in message:
@@ -647,9 +647,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if (msg_seq - 1) % 8 == 0 or is_final:
markdown_enabled = self.config.get('markdown_card', False)
text_message = platform_message.MessageChain([
part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))
])
text_message = platform_message.MessageChain(
[part for part in message if not isinstance(part, (platform_message.Image, platform_message.File))]
)
content, at = await DingTalkMessageConverter.yiri2target(text_message, markdown_enabled)
if not content and bot_message.content:
content = bot_message.content # 兼容直接传入content的情况
+5 -2
View File
@@ -66,7 +66,6 @@ class SlackEventConverter(abstract_platform_adapter.AbstractEventConverter):
yiri_chain = await OmniMessageConverter.target2yiri(event, bot.bot_token)
if event.type == 'channel':
sender = platform_entities.GroupMember(
id=event.user_id,
member_name=str(event.sender_name),
@@ -108,7 +107,10 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
raise command_errors.ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
bot = SlackClient(
bot_token=config['bot_token'], signing_secret=config.get('signing_secret', ''), logger=logger, unified_mode=True
bot_token=config['bot_token'],
signing_secret=config.get('signing_secret', ''),
logger=logger,
unified_mode=True,
)
super().__init__(
@@ -174,6 +176,7 @@ class SlackAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if self.config.get('socket_mode', False):
await self.bot.run_socket(self.config['app_token'])
return
# 统一 webhook 模式下,不启动独立的 Quart 应用
# 保持运行但不启动独立端口
async def keep_alive():
@@ -272,11 +272,14 @@ class SessionManager:
"""获取会话"""
session_key, execution_context = _query_session_key(query)
from ...agent.runner.context_reset import get_reset_generation
generation = None
persistence = getattr(self.ap, 'persistence_mgr', None)
if persistence is not None:
generation = await get_reset_generation(
persistence.get_db_engine(), execution_context.workspace_uuid, query.bot_uuid,
persistence.get_db_engine(),
execution_context.workspace_uuid,
query.bot_uuid,
f'{str(query.launcher_type.value).lower()}_{query.launcher_id}',
)
now = time.monotonic()
+75 -31
View File
@@ -41,44 +41,87 @@ async def reconcile_execution_leases(engine, workspace_uuid, *, now=None, owner_
conditions.extend([AgentRun.queue_name.is_(None), AgentRun.claimed_by_runtime_id.is_(None)])
async with engine.begin() as connection:
if engine.dialect.name == 'postgresql':
await connection.execute(sa.text("SELECT set_config('langbot.workspace_uuid', :workspace, true)"),
{'workspace': workspace_uuid})
await connection.execute(sa.update(model).where(
*conditions, model.execution_owner_id == owner_id,
model.execution_lease_expires_at > now,
).values(execution_lease_expires_at=now + datetime.timedelta(seconds=LEASE_SECONDS)))
await connection.execute(
sa.text("SELECT set_config('langbot.workspace_uuid', :workspace, true)"),
{'workspace': workspace_uuid},
)
await connection.execute(
sa.update(model)
.where(
*conditions,
model.execution_owner_id == owner_id,
model.execution_lease_expires_at > now,
)
.values(execution_lease_expires_at=now + datetime.timedelta(seconds=LEASE_SECONDS))
)
expired = [*conditions, model.execution_lease_expires_at <= now]
rows = (await connection.execute(sa.select(identity, scope).where(*expired)
.order_by(model.execution_lease_expires_at).limit(500))).all()
rows = (
await connection.execute(
sa.select(identity, scope).where(*expired).order_by(model.execution_lease_expires_at).limit(500)
)
).all()
if not rows:
continue
values = {'status': 'failed', 'status_reason': INTERRUPTED_REASON,
'finished_at': now, 'updated_at': now} if model is AgentRun else {
'status': 'error', 'level': 'error',
}
values = (
{'status': 'failed', 'status_reason': INTERRUPTED_REASON, 'finished_at': now, 'updated_at': now}
if model is AgentRun
else {
'status': 'error',
'level': 'error',
}
)
if model is MonitoringMessage:
messages = (await connection.execute(sa.select(model).where(
model.id.in_([row[0] for row in rows]),
))).mappings().all()
messages = (
(
await connection.execute(
sa.select(model).where(
model.id.in_([row[0] for row in rows]),
)
)
)
.mappings()
.all()
)
for message in messages:
result = await connection.execute(sa.update(model).where(
*expired, model.id == message['id'],
).values(**values))
result = await connection.execute(
sa.update(model)
.where(
*expired,
model.id == message['id'],
)
.values(**values)
)
changed += result.rowcount
if result.rowcount:
await connection.execute(sa.insert(MonitoringError).values(
id=str(uuid.uuid4()), timestamp=now,
error_type='ExecutionInterrupted', error_message=INTERRUPTED_REASON,
message_id=message['id'],
**{key: message[key] for key in (
'workspace_uuid', 'bot_id', 'bot_name', 'pipeline_id',
'pipeline_name', 'session_id',
)},
))
await connection.execute(
sa.insert(MonitoringError).values(
id=str(uuid.uuid4()),
timestamp=now,
error_type='ExecutionInterrupted',
error_message=INTERRUPTED_REASON,
message_id=message['id'],
**{
key: message[key]
for key in (
'workspace_uuid',
'bot_id',
'bot_name',
'pipeline_id',
'pipeline_name',
'session_id',
)
},
)
)
else:
result = await connection.execute(sa.update(model).where(
*expired, identity.in_([row[0] for row in rows]),
).values(**values))
result = await connection.execute(
sa.update(model)
.where(
*expired,
identity.in_([row[0] for row in rows]),
)
.values(**values)
)
changed += result.rowcount
workspaces.update(row[1] for row in rows)
for workspace in workspaces:
@@ -92,7 +135,8 @@ async def maintain_execution_leases(ap):
count = 0
for binding in await ap.workspace_service.list_active_execution_bindings():
count += await reconcile_execution_leases(
ap.persistence_mgr.get_db_engine(), binding.workspace_uuid,
ap.persistence_mgr.get_db_engine(),
binding.workspace_uuid,
)
if count:
ap.logger.info(f'Marked {count} interrupted executions as failed after host lease expiry')
Generated
+2 -6
View File
@@ -2185,7 +2185,7 @@ requires-dist = [
{ name = "gewechat-client", specifier = ">=0.1.5" },
{ name = "html2text", specifier = ">=2024.2.26" },
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
{ name = "langbot-plugin", specifier = "==0.7.11rc1" },
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=8c29bc0787be3b514fb379c5154dfc5eaa561cbd" },
{ name = "langchain", specifier = ">=1.3.9" },
{ name = "langchain-core", specifier = ">=1.3.3" },
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
@@ -2256,7 +2256,7 @@ dev = [
[[package]]
name = "langbot-plugin"
version = "0.7.11rc1"
source = { registry = "https://pypi.org/simple" }
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=8c29bc0787be3b514fb379c5154dfc5eaa561cbd#8c29bc0787be3b514fb379c5154dfc5eaa561cbd" }
dependencies = [
{ name = "aiofiles" },
{ name = "aiohttp" },
@@ -2276,10 +2276,6 @@ dependencies = [
{ name = "watchdog" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/e4/882ed1fd15067e3ca778da2850833ad61716741a6f63d3328028496536b8/langbot_plugin-0.7.11rc1.tar.gz", hash = "sha256:2ef8ba6f1998da365c7ade53573b8cd65f1a660f772ed97385ca445f2b0faa58", size = 690128, upload-time = "2026-10-08T07:49:33.789Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/84/21f04d8441c47c910a345a90d5ca5ecc1222269bb951f015f404996b7a36/langbot_plugin-0.7.11rc1-py3-none-any.whl", hash = "sha256:73227613ef827cfcccb04c2535034139ce5315c54dcd32ac01fcbca2150b7d13", size = 442833, upload-time = "2026-10-08T07:49:32.305Z" },
]
[[package]]
name = "langchain"
+32 -21
View File
@@ -63,31 +63,42 @@ export default function AgentDetailContent({
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const runnerStatusKey = JSON.stringify([
userInfo?.account_uuid, currentWorkspace?.workspace.uuid, id, i18n.language,
userInfo?.account_uuid,
currentWorkspace?.workspace.uuid,
id,
i18n.language,
]);
const [runnerStatusState, setRunnerStatusState] = useState<{
key: string; status: RunnerStatus;
key: string;
status: RunnerStatus;
} | null>(null);
const runnerStatus = runnerStatusState?.key === runnerStatusKey
? runnerStatusState.status : runnerStatusCache.get(runnerStatusKey) ?? null;
const updateRunnerStatus = useCallback((status: RunnerStatus) => {
setRunnerStatusState((previous) => {
if (status.tone === 'neutral') {
const cached = previous?.key === runnerStatusKey
? previous.status : runnerStatusCache.get(runnerStatusKey);
if (cached) return { key: runnerStatusKey, status: cached };
const runnerStatus =
runnerStatusState?.key === runnerStatusKey
? runnerStatusState.status
: (runnerStatusCache.get(runnerStatusKey) ?? null);
const updateRunnerStatus = useCallback(
(status: RunnerStatus) => {
setRunnerStatusState((previous) => {
if (status.tone === 'neutral') {
const cached =
previous?.key === runnerStatusKey
? previous.status
: runnerStatusCache.get(runnerStatusKey);
if (cached) return { key: runnerStatusKey, status: cached };
}
return { key: runnerStatusKey, status };
});
if (status.tone !== 'neutral' && !formDirty) {
runnerStatusCache.delete(runnerStatusKey);
runnerStatusCache.set(runnerStatusKey, status);
if (runnerStatusCache.size > 100) {
const oldest = runnerStatusCache.keys().next().value;
if (oldest !== undefined) runnerStatusCache.delete(oldest);
}
}
return { key: runnerStatusKey, status };
});
if (status.tone !== 'neutral' && !formDirty) {
runnerStatusCache.delete(runnerStatusKey);
runnerStatusCache.set(runnerStatusKey, status);
if (runnerStatusCache.size > 100) {
const oldest = runnerStatusCache.keys().next().value;
if (oldest !== undefined) runnerStatusCache.delete(oldest);
}
}
}, [runnerStatusKey, formDirty]);
},
[runnerStatusKey, formDirty],
);
const [availableEventTypes, setAvailableEventTypes] = useState<string[]>([
'message.received',
]);
@@ -30,7 +30,10 @@ import PluginProcessorTrace, {
ProcessorPayload,
} from './components/PluginProcessorTrace';
import ProcessorRunList from './components/ProcessorRunList';
import { processorRunDuration, formatRunDuration } from './components/processor-run-timing';
import {
processorRunDuration,
formatRunDuration,
} from './components/processor-run-timing';
import PluginProcessorSettings from './components/PluginProcessorSettings';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
@@ -55,13 +58,19 @@ export default function PluginProcessorDetailContent({
}) {
const { t } = useTranslation();
const [, setSearchParams] = useSearchParams();
const setActiveTab = useCallback((tab: string) => {
setSearchParams((current) => {
const next = new URLSearchParams(current);
next.set('tab', tab);
return next;
}, { preventScrollReset: true });
}, [setSearchParams]);
const setActiveTab = useCallback(
(tab: string) => {
setSearchParams(
(current) => {
const next = new URLSearchParams(current);
next.set('tab', tab);
return next;
},
{ preventScrollReset: true },
);
},
[setSearchParams],
);
const [platformTools, setPlatformTools] = useState<AgentPlatformTool[]>([]);
const toolLabels = Object.fromEntries(
platformTools.map((tool) => [tool.name, extractI18nObject(tool.label)]),
@@ -83,7 +92,8 @@ export default function PluginProcessorDetailContent({
const [needsInitialSave, setNeedsInitialSave] = useState(
Boolean(agent.component_ref) && !agent.supported_event_patterns?.length,
);
const dirty = needsInitialSave ||
const dirty =
needsInitialSave ||
componentRef !== savedConfig.componentRef ||
!isEqual(parameters, savedConfig.parameters);
const [runs, setRuns] = useState<ProcessorRun[]>([]);
@@ -161,22 +171,25 @@ export default function PluginProcessorDetailContent({
[t, available, hasParameters, canOperate, setActiveTab],
);
const applyInstalledProcessor = useCallback((component: RunnerDescriptor) => {
setComponents((current) => [
...current.filter((item) => item.id !== component.id),
component,
]);
setComponentRef(component.id);
setParameters(
Object.fromEntries(
(component.config_schema ?? [])
.filter((field) => field.default !== undefined)
.map((field) => [field.name, field.default]),
),
);
setActiveTab('config');
validate.current = null;
}, [setActiveTab]);
const applyInstalledProcessor = useCallback(
(component: RunnerDescriptor) => {
setComponents((current) => [
...current.filter((item) => item.id !== component.id),
component,
]);
setComponentRef(component.id);
setParameters(
Object.fromEntries(
(component.config_schema ?? [])
.filter((field) => field.default !== undefined)
.map((field) => [field.name, field.default]),
),
);
setActiveTab('config');
validate.current = null;
},
[setActiveTab],
);
const load = useCallback(async () => {
setLoading(true);
@@ -364,7 +377,11 @@ export default function PluginProcessorDetailContent({
const logsContent = (
<div className="flex h-full min-h-0 flex-col gap-4 p-4">
{failed && (
<LoadErrorState compact title={t('agents.eventProcessor.loadError')} onRetry={refreshLatestRun} />
<LoadErrorState
compact
title={t('agents.eventProcessor.loadError')}
onRetry={refreshLatestRun}
/>
)}
<div className="flex shrink-0 items-center justify-between gap-2">
<span className="text-sm font-medium">
@@ -382,104 +399,112 @@ export default function PluginProcessorDetailContent({
</Button>
</div>
<div className="grid min-h-0 flex-1 grid-cols-1 gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
{runs.length > 0 && (
<ProcessorRunList
className="max-h-64 overflow-y-auto rounded-lg border lg:max-h-none lg:h-full"
runs={runs}
selectedId={selected?.run_id}
onSelect={(run) => void openRun(run)}
footer={
cursor !== null ? (
<Button
className="w-full"
variant="ghost"
disabled={pagingRuns}
onClick={() => void loadMoreRuns()}
>
{t('agents.eventProcessor.loadMore')}
</Button>
) : undefined
}
/>
)}
<ScrollArea className="min-h-0 flex-1">
<div className="space-y-4 pr-2">
{!selected ? (
<Alert>
<AlertDescription>
{loading
? t('common.loading')
: t('agents.eventProcessor.noRuns')}
<Button asChild variant="link" className="h-auto px-0">
<Link to="/home/bots">
{t('agents.eventProcessor.bindBot')}
</Link>
</Button>
</AlertDescription>
</Alert>
) : (
<>
<div className="flex flex-wrap items-center justify-between gap-3 border-b pb-3">
<div className="space-y-1.5">
<p className="font-medium">
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
</p>
<p className="text-xs text-muted-foreground">
{new Date(selected.created_at * 1000).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-3">
{processorRunDuration(selected) !== null && (
<span className="flex items-center gap-1.5 text-xs tabular-nums text-muted-foreground">
<Clock3 className="size-3.5" />
{formatRunDuration(processorRunDuration(selected)!)}
</span>
)}
<Badge status={selected.status}>
{t(`agents.eventProcessor.status_${selected.status}`, { defaultValue: selected.status })}
</Badge>
</div>
</div>
<div className="space-y-2 rounded-lg border p-3">
<h3 className="text-sm font-medium">{t('agents.eventProcessor.input')}</h3>
<ProcessorPayload
title={t('agents.monitoring.eventData')}
value={selected.metadata.input_event}
/>
{selected.metadata.delivery != null && (
<ProcessorPayload
title={t('agents.eventProcessor.destination')}
value={selected.metadata.delivery}
/>
)}
</div>
<section className="space-y-3">
<h2 className="flex items-center gap-2 text-sm font-medium">
<Puzzle className="size-4 text-amber-600 dark:text-amber-400" />
{t('agents.eventProcessor.type')} · {t('agents.eventProcessor.logsTab')}
</h2>
<PluginProcessorTrace events={events} toolLabels={toolLabels} />
</section>
{selected.status === 'failed' && selected.status_reason && (
<Alert variant="destructive">
<AlertDescription className="break-words">
{selected.status_reason}
</AlertDescription>
</Alert>
)}
{eventCursor !== null && (
{runs.length > 0 && (
<ProcessorRunList
className="max-h-64 overflow-y-auto rounded-lg border lg:max-h-none lg:h-full"
runs={runs}
selectedId={selected?.run_id}
onSelect={(run) => void openRun(run)}
footer={
cursor !== null ? (
<Button
className="w-full"
variant="ghost"
disabled={pagingEvents}
onClick={() => void loadMoreEvents()}
disabled={pagingRuns}
onClick={() => void loadMoreRuns()}
>
{t('agents.eventProcessor.loadMore')}
</Button>
)}
</>
)}
</div>
</ScrollArea>
) : undefined
}
/>
)}
<ScrollArea className="min-h-0 flex-1">
<div className="space-y-4 pr-2">
{!selected ? (
<Alert>
<AlertDescription>
{loading
? t('common.loading')
: t('agents.eventProcessor.noRuns')}
<Button asChild variant="link" className="h-auto px-0">
<Link to="/home/bots">
{t('agents.eventProcessor.bindBot')}
</Link>
</Button>
</AlertDescription>
</Alert>
) : (
<>
<div className="flex flex-wrap items-center justify-between gap-3 border-b pb-3">
<div className="space-y-1.5">
<p className="font-medium">
{eventPatternLabel(selected.metadata.event_type ?? '', t)}
</p>
<p className="text-xs text-muted-foreground">
{new Date(selected.created_at * 1000).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-3">
{processorRunDuration(selected) !== null && (
<span className="flex items-center gap-1.5 text-xs tabular-nums text-muted-foreground">
<Clock3 className="size-3.5" />
{formatRunDuration(processorRunDuration(selected)!)}
</span>
)}
<Badge status={selected.status}>
{t(`agents.eventProcessor.status_${selected.status}`, {
defaultValue: selected.status,
})}
</Badge>
</div>
</div>
<div className="space-y-2 rounded-lg border p-3">
<h3 className="text-sm font-medium">
{t('agents.eventProcessor.input')}
</h3>
<ProcessorPayload
title={t('agents.monitoring.eventData')}
value={selected.metadata.input_event}
/>
{selected.metadata.delivery != null && (
<ProcessorPayload
title={t('agents.eventProcessor.destination')}
value={selected.metadata.delivery}
/>
)}
</div>
<section className="space-y-3">
<h2 className="flex items-center gap-2 text-sm font-medium">
<Puzzle className="size-4 text-amber-600 dark:text-amber-400" />
{t('agents.eventProcessor.type')} ·{' '}
{t('agents.eventProcessor.logsTab')}
</h2>
<PluginProcessorTrace
events={events}
toolLabels={toolLabels}
/>
</section>
{selected.status === 'failed' && selected.status_reason && (
<Alert variant="destructive">
<AlertDescription className="break-words">
{selected.status_reason}
</AlertDescription>
</Alert>
)}
{eventCursor !== null && (
<Button
variant="ghost"
disabled={pagingEvents}
onClick={() => void loadMoreEvents()}
>
{t('agents.eventProcessor.loadMore')}
</Button>
)}
</>
)}
</div>
</ScrollArea>
</div>
</div>
);
@@ -489,10 +514,13 @@ export default function PluginProcessorDetailContent({
return (
<>
<form id="event-processor-form" onSubmit={(event) => {
event.preventDefault();
void save();
}} />
<form
id="event-processor-form"
onSubmit={(event) => {
event.preventDefault();
void save();
}}
/>
<GuidedTour
enabled={canManage}
storageKey="langbot_plugin_processor_setup_guide_v1"
@@ -502,7 +530,10 @@ export default function PluginProcessorDetailContent({
<ProcessorDetailWorkbench
title={`${agent.emoji || '🧩'} ${agent.name}`}
titleBadge={
<Badge variant="outline" className="gap-1.5 border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-300">
<Badge
variant="outline"
className="gap-1.5 border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-300"
>
<Puzzle className="size-3.5" />
{t('agents.eventProcessor.type')}
</Badge>
@@ -551,38 +582,40 @@ export default function PluginProcessorDetailContent({
) : undefined
}
configTitle={t('agents.eventProcessor.type')}
configIcon={<Puzzle className="size-4 text-amber-600 dark:text-amber-400" />}
configIcon={
<Puzzle className="size-4 text-amber-600 dark:text-amber-400" />
}
monitoring={{
label: t('pipelines.monitoring.title'),
workbenchLabel: t('pipelines.monitoring.workbench'),
content: logsContent,
}}
configContent={
<div className="h-full overflow-y-auto">
{!component ? (
<p className="text-sm text-muted-foreground">
{t('agents.eventProcessor.selectComponent')}
</p>
) : component.config_schema.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t('agents.eventProcessor.noSettings')}
</p>
) : (
<fieldset disabled={!canManage || saving}>
<DynamicFormComponent
key={componentRef}
itemConfigList={component.config_schema}
initialValues={parameters}
onSubmit={(values) =>
setParameters(values as Record<string, unknown>)
}
onValidate={(fn) => {
validate.current = fn;
}}
/>
</fieldset>
)}
</div>
<div className="h-full overflow-y-auto">
{!component ? (
<p className="text-sm text-muted-foreground">
{t('agents.eventProcessor.selectComponent')}
</p>
) : component.config_schema.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t('agents.eventProcessor.noSettings')}
</p>
) : (
<fieldset disabled={!canManage || saving}>
<DynamicFormComponent
key={componentRef}
itemConfigList={component.config_schema}
initialValues={parameters}
onSubmit={(values) =>
setParameters(values as Record<string, unknown>)
}
onValidate={(fn) => {
validate.current = fn;
}}
/>
</fieldset>
)}
</div>
}
debugTitle={canOperate ? t('agents.debugTab') : undefined}
debugDescription={t('agents.eventProcessor.debugNotice')}
@@ -217,7 +217,8 @@ export default function AgentApiToolPicker({
if (checked) selection.add(tool.name);
else selection.delete(tool.name);
}
if (tools.some((tool) => tool.kind === 'platform')) onPlatformChange([...platform]);
if (tools.some((tool) => tool.kind === 'platform'))
onPlatformChange([...platform]);
if (tools.some((tool) => tool.kind === 'host')) onHostChange([...host]);
};
@@ -294,8 +295,16 @@ export default function AgentApiToolPicker({
<span>{groupLabel(group)}</span>
<label className="flex cursor-pointer items-center gap-2">
<Checkbox
checked={groupTools.every(isSelected) ? true : groupTools.some(isSelected) ? 'indeterminate' : false}
onCheckedChange={(checked) => setGroup(groupTools, checked === true)}
checked={
groupTools.every(isSelected)
? true
: groupTools.some(isSelected)
? 'indeterminate'
: false
}
onCheckedChange={(checked) =>
setGroup(groupTools, checked === true)
}
aria-label={`${groupLabel(group)} · ${t('agents.apiToolsSelectAll')}`}
/>
{t('agents.apiToolsSelectAll')}
@@ -49,15 +49,31 @@ export default function AgentCreateContent({
const createdPipeline = useRef<string | null>(null);
const loadMetadata = useCallback(async () => {
setLoading(true);
try { setMetadata(await httpClient.getAgentMetadata()); }
catch { toast.error(t('agents.createError')); }
finally { setLoading(false); }
try {
setMetadata(await httpClient.getAgentMetadata());
} catch {
toast.error(t('agents.createError'));
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => { void loadMetadata(); }, [loadMetadata]);
const runnerOptions = kind === 'event_processor'
? (metadata?.event_processors ?? []).map((item) => ({ name: item.id, label: { en_US: item.id, zh_Hans: item.id, ...item.label } }))
: metadata?.runner_config?.stages.find((stage) => stage.name === 'runner')?.config.find((item) => item.name === 'id')?.options ?? [];
const selectorLabel = t(kind === 'event_processor' ? 'agents.selectProcessorPlugin' : 'agents.selectAgentRunner');
useEffect(() => {
void loadMetadata();
}, [loadMetadata]);
const runnerOptions =
kind === 'event_processor'
? (metadata?.event_processors ?? []).map((item) => ({
name: item.id,
label: { en_US: item.id, zh_Hans: item.id, ...item.label },
}))
: (metadata?.runner_config?.stages
.find((stage) => stage.name === 'runner')
?.config.find((item) => item.name === 'id')?.options ?? []);
const selectorLabel = t(
kind === 'event_processor'
? 'agents.selectProcessorPlugin'
: 'agents.selectAgentRunner',
);
const formSchema = z.object({
name: z.string().min(1, { message: t('agents.nameRequired') }),
emoji: z.string().optional(),
@@ -75,9 +91,13 @@ export default function AgentCreateContent({
setRunner(value);
if (!form.getValues('name').trim()) {
const option = runnerOptions.find((item) => item.name === value);
const displayName = name || (option ? extractI18nObject(option.label) : '');
const displayName =
name || (option ? extractI18nObject(option.label) : '');
if (displayName) {
form.setValue('name', displayName, { shouldDirty: true, shouldValidate: true });
form.setValue('name', displayName, {
shouldDirty: true,
shouldValidate: true,
});
}
}
}
@@ -102,43 +122,91 @@ export default function AgentCreateContent({
}
async function handleSubmit(values: FormValues) {
if (!runner) { toast.error(selectorLabel); return; }
const runnerStage = metadata?.runner_config?.stages.find((stage) => stage.name === runner);
if (!runner) {
toast.error(selectorLabel);
return;
}
const runnerStage = metadata?.runner_config?.stages.find(
(stage) => stage.name === runner,
);
const parameters = runnerStage ? getDefaultValues(runnerStage.config) : {};
const config = { runner: { id: runner }, runner_config: { [runner]: parameters } };
const config = {
runner: { id: runner },
runner_config: { [runner]: parameters },
};
try {
if (kind === 'pipeline' && runnerStage?.config.some((field) => ['llm-model-selector', 'select-llm-model', 'model-fallback-selector'].includes(field.type))) {
if (
kind === 'pipeline' &&
runnerStage?.config.some((field) =>
[
'llm-model-selector',
'select-llm-model',
'model-fallback-selector',
].includes(field.type),
)
) {
const { uuid } = await httpClient.getDefaultModel();
if (uuid) {
for (const field of runnerStage.config) {
if (['llm-model-selector', 'select-llm-model'].includes(field.type) && !parameters[field.name]) parameters[field.name] = uuid;
if (field.type === 'model-fallback-selector' && !parameters[field.name]?.primary) {
parameters[field.name] = { ...parameters[field.name], primary: uuid, fallbacks: parameters[field.name]?.fallbacks ?? [] };
if (
['llm-model-selector', 'select-llm-model'].includes(field.type) &&
!parameters[field.name]
)
parameters[field.name] = uuid;
if (
field.type === 'model-fallback-selector' &&
!parameters[field.name]?.primary
) {
parameters[field.name] = {
...parameters[field.name],
primary: uuid,
fallbacks: parameters[field.name]?.fallbacks ?? [],
};
}
}
}
}
if (kind === 'pipeline') {
// Keep the created ID if configuration fails, so retry does not create duplicates.
const uuid = createdPipeline.current ?? (await httpClient.createAgent({ kind, name: values.name, description: '', emoji: values.emoji })).uuid;
const uuid =
createdPipeline.current ??
(
await httpClient.createAgent({
kind,
name: values.name,
description: '',
emoji: values.emoji,
})
).uuid;
createdPipeline.current = uuid;
const { pipeline } = await httpClient.getPipeline(uuid);
await httpClient.updatePipeline(uuid, {
name: values.name, emoji: values.emoji,
config: { ...pipeline.config, ai: { ...pipeline.config.ai, ...config } },
name: values.name,
emoji: values.emoji,
config: {
...pipeline.config,
ai: { ...pipeline.config.ai, ...config },
},
});
toast.success(t('agents.createSuccess'));
onCreated(uuid);
} else {
const response = await httpClient.createAgent({
kind, name: values.name, description: '', emoji: values.emoji,
component_ref: runner, ...(kind === 'event_processor' ? {} : { config }),
kind,
name: values.name,
description: '',
emoji: values.emoji,
component_ref: runner,
...(kind === 'event_processor' ? {} : { config }),
});
toast.success(t('agents.createSuccess'));
onCreated(response.uuid);
}
} catch (error) {
toast.error(t('agents.createError') + String((error as { msg?: string }).msg ?? error));
toast.error(
t('agents.createError') +
String((error as { msg?: string }).msg ?? error),
);
}
}
const typeOptions = [
@@ -163,11 +231,17 @@ export default function AgentCreateContent({
];
return (
<div className="flex h-full flex-col">
<div className={embedded ? 'order-last flex justify-end border-t pt-4 shrink-0' : 'flex items-center justify-between pb-4 shrink-0'}>
<div
className={
embedded
? 'order-last flex justify-end border-t pt-4 shrink-0'
: 'flex items-center justify-between pb-4 shrink-0'
}
>
{!embedded && (
<h1 className="text-xl font-semibold">
{t('agents.eventProcessor.createPageTitle')}
</h1>
<h1 className="text-xl font-semibold">
{t('agents.eventProcessor.createPageTitle')}
</h1>
)}
<Button
type="submit"
@@ -193,9 +267,11 @@ export default function AgentCreateContent({
>
{t('agents.chooseType')}
</h2>
{!embedded && <p className="mt-1 text-sm text-muted-foreground">
{t('agents.chooseTypeDescription')}
</p>}
{!embedded && (
<p className="mt-1 text-sm text-muted-foreground">
{t('agents.chooseTypeDescription')}
</p>
)}
</div>
<ToggleGroup
@@ -208,31 +284,36 @@ export default function AgentCreateContent({
spacing={3}
className="grid w-full gap-3 sm:grid-cols-2 lg:grid-cols-1"
>
{typeOptions.filter((option) => !allowedKinds || allowedKinds.includes(option.kind)).map((option) => {
const Icon = option.icon;
return (
<div key={option.kind} className="relative w-full">
<ToggleGroupItem
value={option.kind}
data-processor-kind={option.kind}
aria-label={`${option.title} ${option.description}`}
className="h-auto min-h-28 w-full items-start justify-start gap-3 rounded-lg border px-4 py-4 text-left whitespace-normal shadow-none hover:bg-muted/40 data-[state=on]:border-[#2288ee]/50 data-[state=on]:bg-blue-50/60 data-[state=on]:text-foreground data-[state=on]:shadow-none dark:data-[state=on]:border-blue-500/50 dark:data-[state=on]:bg-blue-500/10"
>
<span className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-background text-[#2288ee] shadow-xs">
<Icon className="size-4" />
</span>
<span className="min-w-0 space-y-1.5">
<span className="block text-sm font-medium text-foreground">
{option.title}
</span>
<span className="block text-sm font-normal leading-relaxed text-muted-foreground">
{option.description}
</span>
</span>
</ToggleGroupItem>
</div>
);
})}
{typeOptions
.filter(
(option) =>
!allowedKinds || allowedKinds.includes(option.kind),
)
.map((option) => {
const Icon = option.icon;
return (
<div key={option.kind} className="relative w-full">
<ToggleGroupItem
value={option.kind}
data-processor-kind={option.kind}
aria-label={`${option.title} ${option.description}`}
className="h-auto min-h-28 w-full items-start justify-start gap-3 rounded-lg border px-4 py-4 text-left whitespace-normal shadow-none hover:bg-muted/40 data-[state=on]:border-[#2288ee]/50 data-[state=on]:bg-blue-50/60 data-[state=on]:text-foreground data-[state=on]:shadow-none dark:data-[state=on]:border-blue-500/50 dark:data-[state=on]:bg-blue-500/10"
>
<span className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-background text-[#2288ee] shadow-xs">
<Icon className="size-4" />
</span>
<span className="min-w-0 space-y-1.5">
<span className="block text-sm font-medium text-foreground">
{option.title}
</span>
<span className="block text-sm font-normal leading-relaxed text-muted-foreground">
{option.description}
</span>
</span>
</ToggleGroupItem>
</div>
);
})}
</ToggleGroup>
</section>
@@ -262,12 +343,43 @@ export default function AgentCreateContent({
disabled={loading || form.formState.isSubmitting}
installScope={`processor-create-${kind === 'event_processor' ? 'event' : 'agent'}`}
onInstalled={(installed) => {
setMetadata((previous) => previous ? { ...previous, runner_config: installed.configTab } : previous);
selectRunner(installed.runner.name, extractI18nObject(installed.runner.label));
setMetadata((previous) =>
previous
? {
...previous,
runner_config: installed.configTab,
}
: previous,
);
selectRunner(
installed.runner.name,
extractI18nObject(installed.runner.label),
);
}}
onEventProcessorInstalled={(component) => {
setMetadata((previous) => previous ? { ...previous, event_processors: [...(previous.event_processors ?? []).filter((item) => item.id !== component.id), component] } : previous);
selectRunner(component.id, extractI18nObject({ en_US: component.id, zh_Hans: component.id, ...component.label }));
setMetadata((previous) =>
previous
? {
...previous,
event_processors: [
...(
previous.event_processors ?? []
).filter(
(item) => item.id !== component.id,
),
component,
],
}
: previous,
);
selectRunner(
component.id,
extractI18nObject({
en_US: component.id,
zh_Hans: component.id,
...component.label,
}),
);
}}
/>
</div>
@@ -305,15 +417,19 @@ export default function AgentCreateContent({
)}
/>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
<Card className={embedded ? 'min-h-[360px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[480px]' : 'min-h-[600px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[680px]'}>
<Card
className={
embedded
? 'min-h-[360px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[480px]'
: 'min-h-[600px] overflow-hidden py-0 dark:border-white/16 lg:min-h-[680px]'
}
>
<CardContent className="flex h-full items-center p-0">
<ProcessorTypeDiagram kind={kind} />
</CardContent>
@@ -212,8 +212,13 @@ export default function AgentMonitoringTab({
(step) => step.kind === 'tool' || step.text.trim() || step.reasoning.trim(),
);
const toolSteps = steps.filter((step) => step.kind === 'tool');
const sentCount = toolSteps.filter((step) => isReplyStep(step) && !isMockStep(step) && step.status === 'completed').length;
const sendFailed = toolSteps.some((step) => isReplyStep(step) && step.status === 'failed');
const sentCount = toolSteps.filter(
(step) =>
isReplyStep(step) && !isMockStep(step) && step.status === 'completed',
).length;
const sendFailed = toolSteps.some(
(step) => isReplyStep(step) && step.status === 'failed',
);
const duration = selected ? processorRunDuration(selected) : null;
const failureReason =
selected?.status_reason ||
@@ -264,7 +269,11 @@ export default function AgentMonitoringTab({
</Button>
</div>
{listError && (
<LoadErrorState compact title={t('monitoring.loadError')} onRetry={refresh} />
<LoadErrorState
compact
title={t('monitoring.loadError')}
onRetry={refresh}
/>
)}
<div className="grid min-h-0 flex-1 gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<section
@@ -383,17 +392,32 @@ export default function AgentMonitoringTab({
</div>
<div className="grid grid-cols-1 divide-y rounded-lg border bg-muted/20 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
<div className="space-y-1 p-4">
<p className="text-xs text-muted-foreground">{t('agents.monitoring.runStatus')}</p>
<p className="text-sm font-semibold">{stateLabel(selected)}</p>
<p className="text-xs text-muted-foreground">
{t('agents.monitoring.runStatus')}
</p>
<p className="text-sm font-semibold">
{stateLabel(selected)}
</p>
</div>
<div className="space-y-1 p-4">
<p className="text-xs text-muted-foreground">{t('agents.monitoring.toolCalls')}</p>
<p className="text-sm font-semibold tabular-nums">{toolSteps.length}</p>
<p className="text-xs text-muted-foreground">
{t('agents.monitoring.toolCalls')}
</p>
<p className="text-sm font-semibold tabular-nums">
{toolSteps.length}
</p>
</div>
<div className="space-y-1 p-4">
<p className="text-xs text-muted-foreground">{t('agents.monitoring.delivery')}</p>
<p className={`text-sm font-semibold ${sendFailed ? 'text-destructive' : ''}`}>
{t(`agents.monitoring.${sendFailed ? 'sendFailed' : sentCount ? 'sendCount' : 'noSend'}`, { count: sentCount })}
<p className="text-xs text-muted-foreground">
{t('agents.monitoring.delivery')}
</p>
<p
className={`text-sm font-semibold ${sendFailed ? 'text-destructive' : ''}`}
>
{t(
`agents.monitoring.${sendFailed ? 'sendFailed' : sentCount ? 'sendCount' : 'noSend'}`,
{ count: sentCount },
)}
</p>
</div>
</div>
@@ -438,13 +462,21 @@ export default function AgentMonitoringTab({
</Card>
<h3 className="text-sm font-medium">
{t('agents.monitoring.execution')}
<span className="ml-2 text-muted-foreground">{steps.length}</span>
<span className="ml-2 text-muted-foreground">
{steps.length}
</span>
</h3>
{!steps.length && <p className="rounded-lg border border-dashed p-6 text-sm text-muted-foreground">{t('agents.monitoring.noSteps')}</p>}
{!steps.length && (
<p className="rounded-lg border border-dashed p-6 text-sm text-muted-foreground">
{t('agents.monitoring.noSteps')}
</p>
)}
<AgentRunTimeline
steps={steps}
labels={labels}
active={activeStatuses.has(selected.status) || eventCursor !== null}
active={
activeStatuses.has(selected.status) || eventCursor !== null
}
/>
{events
.filter((event) => event.type === 'processor.log')
@@ -493,7 +525,10 @@ export default function AgentMonitoringTab({
)
)}
{traceError && (
<LoadErrorState title={t('monitoring.loadError')} onRetry={() => setTraceRevision((value) => value + 1)} />
<LoadErrorState
title={t('monitoring.loadError')}
onRetry={() => setTraceRevision((value) => value + 1)}
/>
)}
</section>
</div>
@@ -6,16 +6,24 @@ import type { ExecutionStep } from './debug-execution';
import { toolDisplay, toolSummary } from './tool-summary';
export function isReplyStep(step: ExecutionStep) {
return step.kind === 'tool' &&
toolDisplay(step.name)?.kind === 'reply';
return step.kind === 'tool' && toolDisplay(step.name)?.kind === 'reply';
}
export function isMockStep(step: ExecutionStep) {
return step.kind === 'tool' && !!step.result &&
typeof step.result === 'object' && 'mock' in step.result && step.result.mock === true;
return (
step.kind === 'tool' &&
!!step.result &&
typeof step.result === 'object' &&
'mock' in step.result &&
step.result.mock === true
);
}
export default function AgentRunTimeline({ steps, labels, active }: {
export default function AgentRunTimeline({
steps,
labels,
active,
}: {
steps: ExecutionStep[];
labels: Record<string, string>;
active: boolean;
@@ -26,43 +34,136 @@ export default function AgentRunTimeline({ steps, labels, active }: {
{steps.map((step, index) => {
const reply = isReplyStep(step);
const mock = isMockStep(step);
const params = step.kind === 'tool' && step.parameters && typeof step.parameters === 'object'
? step.parameters as Record<string, unknown> : {};
const summary = toolSummary(step.kind === 'tool' ? step.name : '', params);
const params =
step.kind === 'tool' &&
step.parameters &&
typeof step.parameters === 'object'
? (step.parameters as Record<string, unknown>)
: {};
const summary = toolSummary(
step.kind === 'tool' ? step.name : '',
params,
);
const replyText = reply ? summary.text : '';
const actionText = reply ? '' : summary.text;
const Icon = step.kind === 'message' ? MessageSquare : reply ? Send : Wrench;
const Icon =
step.kind === 'message' ? MessageSquare : reply ? Send : Wrench;
const status = step.kind === 'tool' ? step.status : null;
const statusKey = status === 'running' ? (active ? 'Running' : 'Interrupted')
: mock ? (status === 'failed' ? 'MockFailed' : 'Simulated')
: status === 'failed' ? 'Failed' : 'Completed';
const statusKey =
status === 'running'
? active
? 'Running'
: 'Interrupted'
: mock
? status === 'failed'
? 'MockFailed'
: 'Simulated'
: status === 'failed'
? 'Failed'
: 'Completed';
return (
<li key={step.kind === 'tool' ? step.id : `message-${index}`} className="relative flex min-w-0 gap-3 pb-5 last:pb-0">
{index < steps.length - 1 && <span className="absolute bottom-0 left-4 top-8 w-px bg-border" aria-hidden="true" />}
<span className="relative flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs font-medium text-muted-foreground">{index + 1}</span>
<li
key={step.kind === 'tool' ? step.id : `message-${index}`}
className="relative flex min-w-0 gap-3 pb-5 last:pb-0"
>
{index < steps.length - 1 && (
<span
className="absolute bottom-0 left-4 top-8 w-px bg-border"
aria-hidden="true"
/>
)}
<span className="relative flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs font-medium text-muted-foreground">
{index + 1}
</span>
<div className="min-w-0 flex-1 rounded-lg border bg-background">
<div className="flex flex-wrap items-center gap-2 border-b bg-muted/30 px-4 py-3">
<Icon className="size-4 shrink-0 text-muted-foreground" />
<h4 className="min-w-0 flex-1 break-words text-sm font-medium">
{step.kind === 'message' ? t('agents.debugTextOutput') : labels[step.name] || step.name}
{step.kind === 'message'
? t('agents.debugTextOutput')
: labels[step.name] || step.name}
</h4>
{status && <Badge status={status === 'failed' ? 'failed' : mock ? 'simulated' : status === 'running' && !active ? 'cancelled' : status}>{t(`agents.debugTool${statusKey}`)}</Badge>}
{status && (
<Badge
status={
status === 'failed'
? 'failed'
: mock
? 'simulated'
: status === 'running' && !active
? 'cancelled'
: status
}
>
{t(`agents.debugTool${statusKey}`)}
</Badge>
)}
</div>
<div className="space-y-3 p-4 text-sm">
{step.kind === 'message' ? <>
{step.text && <p className="whitespace-pre-wrap break-words leading-7 [overflow-wrap:anywhere]">{step.text}</p>}
{step.reasoning && <ProcessorPayload title={t('agents.debugReasoning')} value={step.reasoning} />}
</> : <>
{replyText && <p className="whitespace-pre-wrap break-words leading-7 [overflow-wrap:anywhere]">{replyText}</p>}
{!reply && actionText && <p className="whitespace-pre-wrap break-words rounded-md bg-muted/50 p-3 font-mono text-xs leading-6 [overflow-wrap:anywhere]">{actionText}</p>}
{summary.recipient && <p className="break-all text-xs text-muted-foreground">{t('agents.monitoring.recipient')}: {summary.recipient}</p>}
{step.error && <p role="alert" className="whitespace-pre-wrap break-words text-destructive">{step.error}</p>}
{status === 'completed' && (mock || !reply) && <p className="text-xs text-muted-foreground">{t(`agents.monitoring.${mock ? 'simulatedAction' : 'actionCompleted'}`)}</p>}
{(step.parameters != null || step.result != null) && <div className="rounded-md border border-dashed text-muted-foreground">
{step.parameters != null && <ProcessorPayload title={t('agents.debugToolArguments')} value={step.parameters} />}
{step.result != null && <ProcessorPayload title={t('agents.debugToolResult')} value={step.result} />}
</div>}
</>}
{step.kind === 'message' ? (
<>
{step.text && (
<p className="whitespace-pre-wrap break-words leading-7 [overflow-wrap:anywhere]">
{step.text}
</p>
)}
{step.reasoning && (
<ProcessorPayload
title={t('agents.debugReasoning')}
value={step.reasoning}
/>
)}
</>
) : (
<>
{replyText && (
<p className="whitespace-pre-wrap break-words leading-7 [overflow-wrap:anywhere]">
{replyText}
</p>
)}
{!reply && actionText && (
<p className="whitespace-pre-wrap break-words rounded-md bg-muted/50 p-3 font-mono text-xs leading-6 [overflow-wrap:anywhere]">
{actionText}
</p>
)}
{summary.recipient && (
<p className="break-all text-xs text-muted-foreground">
{t('agents.monitoring.recipient')}: {summary.recipient}
</p>
)}
{step.error && (
<p
role="alert"
className="whitespace-pre-wrap break-words text-destructive"
>
{step.error}
</p>
)}
{status === 'completed' && (mock || !reply) && (
<p className="text-xs text-muted-foreground">
{t(
`agents.monitoring.${mock ? 'simulatedAction' : 'actionCompleted'}`,
)}
</p>
)}
{(step.parameters != null || step.result != null) && (
<div className="rounded-md border border-dashed text-muted-foreground">
{step.parameters != null && (
<ProcessorPayload
title={t('agents.debugToolArguments')}
value={step.parameters}
/>
)}
{step.result != null && (
<ProcessorPayload
title={t('agents.debugToolResult')}
value={step.result}
/>
)}
</div>
)}
</>
)}
</div>
</div>
</li>
@@ -53,33 +53,42 @@ export default function PluginProcessorTrace({
key={event.sequence ?? index}
className="min-w-0 overflow-hidden rounded-lg border bg-background"
>
<div className="flex items-center gap-2 border-b bg-muted/30 px-4 py-3">
<ScrollText className="size-4 text-muted-foreground" />
<span className="flex-1 text-sm font-medium">{t('agents.eventProcessor.logsTab')}</span>
<Badge status={String(event.data.level)}>{String(event.data.level)}</Badge>
</div>
<p className={`min-w-0 whitespace-pre-wrap break-words p-4 text-sm leading-7 [overflow-wrap:anywhere] ${event.data.level === 'error' ? 'text-destructive' : ''}`}>
{String(event.data.text)}
</p>
<div className="flex items-center gap-2 border-b bg-muted/30 px-4 py-3">
<ScrollText className="size-4 text-muted-foreground" />
<span className="flex-1 text-sm font-medium">
{t('agents.eventProcessor.logsTab')}
</span>
<Badge status={String(event.data.level)}>
{String(event.data.level)}
</Badge>
</div>
<p
className={`min-w-0 whitespace-pre-wrap break-words p-4 text-sm leading-7 [overflow-wrap:anywhere] ${event.data.level === 'error' ? 'text-destructive' : ''}`}
>
{String(event.data.text)}
</p>
</div>
) : (
<div key={event.sequence ?? index} className="rounded-lg border bg-background p-3">
<ProcessorPayload
title={
<>
{t(
`agents.eventProcessor.trace_${event.type.replaceAll('.', '_')}`,
{ defaultValue: event.type },
)}
{typeof event.data.tool_name === 'string' && (
<span className="ml-2 text-muted-foreground">
{toolLabels[event.data.tool_name] || event.data.tool_name}
</span>
)}
</>
}
value={event.data}
/>
<div
key={event.sequence ?? index}
className="rounded-lg border bg-background p-3"
>
<ProcessorPayload
title={
<>
{t(
`agents.eventProcessor.trace_${event.type.replaceAll('.', '_')}`,
{ defaultValue: event.type },
)}
{typeof event.data.tool_name === 'string' && (
<span className="ml-2 text-muted-foreground">
{toolLabels[event.data.tool_name] || event.data.tool_name}
</span>
)}
</>
}
value={event.data}
/>
</div>
),
)}
@@ -28,7 +28,9 @@ export default function ProcessorRunList({
<div
role="group"
aria-label={t('agents.eventProcessor.runs')}
className={className ?? 'max-h-56 shrink-0 overflow-y-auto rounded-md border'}
className={
className ?? 'max-h-56 shrink-0 overflow-y-auto rounded-md border'
}
>
{runs.map((run) => {
const duration = processorRunDuration(run);
@@ -521,18 +521,22 @@ export default function RunnerSelect({
</div>
)}
</SelectGroup>
<SelectSeparator />
<a
href="https://langbot.app/docs/zh/plugin/dev/components/runner"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-sm px-2 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
{t(usage === 'event' ? 'bots.routingHelp.docs' : 'bots.routingHelp.runnerDocs')}
<ExternalLink className="size-3" />
</a>
<SelectSeparator />
<a
href="https://langbot.app/docs/zh/plugin/dev/components/runner"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-sm px-2 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
{t(
usage === 'event'
? 'bots.routingHelp.docs'
: 'bots.routingHelp.runnerDocs',
)}
<ExternalLink className="size-3" />
</a>
</SelectContent>
</Select>
@@ -92,35 +92,87 @@ const DEBUG_EVENT_DEFINITIONS: Record<string, DebugEventDefinition> = {
'message.deleted': deletion,
'dingtalk.card_action': {
fields: [user],
defaults: { space_id: 'dtv1.card//IM_GROUP.debug-group', card_instance_id: 'debug-card', action: 'confirm', data: {} },
defaults: {
space_id: 'dtv1.card//IM_GROUP.debug-group',
card_instance_id: 'debug-card',
action: 'confirm',
data: {},
},
},
'wecombot.enter_chat': {
fields: [user],
defaults: { chat_id: 'debug-user', chat_type: 'private', message_id: 'debug-entry', data: {} },
defaults: {
chat_id: 'debug-user',
chat_type: 'private',
message_id: 'debug-entry',
data: {},
},
},
'wecombot.template_card_event': {
fields: [user],
defaults: { chat_id: 'debug-user', chat_type: 'private', message_id: 'debug-card-click', task_id: 'debug-card', event_key: 'confirm', card_type: 'button_interaction', selected_items: [], data: {} },
defaults: {
chat_id: 'debug-user',
chat_type: 'private',
message_id: 'debug-card-click',
task_id: 'debug-card',
event_key: 'confirm',
card_type: 'button_interaction',
selected_items: [],
data: {},
},
},
'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: {} },
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 },
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 },
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: '' },
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 },
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': {
@@ -14,7 +14,11 @@ const TOOL_DISPLAYS: Readonly<Record<string, ToolDisplay>> = {
glob: { fields: ['pattern', 'path'] },
grep: { fields: ['pattern', 'path'] },
event_reply: { fields: ['text'], kind: 'reply' },
platform_send_message: { fields: ['text'], kind: 'reply', recipient: 'target_id' },
platform_send_message: {
fields: ['text'],
kind: 'reply',
recipient: 'target_id',
},
event_delete_message: { fields: [] },
event_get_actor: { fields: [] },
event_get_group: { fields: [] },
@@ -41,21 +45,29 @@ const TOOL_DISPLAYS: Readonly<Record<string, ToolDisplay>> = {
export function toolDisplay(name: string): ToolDisplay | undefined {
return Object.prototype.hasOwnProperty.call(TOOL_DISPLAYS, name)
? TOOL_DISPLAYS[name] : undefined;
? TOOL_DISPLAYS[name]
: undefined;
}
export function toolSummary(name: string, parameters: Record<string, unknown>) {
const display = toolDisplay(name);
const fields = (display?.fields ?? []).flatMap((field) => {
const value = parameters[field];
if (!['string', 'number', 'boolean'].includes(typeof value) || value === '') return [];
if (!['string', 'number', 'boolean'].includes(typeof value) || value === '')
return [];
return [{ field, text: String(value) }];
});
return {
text: fields.map(({ field, text }) =>
display?.kind === 'reply' || display?.fields.length === 1 ? text : `${field}: ${text}`,
).join('\n'),
recipient: display?.recipient && typeof parameters[display.recipient] === 'string'
? String(parameters[display.recipient]) : '',
text: fields
.map(({ field, text }) =>
display?.kind === 'reply' || display?.fields.length === 1
? text
: `${field}: ${text}`,
)
.join('\n'),
recipient:
display?.recipient && typeof parameters[display.recipient] === 'string'
? String(parameters[display.recipient])
: '',
};
}
@@ -316,7 +316,9 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
`${adapter.label} ${adapter.value} ${adapterDescriptionList[adapter.value] ?? ''}`
.toLocaleLowerCase()
.includes(adapterQuery);
const galleryGroups = groupByCategory(activeAdapters.filter(matchesAdapterSearch));
const galleryGroups = groupByCategory(
activeAdapters.filter(matchesAdapterSearch),
);
const galleryLegacyAdapters = legacyAdapters.filter(matchesAdapterSearch);
// Whether the collapsed legacy adapter group is expanded in the Select.
@@ -717,10 +719,12 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
}
const hasProcessing =
(form.watch('event_bindings') ?? []).some((binding) =>
binding.enabled !== false && !!binding.event_pattern &&
(binding.target_type === 'discard' ||
agentNameList.some((agent) => agent.uuid === binding.target_uuid)),
(form.watch('event_bindings') ?? []).some(
(binding) =>
binding.enabled !== false &&
!!binding.event_pattern &&
(binding.target_type === 'discard' ||
agentNameList.some((agent) => agent.uuid === binding.target_uuid)),
) ||
(form.watch('plugin_processors') ?? []).some(
(processor) => processor.enabled !== false && !!processor.processor_uuid,
@@ -807,7 +811,9 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
<Input
type="search"
value={adapterSearch}
onChange={(event) => setAdapterSearch(event.target.value)}
onChange={(event) =>
setAdapterSearch(event.target.value)
}
onKeyDown={(event) => {
if (event.key === 'Enter') event.preventDefault();
}}
@@ -823,11 +829,15 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
</div>
<div className="space-y-8">
{galleryGroups.length === 0 && galleryLegacyAdapters.length === 0 && (
<p className="py-8 text-center text-sm text-muted-foreground" role="status">
{t('bots.noMatchingAdapters')}
</p>
)}
{galleryGroups.length === 0 &&
galleryLegacyAdapters.length === 0 && (
<p
className="py-8 text-center text-sm text-muted-foreground"
role="status"
>
{t('bots.noMatchingAdapters')}
</p>
)}
{galleryGroups.map((group) => (
<section
key={group.categoryId ?? 'uncategorized'}
@@ -889,23 +899,25 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
data-adapter-category="legacy"
>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setShowLegacyAdapters((value) => !value)}
className="flex items-center gap-2 py-2 text-left text-sm font-medium text-muted-foreground hover:text-foreground"
aria-expanded={showLegacyAdapters || !!adapterQuery}
>
{showLegacyAdapters || adapterQuery ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
{t('bots.legacyAdapters')}
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px]">
{galleryLegacyAdapters.length}
</span>
</button>
<LegacyAdapterHelp />
<button
type="button"
onClick={() =>
setShowLegacyAdapters((value) => !value)
}
className="flex items-center gap-2 py-2 text-left text-sm font-medium text-muted-foreground hover:text-foreground"
aria-expanded={showLegacyAdapters || !!adapterQuery}
>
{showLegacyAdapters || adapterQuery ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
{t('bots.legacyAdapters')}
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px]">
{galleryLegacyAdapters.length}
</span>
</button>
<LegacyAdapterHelp />
</div>
{(showLegacyAdapters || !!adapterQuery) && (
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
@@ -1251,12 +1263,18 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
role="status"
className="flex shrink-0 items-start gap-2.5 rounded-t-xl border-b border-amber-200 bg-amber-50 px-6 py-3 text-sm leading-relaxed text-amber-800 dark:border-amber-800/50 dark:bg-amber-950/40 dark:text-amber-200"
>
<TriangleAlert className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
<TriangleAlert
className="mt-0.5 size-4 shrink-0"
aria-hidden="true"
/>
<p>{t('botSetup.warning')}</p>
</div>
)}
<CardHeader>
<CardTitle className="flex items-center gap-1.5">{t('bots.eventRouting')}<EventRoutingHelp /></CardTitle>
<CardTitle className="flex items-center gap-1.5">
{t('bots.eventRouting')}
<EventRoutingHelp />
</CardTitle>
<CardDescription>
{t('bots.eventRoutingDescription')}
</CardDescription>
@@ -1269,7 +1287,12 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
>
<EventBindingsEditor
form={form}
onAgentCreated={(agent) => setAgentNameList((items) => [...items.filter((item) => item.uuid !== agent.uuid), agent])}
onAgentCreated={(agent) =>
setAgentNameList((items) => [
...items.filter((item) => item.uuid !== agent.uuid),
agent,
])
}
botId={initBotId}
supportedEvents={adapterSupportedEvents[currentAdapter] || []}
agentOptions={agentNameList.filter(
@@ -586,7 +586,10 @@ function TargetCombobox({
</PopoverTrigger>
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandInput placeholder={t('bots.searchTarget')} className="h-8" />
<CommandInput
placeholder={t('bots.searchTarget')}
className="h-8"
/>
<CommandList>
<CommandEmpty>{t('bots.noTargetFound')}</CommandEmpty>
{agents.length > 0 && (
@@ -595,7 +598,9 @@ function TargetCombobox({
<CommandItem
key={a.uuid}
value={`agent:${a.uuid}:${a.name}`}
onSelect={() => select(encodeTarget('agent', a.uuid || ''))}
onSelect={() =>
select(encodeTarget('agent', a.uuid || ''))
}
>
<Bot className="mr-2 size-3.5 shrink-0" />
<span className="truncate">{targetLabel(a)}</span>
@@ -650,7 +655,9 @@ function TargetCombobox({
>
<DialogHeader>
<DialogTitle>{t('botSetup.create')}</DialogTitle>
<DialogDescription>{t('botSetup.createDescription')}</DialogDescription>
<DialogDescription>
{t('botSetup.createDescription')}
</DialogDescription>
</DialogHeader>
<AgentCreateContent
embedded
@@ -660,9 +667,10 @@ function TargetCombobox({
try {
const { agent } = await backendClient.getAgent(id);
onAgentCreated(agent);
const compatible = agent.kind === 'pipeline'
? pipelineAllowed
: agentSupportsEventPattern(agent, binding.event_pattern);
const compatible =
agent.kind === 'pipeline'
? pipelineAllowed
: agentSupportsEventPattern(agent, binding.event_pattern);
if (compatible) {
onUpdate({ target_type: agent.kind, target_uuid: id });
} else {
@@ -1570,9 +1578,15 @@ export default function EventBindingsEditor({
setRouteStatusError(null);
try {
const response = await backendClient.getBotEventRouteStatuses(botId);
setRouteStatuses([...response.routes, ...response.unmatched_events, ...response.stale_routes]
.filter((record) => record.last_status)
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)));
setRouteStatuses(
[
...response.routes,
...response.unmatched_events,
...response.stale_routes,
]
.filter((record) => record.last_status)
.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)),
);
} catch (error) {
console.error('Failed to refresh Bot event route status', error);
setRouteStatusError(t('bots.routeStatusRefreshFailed'));
@@ -1805,7 +1819,9 @@ export default function EventBindingsEditor({
type="button"
variant="outline"
size="sm"
className={bindings.length === 0 ? styles.emptyRouteHint : undefined}
className={
bindings.length === 0 ? styles.emptyRouteHint : undefined
}
>
<Plus className="h-4 w-4 mr-1" />
{t('bots.addBehavior')}
@@ -1882,7 +1898,13 @@ export default function EventBindingsEditor({
eventOptions={dryRunEventOptions}
agentOptions={agentOptions}
/>
<Button type="button" variant="outline" size="sm" disabled={!botId} onClick={() => setRecordsOpen(true)}>
<Button
type="button"
variant="outline"
size="sm"
disabled={!botId}
onClick={() => setRecordsOpen(true)}
>
<ListChecks className="mr-1 size-4" />
{t('bots.matchRecords')}
</Button>
@@ -1890,38 +1912,86 @@ export default function EventBindingsEditor({
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{t('bots.matchRecords')}</DialogTitle>
<DialogDescription>{t('bots.matchRecordsDescription')}</DialogDescription>
<DialogDescription>
{t('bots.matchRecordsDescription')}
</DialogDescription>
</DialogHeader>
<div className="flex justify-end">
<Button type="button" variant="outline" size="sm" onClick={refreshRouteStatuses} disabled={routeStatusLoading}>
<RefreshCw className={`mr-2 size-4 ${routeStatusLoading ? 'animate-spin' : ''}`} />
<Button
type="button"
variant="outline"
size="sm"
onClick={refreshRouteStatuses}
disabled={routeStatusLoading}
>
<RefreshCw
className={`mr-2 size-4 ${routeStatusLoading ? 'animate-spin' : ''}`}
/>
{t('monitoring.refreshData')}
</Button>
</div>
{routeStatusError && <p role="alert" className="text-sm text-destructive">{routeStatusError}</p>}
{routeStatusError && (
<p role="alert" className="text-sm text-destructive">
{routeStatusError}
</p>
)}
<div className="max-h-[60vh] space-y-3 overflow-y-auto">
{routeStatuses.map((record, index) => (
<div key={`${record.binding_id}:${record.seq_id}:${index}`} className="space-y-2 rounded-lg border p-3">
<div
key={`${record.binding_id}:${record.seq_id}:${index}`}
className="space-y-2 rounded-lg border p-3"
>
<div className="flex flex-wrap items-center gap-2">
<span className="flex-1 text-sm font-medium">{eventLabel(record.event_type || record.event_pattern || '*', t)}</span>
<Badge variant="outline" className={routeStatusBadgeClass(record.last_status)}>{routeStatusLabel(record.last_status, t)}</Badge>
<span className="flex-1 text-sm font-medium">
{eventLabel(
record.event_type || record.event_pattern || '*',
t,
)}
</span>
<Badge
variant="outline"
className={routeStatusBadgeClass(record.last_status)}
>
{routeStatusLabel(record.last_status, t)}
</Badge>
</div>
<div className="text-xs text-muted-foreground">{formatRouteStatusTime(record.timestamp)}</div>
{record.target_type && <div className="break-words text-sm">{record.target_type}{record.target_uuid ? ` · ${agentOptions.find((agent) => agent.uuid === record.target_uuid)?.name || record.target_uuid}` : ''}</div>}
<p className="text-sm text-muted-foreground">{routeStatusDetail(record, t)}</p>
<div className="text-xs text-muted-foreground">
{formatRouteStatusTime(record.timestamp)}
</div>
{record.target_type && (
<div className="break-words text-sm">
{record.target_type}
{record.target_uuid
? ` · ${agentOptions.find((agent) => agent.uuid === record.target_uuid)?.name || record.target_uuid}`
: ''}
</div>
)}
<p className="text-sm text-muted-foreground">
{routeStatusDetail(record, t)}
</p>
<details className="text-xs text-muted-foreground">
<summary className="cursor-pointer">{t('monitoring.unified.metadata')}</summary>
<pre className="mt-2 whitespace-pre-wrap break-words [overflow-wrap:anywhere]">{JSON.stringify(record, null, 2)}</pre>
<summary className="cursor-pointer">
{t('monitoring.unified.metadata')}
</summary>
<pre className="mt-2 whitespace-pre-wrap break-words [overflow-wrap:anywhere]">
{JSON.stringify(record, null, 2)}
</pre>
</details>
</div>
))}
{!routeStatuses.length && <p className="py-8 text-center text-sm text-muted-foreground">{t(routeStatusLoading ? 'common.loading' : 'monitoring.unified.noRecords')}</p>}
{!routeStatuses.length && (
<p className="py-8 text-center text-sm text-muted-foreground">
{t(
routeStatusLoading
? 'common.loading'
: 'monitoring.unified.noRecords',
)}
</p>
)}
</div>
</DialogContent>
</Dialog>
</div>
</div>
);
}
@@ -2,7 +2,11 @@
import { useState, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import './EventRoutingHelp.css';
const routeColors = [
@@ -18,40 +22,90 @@ export default function EventRoutingHelp() {
return (
<Popover>
<PopoverTrigger asChild>
<button type="button" aria-label={text('title')} className="inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true">
<circle cx="12" cy="12" r="9" /><path d="M12 11v6" /><circle cx="12" cy="7.5" r=".7" fill="currentColor" />
<button
type="button"
aria-label={text('title')}
className="inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v6" />
<circle cx="12" cy="7.5" r=".7" fill="currentColor" />
</svg>
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[min(32rem,calc(100vw-2rem))] max-h-[80vh] overflow-y-auto p-5">
<PopoverContent
align="start"
className="w-[min(32rem,calc(100vw-2rem))] max-h-[80vh] overflow-y-auto p-5"
>
<h3 className="text-sm font-semibold">{text('title')}</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{text('intro')}</p>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{text('intro')}
</p>
<div className="my-4 overflow-hidden rounded-xl border bg-muted/20 p-3">
<div className="mb-3 grid grid-cols-2 text-xs text-muted-foreground">
<span>{text('events')}</span><span className="text-right">{text('agents')}</span>
<span>{text('events')}</span>
<span className="text-right">{text('agents')}</span>
</div>
<div className={`routing-help space-y-3 ${paused ? 'routing-help-paused' : ''}`}>
<div
className={`routing-help space-y-3 ${paused ? 'routing-help-paused' : ''}`}
>
{['message', 'friend', 'group'].map((kind, index) => (
<div key={kind} className={`grid grid-cols-[minmax(0,1fr)_48px_minmax(0,1fr)] items-center ${routeColors[index]}`} style={{ '--route-delay': `${index * 1.6}s` } as CSSProperties}>
<div className="route-node rounded-lg border px-2 py-3 text-center text-xs leading-5">{text(kind)}</div>
<div
key={kind}
className={`grid grid-cols-[minmax(0,1fr)_48px_minmax(0,1fr)] items-center ${routeColors[index]}`}
style={{ '--route-delay': `${index * 1.6}s` } as CSSProperties}
>
<div className="route-node rounded-lg border px-2 py-3 text-center text-xs leading-5">
{text(kind)}
</div>
<div className="relative h-8" aria-hidden="true">
<svg viewBox="0 0 48 32" className="h-full w-full opacity-30"><path d="M0 16H45m-5-4 5 4-5 4" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
<svg viewBox="0 0 48 32" className="h-full w-full opacity-30">
<path
d="M0 16H45m-5-4 5 4-5 4"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
<span className="routing-help-dot absolute left-0 top-[13px] size-1.5 rounded-full bg-current" />
</div>
<div className="route-node routing-help-target rounded-lg border px-2 py-3 text-center text-xs leading-5">{text(`${kind}Action`)}</div>
<div className="route-node routing-help-target rounded-lg border px-2 py-3 text-center text-xs leading-5">
{text(`${kind}Action`)}
</div>
</div>
))}
</div>
<div className="mt-3 flex items-center justify-between gap-3 text-xs text-muted-foreground">
<span>{text('rules')}</span>
<button type="button" className="shrink-0 rounded px-1 underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hidden" onClick={() => setPaused(!paused)}>{text(paused ? 'play' : 'pause')}</button>
<button
type="button"
className="shrink-0 rounded px-1 underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hidden"
onClick={() => setPaused(!paused)}
>
{text(paused ? 'play' : 'pause')}
</button>
</div>
</div>
<p className="text-sm leading-6">{text('instructions')}</p>
<div className="mt-4 border-t pt-3 text-xs leading-6 text-muted-foreground">
<p>{text('plugin')}</p>
<a href="https://langbot.app/docs/zh/plugin/dev/components/runner" target="_blank" rel="noopener noreferrer" className="font-medium text-foreground underline underline-offset-4">{text('docs')} ↗</a>
<a
href="https://langbot.app/docs/zh/plugin/dev/components/runner"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-foreground underline underline-offset-4"
>
{text('docs')} ↗
</a>
</div>
</PopoverContent>
</Popover>
@@ -1,18 +1,40 @@
import { useTranslation } from 'react-i18next';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
export default function LegacyAdapterHelp() {
const { t } = useTranslation();
return (
<Popover>
<PopoverTrigger asChild>
<button type="button" aria-label={t('bots.legacyAdapters')} className="inline-flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M12 11v6" /><circle cx="12" cy="7.5" r=".7" fill="currentColor" /></svg>
<button
type="button"
aria-label={t('bots.legacyAdapters')}
className="inline-flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
aria-hidden="true"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v6" />
<circle cx="12" cy="7.5" r=".7" fill="currentColor" />
</svg>
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-80 max-w-[calc(100vw-2rem)]">
<h4 className="mb-2 text-sm font-medium">{t('bots.legacyAdapters')}</h4>
<p className="whitespace-pre-line text-sm leading-6 text-muted-foreground">{t('bots.legacyAdaptersHint')}</p>
<p className="whitespace-pre-line text-sm leading-6 text-muted-foreground">
{t('bots.legacyAdaptersHint')}
</p>
</PopoverContent>
</Popover>
);
@@ -1,6 +1,13 @@
import { useCurrentWorkspace } from '@/app/infra/http';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import LoadErrorState from '@/components/LoadErrorState';
import React, {
useState,
@@ -436,14 +443,18 @@ const BotSessionMonitor = forwardRef<
if (parsed && typeof parsed === 'object') {
const chain = parsed.message_chain ?? parsed.event?.message_chain;
if (Array.isArray(chain)) return chain as MessageChainComponent[];
if (Array.isArray(chain?.root)) return chain.root as MessageChainComponent[];
if (Array.isArray(chain?.root))
return chain.root as MessageChainComponent[];
if (Array.isArray(parsed.contents)) {
const components: MessageChainComponent[] = [];
for (const part of parsed.contents) {
if (part.type === 'text' && typeof part.text === 'string') {
components.push({ type: 'Plain', text: part.text } as Plain);
} else if (part.type === 'image' && part.image_url) {
components.push({ type: 'Image', url: part.image_url } as MessageChainComponent);
components.push({
type: 'Image',
url: part.image_url,
} as MessageChainComponent);
}
}
if (components.length) return components;
@@ -785,7 +796,11 @@ const BotSessionMonitor = forwardRef<
{t('bots.sessionMonitor.loading')}
</div>
) : sessionError ? (
<LoadErrorState compact title={t('monitoring.loadError')} onRetry={loadSessions} />
<LoadErrorState
compact
title={t('monitoring.loadError')}
onRetry={loadSessions}
/>
) : sessions.length === 0 ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.noSessions')}
@@ -949,8 +964,13 @@ const BotSessionMonitor = forwardRef<
</div>
</div>
{canReset && (
<Button variant="outline" size="sm" className="h-7 shrink-0 gap-1.5"
disabled={resetting} onClick={() => setResetTarget(selectedSessionId)}>
<Button
variant="outline"
size="sm"
className="h-7 shrink-0 gap-1.5"
disabled={resetting}
onClick={() => setResetTarget(selectedSessionId)}
>
<RotateCcw className="size-3.5" />
{t('bots.sessionMonitor.resetContext')}
</Button>
@@ -964,14 +984,25 @@ const BotSessionMonitor = forwardRef<
>
<div className="space-y-4">
{analysisError && !loadingMessages && (
<LoadErrorState compact title={`${t('monitoring.toolCalls.title')}: ${t('monitoring.loadError')}`} onRetry={() => loadMessages(selectedSessionId, messagePage)} />
<LoadErrorState
compact
title={`${t('monitoring.toolCalls.title')}: ${t('monitoring.loadError')}`}
onRetry={() =>
loadMessages(selectedSessionId, messagePage)
}
/>
)}
{loadingMessages ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.loading')}
</div>
) : messageError ? (
<LoadErrorState title={t('monitoring.loadError')} onRetry={() => loadMessages(selectedSessionId, messagePage)} />
<LoadErrorState
title={t('monitoring.loadError')}
onRetry={() =>
loadMessages(selectedSessionId, messagePage)
}
/>
) : timelineItems.length === 0 ? (
<div className="text-center text-muted-foreground py-12 text-sm">
{t('bots.sessionMonitor.noMessages')}
@@ -1226,16 +1257,34 @@ const BotSessionMonitor = forwardRef<
</div>
</div>
<Dialog open={resetTarget !== null} onOpenChange={(open) => { if (!open && !resetting) setResetTarget(null); }}>
<Dialog
open={resetTarget !== null}
onOpenChange={(open) => {
if (!open && !resetting) setResetTarget(null);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('bots.sessionMonitor.resetContext')}</DialogTitle>
<DialogDescription>{t('bots.sessionMonitor.resetDescription')}</DialogDescription>
<DialogDescription>
{t('bots.sessionMonitor.resetDescription')}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" disabled={resetting} onClick={() => setResetTarget(null)}>{t('common.cancel')}</Button>
<Button
variant="outline"
disabled={resetting}
onClick={() => setResetTarget(null)}
>
{t('common.cancel')}
</Button>
<Button disabled={resetting} onClick={resetContext}>
<RotateCcw className={cn('size-4', resetting && 'animate-spin motion-reduce:animate-none')} />
<RotateCcw
className={cn(
'size-4',
resetting && 'animate-spin motion-reduce:animate-none',
)}
/>
{t('bots.sessionMonitor.resetContext')}
</Button>
</DialogFooter>
@@ -318,7 +318,8 @@ function DownloadLinkField({
if (/^https?:\/\//i.test(url)) {
// External templates must not receive backend credentials.
const response = await fetch(url);
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
if (!response.ok)
throw new Error(`Download failed: ${response.status}`);
blob = await response.blob();
} else {
const response = await backendClient.downloadFile(url);
@@ -327,7 +328,8 @@ function DownloadLinkField({
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = filename || url.split('/').pop()?.split('?')[0] || 'download';
link.download =
filename || url.split('/').pop()?.split('?')[0] || 'download';
document.body.appendChild(link);
link.click();
link.remove();
@@ -752,33 +754,39 @@ function DynamicFormFieldList({
// State for QR code login dialog
const [qrDialogOpen, setQrDialogOpen] = useState(false);
const [qrDialogPlatform, setQrDialogPlatform] =
useState<QrLoginPlatform | 'slack'>('feishu');
const [qrDialogPlatform, setQrDialogPlatform] = useState<
QrLoginPlatform | 'slack'
>('feishu');
return (
<div className="min-w-0 max-w-full space-y-4 overflow-x-hidden">
{/* QR code login dialog */}
{qrDialogPlatform === 'slack' ? <SlackSetupDialog
open={qrDialogOpen}
onOpenChange={setQrDialogOpen}
botId={systemContext?.bot_uuid as string | undefined}
webhookUrl={(systemContext?.webhook_url as string) || ''}
socketMode={!!form.getValues('socket_mode')}
onSuccess={(credentials) => {
for (const [key, value] of Object.entries(credentials)) setFormValue(key, value);
}}
/> : <QrCodeLoginDialog
open={qrDialogOpen}
onOpenChange={setQrDialogOpen}
platform={qrDialogPlatform}
onSuccess={(credentials) => {
for (const [key, value] of Object.entries(credentials)) {
if (value) {
form.setValue(key as keyof FormValues, value as never);
{qrDialogPlatform === 'slack' ? (
<SlackSetupDialog
open={qrDialogOpen}
onOpenChange={setQrDialogOpen}
botId={systemContext?.bot_uuid as string | undefined}
webhookUrl={(systemContext?.webhook_url as string) || ''}
socketMode={!!form.getValues('socket_mode')}
onSuccess={(credentials) => {
for (const [key, value] of Object.entries(credentials))
setFormValue(key, value);
}}
/>
) : (
<QrCodeLoginDialog
open={qrDialogOpen}
onOpenChange={setQrDialogOpen}
platform={qrDialogPlatform}
onSuccess={(credentials) => {
for (const [key, value] of Object.entries(credentials)) {
if (value) {
form.setValue(key as keyof FormValues, value as never);
}
}
}
}}
/>}
}}
/>
)}
{itemConfigList.map((config, index) => {
if (hiddenItemNames?.includes(config.name)) return null;
@@ -964,7 +972,8 @@ function DynamicFormFieldList({
onClick={() => {
if (!isEditing) {
setQrDialogPlatform(
(config.login_platform as QrLoginPlatform | 'slack') || 'feishu',
(config.login_platform as QrLoginPlatform | 'slack') ||
'feishu',
);
setQrDialogOpen(true);
}
@@ -996,7 +1005,8 @@ function DynamicFormFieldList({
onClick={(e) => {
e.stopPropagation();
setQrDialogPlatform(
(config.login_platform as QrLoginPlatform | 'slack') || 'feishu',
(config.login_platform as QrLoginPlatform | 'slack') ||
'feishu',
);
setQrDialogOpen(true);
}}
@@ -656,9 +656,13 @@ function NavItems({
return `/home/skills?id=${encodeURIComponent(item.id)}`;
}
const processorTab = searchParams.get('tab');
if (hasDetailPages && (routePrefix === '/home/agents' || routePrefix === '/home/pipelines') &&
(pathname === '/home/agents' || pathname === '/home/pipelines') &&
(processorTab === 'logs' || processorTab === 'config')) {
if (
hasDetailPages &&
(routePrefix === '/home/agents' ||
routePrefix === '/home/pipelines') &&
(pathname === '/home/agents' || pathname === '/home/pipelines') &&
(processorTab === 'logs' || processorTab === 'config')
) {
return `${routePrefix}?id=${encodeURIComponent(item.id)}&tab=${processorTab}`;
}
return hasDetailPages
@@ -241,7 +241,12 @@ export default function ModelItem({
loaded={availabilityLoaded}
/>
)}
{modelType === 'llm' && <ModelStar uuid={model.uuid} disabled={!canManage || isPopoverDisabled} />}
{modelType === 'llm' && (
<ModelStar
uuid={model.uuid}
disabled={!canManage || isPopoverDisabled}
/>
)}
{canManage && !isLangBotModels && (
<Popover
open={isDeleteOpen}
@@ -5,44 +5,79 @@ import { toast } from 'sonner';
import { backendClient, useCurrentWorkspace } from '@/app/infra/http';
import { Button } from '@/components/ui/button';
const records = new Map<string, { uuid: string | null; busy: boolean; loaded: boolean }>();
const records = new Map<
string,
{ uuid: string | null; busy: boolean; loaded: boolean }
>();
const listeners = new Set<() => void>();
const pending = new Map<string, Promise<void>>();
const empty = { uuid: null, busy: false, loaded: false };
const subscribe = (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; };
function update(workspace: string, value: typeof empty | { uuid: string | null; busy: boolean; loaded: boolean }) {
const subscribe = (listener: () => void) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
function update(
workspace: string,
value: typeof empty | { uuid: string | null; busy: boolean; loaded: boolean },
) {
records.set(workspace, value);
listeners.forEach((listener) => listener());
}
export default function ModelStar({ uuid, disabled }: { uuid: string; disabled: boolean }) {
export default function ModelStar({
uuid,
disabled,
}: {
uuid: string;
disabled: boolean;
}) {
const workspace = useCurrentWorkspace()?.workspace.uuid ?? '';
const { t } = useTranslation();
const state = useSyncExternalStore(subscribe, () => records.get(workspace) ?? empty);
const state = useSyncExternalStore(
subscribe,
() => records.get(workspace) ?? empty,
);
useEffect(() => {
if (!workspace || pending.has(workspace)) return;
const request = backendClient.getStarredModel().then((result) => {
update(workspace, { uuid: result.uuid, busy: false, loaded: true });
}).catch(() => { update(workspace, { uuid: null, busy: false, loaded: false }); })
const request = backendClient
.getStarredModel()
.then((result) => {
update(workspace, { uuid: result.uuid, busy: false, loaded: true });
})
.catch(() => {
update(workspace, { uuid: null, busy: false, loaded: false });
})
.finally(() => pending.delete(workspace));
pending.set(workspace, request);
}, [workspace]);
const starred = state.uuid === uuid;
return <Button type="button" variant="ghost" size="icon" className={`size-7 shrink-0 ${starred ? 'text-amber-500 hover:text-amber-600' : 'text-muted-foreground'}`}
disabled={disabled || state.busy || !state.loaded}
aria-pressed={starred} aria-label={t(starred ? 'models.unstarModel' : 'models.starModel')}
title={t(starred ? 'models.unstarModel' : 'models.starModel')}
onPointerDown={(event) => event.stopPropagation()}
onClick={async (event) => {
event.stopPropagation();
const next = starred ? null : uuid;
update(workspace, { ...state, busy: true });
try {
await backendClient.setStarredModel(next);
update(workspace, { uuid: next, busy: false, loaded: true });
} catch {
update(workspace, { ...state, busy: false });
toast.error(t('models.starModelFailed'));
}
}}><Star className={`size-3.5 ${starred ? 'fill-current' : ''}`} /></Button>;
return (
<Button
type="button"
variant="ghost"
size="icon"
className={`size-7 shrink-0 ${starred ? 'text-amber-500 hover:text-amber-600' : 'text-muted-foreground'}`}
disabled={disabled || state.busy || !state.loaded}
aria-pressed={starred}
aria-label={t(starred ? 'models.unstarModel' : 'models.starModel')}
title={t(starred ? 'models.unstarModel' : 'models.starModel')}
onPointerDown={(event) => event.stopPropagation()}
onClick={async (event) => {
event.stopPropagation();
const next = starred ? null : uuid;
update(workspace, { ...state, busy: true });
try {
await backendClient.setStarredModel(next);
update(workspace, { uuid: next, busy: false, loaded: true });
} catch {
update(workspace, { ...state, busy: false });
toast.error(t('models.starModelFailed'));
}
}}
>
<Star className={`size-3.5 ${starred ? 'fill-current' : ''}`} />
</Button>
);
}
@@ -89,7 +89,8 @@ export default function ProcessorDetailWorkbench({
onViewChange,
}: ProcessorDetailWorkbenchProps) {
const [searchParams, setSearchParams] = useSearchParams();
const activeView = searchParams.get('tab') === 'logs' ? 'monitoring' : 'workbench';
const activeView =
searchParams.get('tab') === 'logs' ? 'monitoring' : 'workbench';
const hasDebug = Boolean(debugTitle && debugContent);
const configPanel = (
@@ -148,11 +149,14 @@ export default function ProcessorDetailWorkbench({
value={view ?? activeView}
onValueChange={(value) => {
const view = value as 'workbench' | 'monitoring';
setSearchParams((current) => {
const next = new URLSearchParams(current);
next.set('tab', view === 'monitoring' ? 'logs' : 'config');
return next;
}, { preventScrollReset: true });
setSearchParams(
(current) => {
const next = new URLSearchParams(current);
next.set('tab', view === 'monitoring' ? 'logs' : 'config');
return next;
},
{ preventScrollReset: true },
);
onViewChange?.(view);
}}
className="flex h-full min-h-0 min-w-0 flex-col gap-0"
@@ -20,7 +20,11 @@ function formatDuration(ms: number) {
function parsePayload(value?: string): unknown {
if (!value) return null;
try { return JSON.parse(value); } catch { return value; }
try {
return JSON.parse(value);
} catch {
return value;
}
}
function MessageBody({ content }: { content: string }) {
@@ -31,7 +35,11 @@ function MessageBody({ content }: { content: string }) {
);
}
export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: ConversationTurnListProps) {
export function ConversationTurnList({
turns,
expandedTurnId,
onToggleTurn,
}: ConversationTurnListProps) {
const { t } = useTranslation();
const turn = turns.find((item) => item.id === expandedTurnId) ?? turns[0];
if (!turn) return null;
@@ -62,7 +70,10 @@ export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: Co
aria-pressed={turn.id === item.id}
onClick={() => onToggleTurn(item.id)}
onKeyDown={(event) => {
if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) {
if (
event.target === event.currentTarget &&
(event.key === 'Enter' || event.key === ' ')
) {
event.preventDefault();
onToggleTurn(item.id);
}
@@ -78,8 +89,13 @@ export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: Co
</div>
<div className="pointer-events-none max-h-12 overflow-hidden break-words text-sm">
{item.userMessage ? (
<MessageContentRenderer content={item.userMessage.messageContent} maxLines={2} />
) : t('monitoring.messageList.noUserMessage')}
<MessageContentRenderer
content={item.userMessage.messageContent}
maxLines={2}
/>
) : (
t('monitoring.messageList.noUserMessage')
)}
</div>
<div className="mt-2 text-xs tabular-nums text-muted-foreground">
{item.startedAt.toLocaleString()}
@@ -89,18 +105,30 @@ export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: Co
</div>
</aside>
<article key={turn.id} className="min-w-0 space-y-5 lg:overflow-y-auto lg:pr-2">
<article
key={turn.id}
className="min-w-0 space-y-5 lg:overflow-y-auto lg:pr-2"
>
<header className="space-y-3 border-b pb-4">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold">{turn.botName}</h3>
{statusBadge(turn.status)}
<span className="text-sm text-muted-foreground">{turn.userName || turn.userId}</span>
<span className="text-sm text-muted-foreground">
{turn.userName || turn.userId}
</span>
</div>
<div className="flex flex-wrap gap-x-5 gap-y-2 text-xs tabular-nums text-muted-foreground">
<span>{turn.startedAt.toLocaleString()}</span>
<span>{t('monitoring.llmCalls.title')} {turn.llmCalls.length}</span>
<span>{t('monitoring.toolCalls.title')} {turn.toolCalls.length}</span>
<span>{t('monitoring.llmCalls.totalTokens')} {turn.totalTokens.toLocaleString()}</span>
<span>
{t('monitoring.llmCalls.title')} {turn.llmCalls.length}
</span>
<span>
{t('monitoring.toolCalls.title')} {turn.toolCalls.length}
</span>
<span>
{t('monitoring.llmCalls.totalTokens')}{' '}
{turn.totalTokens.toLocaleString()}
</span>
</div>
</header>
@@ -109,54 +137,130 @@ export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: Co
<MessageCircle className="size-4 text-muted-foreground" />
{t('monitoring.pipelineTrace.received')}
</h4>
{turn.userMessage ? <MessageBody content={turn.userMessage.messageContent} /> : (
<p className="text-sm text-muted-foreground">{t('monitoring.messageList.noUserMessage')}</p>
{turn.userMessage ? (
<MessageBody content={turn.userMessage.messageContent} />
) : (
<p className="text-sm text-muted-foreground">
{t('monitoring.messageList.noUserMessage')}
</p>
)}
</section>
<section>
<h4 className="mb-3 text-sm font-medium">
{t('monitoring.pipelineTrace.processing')}
<span className="ml-2 font-normal text-muted-foreground">{steps.length}</span>
<span className="ml-2 font-normal text-muted-foreground">
{steps.length}
</span>
</h4>
{steps.length === 0 ? (
<p className="text-sm text-muted-foreground">{t('monitoring.pipelineTrace.noSteps')}</p>
<p className="text-sm text-muted-foreground">
{t('monitoring.pipelineTrace.noSteps')}
</p>
) : (
<ol className="space-y-3">
{steps.map((step, index) => {
const { call } = step;
const parameters = step.kind === 'tool' ? parsePayload(step.call.arguments) : null;
const summary = step.kind === 'tool' && parameters && typeof parameters === 'object' && !Array.isArray(parameters)
? toolSummary(step.call.toolName, parameters as Record<string, unknown>).text : '';
const parameters =
step.kind === 'tool'
? parsePayload(step.call.arguments)
: null;
const summary =
step.kind === 'tool' &&
parameters &&
typeof parameters === 'object' &&
!Array.isArray(parameters)
? toolSummary(
step.call.toolName,
parameters as Record<string, unknown>,
).text
: '';
return (
<li key={`${step.kind}:${call.id}`} className="relative flex gap-3">
{index < steps.length - 1 && <div className="absolute bottom-[-12px] left-4 top-8 w-px bg-border" />}
<span className="relative flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs tabular-nums text-muted-foreground">{index + 1}</span>
<li
key={`${step.kind}:${call.id}`}
className="relative flex gap-3"
>
{index < steps.length - 1 && (
<div className="absolute bottom-[-12px] left-4 top-8 w-px bg-border" />
)}
<span className="relative flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs tabular-nums text-muted-foreground">
{index + 1}
</span>
<div className="min-w-0 flex-1 overflow-hidden rounded-xl border">
<div className="flex flex-wrap items-center gap-2 border-b bg-muted/20 px-4 py-3 text-sm">
{step.kind === 'model' ? <Cpu className="size-4" /> : step.kind === 'tool' ? <Wrench className="size-4" /> : <AlertCircle className="size-4 text-destructive" />}
{step.kind === 'model' ? (
<Cpu className="size-4" />
) : step.kind === 'tool' ? (
<Wrench className="size-4" />
) : (
<AlertCircle className="size-4 text-destructive" />
)}
<span className="min-w-0 flex-1 break-words font-medium">
{step.kind === 'model' ? step.call.modelName : step.kind === 'tool' ? step.call.toolName : step.call.errorType}
{step.kind === 'model'
? step.call.modelName
: step.kind === 'tool'
? step.call.toolName
: step.call.errorType}
</span>
{statusBadge(step.kind === 'error' ? 'error' : step.call.status)}
{statusBadge(
step.kind === 'error' ? 'error' : step.call.status,
)}
</div>
<div className="space-y-3 p-4">
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs tabular-nums text-muted-foreground">
<span>{call.timestamp.toLocaleTimeString()}</span>
{step.kind !== 'error' && <span>{formatDuration(step.call.duration)}</span>}
{step.kind === 'tool' && <span>{step.call.toolSource}</span>}
{step.kind === 'model' && <>
<span>{t('monitoring.llmCalls.inputTokens')} {step.call.tokens.input.toLocaleString()}</span>
<span>{t('monitoring.llmCalls.outputTokens')} {step.call.tokens.output.toLocaleString()}</span>
</>}
{step.kind !== 'error' && (
<span>{formatDuration(step.call.duration)}</span>
)}
{step.kind === 'tool' && (
<span>{step.call.toolSource}</span>
)}
{step.kind === 'model' && (
<>
<span>
{t('monitoring.llmCalls.inputTokens')}{' '}
{step.call.tokens.input.toLocaleString()}
</span>
<span>
{t('monitoring.llmCalls.outputTokens')}{' '}
{step.call.tokens.output.toLocaleString()}
</span>
</>
)}
</div>
{summary && <p className="whitespace-pre-wrap break-words text-sm [overflow-wrap:anywhere]">{summary}</p>}
{call.errorMessage && <p className="whitespace-pre-wrap break-words text-sm text-destructive [overflow-wrap:anywhere]">{call.errorMessage}</p>}
{step.kind === 'tool' && <div>
{step.call.arguments && <ProcessorPayload title={t('monitoring.toolCalls.arguments')} value={parameters} />}
{step.call.result && step.call.result !== 'null' && <ProcessorPayload title={t('monitoring.toolCalls.result')} value={parsePayload(step.call.result)} />}
</div>}
{step.kind === 'error' && step.call.stackTrace && <ProcessorPayload title={t('monitoring.unified.metadata')} value={step.call.stackTrace} />}
{summary && (
<p className="whitespace-pre-wrap break-words text-sm [overflow-wrap:anywhere]">
{summary}
</p>
)}
{call.errorMessage && (
<p className="whitespace-pre-wrap break-words text-sm text-destructive [overflow-wrap:anywhere]">
{call.errorMessage}
</p>
)}
{step.kind === 'tool' && (
<div>
{step.call.arguments && (
<ProcessorPayload
title={t('monitoring.toolCalls.arguments')}
value={parameters}
/>
)}
{step.call.result &&
step.call.result !== 'null' && (
<ProcessorPayload
title={t('monitoring.toolCalls.result')}
value={parsePayload(step.call.result)}
/>
)}
</div>
)}
{step.kind === 'error' && step.call.stackTrace && (
<ProcessorPayload
title={t('monitoring.unified.metadata')}
value={step.call.stackTrace}
/>
)}
</div>
</div>
</li>
@@ -170,23 +274,42 @@ export function ConversationTurnList({ turns, expandedTurnId, onToggleTurn }: Co
<h4 className="mb-3 flex items-center gap-2 text-sm font-medium">
<Send className="size-4 text-muted-foreground" />
{t('monitoring.pipelineTrace.replies')}
<span className="font-normal text-muted-foreground">{turn.assistantMessages.length}</span>
<span className="font-normal text-muted-foreground">
{turn.assistantMessages.length}
</span>
</h4>
{turn.assistantMessages.length ? turn.assistantMessages.map((message) => (
<div key={message.id} className="space-y-2 border-t py-3 first:border-t-0 first:pt-0 last:pb-0">
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>{message.timestamp.toLocaleTimeString()}</span>
{statusBadge(message.status)}
{turn.assistantMessages.length ? (
turn.assistantMessages.map((message) => (
<div
key={message.id}
className="space-y-2 border-t py-3 first:border-t-0 first:pt-0 last:pb-0"
>
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>{message.timestamp.toLocaleTimeString()}</span>
{statusBadge(message.status)}
</div>
<MessageBody content={message.messageContent} />
</div>
<MessageBody content={message.messageContent} />
</div>
)) : <p className="text-sm text-muted-foreground">{t('monitoring.messageList.noAssistantMessage')}</p>}
))
) : (
<p className="text-sm text-muted-foreground">
{t('monitoring.messageList.noAssistantMessage')}
</p>
)}
</section>
<ProcessorPayload title={t('monitoring.unified.metadata')} value={{
turn_id: turn.id, session_id: turn.sessionId, bot: turn.botName,
pipeline: turn.pipelineName, runner: turn.runnerName, platform: turn.platform,
user_id: turn.userId, messages: turn.messages,
}} />
<ProcessorPayload
title={t('monitoring.unified.metadata')}
value={{
turn_id: turn.id,
session_id: turn.sessionId,
bot: turn.botName,
pipeline: turn.pipelineName,
runner: turn.runnerName,
platform: turn.platform,
user_id: turn.userId,
messages: turn.messages,
}}
/>
</article>
</div>
);
@@ -39,7 +39,8 @@ export function ExportDropdown({ filterState }: ExportDropdownProps) {
const { t } = useTranslation();
const [exporting, setExporting] = useState<ExportType | null>(null);
const getDateRangeParams = () => resolveMonitoringWindow(filterState.timeRange, filterState.customDateRange);
const getDateRangeParams = () =>
resolveMonitoringWindow(filterState.timeRange, filterState.customDateRange);
const handleExport = async (type: ExportType) => {
setExporting(type);
@@ -174,7 +174,12 @@ export default function TokenMonitoring({
if (error) {
return (
<LoadErrorState title={t('monitoring.loadError')} description={error} onRetry={fetchStats} busy={loading} />
<LoadErrorState
title={t('monitoring.loadError')}
description={error}
onRetry={fetchStats}
busy={loading}
/>
);
}
@@ -41,14 +41,24 @@ const mainSections: ExecutionSection[] = ['inputs', 'outputs', 'deliveries'];
function messageBody(value: unknown): unknown {
if (typeof value === 'string') {
if (/^\s*[\[{]/.test(value)) {
try { return messageBody(JSON.parse(value)); } catch { /* Plain text. */ }
try {
return messageBody(JSON.parse(value));
} catch {
/* Plain text. */
}
}
return value;
}
if (Array.isArray(value)) return value.map(messageBody).filter((item) => item != null && item !== '');
if (Array.isArray(value))
return value.map(messageBody).filter((item) => item != null && item !== '');
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;
if (
['Image', 'File', 'Voice', 'At', 'AtAll', 'Quote'].includes(
String(data.type),
)
)
return value;
for (const key of ['text', 'content', 'message_chain', 'message', 'root']) {
if (data[key] != null) return messageBody(data[key]);
}
@@ -225,17 +235,24 @@ export default function ExecutionDetailSheet({
: ''}
</span>
{item.status && (
<Badge
status={item.status}
>
{t(`monitoring.execution.status.${['success', 'delivered'].includes(item.status) ? 'completed' : item.status}`, { defaultValue: item.status })}
<Badge status={item.status}>
{t(
`monitoring.execution.status.${['success', 'delivered'].includes(item.status) ? 'completed' : item.status}`,
{ defaultValue: item.status },
)}
</Badge>
)}
</div>
{content ? (
<>
<div className="text-sm leading-7">
{hasExecutionValue(body) ? <Value value={body} /> : <p className="text-muted-foreground">{t('monitoring.unified.noRecords')}</p>}
{hasExecutionValue(body) ? (
<Value value={body} />
) : (
<p className="text-muted-foreground">
{t('monitoring.unified.noRecords')}
</p>
)}
</div>
<Value value={item.attachments} />
{item.origin === 'generated' && section === 'conversation' && (
@@ -247,7 +264,9 @@ export default function ExecutionDetailSheet({
</p>
) : null}
<details className="border-t pt-2">
<summary className="cursor-pointer text-xs text-muted-foreground">{t('monitoring.unified.payload')}</summary>
<summary className="cursor-pointer text-xs text-muted-foreground">
{t('monitoring.unified.payload')}
</summary>
<div className="mt-3 max-h-80 overflow-auto rounded-md bg-muted/40 p-3">
<Value value={item} />
</div>
@@ -300,7 +319,11 @@ export default function ExecutionDetailSheet({
<div key={`${item.id}-${i}`}>{renderItem(section, item)}</div>
))}
{sectionError === section && (
<LoadErrorState compact title={t('monitoring.execution.detail.loadError')} onRetry={() => loadMore(section)} />
<LoadErrorState
compact
title={t('monitoring.execution.detail.loadError')}
onRetry={() => loadMore(section)}
/>
)}
{page.has_more && (
<Button
@@ -318,13 +341,19 @@ export default function ExecutionDetailSheet({
return primary ? (
<section key={section} className="space-y-3">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<span className="flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">{mainSections.indexOf(section) + 1}</span>
<span className="flex size-6 items-center justify-center rounded-full bg-muted text-xs text-muted-foreground">
{mainSections.indexOf(section) + 1}
</span>
{title}
</h3>
{body}
</section>
) : (
<details key={section} open={section === 'errors' ? true : undefined} className={`rounded-lg border p-3 ${section === 'errors' ? 'border-red-500/25 bg-red-500/5' : ''}`}>
<details
key={section}
open={section === 'errors' ? true : undefined}
className={`rounded-lg border p-3 ${section === 'errors' ? 'border-red-500/25 bg-red-500/5' : ''}`}
>
<summary className="cursor-pointer text-sm font-medium">
{title}
</summary>
@@ -369,15 +398,24 @@ export default function ExecutionDetailSheet({
size="icon"
disabled={loading}
aria-label={t('monitoring.refreshData')}
title={error ? t('monitoring.execution.detail.loadError') : t('monitoring.refreshData')}
title={
error
? t('monitoring.execution.detail.loadError')
: t('monitoring.refreshData')
}
onClick={() => setRefresh((value) => value + 1)}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''} ${error ? 'text-destructive' : ''}`} />
<RefreshCw
className={`h-4 w-4 ${loading ? 'animate-spin' : ''} ${error ? 'text-destructive' : ''}`}
/>
</Button>
</div>
{loading && !visible && <Skeleton className="h-36 w-full" />}
{error && !visible && (
<LoadErrorState title={t('monitoring.execution.detail.loadError')} onRetry={() => setRefresh((value) => value + 1)} />
<LoadErrorState
title={t('monitoring.execution.detail.loadError')}
onRetry={() => setRefresh((value) => value + 1)}
/>
)}
{record && (
<>
@@ -410,26 +448,48 @@ export default function ExecutionDetailSheet({
</p>
<div className="grid grid-cols-3 divide-x rounded-lg border bg-muted/20">
<div className="space-y-1 p-3">
<p className="text-xs text-muted-foreground">{t('monitoring.execution.detail.duration')}</p>
<p className="text-sm font-semibold tabular-nums">{record.duration_ms != null ? formatRunDuration(record.duration_ms) : '—'}</p>
<p className="text-xs text-muted-foreground">
{t('monitoring.execution.detail.duration')}
</p>
<p className="text-sm font-semibold tabular-nums">
{record.duration_ms != null
? formatRunDuration(record.duration_ms)
: '—'}
</p>
</div>
<div className="space-y-1 p-3">
<p className="text-xs text-muted-foreground">{t('monitoring.llmCalls.totalTokens')}</p>
<p className="text-sm font-semibold tabular-nums">{record.usage?.total_tokens?.toLocaleString() ?? '—'}</p>
<p className="text-xs text-muted-foreground">
{t('monitoring.llmCalls.totalTokens')}
</p>
<p className="text-sm font-semibold tabular-nums">
{record.usage?.total_tokens?.toLocaleString() ?? '—'}
</p>
</div>
<div className="space-y-1 p-3">
<p className="text-xs text-muted-foreground">{t('monitoring.unified.deliveries')}</p>
<p className="text-sm font-semibold tabular-nums">{visible?.pages.deliveries?.items.length ?? 0}{visible?.pages.deliveries?.has_more ? '+' : ''}</p>
<p className="text-xs text-muted-foreground">
{t('monitoring.unified.deliveries')}
</p>
<p className="text-sm font-semibold tabular-nums">
{visible?.pages.deliveries?.items.length ?? 0}
{visible?.pages.deliveries?.has_more ? '+' : ''}
</p>
</div>
</div>
{record.status_reason && !['stop', 'completed', 'success'].includes(record.status_reason) && (
<p className="rounded-md bg-muted p-3 text-sm">
{record.status_reason.split('; ').map((reason) =>
reason === 'failure_reason_not_recorded'
? t('common.failureReasonNotRecorded') : reason,
).join('; ')}
</p>
)}
{record.status_reason &&
!['stop', 'completed', 'success'].includes(
record.status_reason,
) && (
<p className="rounded-md bg-muted p-3 text-sm">
{record.status_reason
.split('; ')
.map((reason) =>
reason === 'failure_reason_not_recorded'
? t('common.failureReasonNotRecorded')
: reason,
)
.join('; ')}
</p>
)}
{mainSections.map((section) => renderSection(section, true))}
{secondarySections.map((section) => renderSection(section))}
<details className="rounded-lg border p-3">
@@ -66,17 +66,22 @@ export function executionKindLabel(row: ExecutionRow, t: TFunction): string {
export function ExecutionKindBadge({ row }: { row: ExecutionRow }) {
const { t } = useTranslation();
const Icon = row.target_kind === 'pipeline' ? Workflow
: row.target_kind === 'agent' ? Bot
: ['processor', 'event_processor'].includes(row.target_kind) ? Puzzle
: CircleDashed;
const color = row.target_kind === 'pipeline'
? 'border-blue-200/70 bg-blue-50/70 text-blue-700 dark:border-blue-800/50 dark:bg-blue-950/30 dark:text-blue-300'
: row.target_kind === 'agent'
? 'border-emerald-200/70 bg-emerald-50/70 text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-300'
: ['processor', 'event_processor'].includes(row.target_kind)
? 'border-amber-200/70 bg-amber-50/70 text-amber-700 dark:border-amber-800/50 dark:bg-amber-950/30 dark:text-amber-300'
: 'border-border bg-muted/40 text-muted-foreground';
const Icon =
row.target_kind === 'pipeline'
? Workflow
: row.target_kind === 'agent'
? Bot
: ['processor', 'event_processor'].includes(row.target_kind)
? Puzzle
: CircleDashed;
const color =
row.target_kind === 'pipeline'
? 'border-blue-200/70 bg-blue-50/70 text-blue-700 dark:border-blue-800/50 dark:bg-blue-950/30 dark:text-blue-300'
: row.target_kind === 'agent'
? 'border-emerald-200/70 bg-emerald-50/70 text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-300'
: ['processor', 'event_processor'].includes(row.target_kind)
? 'border-amber-200/70 bg-amber-50/70 text-amber-700 dark:border-amber-800/50 dark:bg-amber-950/30 dark:text-amber-300'
: 'border-border bg-muted/40 text-muted-foreground';
return (
<Badge variant="outline" className={`gap-1 whitespace-nowrap ${color}`}>
<Icon className="size-3 shrink-0" aria-hidden="true" />
@@ -7,10 +7,20 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { ChevronDown } from 'lucide-react';
import { DateRange } from '../../types/monitoring';
@@ -162,22 +172,32 @@ export default function MonitoringFilters({
const [dateOpen, setDateOpen] = useState(false);
const [fromDate, setFromDate] = useState('');
const [toDate, setToDate] = useState('');
const localDate = (date: Date) => new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
const localDate = (date: Date) =>
new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16);
const openCustom = () => {
setFromDate(localDate(customDateRange?.from ?? new Date(Date.now() - 86400000)));
setFromDate(
localDate(customDateRange?.from ?? new Date(Date.now() - 86400000)),
);
setToDate(localDate(customDateRange?.to ?? new Date()));
setDateOpen(true);
};
const from = new Date(fromDate);
const to = new Date(toDate);
const validDates = Number.isFinite(from.getTime()) && Number.isFinite(to.getTime()) && from < to && to.getTime() - from.getTime() <= 365 * 86400000 && to.getTime() <= Date.now();
const validDates =
Number.isFinite(from.getTime()) &&
Number.isFinite(to.getTime()) &&
from < to &&
to.getTime() - from.getTime() <= 365 * 86400000 &&
to.getTime() <= Date.now();
const handleTimeRangeChange = (value: string) => {
if (value === 'custom') customPending.current = true;
else onTimeRangeChange(value as TimeRangeOption);
};
const toggleProcessors = (ids: string[], checked: boolean) => {
const next = new Set(selectedPipelines);
ids.forEach((id) => checked ? next.add(id) : next.delete(id));
ids.forEach((id) => (checked ? next.add(id) : next.delete(id)));
onPipelinesChange([...next]);
};
return (
@@ -237,28 +257,82 @@ export default function MonitoringFilters({
</Label>
<Popover>
<PopoverTrigger asChild>
<Button id="monitoring-filter-processor" variant="outline" disabled={loadingProcessors} className="h-9 w-full justify-between font-normal">
<span className="truncate">{selectedPipelines.length ? t('agents.apiToolsSelected', { count: selectedPipelines.length }) : t('monitoring.filters.allProcessors')}</span>
<Button
id="monitoring-filter-processor"
variant="outline"
disabled={loadingProcessors}
className="h-9 w-full justify-between font-normal"
>
<span className="truncate">
{selectedPipelines.length
? t('agents.apiToolsSelected', {
count: selectedPipelines.length,
})
: t('monitoring.filters.allProcessors')}
</span>
<ChevronDown className="ml-2 size-4 shrink-0 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="max-h-96 w-72 overflow-y-auto p-2">
<Button variant="ghost" size="sm" className="mb-1 w-full justify-start" onClick={() => onPipelinesChange([])}>{t('monitoring.filters.allProcessors')}</Button>
<PopoverContent
align="start"
className="max-h-96 w-72 overflow-y-auto p-2"
>
<Button
variant="ghost"
size="sm"
className="mb-1 w-full justify-start"
onClick={() => onPipelinesChange([])}
>
{t('monitoring.filters.allProcessors')}
</Button>
{['pipeline', 'agent', 'event_processor'].map((kind) => {
const group = processors.filter((item) => item.kind === kind);
if (!group.length) return null;
const selected = group.filter((item) => selectedPipelines.includes(item.uuid)).length;
return <div key={kind} className="border-t py-2">
<label className="mb-1 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted">
<Checkbox checked={selected === group.length ? true : selected ? 'indeterminate' : false} onCheckedChange={(checked) => toggleProcessors(group.map((item) => item.uuid), checked === true)} />
<span className="flex-1">{t(`monitoring.execution.kind.${kind === 'event_processor' ? 'processor' : kind}`)}</span>
<span>{t('agents.apiToolsSelectAll')}</span>
</label>
{group.map((item) => <label key={item.uuid} className="flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-sm hover:bg-muted">
<Checkbox checked={selectedPipelines.includes(item.uuid)} onCheckedChange={(checked) => toggleProcessors([item.uuid], checked === true)} />
<span aria-hidden="true">{item.emoji}</span><span className="truncate">{item.name}</span>
</label>)}
</div>;
const selected = group.filter((item) =>
selectedPipelines.includes(item.uuid),
).length;
return (
<div key={kind} className="border-t py-2">
<label className="mb-1 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted">
<Checkbox
checked={
selected === group.length
? true
: selected
? 'indeterminate'
: false
}
onCheckedChange={(checked) =>
toggleProcessors(
group.map((item) => item.uuid),
checked === true,
)
}
/>
<span className="flex-1">
{t(
`monitoring.execution.kind.${kind === 'event_processor' ? 'processor' : kind}`,
)}
</span>
<span>{t('agents.apiToolsSelectAll')}</span>
</label>
{group.map((item) => (
<label
key={item.uuid}
className="flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-sm hover:bg-muted"
>
<Checkbox
checked={selectedPipelines.includes(item.uuid)}
onCheckedChange={(checked) =>
toggleProcessors([item.uuid], checked === true)
}
/>
<span aria-hidden="true">{item.emoji}</span>
<span className="truncate">{item.name}</span>
</label>
))}
</div>
);
})}
</PopoverContent>
</Popover>
@@ -349,21 +423,44 @@ export default function MonitoringFilters({
>
{t('monitoring.filters.timeRange')}
</Label>
<Select value={timeRange === 'custom' ? '' : timeRange} onValueChange={handleTimeRangeChange}>
<Select
value={timeRange === 'custom' ? '' : timeRange}
onValueChange={handleTimeRangeChange}
>
<SelectTrigger
id="monitoring-filter-time"
className="h-9 w-full min-w-0 gap-2 [&>svg]:shrink-0"
title={timeRange === 'custom' && customDateRange ? `${customDateRange.from.toLocaleString()} – ${customDateRange.to.toLocaleString()}` : undefined}
title={
timeRange === 'custom' && customDateRange
? `${customDateRange.from.toLocaleString()} – ${customDateRange.to.toLocaleString()}`
: undefined
}
>
{timeRange === 'custom' && customDateRange ? (
<span className="min-w-0 truncate text-xs tabular-nums">
{customDateRange.from.toLocaleDateString(undefined, { month: '2-digit', day: '2-digit' })}
{customDateRange.from.toLocaleDateString(undefined, {
month: '2-digit',
day: '2-digit',
})}
{' – '}
{customDateRange.to.toLocaleDateString(undefined, { month: '2-digit', day: '2-digit' })}
{customDateRange.to.toLocaleDateString(undefined, {
month: '2-digit',
day: '2-digit',
})}
</span>
) : <SelectValue />}
) : (
<SelectValue />
)}
</SelectTrigger>
<SelectContent onCloseAutoFocus={(event) => { if (customPending.current) { event.preventDefault(); customPending.current = false; openCustom(); } }}>
<SelectContent
onCloseAutoFocus={(event) => {
if (customPending.current) {
event.preventDefault();
customPending.current = false;
openCustom();
}
}}
>
<SelectItem value="lastHour">
{t('monitoring.filters.lastHour')}
</SelectItem>
@@ -376,17 +473,64 @@ export default function MonitoringFilters({
<SelectItem value="last7Days">
{t('monitoring.filters.last7Days')}
</SelectItem>
{[30, 90, 180, 365].map((days) => <SelectItem key={days} value={`last${days}Days`}>{t('monitoring.rangeDays', { days })}</SelectItem>)}
{onCustomDateRangeChange && <SelectItem value="custom">{t('monitoring.customRange')}</SelectItem>} </SelectContent>
</Select> {onCustomDateRangeChange && <>
<Dialog open={dateOpen} onOpenChange={setDateOpen}><DialogContent className="sm:max-w-md space-y-3">
<DialogHeader><DialogTitle>{t('monitoring.customRange')}</DialogTitle><DialogDescription>{t('monitoring.rangeHint')}</DialogDescription></DialogHeader>
<label className="block space-y-1 text-xs">{t('monitoring.rangeStart')}<Input type="datetime-local" value={fromDate} max={toDate} onChange={(event) => setFromDate(event.target.value)} /></label>
<label className="block space-y-1 text-xs">{t('monitoring.rangeEnd')}<Input type="datetime-local" value={toDate} min={fromDate} max={localDate(new Date())} onChange={(event) => setToDate(event.target.value)} /></label>
{[30, 90, 180, 365].map((days) => (
<SelectItem key={days} value={`last${days}Days`}>
{t('monitoring.rangeDays', { days })}
</SelectItem>
))}
{onCustomDateRangeChange && (
<SelectItem value="custom">
{t('monitoring.customRange')}
</SelectItem>
)}{' '}
</SelectContent>
</Select>{' '}
{onCustomDateRangeChange && (
<>
<Dialog open={dateOpen} onOpenChange={setDateOpen}>
<DialogContent className="sm:max-w-md space-y-3">
<DialogHeader>
<DialogTitle>{t('monitoring.customRange')}</DialogTitle>
<DialogDescription>
{t('monitoring.rangeHint')}
</DialogDescription>
</DialogHeader>
<label className="block space-y-1 text-xs">
{t('monitoring.rangeStart')}
<Input
type="datetime-local"
value={fromDate}
max={toDate}
onChange={(event) => setFromDate(event.target.value)}
/>
</label>
<label className="block space-y-1 text-xs">
{t('monitoring.rangeEnd')}
<Input
type="datetime-local"
value={toDate}
min={fromDate}
max={localDate(new Date())}
onChange={(event) => setToDate(event.target.value)}
/>
</label>
<Button size="sm" className="w-full" disabled={!validDates} onClick={() => { onCustomDateRangeChange({ from, to }); onTimeRangeChange('custom'); setDateOpen(false); }}>{t('common.confirm')}</Button>
</DialogContent></Dialog>
</>}
<Button
size="sm"
className="w-full"
disabled={!validDates}
onClick={() => {
onCustomDateRangeChange({ from, to });
onTimeRangeChange('custom');
setDateOpen(false);
}}
>
{t('common.confirm')}
</Button>
</DialogContent>
</Dialog>
</>
)}
</div>
</div>
);
@@ -11,7 +11,11 @@ import {
ResponsiveContainer,
} from 'recharts';
import { Card } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { Skeleton } from '@/components/ui/skeleton';
import { MonitoringData } from '../../types/monitoring';
@@ -23,24 +27,50 @@ interface TrafficChartProps {
export default function TrafficChart(props: TrafficChartProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(() => {
try { return localStorage.getItem('langbot-traffic-chart-open') !== 'false'; }
catch { return true; }
try {
return localStorage.getItem('langbot-traffic-chart-open') !== 'false';
} catch {
return true;
}
});
return (
<Collapsible open={open} onOpenChange={(value) => {
setOpen(value);
try { localStorage.setItem('langbot-traffic-chart-open', String(value)); } catch { /* Storage may be unavailable. */ }
}} asChild>
<Collapsible
open={open}
onOpenChange={(value) => {
setOpen(value);
try {
localStorage.setItem('langbot-traffic-chart-open', String(value));
} catch {
/* Storage may be unavailable. */
}
}}
asChild
>
<Card className="gap-0 overflow-hidden py-0">
<CollapsibleTrigger asChild>
<button type="button" className="group flex w-full items-center gap-3 px-5 py-4 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400"><BarChart3 className="size-4" /></span>
<span className="flex-1 text-sm font-semibold">{t('monitoring.trafficChart.title')}</span>
<span className="hidden items-center gap-4 text-xs text-muted-foreground sm:flex">
<span className="flex items-center gap-1.5"><span className="size-1.5 rounded-full bg-blue-500" />{t('monitoring.trafficChart.messages')}</span>
<span className="flex items-center gap-1.5"><span className="size-1.5 rounded-full bg-violet-500" />{t('monitoring.trafficChart.llmCalls')}</span>
<button
type="button"
className="group flex w-full items-center gap-3 px-5 py-4 text-left transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400">
<BarChart3 className="size-4" />
</span>
<ChevronDown className={`ml-2 size-4 text-muted-foreground transition-transform motion-reduce:transition-none ${open ? 'rotate-180' : ''}`} />
<span className="flex-1 text-sm font-semibold">
{t('monitoring.trafficChart.title')}
</span>
<span className="hidden items-center gap-4 text-xs text-muted-foreground sm:flex">
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-blue-500" />
{t('monitoring.trafficChart.messages')}
</span>
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-violet-500" />
{t('monitoring.trafficChart.llmCalls')}
</span>
</span>
<ChevronDown
className={`ml-2 size-4 text-muted-foreground transition-transform motion-reduce:transition-none ${open ? 'rotate-180' : ''}`}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
@@ -31,7 +31,10 @@ export function useExecutions(params: {
const [nonce, setNonce] = useState(0);
const workspaceUuid = useCurrentWorkspace()?.workspace.uuid;
const requestIdRef = useRef(0);
const scope = JSON.stringify([workspaceUuid, { ...params, refreshKey: undefined }]);
const scope = JSON.stringify([
workspaceUuid,
{ ...params, refreshKey: undefined },
]);
const [requestScope, setRequestScope] = useState<string | null>(null);
// Memoize filter parameters to prevent unnecessary re-renders
@@ -90,7 +93,7 @@ export function useExecutions(params: {
} catch (err) {
if (!isCurrent()) return;
setRequestScope(scope);
setResult((current) => requestScope === scope ? current : null);
setResult((current) => (requestScope === scope ? current : null));
setError(err as Error);
console.error('Failed to fetch executions:', err);
} finally {
@@ -391,7 +391,7 @@ export function useMonitoringData(filterState: FilterState) {
} catch (err) {
if (!isCurrent()) return;
setRequestScope(scope);
setData((current) => requestScope === scope ? current : null);
setData((current) => (requestScope === scope ? current : null));
setError(err as Error);
console.error('Failed to fetch monitoring data:', err);
} finally {
@@ -55,7 +55,8 @@ function restore(key: string): FilterState {
if (
Number.isFinite(from.getTime()) &&
Number.isFinite(to.getTime()) &&
from < to && to.getTime() - from.getTime() <= 365 * 86400000
from < to &&
to.getTime() - from.getTime() <= 365 * 86400000
)
state.customDateRange = { from, to };
}
+133 -67
View File
@@ -9,7 +9,12 @@ import React, {
import { useTranslation } from 'react-i18next';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@/components/ui/select';
import LoadErrorState from '@/components/LoadErrorState';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -62,7 +67,12 @@ function MonitoringPageContent() {
} = useMonitoringFilters();
const executionMode = filterState.mode ?? 'all';
const executionStatus = filterState.statusGroup ?? 'all';
const { data, loading: monitoringRefreshing, error, refetch } = useMonitoringData({
const {
data,
loading: monitoringRefreshing,
error,
refetch,
} = useMonitoringData({
...filterState,
mode: executionMode,
statusGroup: executionStatus,
@@ -140,7 +150,8 @@ function MonitoringPageContent() {
const loading = monitoringRefreshing && !data;
const executionLoading = executionRefreshing && !executionResult;
const feedbackLoading = feedbackRefreshing && !feedbackStats;
const refreshing = monitoringRefreshing || executionRefreshing || feedbackRefreshing;
const refreshing =
monitoringRefreshing || executionRefreshing || feedbackRefreshing;
const countdown = useRef(0);
const intervalRef = useRef(0);
const [remainingSeconds, setRemainingSeconds] = useState(0);
@@ -155,9 +166,13 @@ function MonitoringPageContent() {
const [refreshInterval, setRefreshInterval] = useState(() => {
try {
const saved = Number(localStorage.getItem('langbot-dashboard-refresh-interval'));
const saved = Number(
localStorage.getItem('langbot-dashboard-refresh-interval'),
);
return [5, 15, 30, 60, 300].includes(saved) ? saved : 0;
} catch { return 0; }
} catch {
return 0;
}
});
const refreshState = useRef({ handleRefresh, busy: false });
useEffect(() => {
@@ -169,7 +184,10 @@ function MonitoringPageContent() {
setRemainingSeconds(refreshInterval);
if (!refreshInterval || error) return;
const timer = window.setInterval(() => {
if (document.visibilityState === 'visible' && !refreshState.current.busy) {
if (
document.visibilityState === 'visible' &&
!refreshState.current.busy
) {
countdown.current = Math.max(0, countdown.current - 1);
setRemainingSeconds(countdown.current);
if (countdown.current === 0) {
@@ -204,71 +222,115 @@ function MonitoringPageContent() {
return (
<div className="w-full h-full overflow-y-auto overflow-x-hidden">
{/* Filters and Refresh Button - Sticky */}
{!error && <div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
<div>
<Card className="flex flex-col gap-4 p-4 xl:flex-row xl:items-end">
<MonitoringFilters
selectedBots={filterState.selectedBots}
selectedPipelines={filterState.selectedPipelines}
timeRange={filterState.timeRange}
onBotsChange={setSelectedBots}
onPipelinesChange={setSelectedPipelines}
onTimeRangeChange={setTimeRange}
customDateRange={filterState.customDateRange}
onCustomDateRangeChange={setCustomDateRange}
mode={executionMode}
onModeChange={setExecutionMode}
statusGroup={executionStatus}
onStatusGroupChange={setExecutionStatus}
/>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-3 xl:border-l xl:border-t-0 xl:pl-4 xl:pt-0 [&_button]:h-9">
<Button variant="outline" size="sm" onClick={resetFilters}>
<RotateCcw className="mr-2 h-4 w-4" />
{t('monitoring.filters.reset')}
</Button>
{canExport && <ExportDropdown filterState={filterState} />}
<div className="relative flex items-center overflow-hidden rounded-lg border bg-background shadow-xs">
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
disabled={refreshing}
className="gap-2 rounded-none px-3"
>
<RefreshCw className={cn('size-3.5', refreshing && 'animate-spin')} />
{t('monitoring.refreshData')}
{refreshInterval > 0 && <span className="min-w-8 text-right text-xs tabular-nums text-muted-foreground">{remainingSeconds}s</span>}
</Button>
<Select value={String(refreshInterval)} onValueChange={(value) => {
setRefreshInterval(Number(value));
try { localStorage.setItem('langbot-dashboard-refresh-interval', value); } catch { /* Storage may be unavailable. */ }
}}>
<SelectTrigger className="w-11 justify-center rounded-l-none border-0 border-l px-2 shadow-none [&>svg:last-child]:hidden" aria-label={t('monitoring.autoRefreshLabel')} title={refreshInterval ? t('monitoring.autoRefreshEvery', { seconds: refreshInterval }) : t('monitoring.autoRefreshOff')}>
<span aria-hidden="true"><ChevronDown className="size-3.5" /></span>
</SelectTrigger>
<SelectContent>
<SelectItem value="0">{t('monitoring.autoRefreshOff')}</SelectItem>
{[5, 15, 30, 60, 300].map((seconds) => (
<SelectItem key={seconds} value={String(seconds)}>
{t('monitoring.autoRefreshEvery', { seconds })}
</SelectItem>
))}
</SelectContent>
</Select>
{refreshInterval > 0 && <div className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 bg-blue-500/10" aria-hidden="true">
<div className="h-full origin-left bg-blue-500/70 transition-transform duration-700 ease-linear motion-reduce:transition-none" style={{ transform: `scaleX(${remainingSeconds / refreshInterval})` }} />
</div>}
{!error && (
<div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
<div>
<Card className="flex flex-col gap-4 p-4 xl:flex-row xl:items-end">
<MonitoringFilters
selectedBots={filterState.selectedBots}
selectedPipelines={filterState.selectedPipelines}
timeRange={filterState.timeRange}
onBotsChange={setSelectedBots}
onPipelinesChange={setSelectedPipelines}
onTimeRangeChange={setTimeRange}
customDateRange={filterState.customDateRange}
onCustomDateRangeChange={setCustomDateRange}
mode={executionMode}
onModeChange={setExecutionMode}
statusGroup={executionStatus}
onStatusGroupChange={setExecutionStatus}
/>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-3 xl:border-l xl:border-t-0 xl:pl-4 xl:pt-0 [&_button]:h-9">
<Button variant="outline" size="sm" onClick={resetFilters}>
<RotateCcw className="mr-2 h-4 w-4" />
{t('monitoring.filters.reset')}
</Button>
{canExport && <ExportDropdown filterState={filterState} />}
<div className="relative flex items-center overflow-hidden rounded-lg border bg-background shadow-xs">
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
disabled={refreshing}
className="gap-2 rounded-none px-3"
>
<RefreshCw
className={cn('size-3.5', refreshing && 'animate-spin')}
/>
{t('monitoring.refreshData')}
{refreshInterval > 0 && (
<span className="min-w-8 text-right text-xs tabular-nums text-muted-foreground">
{remainingSeconds}s
</span>
)}
</Button>
<Select
value={String(refreshInterval)}
onValueChange={(value) => {
setRefreshInterval(Number(value));
try {
localStorage.setItem(
'langbot-dashboard-refresh-interval',
value,
);
} catch {
/* Storage may be unavailable. */
}
}}
>
<SelectTrigger
className="w-11 justify-center rounded-l-none border-0 border-l px-2 shadow-none [&>svg:last-child]:hidden"
aria-label={t('monitoring.autoRefreshLabel')}
title={
refreshInterval
? t('monitoring.autoRefreshEvery', {
seconds: refreshInterval,
})
: t('monitoring.autoRefreshOff')
}
>
<span aria-hidden="true">
<ChevronDown className="size-3.5" />
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="0">
{t('monitoring.autoRefreshOff')}
</SelectItem>
{[5, 15, 30, 60, 300].map((seconds) => (
<SelectItem key={seconds} value={String(seconds)}>
{t('monitoring.autoRefreshEvery', { seconds })}
</SelectItem>
))}
</SelectContent>
</Select>
{refreshInterval > 0 && (
<div
className="pointer-events-none absolute inset-x-0 bottom-0 h-0.5 bg-blue-500/10"
aria-hidden="true"
>
<div
className="h-full origin-left bg-blue-500/70 transition-transform duration-700 ease-linear motion-reduce:transition-none"
style={{
transform: `scaleX(${remainingSeconds / refreshInterval})`,
}}
/>
</div>
)}
</div>
</div>
</div>
</Card>
</Card>
</div>
</div>
</div>
}
)}
{/* Content Area */}
{error ? (
<LoadErrorState title={t('monitoring.loadError')} onRetry={handleRefresh} busy={refreshing} />
<LoadErrorState
title={t('monitoring.loadError')}
onRetry={handleRefresh}
busy={refreshing}
/>
) : (
<div className="relative z-0 flex flex-col gap-6 pb-4 pt-3">
{/* Overview Section: the execution metrics and the runtime status share
@@ -360,7 +422,11 @@ function MonitoringPageContent() {
{executionError ? (
<div className="px-3 pt-4 sm:px-6">
<LoadErrorState title={t('monitoring.execution.detail.loadError')} onRetry={handleRefresh} busy={refreshing} />
<LoadErrorState
title={t('monitoring.execution.detail.loadError')}
onRetry={handleRefresh}
busy={refreshing}
/>
</div>
) : (
<>
@@ -26,7 +26,9 @@ export function getPresetDateRange(option: TimeRangeOption): DateRange | null {
case 'last180Days':
case 'last365Days':
case 'last30Days':
from.setTime(now.getTime() - Number(option.match(/\d+/)?.[0] ?? 30) * 86400000);
from.setTime(
now.getTime() - Number(option.match(/\d+/)?.[0] ?? 30) * 86400000,
);
break;
default:
return null;
@@ -150,7 +152,9 @@ export function resolveMonitoringWindow(
case 'last180Days':
case 'last365Days':
case 'last30Days':
startTime = new Date(now.getTime() - Number(timeRange.match(/\d+/)?.[0] ?? 30) * 86400000);
startTime = new Date(
now.getTime() - Number(timeRange.match(/\d+/)?.[0] ?? 30) * 86400000,
);
break;
case 'custom':
if (customDateRange) {
@@ -52,7 +52,8 @@ export default function PipelineDetailContent({
}, [id, isCreateMode, pipelines, setDetailEntityName, t]);
const [searchParams] = useSearchParams();
const activeView = searchParams.get('tab') === 'logs' ? 'monitoring' : 'workbench';
const activeView =
searchParams.get('tab') === 'logs' ? 'monitoring' : 'workbench';
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
const [formDirty, setFormDirty] = useState(false);
const [formSaving, setFormSaving] = useState(false);
+5 -1
View File
@@ -203,7 +203,11 @@ function PluginPageIframe({
return (
<div className="flex flex-col h-full w-full">
{assetError ? (
<LoadErrorState title={t('plugins.loadFailed')} className="flex-1" onRetry={() => window.location.reload()} />
<LoadErrorState
title={t('plugins.loadFailed')}
className="flex-1"
onRetry={() => window.location.reload()}
/>
) : loading || !assetUrl ? (
<EntityLoadState />
) : null}
+6 -1
View File
@@ -168,7 +168,12 @@ function PluginListView() {
}
if (statusError) {
return <LoadErrorState title={t('plugins.failedToGetStatus')} onRetry={() => setStatusRevision((value) => value + 1)} />;
return (
<LoadErrorState
title={t('plugins.failedToGetStatus')}
onRetry={() => setStatusRevision((value) => value + 1)}
/>
);
}
if (!pluginSystemStatus?.is_enable) {
+21 -6
View File
@@ -822,9 +822,13 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/monitoring/sessions?${queryParams.toString()}`);
}
public resetSessionContext(botId: string, sessionId: string): Promise<{ reset: boolean }> {
public resetSessionContext(
botId: string,
sessionId: string,
): Promise<{ reset: boolean }> {
return this.post('/api/v1/monitoring/sessions/reset-context', {
bot_id: botId, session_id: sessionId,
bot_id: botId,
session_id: sessionId,
});
}
@@ -2210,10 +2214,19 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/monitoring/data?${queryParams.toString()}`);
}
public async streamInflight(onFrame: (frame: InflightFrame) => void, signal: AbortSignal): Promise<void> {
public async streamInflight(
onFrame: (frame: InflightFrame) => void,
signal: AbortSignal,
): Promise<void> {
const response = await this.instance.get<ReadableStream<Uint8Array>>(
'/api/v1/monitoring/in-flight/stream',
{ adapter: 'fetch', responseType: 'stream', timeout: 0, signal, headers: { Accept: 'text/event-stream' } },
{
adapter: 'fetch',
responseType: 'stream',
timeout: 0,
signal,
headers: { Accept: 'text/event-stream' },
},
);
const reader = response.data.getReader();
const decoder = new TextDecoder();
@@ -2227,9 +2240,11 @@ export class BackendClient extends BaseHttpClient {
while ((end = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
if (frame.startsWith('data: ')) onFrame(JSON.parse(frame.slice(6)) as InflightFrame);
if (frame.startsWith('data: '))
onFrame(JSON.parse(frame.slice(6)) as InflightFrame);
}
if (buffer.length > 2_000_000) throw new Error('Monitoring frame exceeds limit');
if (buffer.length > 2_000_000)
throw new Error('Monitoring frame exceeds limit');
}
} finally {
await reader.cancel().catch(() => undefined);
+2 -1
View File
@@ -1,4 +1,5 @@
export const MODEL_CONFIGURATION_CHANGED = 'langbot:model-configuration-changed';
export const MODEL_CONFIGURATION_CHANGED =
'langbot:model-configuration-changed';
export async function notifyAfterModelConfigurationChange<T>(
request: Promise<T>,
+5 -1
View File
@@ -68,7 +68,11 @@ export default function LoadErrorState({
>
<RefreshCw
aria-hidden="true"
className={cn('size-3.5', (retrying || busy) && 'animate-spin motion-reduce:animate-none')}
className={cn(
'size-3.5',
(retrying || busy) &&
'animate-spin motion-reduce:animate-none',
)}
/>
{retrying || busy ? t('common.loading') : t('common.retry')}
</Button>
+21 -3
View File
@@ -38,20 +38,38 @@ function Badge({
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant: status ? 'outline' : variant }), status && statusBadgeClass(status), className)}
className={cn(
badgeVariants({ variant: status ? 'outline' : variant }),
status && statusBadgeClass(status),
className,
)}
{...props}
/>
);
}
function statusBadgeClass(status: string): string {
if (['completed', 'success', 'succeeded', 'delivered', 'ready', 'enabled', 'connected'].includes(status))
if (
[
'completed',
'success',
'succeeded',
'delivered',
'ready',
'enabled',
'connected',
].includes(status)
)
return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300';
if (['failed', 'error', 'timeout', 'disconnected'].includes(status))
return 'border-red-500/25 bg-red-500/10 text-red-700 dark:text-red-300';
if (['running', 'active', 'connecting'].includes(status))
return 'border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300';
if (['queued', 'pending', 'created', 'claimed', 'waiting', 'warning'].includes(status))
if (
['queued', 'pending', 'created', 'claimed', 'waiting', 'warning'].includes(
status,
)
)
return 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-300';
if (['mock', 'simulated', 'debug'].includes(status))
return 'border-violet-500/25 bg-violet-500/10 text-violet-700 dark:text-violet-300';
+58 -41
View File
@@ -1,50 +1,67 @@
export default {
"en-US": {
"warning": "No active routing. Add a rule and choose a processor to handle events.",
"create": "Create new processor",
"createDescription": "Create a processor and select it for this rule. Save the bot to apply the change.",
"incompatible": "The processor was created, but it does not support this event. Choose a compatible processor."
'en-US': {
warning:
'No active routing. Add a rule and choose a processor to handle events.',
create: 'Create new processor',
createDescription:
'Create a processor and select it for this rule. Save the bot to apply the change.',
incompatible:
'The processor was created, but it does not support this event. Choose a compatible processor.',
},
"zh-Hans": {
"warning": "尚未配置事件处理,请添加规则并选择处理器。",
"create": "创建新处理器",
"createDescription": "创建成功后会选入当前规则,保存机器人后生效。",
"incompatible": "处理器已创建,但不支持当前事件,请选择兼容的处理器。"
'zh-Hans': {
warning: '尚未配置事件处理,请添加规则并选择处理器。',
create: '创建新处理器',
createDescription: '创建成功后会选入当前规则,保存机器人后生效。',
incompatible: '处理器已创建,但不支持当前事件,请选择兼容的处理器。',
},
"zh-Hant": {
"warning": "尚未設定事件處理,請新增規則並選擇處理器。",
"create": "建立新處理器",
"createDescription": "建立成功後會選入目前規則,儲存機器人後生效。",
"incompatible": "處理器已建立,但不支援目前事件,請選擇相容的處理器。"
'zh-Hant': {
warning: '尚未設定事件處理,請新增規則並選擇處理器。',
create: '建立新處理器',
createDescription: '建立成功後會選入目前規則,儲存機器人後生效。',
incompatible: '處理器已建立,但不支援目前事件,請選擇相容的處理器。',
},
"ja-JP": {
"warning": "イベント処理が未設定です。ルールを追加し、処理を選択してください。",
"create": "新しい処理を作成",
"createDescription": "作成した処理を現在のルールに選択します。ボットを保存すると反映されます。",
"incompatible": "処理は作成されましたが、このイベントに対応していません。対応する処理を選択してください。"
'ja-JP': {
warning:
'イベント処理が未設定です。ルールを追加し、処理を選択してください。',
create: '新しい処理を作成',
createDescription:
'作成した処理を現在のルールに選択します。ボットを保存すると反映されます。',
incompatible:
'処理は作成されましたが、このイベントに対応していません。対応する処理を選択してください。',
},
"es-ES": {
"warning": "Sin procesamiento de eventos. Añade una regla y elige un procesador.",
"create": "Crear procesador",
"createDescription": "El procesador se seleccionará para esta regla. Guarda el bot para aplicar el cambio.",
"incompatible": "El procesador se creó, pero no admite este evento. Elige uno compatible."
'es-ES': {
warning:
'Sin procesamiento de eventos. Añade una regla y elige un procesador.',
create: 'Crear procesador',
createDescription:
'El procesador se seleccionará para esta regla. Guarda el bot para aplicar el cambio.',
incompatible:
'El procesador se creó, pero no admite este evento. Elige uno compatible.',
},
"ru-RU": {
"warning": "Обработка событий не настроена. Добавьте правило и выберите обработчик.",
"create": "Создать обработчик",
"createDescription": "Созданный обработчик будет выбран для правила. Сохраните бота для применения.",
"incompatible": "Обработчик создан, но не поддерживает это событие. Выберите совместимый обработчик."
'ru-RU': {
warning:
'Обработка событий не настроена. Добавьте правило и выберите обработчик.',
create: 'Создать обработчик',
createDescription:
'Созданный обработчик будет выбран для правила. Сохраните бота для применения.',
incompatible:
'Обработчик создан, но не поддерживает это событие. Выберите совместимый обработчик.',
},
"vi-VN": {
"warning": "Chưa thiết lập xử lý sự kiện. Hãy thêm quy tắc và chọn bộ xử lý.",
"create": "Tạo bộ xử lý mới",
"createDescription": "Bộ xử lý sẽ được chọn cho quy tắc này. Lưu bot để áp dụng.",
"incompatible": "Đã tạo bộ xử lý nhưng không hỗ trợ sự kiện này. Hãy chọn bộ xử lý tương thích."
'vi-VN': {
warning: 'Chưa thiết lập xử lý sự kiện. Hãy thêm quy tắc và chọn bộ xử lý.',
create: 'Tạo bộ xử lý mới',
createDescription:
'Bộ xử lý sẽ được chọn cho quy tắc này. Lưu bot để áp dụng.',
incompatible:
'Đã tạo bộ xử lý nhưng không hỗ trợ sự kiện này. Hãy chọn bộ xử lý tương thích.',
},
'th-TH': {
warning:
'ยังไม่ได้ตั้งค่าการจัดการเหตุการณ์ โปรดเพิ่มกฎและเลือกตัวประมวลผล',
create: 'สร้างตัวประมวลผลใหม่',
createDescription:
'ระบบจะเลือกตัวประมวลผลให้กฎนี้ บันทึกบอตเพื่อใช้การเปลี่ยนแปลง',
incompatible:
'สร้างตัวประมวลผลแล้ว แต่ไม่รองรับเหตุการณ์นี้ โปรดเลือกตัวประมวลผลที่รองรับ',
},
"th-TH": {
"warning": "ยังไม่ได้ตั้งค่าการจัดการเหตุการณ์ โปรดเพิ่มกฎและเลือกตัวประมวลผล",
"create": "สร้างตัวประมวลผลใหม่",
"createDescription": "ระบบจะเลือกตัวประมวลผลให้กฎนี้ บันทึกบอตเพื่อใช้การเปลี่ยนแปลง",
"incompatible": "สร้างตัวประมวลผลแล้ว แต่ไม่รองรับเหตุการณ์นี้ โปรดเลือกตัวประมวลผลที่รองรับ"
}
};
+117 -96
View File
@@ -3,30 +3,30 @@ import pipelineMigration from './pipeline-migration/en-US';
const enUS = {
slackSetup,
inflight: {
autoCollapse: "Auto-collapse",
empty: "No events are being processed",
move: "Drag vertically; use arrow keys to move",
botLogs: "Robot logs",
processorLogs: "Processor logs",
autoCollapse: 'Auto-collapse',
empty: 'No events are being processed',
move: 'Drag vertically; use arrow keys to move',
botLogs: 'Robot logs',
processorLogs: 'Processor logs',
"title": "In flight",
"expand": "Expand",
"collapse": "Collapse",
"reconnecting": "Reconnecting; showing the last received state.",
"details": "View details",
"debug": "Debug event",
"limited": "Showing a limited set. Open the dashboard for all executions.",
"queued": "Queued",
"running": "Processing",
"waiting": "Waiting for input",
"tool": "Calling a tool",
"thinking": "Thinking",
"replying": "Preparing reply",
"generating": "Generating reply",
"completed": "Completed",
"failed": "Failed",
"cancelled": "Cancelled",
"ignored": "Ignored"
title: 'In flight',
expand: 'Expand',
collapse: 'Collapse',
reconnecting: 'Reconnecting; showing the last received state.',
details: 'View details',
debug: 'Debug event',
limited: 'Showing a limited set. Open the dashboard for all executions.',
queued: 'Queued',
running: 'Processing',
waiting: 'Waiting for input',
tool: 'Calling a tool',
thinking: 'Thinking',
replying: 'Preparing reply',
generating: 'Generating reply',
completed: 'Completed',
failed: 'Failed',
cancelled: 'Cancelled',
ignored: 'Ignored',
},
assistant: {
movePosition: 'Move assistant horizontally',
@@ -104,14 +104,14 @@ const enUS = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
title: 'Review activity',
description:
'Review bot activity, model usage, message volume, and system performance at a glance.',
@@ -193,14 +193,14 @@ const enUS = {
'Send test messages to check triggers, AI replies, and output processing. Any configuration changes are saved before testing.',
},
monitoring: {
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
title: 'Review run history',
description:
'Use the tab beside the title to review past conversations, processing steps, and errors when replies are missing or unexpected.',
@@ -368,9 +368,11 @@ const enUS = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'The failure reason was not recorded. Check the bot logs or retry to capture more details.',
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.',
loadFailedHint:
'Please try again in a moment. If this continues, check your connection and service status.',
customValue: 'Custom',
loadFailed: 'Failed to load. Please try again.',
search: 'Search',
@@ -864,7 +866,8 @@ const enUS = {
'Bind the pipeline that processes messages for this bot',
routingHelp: {
title: 'What is event routing?',
intro: 'Bots receive more than messages: friend requests, group invitations and other events too.',
intro:
'Bots receive more than messages: friend requests, group invitations and other events too.',
events: 'Incoming events',
agents: 'Different agents',
message: 'Message received',
@@ -874,8 +877,10 @@ const enUS = {
friendAction: 'Review agent → Review',
groupAction: 'Welcome agent → Greet',
rules: 'Your routing rules · Example',
instructions: 'Agents follow your instructions and use tools to reply, review requests and more.',
plugin: 'For advanced workflows, plugin processors register events in bulk and handle them with code. Add a plugin processor configuration below.',
instructions:
'Agents follow your instructions and use tools to reply, review requests and more.',
plugin:
'For advanced workflows, plugin processors register events in bulk and handle them with code. Add a plugin processor configuration below.',
docs: 'Develop your own plugin processor',
runnerDocs: 'Develop your own Agent runner',
pause: 'Pause',
@@ -919,7 +924,8 @@ const enUS = {
enabled: 'Enabled',
eventBindingDescriptionPlaceholder: 'Rule description',
noEventBindings: 'No event routes',
unsupportedPipelineEvent: 'Pipelines can only be used for message.received events',
unsupportedPipelineEvent:
'Pipelines can only be used for message.received events',
disable: 'Disable',
enable: 'Enable',
disabledBindings: 'Disabled',
@@ -929,7 +935,7 @@ const enUS = {
advancedEventValues: 'View all',
eventGroup: 'Group',
eventGroupNames: {
wecomcs: "WeCom Customer Service",
wecomcs: 'WeCom Customer Service',
bot: 'Bot status',
feedback: 'Feedback',
friend: 'Friends',
@@ -945,7 +951,8 @@ const enUS = {
routeFallbackIgnored:
'Events that match no route are ignored. Add a catch-all route only when every event needs an explicit outcome.',
matchRecords: 'Match records',
matchRecordsDescription: 'Latest result for each rule and recent unmatched events, from bot memory logs. Cleared on restart.',
matchRecordsDescription:
'Latest result for each rule and recent unmatched events, from bot memory logs. Cleared on restart.',
testRoute: 'Check route',
adapterEventDebugAction: 'Test listener',
adapterEventDebugTitle: 'Platform event debugging',
@@ -1031,14 +1038,15 @@ const enUS = {
eventWildcard: 'All events',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "User entered bot chat",
wecombot_template_card_event: "Template card interaction",
dingtalk_card_action: "DingTalk card interaction",
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",
wecombot_enter_chat: 'User entered bot chat',
wecombot_template_card_event: 'Template card interaction',
dingtalk_card_action: 'DingTalk card interaction',
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',
@@ -1048,7 +1056,7 @@ const enUS = {
friend_added: 'Friend added',
group_member_joined: 'Member joined group',
group_member_left: 'Member left group',
group_info_updated: "Group information updated",
group_info_updated: 'Group information updated',
group_member_banned: 'Member banned',
bot_invited_to_group: 'Bot invited to group',
bot_removed_from_group: 'Bot removed from group',
@@ -1057,21 +1065,30 @@ const enUS = {
platform_specific: 'Platform-specific event',
},
eventDescriptions: {
wecombot_enter_chat: "A user opened the bot chat. Use this event for welcome workflows.",
wecombot_template_card_event: "A user clicked or submitted a template card. Managed form callbacks are handled separately.",
dingtalk_card_action: "Card clicks and submissions; feedback and managed forms are handled separately.",
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.",
wecombot_enter_chat:
'A user opened the bot chat. Use this event for welcome workflows.',
wecombot_template_card_event:
'A user clicked or submitted a template card. Managed form callbacks are handled separately.',
dingtalk_card_action:
'Card clicks and submissions; feedback and managed forms are handled separately.',
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:
'Matches bot invitations, removals, mutes, and other status events.',
namespace_feedback: 'Matches feedback events from a platform or user.',
namespace_friend: 'Matches friend requests and friendship changes.',
namespace_group: "Matches group events such as membership changes and group information updates.",
namespace_group:
'Matches group events such as membership changes and group information updates.',
namespace_message:
'Matches received, edited, deleted, and reaction message events.',
namespace_platform:
@@ -1087,7 +1104,8 @@ const enUS = {
friend_added: 'A friend relationship was created.',
group_member_joined: 'A member joins a group where the bot is present.',
group_member_left: 'A member leaves a group where the bot is present.',
group_info_updated: "Group details such as its name or owner have changed.",
group_info_updated:
'Group details such as its name or owner have changed.',
group_member_banned: 'A group member is banned or removed.',
bot_invited_to_group: 'The bot is invited to a group.',
bot_removed_from_group: 'The bot is removed from a group.',
@@ -1174,10 +1192,13 @@ const enUS = {
imagesAttached: 'image(s) attached',
noLogs: 'No logs yet',
sessionMonitor: {
resetContext: "Reset context",
resetDescription: "New messages will start with an empty context. Monitoring history is kept; files and long-term memory are unchanged. Wait for active tasks to finish before resetting.",
resetSuccess: "Context reset. New messages will start a fresh conversation.",
resetError: "Could not reset context. Wait for active tasks to finish and try again.",
resetContext: 'Reset context',
resetDescription:
'New messages will start with an empty context. Monitoring history is kept; files and long-term memory are unchanged. Wait for active tasks to finish before resetting.',
resetSuccess:
'Context reset. New messages will start a fresh conversation.',
resetError:
'Could not reset context. Wait for active tasks to finish and try again.',
title: 'Sessions',
sessions: 'Sessions',
@@ -1228,24 +1249,24 @@ const enUS = {
agents: {
apiToolsSelectAll: 'Select all',
monitoring: {
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
runStatus: "Run status",
toolCalls: "Tool calls",
delivery: "Message delivery",
sendFailed: "Send failed",
sendCount: "{{count}} send calls succeeded",
noSend: "No send recorded",
noSteps: "No execution steps recorded yet.",
recipient: "Recipient",
actionCompleted: "Tool completed successfully.",
simulatedAction: "Simulated action; no real message was sent.",
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
runStatus: 'Run status',
toolCalls: 'Tool calls',
delivery: 'Message delivery',
sendFailed: 'Send failed',
sendCount: '{{count}} send calls succeeded',
noSend: 'No send recorded',
noSteps: 'No execution steps recorded yet.',
recipient: 'Recipient',
actionCompleted: 'Tool completed successfully.',
simulatedAction: 'Simulated action; no real message was sent.',
description:
'Follow each task from its triggering event through model output and tool execution.',
empty:
@@ -2171,14 +2192,14 @@ const enUS = {
uploading: 'Uploading...',
},
monitoring: {
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
rangeDays: 'Last {{days}} days',
customRange: 'Custom time range',
rangeStart: 'Start time',
rangeEnd: 'End time',
rangeHint: 'Start must precede end. Maximum range: 365 days.',
autoRefreshLabel: 'Auto refresh',
autoRefreshOff: 'Auto refresh: off',
autoRefreshEvery: 'Refresh every {{seconds}} s',
title: 'Run logs',
workbench: 'Configure & debug',
description:
+117 -96
View File
@@ -1,30 +1,31 @@
import pipelineMigration from './pipeline-migration/es-ES';
const esES = {
inflight: {
autoCollapse: "Plegar auto.",
empty: "No hay eventos en proceso",
move: "Arrastra verticalmente o usa las flechas",
botLogs: "Registros del bot",
processorLogs: "Registros del procesador",
autoCollapse: 'Plegar auto.',
empty: 'No hay eventos en proceso',
move: 'Arrastra verticalmente o usa las flechas',
botLogs: 'Registros del bot',
processorLogs: 'Registros del procesador',
"title": "En curso",
"expand": "Expandir",
"collapse": "Contraer",
"reconnecting": "Reconectando; se muestra el último estado recibido.",
"details": "Ver detalles",
"debug": "Evento de depuración",
"limited": "Se muestra un subconjunto. Consulte todas las ejecuciones en el panel.",
"queued": "En cola",
"running": "Procesando",
"waiting": "Esperando entrada",
"tool": "Ejecutando herramienta",
"thinking": "Pensando",
"replying": "Preparando respuesta",
"generating": "Generando respuesta",
"completed": "Completado",
"failed": "Fallido",
"cancelled": "Cancelado",
"ignored": "Ignorado"
title: 'En curso',
expand: 'Expandir',
collapse: 'Contraer',
reconnecting: 'Reconectando; se muestra el último estado recibido.',
details: 'Ver detalles',
debug: 'Evento de depuración',
limited:
'Se muestra un subconjunto. Consulte todas las ejecuciones en el panel.',
queued: 'En cola',
running: 'Procesando',
waiting: 'Esperando entrada',
tool: 'Ejecutando herramienta',
thinking: 'Pensando',
replying: 'Preparando respuesta',
generating: 'Generando respuesta',
completed: 'Completado',
failed: 'Fallido',
cancelled: 'Cancelado',
ignored: 'Ignorado',
},
assistant: {
movePosition: 'Mover el asistente horizontalmente',
@@ -105,14 +106,14 @@ const esES = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
title: 'Revisar la actividad',
description:
'Consulta la actividad de los bots, el uso de modelos, los mensajes y el rendimiento del sistema.',
@@ -194,14 +195,14 @@ const esES = {
'Envía mensajes de prueba para comprobar los activadores, las respuestas de IA y el procesamiento de salida. Los cambios se guardan antes de la prueba.',
},
monitoring: {
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
title: 'Revisar el historial',
description:
'Usa la pestaña junto al título para revisar conversaciones, pasos de procesamiento y errores cuando falten respuestas o sean inesperadas.',
@@ -369,9 +370,11 @@ 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.',
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.',
loadFailedHint:
'Inténtalo de nuevo. Si el problema persiste, comprueba la conexión y el estado del servicio.',
customValue: 'Personalizado',
loadFailed: 'No se pudo cargar. Inténtalo de nuevo.',
search: 'Buscar',
@@ -878,7 +881,8 @@ const esES = {
'Vincula el Pipeline que procesa los mensajes de este Bot',
routingHelp: {
title: '¿Qué es el enrutamiento de eventos?',
intro: 'Los bots reciben mensajes, solicitudes de amistad y eventos como unirse a un grupo.',
intro:
'Los bots reciben mensajes, solicitudes de amistad y eventos como unirse a un grupo.',
events: 'Eventos recibidos',
agents: 'Distintos agentes',
message: 'Mensaje recibido',
@@ -888,8 +892,10 @@ const esES = {
friendAction: 'Agente de revisión',
groupAction: 'Agente de bienvenida',
rules: 'Según tus reglas · Ejemplo',
instructions: 'Los agentes siguen tus instrucciones y usan herramientas para responder o revisar solicitudes.',
plugin: 'Para casos avanzados, los procesadores de plugins registran eventos en lote y los gestionan con código. Añade su configuración abajo.',
instructions:
'Los agentes siguen tus instrucciones y usan herramientas para responder o revisar solicitudes.',
plugin:
'Para casos avanzados, los procesadores de plugins registran eventos en lote y los gestionan con código. Añade su configuración abajo.',
docs: 'Desarrolla tu propio procesador de plugins',
runnerDocs: 'Desarrolla tu propio ejecutor de Agent',
pause: 'Pausar',
@@ -935,7 +941,8 @@ const esES = {
enabled: 'Activado',
eventBindingDescriptionPlaceholder: 'Descripción de la regla',
noEventBindings: 'No hay rutas de eventos',
unsupportedPipelineEvent: 'Los flujos solo admiten eventos message.received',
unsupportedPipelineEvent:
'Los flujos solo admiten eventos message.received',
disable: 'Desactivar',
enable: 'Activar',
disabledBindings: 'Desactivado',
@@ -945,7 +952,7 @@ const esES = {
advancedEventValues: 'Ver todos',
eventGroup: 'Grupo',
eventGroupNames: {
wecomcs: "Atención al cliente de WeCom",
wecomcs: 'Atención al cliente de WeCom',
bot: 'Estado del bot',
feedback: 'Comentarios',
friend: 'Amigos',
@@ -961,7 +968,8 @@ const esES = {
routeFallbackIgnored:
'Se ignoran los eventos sin ruta coincidente. Añade una ruta general solo si todos los eventos necesitan un resultado explícito.',
matchRecords: 'Registros de coincidencias',
matchRecordsDescription: 'Último resultado de cada regla y eventos recientes sin coincidencia. Los registros en memoria se borran al reiniciar.',
matchRecordsDescription:
'Último resultado de cada regla y eventos recientes sin coincidencia. Los registros en memoria se borran al reiniciar.',
testRoute: 'Comprobar ruta',
adapterEventDebugAction: 'Probar escucha',
adapterEventDebugTitle: 'Depuración de eventos de plataforma',
@@ -1048,14 +1056,15 @@ const esES = {
eventWildcard: 'Todos los eventos',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "Usuario entró al chat del bot",
wecombot_template_card_event: "Interacción con tarjeta",
dingtalk_card_action: "Interacción con tarjeta de DingTalk",
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",
wecombot_enter_chat: 'Usuario entró al chat del bot',
wecombot_template_card_event: 'Interacción con tarjeta',
dingtalk_card_action: 'Interacción con tarjeta de DingTalk',
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',
@@ -1065,7 +1074,7 @@ const esES = {
friend_added: 'Amigo añadido',
group_member_joined: 'Miembro unido al grupo',
group_member_left: 'Miembro salió del grupo',
group_info_updated: "Información del grupo actualizada",
group_info_updated: 'Información del grupo actualizada',
group_member_banned: 'Miembro bloqueado',
bot_invited_to_group: 'Bot invitado al grupo',
bot_removed_from_group: 'Bot eliminado del grupo',
@@ -1074,14 +1083,22 @@ const esES = {
platform_specific: 'Evento específico de la plataforma',
},
eventDescriptions: {
wecombot_enter_chat: "Un usuario abrió el chat del bot. Úsalo para flujos de bienvenida.",
wecombot_template_card_event: "Un usuario pulsó o envió una tarjeta. Las respuestas de formularios gestionados se procesan por separado.",
dingtalk_card_action: "Clics y envíos de tarjetas; las valoraciones y los formularios gestionados se procesan por separado.",
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.",
wecombot_enter_chat:
'Un usuario abrió el chat del bot. Úsalo para flujos de bienvenida.',
wecombot_template_card_event:
'Un usuario pulsó o envió una tarjeta. Las respuestas de formularios gestionados se procesan por separado.',
dingtalk_card_action:
'Clics y envíos de tarjetas; las valoraciones y los formularios gestionados se procesan por separado.',
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:
@@ -1089,7 +1106,8 @@ const esES = {
namespace_feedback:
'Coincide con comentarios recibidos de la plataforma o de usuarios.',
namespace_friend: 'Coincide con solicitudes y cambios de amistad.',
namespace_group: "Coincide con cambios de miembros y actualizaciones de información del grupo.",
namespace_group:
'Coincide con cambios de miembros y actualizaciones de información del grupo.',
namespace_message:
'Coincide con mensajes recibidos, editados, eliminados y reacciones.',
namespace_platform:
@@ -1106,7 +1124,8 @@ const esES = {
friend_added: 'Se ha creado una relación de amistad.',
group_member_joined: 'Un miembro se une a un grupo donde está el bot.',
group_member_left: 'Un miembro abandona un grupo donde está el bot.',
group_info_updated: "Han cambiado datos del grupo, como su nombre o propietario.",
group_info_updated:
'Han cambiado datos del grupo, como su nombre o propietario.',
group_member_banned: 'Un miembro del grupo es bloqueado o expulsado.',
bot_invited_to_group: 'El bot es invitado a un grupo.',
bot_removed_from_group: 'El bot es expulsado de un grupo.',
@@ -1194,10 +1213,12 @@ const esES = {
imagesAttached: 'imagen(es) adjunta(s)',
noLogs: 'Aún no hay registros',
sessionMonitor: {
resetContext: "Restablecer contexto",
resetDescription: "Los mensajes nuevos comenzarán sin contexto previo. Se conservan el historial de monitoreo, los archivos y la memoria a largo plazo. Espere a que terminen las tareas activas.",
resetSuccess: "Contexto restablecido.",
resetError: "No se pudo restablecer. Espere a que terminen las tareas activas y vuelva a intentarlo.",
resetContext: 'Restablecer contexto',
resetDescription:
'Los mensajes nuevos comenzarán sin contexto previo. Se conservan el historial de monitoreo, los archivos y la memoria a largo plazo. Espere a que terminen las tareas activas.',
resetSuccess: 'Contexto restablecido.',
resetError:
'No se pudo restablecer. Espere a que terminen las tareas activas y vuelva a intentarlo.',
title: 'Sesiones',
sessions: 'Sesiones',
@@ -1248,24 +1269,24 @@ const esES = {
agents: {
apiToolsSelectAll: 'Seleccionar todo',
monitoring: {
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
runStatus: "Estado de ejecución",
toolCalls: "Llamadas a herramientas",
delivery: "Envío de mensajes",
sendFailed: "Error de envío",
sendCount: "{{count}} llamadas de envío correctas",
noSend: "Sin envíos registrados",
noSteps: "Aún no hay pasos registrados.",
recipient: "Destinatario",
actionCompleted: "Herramienta ejecutada correctamente.",
simulatedAction: "Acción simulada; no se envió ningún mensaje real.",
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
runStatus: 'Estado de ejecución',
toolCalls: 'Llamadas a herramientas',
delivery: 'Envío de mensajes',
sendFailed: 'Error de envío',
sendCount: '{{count}} llamadas de envío correctas',
noSend: 'Sin envíos registrados',
noSteps: 'Aún no hay pasos registrados.',
recipient: 'Destinatario',
actionCompleted: 'Herramienta ejecutada correctamente.',
simulatedAction: 'Acción simulada; no se envió ningún mensaje real.',
description:
'Consulta el evento, la salida del modelo y las herramientas de cada tarea.',
empty:
@@ -2206,14 +2227,14 @@ const esES = {
uploading: 'Subiendo...',
},
monitoring: {
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
rangeDays: 'Últimos {{days}} días',
customRange: 'Periodo personalizado',
rangeStart: 'Inicio',
rangeEnd: 'Fin',
rangeHint: 'El inicio debe preceder al fin. Máximo: 365 días.',
autoRefreshLabel: 'Actualización automática',
autoRefreshOff: 'Actualización: desactivada',
autoRefreshEvery: 'Actualizar cada {{seconds}} s',
title: 'Registros de ejecución',
workbench: 'Configurar y depurar',
description:
+113 -95
View File
@@ -3,30 +3,30 @@ import pipelineMigration from './pipeline-migration/ja-JP';
const jaJP = {
slackSetup,
inflight: {
autoCollapse: "自動折りたたみ",
empty: "処理中のイベントはありません",
move: "上下にドラッグ、または矢印キーで移動",
botLogs: "ボットのログ",
processorLogs: "プロセッサーのログ",
autoCollapse: '自動折りたたみ',
empty: '処理中のイベントはありません',
move: '上下にドラッグ、または矢印キーで移動',
botLogs: 'ボットのログ',
processorLogs: 'プロセッサーのログ',
"title": "実行中",
"expand": "展開",
"collapse": "折りたたむ",
"reconnecting": "再接続中。最後に受信した状態を表示しています。",
"details": "詳細を見る",
"debug": "デバッグイベント",
"limited": "一部のみ表示しています。全実行はダッシュボードで確認できます。",
"queued": "待機中",
"running": "処理中",
"waiting": "入力待ち",
"tool": "ツール呼び出し中",
"thinking": "思考中",
"replying": "返信準備中",
"generating": "返信生成中",
"completed": "完了",
"failed": "失敗",
"cancelled": "キャンセル済み",
"ignored": "無視"
title: '実行中',
expand: '展開',
collapse: '折りたたむ',
reconnecting: '再接続中。最後に受信した状態を表示しています。',
details: '詳細を見る',
debug: 'デバッグイベント',
limited: '一部のみ表示しています。全実行はダッシュボードで確認できます。',
queued: '待機中',
running: '処理中',
waiting: '入力待ち',
tool: 'ツール呼び出し中',
thinking: '思考中',
replying: '返信準備中',
generating: '返信生成中',
completed: '完了',
failed: '失敗',
cancelled: 'キャンセル済み',
ignored: '無視',
},
assistant: {
movePosition: 'アシスタントを左右に移動',
@@ -105,14 +105,14 @@ const jaJP = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
title: '稼働状況を確認する',
description:
'ボットの稼働状況、モデルの使用量、メッセージ数、システムの状態をまとめて確認できます。',
@@ -194,14 +194,14 @@ const jaJP = {
'テストメッセージで起動条件、AI の返信、出力処理を確認します。設定の変更はテスト前に保存されます。',
},
monitoring: {
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
title: '実行履歴を確認する',
description:
'タイトル横のタブで過去の会話、処理過程、エラーを確認し、返信がない場合や想定外の回答を調べます。',
@@ -367,9 +367,11 @@ const jaJP = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'この記録には失敗の理由が保存されていません。ボットのログを確認するか、再送信して詳細を確認してください。',
failureReasonNotRecorded:
'この記録には失敗の理由が保存されていません。ボットのログを確認するか、再送信して詳細を確認してください。',
loadFailedTitle: '読み込めませんでした',
loadFailedHint: 'しばらくしてから再試行してください。解決しない場合は、接続とサービスの状態を確認してください。',
loadFailedHint:
'しばらくしてから再試行してください。解決しない場合は、接続とサービスの状態を確認してください。',
customValue: 'カスタム',
loadFailed: '読み込みに失敗しました。再試行してください。',
search: '検索',
@@ -867,7 +869,8 @@ const jaJP = {
'このボットのメッセージを処理するパイプラインを紐付け',
routingHelp: {
title: 'イベントルーティングとは?',
intro: 'ボットはメッセージのほか、友達リクエストやグループ参加などのイベントも受け取ります。',
intro:
'ボットはメッセージのほか、友達リクエストやグループ参加などのイベントも受け取ります。',
events: '受信イベント',
agents: '各 Agent へ',
message: 'メッセージ受信',
@@ -877,8 +880,10 @@ const jaJP = {
friendAction: '審査 Agent → 確認',
groupAction: '歓迎 Agent → 挨拶',
rules: '設定したルールで振り分け · 例',
instructions: 'Agent は指示に従ってツールを使い、返信や申請の確認を行います。',
plugin: '高度な用途にはプラグイン処理器を利用できます。イベントを一括登録してコードで処理し、下の欄で設定を追加します。',
instructions:
'Agent は指示に従ってツールを使い、返信や申請の確認を行います。',
plugin:
'高度な用途にはプラグイン処理器を利用できます。イベントを一括登録してコードで処理し、下の欄で設定を追加します。',
docs: '独自のプラグイン処理器を開発',
runnerDocs: '独自の Agent ランナーを開発',
pause: '一時停止',
@@ -934,7 +939,7 @@ const jaJP = {
advancedEventValues: 'すべて表示',
eventGroup: 'グループ',
eventGroupNames: {
wecomcs: "WeCom カスタマーサービス",
wecomcs: 'WeCom カスタマーサービス',
bot: 'ボットの状態',
feedback: 'フィードバック',
friend: '友だち',
@@ -950,7 +955,8 @@ const jaJP = {
routeFallbackIgnored:
'どのルートにも一致しないイベントは無視されます。すべてのイベントに明示的な結果が必要な場合のみ、フォールバックを追加してください。',
matchRecords: 'マッチ記録',
matchRecordsDescription: '各ルールの最新結果と最近の未一致イベントを表示します。ボットのメモリ内ログは再起動時に消去されます。',
matchRecordsDescription:
'各ルールの最新結果と最近の未一致イベントを表示します。ボットのメモリ内ログは再起動時に消去されます。',
testRoute: 'ルートを確認',
adapterEventDebugAction: 'イベント監視テスト',
adapterEventDebugTitle: 'プラットフォームイベントのデバッグ',
@@ -1036,14 +1042,15 @@ const jaJP = {
eventWildcard: 'すべてのイベント',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "ボットとのチャットに参加",
wecombot_template_card_event: "テンプレートカード操作",
dingtalk_card_action: "DingTalk カード操作",
wecomcs_enter_session: "顧客が会話に参加",
wecomcs_msg_send_fail: "カスタマーサービスの送信失敗",
wecomcs_servicer_status_change: "担当者の受付状態変更",
wecomcs_session_status_change: "カスタマーサービスの会話状態変更",
wecomcs_reject_customer_msg_switch_change: "顧客メッセージの受信拒否設定変更",
wecombot_enter_chat: 'ボットとのチャットに参加',
wecombot_template_card_event: 'テンプレートカード操作',
dingtalk_card_action: 'DingTalk カード操作',
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: 'メッセージ削除',
@@ -1053,7 +1060,7 @@ const jaJP = {
friend_added: '友達追加',
group_member_joined: 'メンバー参加',
group_member_left: 'メンバー退出',
group_info_updated: "グループ情報更新",
group_info_updated: 'グループ情報更新',
group_member_banned: 'メンバーBAN',
bot_invited_to_group: 'ボットがグループに招待された',
bot_removed_from_group: 'ボットがグループから削除された',
@@ -1062,14 +1069,22 @@ const jaJP = {
platform_specific: 'プラットフォーム固有イベント',
},
eventDescriptions: {
wecombot_enter_chat: "ユーザーがボットのチャットを開きました。歓迎や案内のフローに使用できます。",
wecombot_template_card_event: "ユーザーがカードをクリックまたは送信しました。管理対象フォームの応答は別途処理します。",
dingtalk_card_action: "カードのクリックや送信。フィードバックと管理対象フォームは別途処理します。",
wecomcs_enter_session: "顧客がリンクから会話を開きました。流入元と歓迎メッセージ用コードを含みます。",
wecomcs_msg_send_fail: "WeChat が送信失敗を通知しました。メッセージ ID と失敗種別を含みます。",
wecomcs_servicer_status_change: "担当者が受付を開始、停止、または一時停止しました。",
wecomcs_session_status_change: "担当者が会話を受付、転送、終了、または再開しました。",
wecomcs_reject_customer_msg_switch_change: "担当者が顧客メッセージの受信拒否を有効または無効にしました。",
wecombot_enter_chat:
'ユーザーがボットのチャットを開きました。歓迎や案内のフローに使用できます。',
wecombot_template_card_event:
'ユーザーがカードをクリックまたは送信しました。管理対象フォームの応答は別途処理します。',
dingtalk_card_action:
'カードのクリックや送信。フィードバックと管理対象フォームは別途処理します。',
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:
@@ -1077,7 +1092,7 @@ const jaJP = {
namespace_feedback:
'プラットフォームまたはユーザーからのフィードバックイベントに一致します。',
namespace_friend: '友達リクエストや友達関係の変更イベントに一致します。',
namespace_group: "メンバーの変更やグループ情報の更新などに一致します。",
namespace_group: 'メンバーの変更やグループ情報の更新などに一致します。',
namespace_message:
'メッセージの受信、編集、削除、リアクションイベントに一致します。',
namespace_platform:
@@ -1097,7 +1112,7 @@ const jaJP = {
friend_added: '友達関係が作成されました。',
group_member_joined: 'ボットがいるグループにメンバーが参加しました。',
group_member_left: 'ボットがいるグループからメンバーが退出しました。',
group_info_updated: "グループ名や所有者などの情報が変更されました。",
group_info_updated: 'グループ名や所有者などの情報が変更されました。',
group_member_banned: 'グループメンバーがBANまたは削除されました。',
bot_invited_to_group: 'ボットがグループに招待されました。',
bot_removed_from_group: 'ボットがグループから削除されました。',
@@ -1184,10 +1199,12 @@ const jaJP = {
imagesAttached: '枚の画像が添付されています',
noLogs: 'ログはまだありません',
sessionMonitor: {
resetContext: "コンテキストをリセット",
resetDescription: "新しいメッセージは空のコンテキストから開始します。監視履歴、ファイル、長期記憶は保持されます。実行中のタスクが完了してからリセットしてください。",
resetSuccess: "コンテキストをリセットしました。",
resetError: "リセットできませんでした。実行中のタスクが完了してから再試行してください。",
resetContext: 'コンテキストをリセット',
resetDescription:
'新しいメッセージは空のコンテキストから開始します。監視履歴、ファイル、長期記憶は保持されます。実行中のタスクが完了してからリセットしてください。',
resetSuccess: 'コンテキストをリセットしました。',
resetError:
'リセットできませんでした。実行中のタスクが完了してから再試行してください。',
title: 'セッション監視',
sessions: 'セッション一覧',
@@ -1238,24 +1255,25 @@ const jaJP = {
agents: {
apiToolsSelectAll: 'すべて選択',
monitoring: {
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
runStatus: "実行状態",
toolCalls: "ツール呼び出し",
delivery: "メッセージ送信",
sendFailed: "送信失敗",
sendCount: "送信 API 呼び出し成功:{{count}} 回",
noSend: "送信記録なし",
noSteps: "実行ステップはまだありません。",
recipient: "送信先",
actionCompleted: "ツールの実行が完了しました。",
simulatedAction: "シミュレーションです。実際のメッセージは送信されていません。",
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
runStatus: '実行状態',
toolCalls: 'ツール呼び出し',
delivery: 'メッセージ送信',
sendFailed: '送信失敗',
sendCount: '送信 API 呼び出し成功:{{count}} 回',
noSend: '送信記録なし',
noSteps: '実行ステップはまだありません。',
recipient: '送信先',
actionCompleted: 'ツールの実行が完了しました。',
simulatedAction:
'シミュレーションです。実際のメッセージは送信されていません。',
description:
'各タスクのトリガーイベント、モデル出力、ツール実行を確認します。',
empty:
@@ -2186,14 +2204,14 @@ const jaJP = {
uploading: 'アップロード中...',
},
monitoring: {
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
rangeDays: '過去 {{days}} 日間',
customRange: '期間を指定',
rangeStart: '開始日時',
rangeEnd: '終了日時',
rangeHint: '開始は終了より前、期間は最大365日です。',
autoRefreshLabel: '自動更新',
autoRefreshOff: '自動更新:オフ',
autoRefreshEvery: '{{seconds}} 秒ごとに更新',
title: '実行ログ',
workbench: '設定とデバッグ',
description: 'このパイプラインの実行ログとエラー情報を表示(過去24時間)',
+117 -96
View File
@@ -1,30 +1,31 @@
import pipelineMigration from './pipeline-migration/ru-RU';
const ruRU = {
inflight: {
autoCollapse: "Автосворачивание",
empty: "Нет событий в обработке",
move: "Перетащите по вертикали или используйте стрелки",
botLogs: "Журнал бота",
processorLogs: "Журнал обработчика",
autoCollapse: 'Автосворачивание',
empty: 'Нет событий в обработке',
move: 'Перетащите по вертикали или используйте стрелки',
botLogs: 'Журнал бота',
processorLogs: 'Журнал обработчика',
"title": "В работе",
"expand": "Развернуть",
"collapse": "Свернуть",
"reconnecting": "Переподключение; показано последнее полученное состояние.",
"details": "Подробнее",
"debug": "Отладочное событие",
"limited": "Показана часть запусков. Все запуски доступны на панели мониторинга.",
"queued": "В очереди",
"running": "Обработка",
"waiting": "Ожидание ввода",
"tool": "Вызов инструмента",
"thinking": "Размышление",
"replying": "Подготовка ответа",
"generating": "Генерация ответа",
"completed": "Завершено",
"failed": "Ошибка",
"cancelled": "Отменено",
"ignored": "Пропущено"
title: 'В работе',
expand: 'Развернуть',
collapse: 'Свернуть',
reconnecting: 'Переподключение; показано последнее полученное состояние.',
details: 'Подробнее',
debug: 'Отладочное событие',
limited:
'Показана часть запусков. Все запуски доступны на панели мониторинга.',
queued: 'В очереди',
running: 'Обработка',
waiting: 'Ожидание ввода',
tool: 'Вызов инструмента',
thinking: 'Размышление',
replying: 'Подготовка ответа',
generating: 'Генерация ответа',
completed: 'Завершено',
failed: 'Ошибка',
cancelled: 'Отменено',
ignored: 'Пропущено',
},
assistant: {
movePosition: 'Переместить помощника по горизонтали',
@@ -103,14 +104,14 @@ const ruRU = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
title: 'Проверьте активность',
description:
'Просматривайте активность ботов, использование моделей, объём сообщений и производительность системы.',
@@ -192,14 +193,14 @@ const ruRU = {
'Отправьте тестовые сообщения, чтобы проверить условия запуска, ответы ИИ и обработку вывода. Изменения сохраняются перед тестом.',
},
monitoring: {
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
title: 'Посмотрите историю запусков',
description:
'На вкладке рядом с заголовком доступны прошлые диалоги, этапы обработки и ошибки для диагностики отсутствующих или неожиданных ответов.',
@@ -368,9 +369,11 @@ const ruRU = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'Причина сбоя не сохранена. Проверьте журнал бота или отправьте сообщение повторно для получения подробностей.',
failureReasonNotRecorded:
'Причина сбоя не сохранена. Проверьте журнал бота или отправьте сообщение повторно для получения подробностей.',
loadFailedTitle: 'Не удалось загрузить данные',
loadFailedHint: 'Повторите попытку позже. Если проблема остаётся, проверьте подключение и состояние сервиса.',
loadFailedHint:
'Повторите попытку позже. Если проблема остаётся, проверьте подключение и состояние сервиса.',
customValue: 'Свой вариант',
loadFailed: 'Не удалось загрузить. Попробуйте ещё раз.',
search: 'Поиск',
@@ -873,7 +876,8 @@ const ruRU = {
'Привяжите конвейер, обрабатывающий сообщения для этого бота',
routingHelp: {
title: 'Что такое маршрутизация событий?',
intro: 'Бот получает сообщения, запросы в друзья и события вступления в группу.',
intro:
'Бот получает сообщения, запросы в друзья и события вступления в группу.',
events: 'Входящие события',
agents: 'Разные агенты',
message: 'Сообщение',
@@ -883,8 +887,10 @@ const ruRU = {
friendAction: 'Агент проверки',
groupAction: 'Агент приветствия',
rules: 'По вашим правилам · Пример',
instructions: 'Агенты следуют вашим инструкциям и вызывают инструменты для ответов и обработки запросов.',
plugin: 'Для сложных задач обработчики плагинов регистрируют события пакетно и обрабатывают их кодом. Добавьте конфигурацию обработчика ниже.',
instructions:
'Агенты следуют вашим инструкциям и вызывают инструменты для ответов и обработки запросов.',
plugin:
'Для сложных задач обработчики плагинов регистрируют события пакетно и обрабатывают их кодом. Добавьте конфигурацию обработчика ниже.',
docs: 'Разработать свой обработчик плагина',
runnerDocs: 'Разработать свой исполнитель Agent',
pause: 'Пауза',
@@ -930,7 +936,8 @@ const ruRU = {
enabled: 'Включено',
eventBindingDescriptionPlaceholder: 'Описание правила',
noEventBindings: 'Нет маршрутов событий',
unsupportedPipelineEvent: 'Конвейеры поддерживают только события message.received',
unsupportedPipelineEvent:
'Конвейеры поддерживают только события message.received',
disable: 'Отключить',
enable: 'Включить',
disabledBindings: 'Отключено',
@@ -940,7 +947,7 @@ const ruRU = {
advancedEventValues: 'Показать все',
eventGroup: 'Группа',
eventGroupNames: {
wecomcs: "Поддержка WeCom",
wecomcs: 'Поддержка WeCom',
bot: 'Состояние бота',
feedback: 'Обратная связь',
friend: 'Друзья',
@@ -956,7 +963,8 @@ const ruRU = {
routeFallbackIgnored:
'События без подходящего маршрута игнорируются. Добавляйте общий маршрут, только если каждое событие требует явного результата.',
matchRecords: 'История совпадений',
matchRecordsDescription: 'Последний результат каждого правила и недавние события без совпадений. Журнал в памяти очищается при перезапуске.',
matchRecordsDescription:
'Последний результат каждого правила и недавние события без совпадений. Журнал в памяти очищается при перезапуске.',
testRoute: 'Проверить маршрут',
adapterEventDebugAction: 'Тест прослушивания',
adapterEventDebugTitle: 'Отладка событий платформы',
@@ -1044,14 +1052,15 @@ const ruRU = {
eventWildcard: 'Все события',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "Пользователь открыл чат бота",
wecombot_template_card_event: "Взаимодействие с карточкой",
dingtalk_card_action: "Взаимодействие с карточкой DingTalk",
wecomcs_enter_session: "Клиент вошёл в диалог",
wecomcs_msg_send_fail: "Ошибка отправки сообщения поддержки",
wecomcs_servicer_status_change: "Статус оператора изменён",
wecomcs_session_status_change: "Статус диалога поддержки изменён",
wecomcs_reject_customer_msg_switch_change: "Приём сообщений клиента изменён",
wecombot_enter_chat: 'Пользователь открыл чат бота',
wecombot_template_card_event: 'Взаимодействие с карточкой',
dingtalk_card_action: 'Взаимодействие с карточкой DingTalk',
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: 'Сообщение удалено',
@@ -1061,7 +1070,7 @@ const ruRU = {
friend_added: 'Друг добавлен',
group_member_joined: 'Участник вступил в группу',
group_member_left: 'Участник покинул группу',
group_info_updated: "Информация о группе обновлена",
group_info_updated: 'Информация о группе обновлена',
group_member_banned: 'Участник заблокирован',
bot_invited_to_group: 'Бот приглашён в группу',
bot_removed_from_group: 'Бот удалён из группы',
@@ -1070,14 +1079,22 @@ const ruRU = {
platform_specific: 'Событие конкретной платформы',
},
eventDescriptions: {
wecombot_enter_chat: "Пользователь открыл чат бота. Событие подходит для сценариев приветствия.",
wecombot_template_card_event: "Пользователь нажал или отправил карточку. Ответы управляемых форм обрабатываются отдельно.",
dingtalk_card_action: "Нажатия и отправка карточек; отзывы и управляемые формы обрабатываются отдельно.",
wecomcs_enter_session: "Клиент открыл ссылку поддержки; событие содержит источник и код приветствия.",
wecomcs_msg_send_fail: "WeChat сообщил об ошибке отправки с ID сообщения и типом ошибки.",
wecomcs_servicer_status_change: "Оператор начал, остановил или приостановил приём обращений.",
wecomcs_session_status_change: "Оператор принял, передал, завершил или возобновил диалог.",
wecomcs_reject_customer_msg_switch_change: "Оператор включил или отключил отказ в приёме сообщений клиента.",
wecombot_enter_chat:
'Пользователь открыл чат бота. Событие подходит для сценариев приветствия.',
wecombot_template_card_event:
'Пользователь нажал или отправил карточку. Ответы управляемых форм обрабатываются отдельно.',
dingtalk_card_action:
'Нажатия и отправка карточек; отзывы и управляемые формы обрабатываются отдельно.',
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:
@@ -1086,7 +1103,7 @@ const ruRU = {
'Соответствует обратной связи от платформы или пользователя.',
namespace_friend:
'Соответствует заявкам в друзья и изменениям дружеских связей.',
namespace_group: "Соответствует изменениям состава и информации группы.",
namespace_group: 'Соответствует изменениям состава и информации группы.',
namespace_message:
'Соответствует получению, редактированию, удалению сообщений и реакциям.',
namespace_platform:
@@ -1105,7 +1122,8 @@ const ruRU = {
friend_added: 'Установлена дружеская связь.',
group_member_joined: 'Участник вступает в группу, где есть бот.',
group_member_left: 'Участник покидает группу, где есть бот.',
group_info_updated: "Изменились сведения о группе, например название или владелец.",
group_info_updated:
'Изменились сведения о группе, например название или владелец.',
group_member_banned: 'Участник группы заблокирован или удалён.',
bot_invited_to_group: 'Бот приглашён в группу.',
bot_removed_from_group: 'Бот удалён из группы.',
@@ -1192,10 +1210,12 @@ const ruRU = {
imagesAttached: 'изображений прикреплено',
noLogs: 'Журналов пока нет',
sessionMonitor: {
resetContext: "Сбросить контекст",
resetDescription: "Новые сообщения начнутся с пустого контекста. История мониторинга, файлы и долговременная память сохранятся. Дождитесь завершения активных задач.",
resetSuccess: "Контекст сброшен.",
resetError: "Не удалось сбросить контекст. Дождитесь завершения активных задач и повторите попытку.",
resetContext: 'Сбросить контекст',
resetDescription:
'Новые сообщения начнутся с пустого контекста. История мониторинга, файлы и долговременная память сохранятся. Дождитесь завершения активных задач.',
resetSuccess: 'Контекст сброшен.',
resetError:
'Не удалось сбросить контекст. Дождитесь завершения активных задач и повторите попытку.',
title: 'Сессии',
sessions: 'Сессии',
@@ -1246,24 +1266,25 @@ const ruRU = {
agents: {
apiToolsSelectAll: 'Выбрать все',
monitoring: {
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
runStatus: "Статус выполнения",
toolCalls: "Вызовы инструментов",
delivery: "Отправка сообщений",
sendFailed: "Ошибка отправки",
sendCount: "Успешных вызовов отправки: {{count}}",
noSend: "Отправки не зарегистрированы",
noSteps: "Шаги выполнения ещё не зарегистрированы.",
recipient: "Получатель",
actionCompleted: "Инструмент выполнен успешно.",
simulatedAction: "Действие смоделировано; реальное сообщение не отправлено.",
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
runStatus: 'Статус выполнения',
toolCalls: 'Вызовы инструментов',
delivery: 'Отправка сообщений',
sendFailed: 'Ошибка отправки',
sendCount: 'Успешных вызовов отправки: {{count}}',
noSend: 'Отправки не зарегистрированы',
noSteps: 'Шаги выполнения ещё не зарегистрированы.',
recipient: 'Получатель',
actionCompleted: 'Инструмент выполнен успешно.',
simulatedAction:
'Действие смоделировано; реальное сообщение не отправлено.',
description:
'Просмотр события, ответа модели и вызовов инструментов для каждой задачи.',
empty:
@@ -2196,14 +2217,14 @@ const ruRU = {
uploading: 'Загрузка...',
},
monitoring: {
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
rangeDays: 'Последние {{days}} дней',
customRange: 'Свой период',
rangeStart: 'Начало',
rangeEnd: 'Конец',
rangeHint: 'Начало должно быть раньше конца. Максимум 365 дней.',
autoRefreshLabel: 'Автообновление',
autoRefreshOff: 'Автообновление: выкл.',
autoRefreshEvery: 'Обновлять каждые {{seconds}} с',
title: 'Журнал выполнения',
workbench: 'Настройка и отладка',
description:
+112 -95
View File
@@ -1,30 +1,30 @@
import pipelineMigration from './pipeline-migration/th-TH';
const thTH = {
inflight: {
autoCollapse: "ยุบอัตโนมัติ",
empty: "ไม่มีเหตุการณ์ที่กำลังประมวลผล",
move: "ลากขึ้นลงหรือใช้ปุ่มลูกศรเพื่อย้าย",
botLogs: "บันทึกบอต",
processorLogs: "บันทึกตัวประมวลผล",
autoCollapse: 'ยุบอัตโนมัติ',
empty: 'ไม่มีเหตุการณ์ที่กำลังประมวลผล',
move: 'ลากขึ้นลงหรือใช้ปุ่มลูกศรเพื่อย้าย',
botLogs: 'บันทึกบอต',
processorLogs: 'บันทึกตัวประมวลผล',
"title": "กำลังดำเนินการ",
"expand": "ขยาย",
"collapse": "ย่อ",
"reconnecting": "กำลังเชื่อมต่อใหม่ แสดงสถานะล่าสุดที่ได้รับ",
"details": "ดูรายละเอียด",
"debug": "เหตุการณ์ทดสอบ",
"limited": "แสดงบางรายการ ดูทั้งหมดได้ที่แดชบอร์ด",
"queued": "อยู่ในคิว",
"running": "กำลังประมวลผล",
"waiting": "รอข้อมูล",
"tool": "กำลังเรียกเครื่องมือ",
"thinking": "กำลังคิด",
"replying": "กำลังเตรียมคำตอบ",
"generating": "กำลังสร้างคำตอบ",
"completed": "เสร็จสิ้น",
"failed": "ล้มเหลว",
"cancelled": "ยกเลิกแล้ว",
"ignored": "ละเว้นแล้ว"
title: 'กำลังดำเนินการ',
expand: 'ขยาย',
collapse: 'ย่อ',
reconnecting: 'กำลังเชื่อมต่อใหม่ แสดงสถานะล่าสุดที่ได้รับ',
details: 'ดูรายละเอียด',
debug: 'เหตุการณ์ทดสอบ',
limited: 'แสดงบางรายการ ดูทั้งหมดได้ที่แดชบอร์ด',
queued: 'อยู่ในคิว',
running: 'กำลังประมวลผล',
waiting: 'รอข้อมูล',
tool: 'กำลังเรียกเครื่องมือ',
thinking: 'กำลังคิด',
replying: 'กำลังเตรียมคำตอบ',
generating: 'กำลังสร้างคำตอบ',
completed: 'เสร็จสิ้น',
failed: 'ล้มเหลว',
cancelled: 'ยกเลิกแล้ว',
ignored: 'ละเว้นแล้ว',
},
assistant: {
movePosition: 'เลื่อนผู้ช่วยในแนวนอน',
@@ -101,14 +101,14 @@ const thTH = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
title: 'ดูภาพรวมการทำงาน',
description:
'ดูการทำงานของบอท การใช้โมเดล ปริมาณข้อความ และประสิทธิภาพระบบ',
@@ -189,14 +189,14 @@ const thTH = {
'ส่งข้อความทดสอบเพื่อตรวจเงื่อนไข คำตอบ AI และการประมวลผลผลลัพธ์ การเปลี่ยนแปลงจะถูกบันทึกก่อนทดสอบ',
},
monitoring: {
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
title: 'ดูประวัติการทำงาน',
description:
'ใช้แท็บข้างชื่อเพื่อดูบทสนทนา ขั้นตอนการประมวลผล และข้อผิดพลาด เมื่อไม่มีคำตอบหรือได้คำตอบที่ไม่คาดคิด',
@@ -361,9 +361,11 @@ const thTH = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: 'ไม่ได้บันทึกสาเหตุของข้อผิดพลาด โปรดตรวจสอบบันทึกของบอตหรือส่งข้อความอีกครั้งเพื่อดูรายละเอียด',
failureReasonNotRecorded:
'ไม่ได้บันทึกสาเหตุของข้อผิดพลาด โปรดตรวจสอบบันทึกของบอตหรือส่งข้อความอีกครั้งเพื่อดูรายละเอียด',
loadFailedTitle: 'ไม่สามารถโหลดเนื้อหาได้',
loadFailedHint: 'โปรดลองอีกครั้ง หากยังโหลดไม่ได้ ให้ตรวจสอบการเชื่อมต่อและสถานะบริการ',
loadFailedHint:
'โปรดลองอีกครั้ง หากยังโหลดไม่ได้ ให้ตรวจสอบการเชื่อมต่อและสถานะบริการ',
customValue: 'กำหนดเอง',
loadFailed: 'โหลดไม่สำเร็จ โปรดลองอีกครั้ง',
search: 'ค้นหา',
@@ -859,8 +861,10 @@ const thTH = {
friendAction: 'Agent ตรวจคำขอ',
groupAction: 'Agent ต้อนรับ',
rules: 'ตามกฎที่คุณตั้ง · ตัวอย่าง',
instructions: 'Agent ทำตามคำสั่งของคุณและใช้เครื่องมือเพื่อตอบข้อความหรือตรวจคำขอ',
plugin: 'สำหรับงานขั้นสูง ตัวประมวลผลปลั๊กอินลงทะเบียนหลายเหตุการณ์และจัดการด้วยโค้ดได้ เพิ่มการตั้งค่าตัวประมวลผลปลั๊กอินด้านล่าง',
instructions:
'Agent ทำตามคำสั่งของคุณและใช้เครื่องมือเพื่อตอบข้อความหรือตรวจคำขอ',
plugin:
'สำหรับงานขั้นสูง ตัวประมวลผลปลั๊กอินลงทะเบียนหลายเหตุการณ์และจัดการด้วยโค้ดได้ เพิ่มการตั้งค่าตัวประมวลผลปลั๊กอินด้านล่าง',
docs: 'พัฒนาตัวประมวลผลปลั๊กอินของคุณเอง',
runnerDocs: 'พัฒนารันเนอร์ Agent ของคุณเอง',
pause: 'หยุดชั่วคราว',
@@ -904,7 +908,8 @@ const thTH = {
enabled: 'เปิดใช้งาน',
eventBindingDescriptionPlaceholder: 'คำอธิบายกฎ',
noEventBindings: 'ไม่มีเส้นทางเหตุการณ์',
unsupportedPipelineEvent: 'ไปป์ไลน์ใช้ได้กับเหตุการณ์ message.received เท่านั้น',
unsupportedPipelineEvent:
'ไปป์ไลน์ใช้ได้กับเหตุการณ์ message.received เท่านั้น',
disable: 'ปิดใช้งาน',
enable: 'เปิดใช้งาน',
disabledBindings: 'ปิดใช้งาน',
@@ -914,7 +919,7 @@ const thTH = {
advancedEventValues: 'ดูทั้งหมด',
eventGroup: 'กลุ่ม',
eventGroupNames: {
wecomcs: "บริการลูกค้า WeCom",
wecomcs: 'บริการลูกค้า WeCom',
bot: 'สถานะบอท',
feedback: 'ข้อเสนอแนะ',
friend: 'เพื่อน',
@@ -930,7 +935,8 @@ const thTH = {
routeFallbackIgnored:
'เหตุการณ์ที่ไม่ตรงกับเส้นทางจะถูกละเว้น เพิ่มเส้นทางรับทั้งหมดเฉพาะเมื่อต้องกำหนดผลลัพธ์ให้ทุกเหตุการณ์',
matchRecords: 'บันทึกการจับคู่',
matchRecordsDescription: 'แสดงผลล่าสุดของแต่ละกฎและเหตุการณ์ล่าสุดที่ไม่ตรงกัน บันทึกในหน่วยความจำจะถูกล้างเมื่อรีสตาร์ท',
matchRecordsDescription:
'แสดงผลล่าสุดของแต่ละกฎและเหตุการณ์ล่าสุดที่ไม่ตรงกัน บันทึกในหน่วยความจำจะถูกล้างเมื่อรีสตาร์ท',
testRoute: 'ตรวจเส้นทาง',
adapterEventDebugAction: 'ทดสอบการรับเหตุการณ์',
adapterEventDebugTitle: 'ดีบักเหตุการณ์แพลตฟอร์ม',
@@ -1012,14 +1018,15 @@ const thTH = {
eventWildcard: 'ทุกเหตุการณ์',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "ผู้ใช้เข้าสู่แชตบอต",
wecombot_template_card_event: "การโต้ตอบกับการ์ด",
dingtalk_card_action: "การโต้ตอบกับการ์ด DingTalk",
wecomcs_enter_session: "ลูกค้าเข้าสู่การสนทนา",
wecomcs_msg_send_fail: "ส่งข้อความบริการลูกค้าไม่สำเร็จ",
wecomcs_servicer_status_change: "สถานะเจ้าหน้าที่เปลี่ยนแปลง",
wecomcs_session_status_change: "สถานะการสนทนาบริการลูกค้าเปลี่ยนแปลง",
wecomcs_reject_customer_msg_switch_change: "การปฏิเสธข้อความลูกค้าเปลี่ยนแปลง",
wecombot_enter_chat: 'ผู้ใช้เข้าสู่แชตบอต',
wecombot_template_card_event: 'การโต้ตอบกับการ์ด',
dingtalk_card_action: 'การโต้ตอบกับการ์ด DingTalk',
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,7 +1036,7 @@ const thTH = {
friend_added: 'เพิ่มเพื่อน',
group_member_joined: 'สมาชิกเข้าร่วมกลุ่ม',
group_member_left: 'สมาชิกออกจากกลุ่ม',
group_info_updated: "อัปเดตข้อมูลกลุ่ม",
group_info_updated: 'อัปเดตข้อมูลกลุ่ม',
group_member_banned: 'สมาชิกถูกแบน',
bot_invited_to_group: 'บอตได้รับเชิญเข้ากลุ่ม',
bot_removed_from_group: 'บอตถูกนำออกจากกลุ่ม',
@@ -1038,21 +1045,28 @@ const thTH = {
platform_specific: 'เหตุการณ์เฉพาะแพลตฟอร์ม',
},
eventDescriptions: {
wecombot_enter_chat: "ผู้ใช้เปิดแชตบอต ใช้สำหรับขั้นตอนต้อนรับและแนะนำ",
wecombot_template_card_event: "ผู้ใช้คลิกหรือส่งการ์ด การตอบกลับแบบฟอร์มที่ระบบจัดการจะประมวลผลแยกกัน",
dingtalk_card_action: "การคลิกและส่งการ์ด ความคิดเห็นและแบบฟอร์มที่ระบบจัดการจะประมวลผลแยกกัน",
wecomcs_enter_session: "ลูกค้าเปิดลิงก์บริการ พร้อมข้อมูลแหล่งที่มาและรหัสต้อนรับ",
wecomcs_msg_send_fail: "WeChat แจ้งการส่งล้มเหลว พร้อม ID ข้อความและประเภทข้อผิดพลาด",
wecomcs_servicer_status_change: "เจ้าหน้าที่เริ่ม หยุด หรือพักการให้บริการ",
wecomcs_session_status_change: "เจ้าหน้าที่รับ โอน ปิด หรือเปิดการสนทนาอีกครั้ง",
wecomcs_reject_customer_msg_switch_change: "เจ้าหน้าที่เปิดหรือปิดการปฏิเสธข้อความลูกค้า",
wecombot_enter_chat: 'ผู้ใช้เปิดแชตบอต ใช้สำหรับขั้นตอนต้อนรับและแนะนำ',
wecombot_template_card_event:
'ผู้ใช้คลิกหรือส่งการ์ด การตอบกลับแบบฟอร์มที่ระบบจัดการจะประมวลผลแยกกัน',
dingtalk_card_action:
'การคลิกและส่งการ์ด ความคิดเห็นและแบบฟอร์มที่ระบบจัดการจะประมวลผลแยกกัน',
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:
'ตรงกับการเชิญ การนำบอทออก การปิดเสียง และเหตุการณ์สถานะอื่น',
namespace_feedback: 'ตรงกับเหตุการณ์ข้อเสนอแนะจากแพลตฟอร์มหรือผู้ใช้',
namespace_friend: 'ตรงกับคำขอเป็นเพื่อนและการเปลี่ยนแปลงความสัมพันธ์',
namespace_group: "ตรงกับการเปลี่ยนแปลงสมาชิกและการอัปเดตข้อมูลกลุ่ม",
namespace_group: 'ตรงกับการเปลี่ยนแปลงสมาชิกและการอัปเดตข้อมูลกลุ่ม',
namespace_message: 'ตรงกับการรับ แก้ไข ลบข้อความ และแสดงปฏิกิริยา',
namespace_platform: 'ตรงกับเหตุการณ์เฉพาะแพลตฟอร์มที่อะแดปเตอร์ให้มา',
custom: 'เหตุการณ์กำหนดเองหรือยังไม่มีคำอธิบาย',
@@ -1065,7 +1079,8 @@ const thTH = {
friend_added: 'สร้างความสัมพันธ์เพื่อนแล้ว',
group_member_joined: 'สมาชิกเข้าร่วมกลุ่มที่มีบอท',
group_member_left: 'สมาชิกออกจากกลุ่มที่มีบอท',
group_info_updated: "ข้อมูลกลุ่ม เช่น ชื่อหรือเจ้าของกลุ่ม มีการเปลี่ยนแปลง",
group_info_updated:
'ข้อมูลกลุ่ม เช่น ชื่อหรือเจ้าของกลุ่ม มีการเปลี่ยนแปลง',
group_member_banned: 'สมาชิกกลุ่มถูกแบนหรือนำออก',
bot_invited_to_group: 'บอทได้รับเชิญเข้ากลุ่ม',
bot_removed_from_group: 'บอทถูกนำออกจากกลุ่ม',
@@ -1152,10 +1167,12 @@ const thTH = {
imagesAttached: 'รูปภาพที่แนบ',
noLogs: 'ยังไม่มีบันทึก',
sessionMonitor: {
resetContext: "รีเซ็ตบริบท",
resetDescription: "ข้อความใหม่จะเริ่มด้วยบริบทว่าง ประวัติการตรวจสอบ ไฟล์ และความจำระยะยาวจะยังคงอยู่ โปรดรอให้งานที่กำลังทำอยู่เสร็จก่อน",
resetSuccess: "รีเซ็ตบริบทแล้ว",
resetError: "รีเซ็ตไม่สำเร็จ โปรดรอให้งานที่กำลังทำอยู่เสร็จแล้วลองอีกครั้ง",
resetContext: 'รีเซ็ตบริบท',
resetDescription:
'ข้อความใหม่จะเริ่มด้วยบริบทว่าง ประวัติการตรวจสอบ ไฟล์ และความจำระยะยาวจะยังคงอยู่ โปรดรอให้งานที่กำลังทำอยู่เสร็จก่อน',
resetSuccess: 'รีเซ็ตบริบทแล้ว',
resetError:
'รีเซ็ตไม่สำเร็จ โปรดรอให้งานที่กำลังทำอยู่เสร็จแล้วลองอีกครั้ง',
title: 'เซสชัน',
sessions: 'เซสชัน',
@@ -1206,24 +1223,24 @@ const thTH = {
agents: {
apiToolsSelectAll: 'เลือกทั้งหมด',
monitoring: {
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
runStatus: "สถานะการทำงาน",
toolCalls: "การเรียกเครื่องมือ",
delivery: "การส่งข้อความ",
sendFailed: "ส่งไม่สำเร็จ",
sendCount: "เรียกส่งสำเร็จ {{count}} ครั้ง",
noSend: "ไม่มีบันทึกการส่ง",
noSteps: "ยังไม่มีบันทึกขั้นตอนการทำงาน",
recipient: "ผู้รับ",
actionCompleted: "เครื่องมือทำงานสำเร็จ",
simulatedAction: "การจำลอง ไม่ได้ส่งข้อความจริง",
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
runStatus: 'สถานะการทำงาน',
toolCalls: 'การเรียกเครื่องมือ',
delivery: 'การส่งข้อความ',
sendFailed: 'ส่งไม่สำเร็จ',
sendCount: 'เรียกส่งสำเร็จ {{count}} ครั้ง',
noSend: 'ไม่มีบันทึกการส่ง',
noSteps: 'ยังไม่มีบันทึกขั้นตอนการทำงาน',
recipient: 'ผู้รับ',
actionCompleted: 'เครื่องมือทำงานสำเร็จ',
simulatedAction: 'การจำลอง ไม่ได้ส่งข้อความจริง',
description:
'ดูเหตุการณ์เริ่มต้น ผลลัพธ์โมเดล และการเรียกเครื่องมือของแต่ละงาน',
empty: 'ยังไม่มีการทำงาน เริ่มเหตุการณ์หรือทดสอบการดีบักเพื่อดูบันทึก',
@@ -2131,14 +2148,14 @@ const thTH = {
uploading: 'กำลังอัปโหลด...',
},
monitoring: {
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
rangeDays: '{{days}} วันที่ผ่านมา',
customRange: 'กำหนดช่วงเวลาเอง',
rangeStart: 'เวลาเริ่มต้น',
rangeEnd: 'เวลาสิ้นสุด',
rangeHint: 'เวลาเริ่มต้องก่อนเวลาสิ้นสุด สูงสุด 365 วัน',
autoRefreshLabel: 'รีเฟรชอัตโนมัติ',
autoRefreshOff: 'รีเฟรชอัตโนมัติ: ปิด',
autoRefreshEvery: 'รีเฟรชทุก {{seconds}} วินาที',
title: 'บันทึกการทำงาน',
workbench: 'กำหนดค่าและดีบัก',
description:
+114 -95
View File
@@ -1,30 +1,31 @@
import pipelineMigration from './pipeline-migration/vi-VN';
const viVN = {
inflight: {
autoCollapse: "Tự thu gọn",
empty: "Không có sự kiện đang xử lý",
move: "Kéo lên xuống hoặc dùng phím mũi tên",
botLogs: "Nhật ký bot",
processorLogs: "Nhật ký bộ xử lý",
autoCollapse: 'Tự thu gọn',
empty: 'Không có sự kiện đang xử lý',
move: 'Kéo lên xuống hoặc dùng phím mũi tên',
botLogs: 'Nhật ký bot',
processorLogs: 'Nhật ký bộ xử lý',
"title": "Đang thực hiện",
"expand": "Mở rộng",
"collapse": "Thu gọn",
"reconnecting": "Đang kết nối lại; hiển thị trạng thái nhận được gần nhất.",
"details": "Xem chi tiết",
"debug": "Sự kiện gỡ lỗi",
"limited": "Chỉ hiển thị một phần. Xem bảng điều khiển để biết tất cả lượt chạy.",
"queued": "Đang chờ",
"running": "Đang xử lý",
"waiting": "Chờ nhập liệu",
"tool": "Đang gọi công cụ",
"thinking": "Đang suy nghĩ",
"replying": "Chuẩn bị trả lời",
"generating": "Đang tạo câu trả lời",
"completed": "Hoàn tất",
"failed": "Thất bại",
"cancelled": "Đã hủy",
"ignored": "Đã bỏ qua"
title: 'Đang thực hiện',
expand: 'Mở rộng',
collapse: 'Thu gọn',
reconnecting: 'Đang kết nối lại; hiển thị trạng thái nhận được gần nhất.',
details: 'Xem chi tiết',
debug: 'Sự kiện gỡ lỗi',
limited:
'Chỉ hiển thị một phần. Xem bảng điều khiển để biết tất cả lượt chạy.',
queued: 'Đang chờ',
running: 'Đang xử lý',
waiting: 'Chờ nhập liệu',
tool: 'Đang gọi công cụ',
thinking: 'Đang suy nghĩ',
replying: 'Chuẩn bị trả lời',
generating: 'Đang tạo câu trả lời',
completed: 'Hoàn tất',
failed: 'Thất bại',
cancelled: 'Đã hủy',
ignored: 'Đã bỏ qua',
},
assistant: {
movePosition: 'Di chuyển trợ lý theo chiều ngang',
@@ -103,14 +104,14 @@ const viVN = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
title: 'Xem hoạt động',
description:
'Xem hoạt động bot, mức sử dụng mô hình, lượng tin nhắn và hiệu suất hệ thống.',
@@ -191,14 +192,14 @@ const viVN = {
'Gửi tin nhắn thử để kiểm tra điều kiện kích hoạt, câu trả lời AI và xử lý đầu ra. Thay đổi cấu hình được lưu trước khi thử.',
},
monitoring: {
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
title: 'Xem lịch sử chạy',
description:
'Dùng thẻ cạnh tiêu đề để xem cuộc trò chuyện, các bước xử lý và lỗi khi không có phản hồi hoặc phản hồi không đúng dự kiến.',
@@ -365,9 +366,11 @@ 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.',
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ụ.',
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',
loadFailed: 'Không tải được. Vui lòng thử lại.',
search: 'Tìm kiếm',
@@ -863,7 +866,8 @@ const viVN = {
'Liên kết Pipeline xử lý tin nhắn cho Bot này',
routingHelp: {
title: 'Định tuyến sự kiện là gì?',
intro: 'Bot nhận tin nhắn, lời mời kết bạn và các sự kiện như tham gia nhóm.',
intro:
'Bot nhận tin nhắn, lời mời kết bạn và các sự kiện như tham gia nhóm.',
events: 'Sự kiện đến',
agents: 'Các Agent khác nhau',
message: 'Nhận tin nhắn',
@@ -873,8 +877,10 @@ const viVN = {
friendAction: 'Agent xét duyệt',
groupAction: 'Agent chào mừng',
rules: 'Theo quy tắc của bạn · Ví dụ',
instructions: 'Agent làm theo chỉ dẫn của bạn và gọi công cụ để trả lời, xét duyệt yêu cầu và thực hiện thao tác khác.',
plugin: 'Với nhu cầu nâng cao, bộ xử lý plugin đăng ký hàng loạt sự kiện và xử lý bằng mã. Thêm cấu hình bộ xử lý plugin bên dưới.',
instructions:
'Agent làm theo chỉ dẫn của bạn và gọi công cụ để trả lời, xét duyệt yêu cầu và thực hiện thao tác khác.',
plugin:
'Với nhu cầu nâng cao, bộ xử lý plugin đăng ký hàng loạt sự kiện và xử lý bằng mã. Thêm cấu hình bộ xử lý plugin bên dưới.',
docs: 'Phát triển bộ xử lý plugin của riêng bạn',
runnerDocs: 'Phát triển trình chạy Agent của riêng bạn',
pause: 'Tạm dừng',
@@ -929,7 +935,7 @@ const viVN = {
advancedEventValues: 'Xem tất cả',
eventGroup: 'Nhóm',
eventGroupNames: {
wecomcs: "Dịch vụ khách hàng WeCom",
wecomcs: 'Dịch vụ khách hàng WeCom',
bot: 'Trạng thái bot',
feedback: 'Phản hồi',
friend: 'Bạn bè',
@@ -945,7 +951,8 @@ const viVN = {
routeFallbackIgnored:
'Sự kiện không khớp tuyến nào sẽ bị bỏ qua. Chỉ thêm tuyến nhận tất cả khi mọi sự kiện cần kết quả rõ ràng.',
matchRecords: 'Lịch sử khớp',
matchRecordsDescription: 'Kết quả gần nhất của mỗi quy tắc và các sự kiện không khớp gần đây. Nhật ký trong bộ nhớ được xóa khi khởi động lại.',
matchRecordsDescription:
'Kết quả gần nhất của mỗi quy tắc và các sự kiện không khớp gần đây. Nhật ký trong bộ nhớ được xóa khi khởi động lại.',
testRoute: 'Kiểm tra tuyến',
adapterEventDebugAction: 'Kiểm tra lắng nghe',
adapterEventDebugTitle: 'Gỡ lỗi sự kiện nền tảng',
@@ -1029,14 +1036,15 @@ const viVN = {
eventWildcard: 'Tất cả sự kiện',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "Người dùng vào cuộc trò chuyện với bot",
wecombot_template_card_event: "Tương tác thẻ mẫu",
dingtalk_card_action: "Tương tác thẻ DingTalk",
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",
wecombot_enter_chat: 'Người dùng vào cuộc trò chuyện với bot',
wecombot_template_card_event: 'Tương tác thẻ mẫu',
dingtalk_card_action: 'Tương tác thẻ DingTalk',
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',
@@ -1046,7 +1054,7 @@ const viVN = {
friend_added: 'Đã thêm bạn',
group_member_joined: 'Thành viên tham gia nhóm',
group_member_left: 'Thành viên rời nhóm',
group_info_updated: "Thông tin nhóm được cập nhật",
group_info_updated: 'Thông tin nhóm được cập nhật',
group_member_banned: 'Thành viên bị cấm tham gia nhóm',
bot_invited_to_group: 'Bot được mời vào nhóm',
bot_removed_from_group: 'Bot bị xóa khỏi nhóm',
@@ -1055,21 +1063,30 @@ const viVN = {
platform_specific: 'Sự kiện riêng của nền tảng',
},
eventDescriptions: {
wecombot_enter_chat: "Người dùng mở cuộc trò chuyện với bot. Dùng cho quy trình chào mừng.",
wecombot_template_card_event: "Người dùng nhấp hoặc gửi thẻ. Phản hồi biểu mẫu do hệ thống quản lý được xử lý riêng.",
dingtalk_card_action: "Nhấp và gửi thẻ; phản hồi và biểu mẫu được quản lý được xử lý riêng.",
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.",
wecombot_enter_chat:
'Người dùng mở cuộc trò chuyện với bot. Dùng cho quy trình chào mừng.',
wecombot_template_card_event:
'Người dùng nhấp hoặc gửi thẻ. Phản hồi biểu mẫu do hệ thống quản lý được xử lý riêng.',
dingtalk_card_action:
'Nhấp và gửi thẻ; phản hồi và biểu mẫu được quản lý được xử lý riêng.',
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:
'Khớp sự kiện mời, xóa, tắt tiếng bot và các thay đổi trạng thái khác.',
namespace_feedback: 'Khớp sự kiện phản hồi từ nền tảng hoặc người dùng.',
namespace_friend: 'Khớp yêu cầu kết bạn và thay đổi quan hệ bạn bè.',
namespace_group: "Khớp các thay đổi thành viên và cập nhật thông tin nhóm.",
namespace_group:
'Khớp các thay đổi thành viên và cập nhật thông tin nhóm.',
namespace_message: 'Khớp sự kiện nhận, sửa, xóa và thả cảm xúc tin nhắn.',
namespace_platform:
'Khớp sự kiện riêng của nền tảng do bộ chuyển đổi cung cấp.',
@@ -1083,7 +1100,7 @@ const viVN = {
friend_added: 'Đã tạo quan hệ bạn bè.',
group_member_joined: 'Thành viên tham gia nhóm có bot.',
group_member_left: 'Thành viên rời nhóm có bot.',
group_info_updated: "Thông tin nhóm như tên hoặc chủ sở hữu đã thay đổi.",
group_info_updated: 'Thông tin nhóm như tên hoặc chủ sở hữu đã thay đổi.',
group_member_banned: 'Thành viên nhóm bị cấm hoặc xóa.',
bot_invited_to_group: 'Bot được mời vào nhóm.',
bot_removed_from_group: 'Bot bị xóa khỏi nhóm.',
@@ -1170,10 +1187,12 @@ const viVN = {
imagesAttached: 'hình ảnh đính kèm',
noLogs: 'Chưa có nhật ký',
sessionMonitor: {
resetContext: "Đặt lại ngữ cảnh",
resetDescription: "Tin nhắn mới sẽ bắt đầu với ngữ cảnh trống. Lịch sử giám sát, tệp và bộ nhớ dài hạn được giữ lại. Hãy đợi các tác vụ đang chạy hoàn tất.",
resetSuccess: "Đã đặt lại ngữ cảnh.",
resetError: "Không thể đặt lại. Hãy đợi các tác vụ đang chạy hoàn tất rồi thử lại.",
resetContext: 'Đặt lại ngữ cảnh',
resetDescription:
'Tin nhắn mới sẽ bắt đầu với ngữ cảnh trống. Lịch sử giám sát, tệp và bộ nhớ dài hạn được giữ lại. Hãy đợi các tác vụ đang chạy hoàn tất.',
resetSuccess: 'Đã đặt lại ngữ cảnh.',
resetError:
'Không thể đặt lại. Hãy đợi các tác vụ đang chạy hoàn tất rồi thử lại.',
title: 'Phiên',
sessions: 'Phiên',
@@ -1224,24 +1243,24 @@ const viVN = {
agents: {
apiToolsSelectAll: 'Chọn tất cả',
monitoring: {
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
runStatus: "Trạng thái thực thi",
toolCalls: "Lượt gọi công cụ",
delivery: "Gửi tin nhắn",
sendFailed: "Gửi thất bại",
sendCount: "{{count}} lượt gọi gửi thành công",
noSend: "Chưa ghi nhận lượt gửi",
noSteps: "Chưa ghi nhận bước thực thi.",
recipient: "Người nhận",
actionCompleted: "Công cụ đã thực thi thành công.",
simulatedAction: "Thao tác mô phỏng; không gửi tin nhắn thật.",
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
runStatus: 'Trạng thái thực thi',
toolCalls: 'Lượt gọi công cụ',
delivery: 'Gửi tin nhắn',
sendFailed: 'Gửi thất bại',
sendCount: '{{count}} lượt gọi gửi thành công',
noSend: 'Chưa ghi nhận lượt gửi',
noSteps: 'Chưa ghi nhận bước thực thi.',
recipient: 'Người nhận',
actionCompleted: 'Công cụ đã thực thi thành công.',
simulatedAction: 'Thao tác mô phỏng; không gửi tin nhắn thật.',
description:
'Xem sự kiện kích hoạt, đầu ra mô hình và quá trình gọi công cụ của mỗi tác vụ.',
empty: 'Chưa có lượt chạy. Kích hoạt sự kiện hoặc chạy thử gỡ lỗi.',
@@ -2161,14 +2180,14 @@ const viVN = {
uploading: 'Đang tải lên...',
},
monitoring: {
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
rangeDays: '{{days}} ngày qua',
customRange: 'Khoảng thời gian tùy chỉnh',
rangeStart: 'Bắt đầu',
rangeEnd: 'Kết thúc',
rangeHint: 'Bắt đầu phải trước kết thúc. Tối đa 365 ngày.',
autoRefreshLabel: 'Tự động làm mới',
autoRefreshOff: 'Tự động làm mới: tắt',
autoRefreshEvery: 'Làm mới mỗi {{seconds}} giây',
title: 'Nhật ký chạy',
workbench: 'Cấu hình và gỡ lỗi',
description: 'Xem nhật ký thực thi và lỗi của Pipeline này (24 giờ qua)',
+101 -92
View File
@@ -3,30 +3,30 @@ import pipelineMigration from './pipeline-migration/zh-Hans';
const zhHans = {
slackSetup,
inflight: {
autoCollapse: "自动折叠",
empty: "当前没有正在处理的事件",
move: "上下拖动;也可用方向键移动",
botLogs: "机器人日志",
processorLogs: "处理器日志",
autoCollapse: '自动折叠',
empty: '当前没有正在处理的事件',
move: '上下拖动;也可用方向键移动',
botLogs: '机器人日志',
processorLogs: '处理器日志',
"title": "处理中",
"expand": "展开",
"collapse": "收起",
"reconnecting": "连接中断,正在重连;显示的是最后收到的状态。",
"details": "查看详情",
"debug": "调试事件",
"limited": "仅展示部分执行,请在仪表盘查看全部记录。",
"queued": "排队中",
"running": "处理中",
"waiting": "等待用户输入",
"tool": "调用工具",
"thinking": "思考中",
"replying": "准备回复",
"generating": "生成回复",
"completed": "已完成",
"failed": "失败",
"cancelled": "已取消",
"ignored": "已忽略"
title: '处理中',
expand: '展开',
collapse: '收起',
reconnecting: '连接中断,正在重连;显示的是最后收到的状态。',
details: '查看详情',
debug: '调试事件',
limited: '仅展示部分执行,请在仪表盘查看全部记录。',
queued: '排队中',
running: '处理中',
waiting: '等待用户输入',
tool: '调用工具',
thinking: '思考中',
replying: '准备回复',
generating: '生成回复',
completed: '已完成',
failed: '失败',
cancelled: '已取消',
ignored: '已忽略',
},
assistant: {
movePosition: '左右移动助手',
@@ -99,14 +99,14 @@ const zhHans = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
title: '查看运行概况',
description: '集中查看机器人活动、模型调用、消息量和系统运行情况。',
},
@@ -182,14 +182,14 @@ const zhHans = {
'发送测试消息,检查触发规则、AI 回复和输出处理是否符合预期;修改配置后,测试前会先保存。',
},
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
title: '查看运行日志',
description:
'打开标题旁的“运行日志”,查看历史对话、处理过程和错误,排查回复异常。',
@@ -347,7 +347,8 @@ const zhHans = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: '这条历史记录未保存失败原因,请查看机器人日志,或重新发送消息获取详细错误。',
failureReasonNotRecorded:
'这条历史记录未保存失败原因,请查看机器人日志,或重新发送消息获取详细错误。',
loadFailedTitle: '暂时无法加载',
loadFailedHint: '请稍后重试。如果仍无法加载,请检查网络连接与服务状态。',
customValue: '自定义',
@@ -827,7 +828,8 @@ const zhHans = {
groupAction: '欢迎 Agent → 打招呼',
rules: '按您设置的规则分配 · 示例',
instructions: 'Agent 按您的指令调用工具,完成回复、审核等操作。',
plugin: '专业场景可用插件处理器:批量注册事件,用代码处理,再在下方添加插件处理器配置。',
plugin:
'专业场景可用插件处理器:批量注册事件,用代码处理,再在下方添加插件处理器配置。',
docs: '开发自己的插件处理器',
runnerDocs: '开发自己的 Agent 运行器',
pause: '暂停',
@@ -876,7 +878,7 @@ const zhHans = {
advancedEventValues: '查看全部',
eventGroup: '事件组',
eventGroupNames: {
wecomcs: "企业微信客服",
wecomcs: '企业微信客服',
bot: '机器人状态',
feedback: '反馈',
friend: '好友',
@@ -892,7 +894,8 @@ const zhHans = {
routeFallbackIgnored:
'未命中任何路由的事件会被忽略。只有需要为每个事件指定结果时,才添加全局兜底路由。',
matchRecords: '匹配记录',
matchRecordsDescription: '查看各规则最近一次匹配结果及近期未匹配事件。记录来自机器人内存日志,重启后清空。',
matchRecordsDescription:
'查看各规则最近一次匹配结果及近期未匹配事件。记录来自机器人内存日志,重启后清空。',
testRoute: '检查路由',
adapterEventDebugAction: '测试监听',
adapterEventDebugTitle: '平台事件调试',
@@ -974,14 +977,14 @@ const zhHans = {
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "用户进入机器人会话",
wecombot_template_card_event: "模板卡片交互",
dingtalk_card_action: "钉钉卡片交互",
wecomcs_enter_session: "客户进入会话",
wecomcs_msg_send_fail: "客服消息发送失败",
wecomcs_servicer_status_change: "接待人员状态变更",
wecomcs_session_status_change: "客服会话状态变更",
wecomcs_reject_customer_msg_switch_change: "拒收客户消息设置变更",
wecombot_enter_chat: '用户进入机器人会话',
wecombot_template_card_event: '模板卡片交互',
dingtalk_card_action: '钉钉卡片交互',
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: '消息被删除',
@@ -991,7 +994,7 @@ const zhHans = {
friend_added: '好友添加成功',
group_member_joined: '成员加入群组',
group_member_left: '成员离开群组',
group_info_updated: "群信息更新",
group_info_updated: '群信息更新',
group_member_banned: '成员被封禁',
bot_invited_to_group: '机器人被邀入群',
bot_removed_from_group: '机器人被移出群',
@@ -1000,20 +1003,25 @@ const zhHans = {
platform_specific: '平台特定事件',
},
eventDescriptions: {
wecombot_enter_chat: "用户进入机器人会话,可用于欢迎和引导流程。",
wecombot_template_card_event: "用户点击或提交模板卡片;已有交互表单的回调单独处理。",
dingtalk_card_action: "用户点击或提交钉钉卡片;反馈和受管理的表单回调单独处理。",
wecomcs_enter_session: "客户通过客服链接进入会话,可获取来源和欢迎语凭据。",
wecomcs_msg_send_fail: "微信通知客服消息发送失败,包含消息 ID 和失败类型。",
wecomcs_servicer_status_change: "接待人员开始、停止接待或暂时挂起。",
wecomcs_session_status_change: "人工客服接入、转接、结束或重新接入会话。",
wecomcs_reject_customer_msg_switch_change: "接待人员开启或取消拒收客户消息。",
wecombot_enter_chat: '用户进入机器人会话,可用于欢迎和引导流程。',
wecombot_template_card_event:
'用户点击或提交模板卡片;已有交互表单的回调单独处理。',
dingtalk_card_action:
'用户点击或提交钉钉卡片;反馈和受管理的表单回调单独处理。',
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: '匹配机器人入群、退群、禁言和解除禁言等状态事件。',
namespace_feedback: '匹配平台或用户反馈事件。',
namespace_friend: '匹配好友请求、好友添加成功等好友关系事件。',
namespace_group: "匹配群成员变动、群信息更新等群组事件。",
namespace_group: '匹配群成员变动、群信息更新等群组事件。',
namespace_message: '匹配消息接收、编辑、删除和表态事件。',
namespace_platform: '匹配适配器提供的平台专属事件。',
custom: '自定义或暂未提供说明的事件。',
@@ -1026,7 +1034,7 @@ const zhHans = {
friend_added: '好友关系建立成功。',
group_member_joined: '有成员加入机器人所在群组。',
group_member_left: '有成员离开机器人所在群组。',
group_info_updated: "群名称、群主等群资料发生变更。",
group_info_updated: '群名称、群主等群资料发生变更。',
group_member_banned: '群成员被封禁或移出。',
bot_invited_to_group: '机器人被邀请加入群组。',
bot_removed_from_group: '机器人被移出群组。',
@@ -1110,10 +1118,11 @@ const zhHans = {
imagesAttached: '张图片',
noLogs: '暂无日志',
sessionMonitor: {
resetContext: "重置上下文",
resetDescription: "重置后,新消息将从空上下文开始。监控历史会保留,文件和长期记忆不受影响。请等待当前任务结束后再重置。",
resetSuccess: "上下文已重置,新消息将开启新的对话。",
resetError: "重置失败,请等待当前任务结束后重试。",
resetContext: '重置上下文',
resetDescription:
'重置后,新消息将从空上下文开始。监控历史会保留,文件和长期记忆不受影响。请等待当前任务结束后再重置。',
resetSuccess: '上下文已重置,新消息将开启新的对话。',
resetError: '重置失败,请等待当前任务结束后重试。',
title: '会话监控',
sessions: '会话列表',
@@ -1163,24 +1172,24 @@ const zhHans = {
agents: {
apiToolsSelectAll: '全选',
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
runStatus: "执行状态",
toolCalls: "工具调用",
delivery: "消息发送",
sendFailed: "发送失败",
sendCount: "{{count}} 次发送调用成功",
noSend: "暂无发送记录",
noSteps: "暂未记录执行步骤。",
recipient: "接收目标",
actionCompleted: "工具已执行完成。",
simulatedAction: "模拟执行,未发送真实消息。",
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
runStatus: '执行状态',
toolCalls: '工具调用',
delivery: '消息发送',
sendFailed: '发送失败',
sendCount: '{{count}} 次发送调用成功',
noSend: '暂无发送记录',
noSteps: '暂未记录执行步骤。',
recipient: '接收目标',
actionCompleted: '工具已执行完成。',
simulatedAction: '模拟执行,未发送真实消息。',
description: '查看每次任务的触发事件、模型输出和工具执行过程。',
empty: '暂无运行记录。触发平台事件或运行调试后,可在这里查看。',
input: '触发输入',
@@ -2061,14 +2070,14 @@ const zhHans = {
uploading: '上传中...',
},
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
rangeDays: '最近 {{days}} 天',
customRange: '自定义时间范围',
rangeStart: '开始时间',
rangeEnd: '结束时间',
rangeHint: '开始时间需早于结束时间,最多 365 天。',
autoRefreshLabel: '定时刷新',
autoRefreshOff: '定时刷新:关闭',
autoRefreshEvery: '每 {{seconds}} 秒刷新',
title: '运行日志',
workbench: '配置与调试',
description: '查看此流水线的运行记录和错误信息(最近24小时)',
+102 -94
View File
@@ -3,30 +3,30 @@ import pipelineMigration from './pipeline-migration/zh-Hant';
const zhHant = {
slackSetup,
inflight: {
autoCollapse: "自動摺疊",
empty: "目前沒有正在處理的事件",
move: "上下拖動;也可用方向鍵移動",
botLogs: "機器人日誌",
processorLogs: "處理器日誌",
autoCollapse: '自動摺疊',
empty: '目前沒有正在處理的事件',
move: '上下拖動;也可用方向鍵移動',
botLogs: '機器人日誌',
processorLogs: '處理器日誌',
"title": "處理中",
"expand": "展開",
"collapse": "收起",
"reconnecting": "連線中斷,正在重新連線;顯示最後收到的狀態。",
"details": "查看詳情",
"debug": "除錯事件",
"limited": "僅顯示部分執行,請在儀表板查看全部記錄。",
"queued": "排隊中",
"running": "處理中",
"waiting": "等待使用者輸入",
"tool": "呼叫工具",
"thinking": "思考中",
"replying": "準備回覆",
"generating": "產生回覆",
"completed": "已完成",
"failed": "失敗",
"cancelled": "已取消",
"ignored": "已忽略"
title: '處理中',
expand: '展開',
collapse: '收起',
reconnecting: '連線中斷,正在重新連線;顯示最後收到的狀態。',
details: '查看詳情',
debug: '除錯事件',
limited: '僅顯示部分執行,請在儀表板查看全部記錄。',
queued: '排隊中',
running: '處理中',
waiting: '等待使用者輸入',
tool: '呼叫工具',
thinking: '思考中',
replying: '準備回覆',
generating: '產生回覆',
completed: '已完成',
failed: '失敗',
cancelled: '已取消',
ignored: '已忽略',
},
assistant: {
movePosition: '左右移動助手',
@@ -35,8 +35,7 @@ const zhHant = {
currentChat: '目前對話',
stop: '停止任務',
stopped: '已停止。執行中的工具可能已經生效,請檢查相關資源後再嘗試。',
setupRequired:
'登入 LangBot 帳戶或設定模型,即可開始使用工作區助手。',
setupRequired: '登入 LangBot 帳戶或設定模型,即可開始使用工作區助手。',
loginAccount: '登入 LangBot Account',
configureModels: '設定模型',
providerCheckFailed: '無法檢查模型供應商,請重試。',
@@ -100,14 +99,14 @@ const zhHant = {
sidebarGuide: {
steps: {
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
title: '查看執行概況',
description: '集中查看機器人活動、模型呼叫、訊息量和系統運行情況。',
},
@@ -183,14 +182,14 @@ const zhHant = {
'傳送測試訊息,檢查觸發規則、AI 回覆和輸出處理是否符合預期;修改設定後,測試前會先儲存。',
},
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
title: '查看執行日誌',
description:
'開啟標題旁的「執行日誌」,查看歷史對話、處理過程和錯誤,排查回覆異常。',
@@ -344,7 +343,8 @@ const zhHant = {
editionCloud: 'Cloud',
},
common: {
failureReasonNotRecorded: '這筆歷史記錄未儲存失敗原因,請查看機器人日誌,或重新傳送訊息取得詳細錯誤。',
failureReasonNotRecorded:
'這筆歷史記錄未儲存失敗原因,請查看機器人日誌,或重新傳送訊息取得詳細錯誤。',
loadFailedTitle: '暫時無法載入',
loadFailedHint: '請稍後重試。如果仍無法載入,請檢查網路連線與服務狀態。',
customValue: '自訂',
@@ -825,7 +825,8 @@ const zhHant = {
groupAction: '歡迎 Agent → 打招呼',
rules: '依您設定的規則分配 · 範例',
instructions: 'Agent 依您的指令呼叫工具,完成回覆、審核等操作。',
plugin: '專業場景可用插件處理器:批次註冊事件,用程式碼處理,再於下方新增插件處理器設定。',
plugin:
'專業場景可用插件處理器:批次註冊事件,用程式碼處理,再於下方新增插件處理器設定。',
docs: '開發自己的插件處理器',
runnerDocs: '開發自己的 Agent 運行器',
pause: '暫停',
@@ -875,7 +876,7 @@ const zhHant = {
advancedEventValues: '查看全部',
eventGroup: '事件組',
eventGroupNames: {
wecomcs: "企業微信客服",
wecomcs: '企業微信客服',
bot: '機器人狀態',
feedback: '反饋',
friend: '好友',
@@ -891,7 +892,8 @@ const zhHant = {
routeFallbackIgnored:
'未命中任何路由的事件會被忽略。只有需要爲每個事件指定結果時,才添加全局兜底路由。',
matchRecords: '匹配記錄',
matchRecordsDescription: '查看各規則最近一次匹配結果及近期未匹配事件。記錄來自機器人記憶體日誌,重新啟動後清空。',
matchRecordsDescription:
'查看各規則最近一次匹配結果及近期未匹配事件。記錄來自機器人記憶體日誌,重新啟動後清空。',
testRoute: '檢查路由',
adapterEventDebugAction: '測試監聽',
adapterEventDebugTitle: '平臺事件調試',
@@ -973,14 +975,14 @@ const zhHant = {
eventWildcard: '全部事件',
eventNamespaceWildcard: '{{namespace}}.*',
eventNames: {
wecombot_enter_chat: "使用者進入機器人會話",
wecombot_template_card_event: "範本卡片互動",
dingtalk_card_action: "釘釘卡片互動",
wecomcs_enter_session: "客戶進入會話",
wecomcs_msg_send_fail: "客服訊息傳送失敗",
wecomcs_servicer_status_change: "接待人員狀態變更",
wecomcs_session_status_change: "客服會話狀態變更",
wecomcs_reject_customer_msg_switch_change: "拒收客戶訊息設定變更",
wecombot_enter_chat: '使用者進入機器人會話',
wecombot_template_card_event: '範本卡片互動',
dingtalk_card_action: '釘釘卡片互動',
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: '訊息已刪除',
@@ -990,7 +992,7 @@ const zhHant = {
friend_added: '新增好友',
group_member_joined: '成員加入群組',
group_member_left: '成員離開群組',
group_info_updated: "群組資訊更新",
group_info_updated: '群組資訊更新',
group_member_banned: '成員被封禁',
bot_invited_to_group: '機器人被邀請加入群組',
bot_removed_from_group: '機器人被移出群組',
@@ -999,20 +1001,25 @@ const zhHant = {
platform_specific: '平台特定事件',
},
eventDescriptions: {
wecombot_enter_chat: "使用者進入機器人會話,可用於歡迎和引導流程。",
wecombot_template_card_event: "使用者點擊或提交範本卡片;已有互動表單的回呼單獨處理。",
dingtalk_card_action: "使用者點擊或提交釘釘卡片;回饋和受管理的表單回呼單獨處理。",
wecomcs_enter_session: "客戶透過客服連結進入會話,可取得來源和歡迎語憑證。",
wecomcs_msg_send_fail: "微信通知客服訊息傳送失敗,包含訊息 ID 和失敗類型。",
wecomcs_servicer_status_change: "接待人員開始、停止接待或暫時掛起。",
wecomcs_session_status_change: "人工客服接入、轉接、結束或重新接入會話。",
wecomcs_reject_customer_msg_switch_change: "接待人員開啟或取消拒收客戶訊息。",
wecombot_enter_chat: '使用者進入機器人會話,可用於歡迎和引導流程。',
wecombot_template_card_event:
'使用者點擊或提交範本卡片;已有互動表單的回呼單獨處理。',
dingtalk_card_action:
'使用者點擊或提交釘釘卡片;回饋和受管理的表單回呼單獨處理。',
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: '匹配機器人入羣、退羣、禁言和解除禁言等狀態事件。',
namespace_feedback: '匹配平臺或使用者反饋事件。',
namespace_friend: '匹配好友請求、好友添加成功等好友關係事件。',
namespace_group: "匹配群組成員變動、群組資訊更新等事件。",
namespace_group: '匹配群組成員變動、群組資訊更新等事件。',
namespace_message: '匹配訊息接收、編輯、刪除和表態事件。',
namespace_platform: '匹配適配器提供的平臺專屬事件。',
custom: '自定義或暫未提供說明的事件。',
@@ -1025,7 +1032,7 @@ const zhHant = {
friend_added: '好友關係建立成功。',
group_member_joined: '有成員加入機器人所在羣組。',
group_member_left: '有成員離開機器人所在羣組。',
group_info_updated: "群組名稱、群主等群組資料發生變更。",
group_info_updated: '群組名稱、群主等群組資料發生變更。',
group_member_banned: '羣成員被封禁或移出。',
bot_invited_to_group: '機器人被邀請加入羣組。',
bot_removed_from_group: '機器人被移出羣組。',
@@ -1109,10 +1116,11 @@ const zhHant = {
imagesAttached: '張圖片已附加',
noLogs: '暫無日誌',
sessionMonitor: {
resetContext: "重設上下文",
resetDescription: "重設後,新訊息將從空上下文開始。監控歷史會保留,檔案和長期記憶不受影響。請等待目前任務結束後再重設。",
resetSuccess: "上下文已重設,新訊息將開始新的對話。",
resetError: "重設失敗,請等待目前任務結束後重試。",
resetContext: '重設上下文',
resetDescription:
'重設後,新訊息將從空上下文開始。監控歷史會保留,檔案和長期記憶不受影響。請等待目前任務結束後再重設。',
resetSuccess: '上下文已重設,新訊息將開始新的對話。',
resetError: '重設失敗,請等待目前任務結束後重試。',
title: '會話監控',
sessions: '會話列表',
@@ -1162,24 +1170,24 @@ const zhHant = {
agents: {
apiToolsSelectAll: '全選',
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
runStatus: "執行狀態",
toolCalls: "工具呼叫",
delivery: "訊息傳送",
sendFailed: "傳送失敗",
sendCount: "{{count}} 次傳送呼叫成功",
noSend: "暫無傳送記錄",
noSteps: "尚未記錄執行步驟。",
recipient: "接收目標",
actionCompleted: "工具已執行完成。",
simulatedAction: "模擬執行,未傳送真實訊息。",
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
runStatus: '執行狀態',
toolCalls: '工具呼叫',
delivery: '訊息傳送',
sendFailed: '傳送失敗',
sendCount: '{{count}} 次傳送呼叫成功',
noSend: '暫無傳送記錄',
noSteps: '尚未記錄執行步驟。',
recipient: '接收目標',
actionCompleted: '工具已執行完成。',
simulatedAction: '模擬執行,未傳送真實訊息。',
description: '查看每次任務的觸發事件、模型輸出和工具執行過程。',
empty: '尚無執行紀錄。觸發平台事件或執行除錯後,可在此查看。',
input: '觸發輸入',
@@ -2059,14 +2067,14 @@ const zhHant = {
uploading: '上傳中...',
},
monitoring: {
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
rangeDays: '最近 {{days}} 天',
customRange: '自訂時間範圍',
rangeStart: '開始時間',
rangeEnd: '結束時間',
rangeHint: '開始時間須早於結束時間,最多 365 天。',
autoRefreshLabel: '定時重新整理',
autoRefreshOff: '定時重新整理:關閉',
autoRefreshEvery: '每 {{seconds}} 秒重新整理',
title: '執行日誌',
workbench: '設定與除錯',
description: '檢視此流水線的執行記錄和錯誤資訊(最近24小時)',
+1 -1
View File
@@ -24,7 +24,7 @@ test('all locale catalogs cover Codex states and preserve the expiry placeholder
const directory = new URL('../../src/i18n/locales/', import.meta.url);
const files = fs
.readdirSync(directory)
.filter((file) => file.endsWith('.ts'));
.filter((file) => /^[a-z]{2}-[A-Za-z]+\.ts$/.test(file));
assert.equal(files.length, 8);
let expected;
for (const file of files) {