From 0d6aa8dcd45c552774fa53294396f18017248a27 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Wed, 1 Jul 2026 21:04:21 +0800 Subject: [PATCH] refactor: consolidate bot event routing --- .../http/controller/groups/pipelines/embed.py | 9 +- src/langbot/pkg/api/http/service/agent.py | 2 +- src/langbot/pkg/api/http/service/bot.py | 35 ++- src/langbot/pkg/api/http/service/pipeline.py | 13 - src/langbot/pkg/api/mcp/server.py | 8 +- src/langbot/pkg/entity/persistence/agent.py | 2 +- src/langbot/pkg/entity/persistence/bot.py | 3 - .../0009_migrate_routing_to_event_bindings.py | 15 ++ .../versions/0011_drop_legacy_bot_routing.py | 55 ++++ src/langbot/pkg/platform/botmgr.py | 243 +++++++++--------- .../pkg/platform/sources/websocket_adapter.py | 29 ++- .../agents/components/AgentCreateContent.tsx | 4 +- .../agents/components/AgentFormComponent.tsx | 6 +- .../home/bots/components/bot-form/BotForm.tsx | 6 +- .../bot-form/EventBindingsEditor.tsx | 14 +- .../home-sidebar/SidebarDataContext.tsx | 2 +- web/src/app/infra/entities/api/index.ts | 22 -- web/src/app/wizard/page.tsx | 12 +- web/src/i18n/locales/en-US.ts | 54 ++-- web/src/i18n/locales/es-ES.ts | 34 +-- web/src/i18n/locales/ja-JP.ts | 49 ++-- web/src/i18n/locales/ru-RU.ts | 33 +-- web/src/i18n/locales/th-TH.ts | 33 +-- web/src/i18n/locales/vi-VN.ts | 33 +-- web/src/i18n/locales/zh-Hans.ts | 50 ++-- web/src/i18n/locales/zh-Hant.ts | 31 ++- 26 files changed, 427 insertions(+), 370 deletions(-) create mode 100644 src/langbot/pkg/persistence/alembic/versions/0011_drop_legacy_bot_routing.py diff --git a/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py b/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py index 99c00944f..7a1b64922 100644 --- a/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py +++ b/src/langbot/pkg/api/http/controller/groups/pipelines/embed.py @@ -3,7 +3,7 @@ All user-facing URLs are keyed by **bot_uuid** (not pipeline_uuid) so that internal pipeline identifiers are never exposed to end-users. Each handler resolves the bot_uuid to the owning ``web_page_bot`` RuntimeBot and extracts -the bound pipeline_uuid for internal routing. +the message event route's Pipeline target for internal routing. """ import asyncio @@ -62,16 +62,17 @@ class EmbedRouterGroup(group.RouterGroup): """Resolve *bot_uuid* to ``(runtime_bot, pipeline_uuid)``. Returns ``(None, None)`` when the bot does not exist, is not a - ``web_page_bot``, is disabled, or has no pipeline bound. + ``web_page_bot``, is disabled, or has no Pipeline target for messages. """ for bot in self.ap.platform_mgr.bots: + pipeline_uuid = bot.get_pipeline_target_for_event_type('message.received') if ( bot.bot_entity.uuid == bot_uuid and bot.bot_entity.adapter == 'web_page_bot' and bot.bot_entity.enable - and bot.bot_entity.use_pipeline_uuid + and pipeline_uuid ): - return bot, bot.bot_entity.use_pipeline_uuid + return bot, pipeline_uuid return None, None def _get_bot_config(self, bot_uuid: str) -> dict: diff --git a/src/langbot/pkg/api/http/service/agent.py b/src/langbot/pkg/api/http/service/agent.py index 916770dcc..a77c16a34 100644 --- a/src/langbot/pkg/api/http/service/agent.py +++ b/src/langbot/pkg/api/http/service/agent.py @@ -17,7 +17,7 @@ AGENT_DEFAULT_EVENT_PATTERNS = ['*'] class AgentService: - """Unified product surface for Agent orchestration instances and Pipelines.""" + """Unified product surface for Agent processors and Pipelines.""" ap: app.Application diff --git a/src/langbot/pkg/api/http/service/bot.py b/src/langbot/pkg/api/http/service/bot.py index 6013ad2b9..93237ec12 100644 --- a/src/langbot/pkg/api/http/service/bot.py +++ b/src/langbot/pkg/api/http/service/bot.py @@ -15,6 +15,15 @@ class BotService: """Bot service""" ap: app.Application + BOT_FIELDS = { + 'uuid', + 'name', + 'description', + 'adapter', + 'adapter_config', + 'enable', + 'event_bindings', + } def __init__(self, ap: app.Application) -> None: self.ap = ap @@ -115,6 +124,17 @@ class BotService: return normalized + async def _prepare_bot_data(self, bot_data: dict, *, include_uuid: bool) -> dict: + """Normalize Bot write payloads to the current event-routing model.""" + update_data = bot_data.copy() + if not include_uuid: + update_data.pop('uuid', None) + + update_data = {key: value for key, value in update_data.items() if key in self.BOT_FIELDS} + if 'event_bindings' in update_data: + update_data['event_bindings'] = await self._normalize_event_bindings(update_data.get('event_bindings')) + return update_data + async def get_bots(self, include_secret: bool = True) -> list[dict]: """获取所有机器人""" result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot)) @@ -188,7 +208,9 @@ class BotService: raise ValueError(f'Maximum number of bots ({max_bots}) reached') # TODO: 检查配置信息格式 + bot_data = await self._prepare_bot_data(bot_data, include_uuid=True) bot_data['uuid'] = str(uuid.uuid4()) + bot_data.setdefault('event_bindings', []) await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(bot_data)) @@ -200,18 +222,7 @@ class BotService: async def update_bot(self, bot_uuid: str, bot_data: dict) -> None: """Update bot""" - update_data = bot_data.copy() - - if 'uuid' in update_data: - del update_data['uuid'] - - if 'event_bindings' in update_data: - update_data['event_bindings'] = await self._normalize_event_bindings(update_data.get('event_bindings')) - - # clear legacy routing fields — routing is now fully managed via event_bindings - update_data.pop('use_pipeline_uuid', None) - update_data.pop('use_pipeline_name', None) - update_data.pop('pipeline_routing_rules', None) + update_data = await self._prepare_bot_data(bot_data, include_uuid=False) await self.ap.persistence_mgr.execute_async( sqlalchemy.update(persistence_bot.Bot).values(update_data).where(persistence_bot.Bot.uuid == bot_uuid) diff --git a/src/langbot/pkg/api/http/service/pipeline.py b/src/langbot/pkg/api/http/service/pipeline.py index b5b48b177..e7ca990df 100644 --- a/src/langbot/pkg/api/http/service/pipeline.py +++ b/src/langbot/pkg/api/http/service/pipeline.py @@ -218,19 +218,6 @@ class PipelineService: pipeline = await self.get_pipeline(pipeline_uuid) - if 'name' in pipeline_data: - from ....entity.persistence import bot as persistence_bot - - result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.use_pipeline_uuid == pipeline_uuid) - ) - - bots = result.all() - - for bot in bots: - bot_data = {'use_pipeline_name': pipeline_data['name']} - await self.ap.bot_service.update_bot(bot.uuid, bot_data) - await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid) await self.ap.pipeline_mgr.load_pipeline(pipeline) diff --git a/src/langbot/pkg/api/mcp/server.py b/src/langbot/pkg/api/mcp/server.py index 8e07882d5..883d4ade9 100644 --- a/src/langbot/pkg/api/mcp/server.py +++ b/src/langbot/pkg/api/mcp/server.py @@ -142,7 +142,7 @@ class LangBotMCPServer: return _dump({'ok': True}) # ----- Agents -------------------------------------------------- # - @mcp.tool(description='List product-level Agents, including Agent orchestrations and Pipelines.') + @mcp.tool(description='List product-level processors, including Agents and Pipelines.') async def list_agents() -> str: return _dump(await ap.agent_service.get_agents()) @@ -152,19 +152,19 @@ class LangBotMCPServer: @mcp.tool( description=( - 'Create an Agent orchestration or Pipeline. `agent_data` matches ' + 'Create an Agent processor or Pipeline. `agent_data` matches ' 'POST /api/v1/agents; set kind to `agent` or `pipeline`. Returns the new UUID and kind.' ) ) async def create_agent(agent_data: dict) -> str: return _dump(await ap.agent_service.create_agent(agent_data)) - @mcp.tool(description='Update an Agent orchestration or Pipeline by UUID.') + @mcp.tool(description='Update an Agent processor or Pipeline by UUID.') async def update_agent(agent_uuid: str, agent_data: dict) -> str: await ap.agent_service.update_agent(agent_uuid, agent_data) return _dump({'ok': True}) - @mcp.tool(description='Delete an Agent orchestration or Pipeline by UUID.') + @mcp.tool(description='Delete an Agent processor or Pipeline by UUID.') async def delete_agent(agent_uuid: str) -> str: await ap.agent_service.delete_agent(agent_uuid) return _dump({'ok': True}) diff --git a/src/langbot/pkg/entity/persistence/agent.py b/src/langbot/pkg/entity/persistence/agent.py index dc80fc23e..577d60f95 100644 --- a/src/langbot/pkg/entity/persistence/agent.py +++ b/src/langbot/pkg/entity/persistence/agent.py @@ -4,7 +4,7 @@ from .base import Base class Agent(Base): - """Product-level Agent orchestration instance.""" + """Product-level Agent processor.""" __tablename__ = 'agents' diff --git a/src/langbot/pkg/entity/persistence/bot.py b/src/langbot/pkg/entity/persistence/bot.py index 9b9f7e4d6..d8270b106 100644 --- a/src/langbot/pkg/entity/persistence/bot.py +++ b/src/langbot/pkg/entity/persistence/bot.py @@ -28,9 +28,6 @@ class Bot(Base): adapter = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) adapter_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) enable = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) - use_pipeline_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) - use_pipeline_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) - pipeline_routing_rules = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, server_default='[]') event_bindings = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, server_default='[]') created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) updated_at = sqlalchemy.Column( diff --git a/src/langbot/pkg/persistence/alembic/versions/0009_migrate_routing_to_event_bindings.py b/src/langbot/pkg/persistence/alembic/versions/0009_migrate_routing_to_event_bindings.py index fef63a2ff..767629dda 100644 --- a/src/langbot/pkg/persistence/alembic/versions/0009_migrate_routing_to_event_bindings.py +++ b/src/langbot/pkg/persistence/alembic/versions/0009_migrate_routing_to_event_bindings.py @@ -16,6 +16,16 @@ down_revision = '0008_agent_product_surface' depends_on = None +def _table_exists(table_name: str) -> bool: + return table_name in sa.inspect(op.get_bind()).get_table_names() + + +def _column_exists(table_name: str, column_name: str) -> bool: + if not _table_exists(table_name): + return False + return column_name in {column['name'] for column in sa.inspect(op.get_bind()).get_columns(table_name)} + + def _rule_to_filters(rule: dict) -> list[dict] | None: """Convert a pipeline_routing_rule to event_binding filters (best effort). @@ -38,6 +48,11 @@ def _rule_to_filters(rule: dict) -> list[dict] | None: def upgrade() -> None: + if not _table_exists('bots') or not _column_exists('bots', 'event_bindings'): + return + if not _column_exists('bots', 'use_pipeline_uuid') or not _column_exists('bots', 'pipeline_routing_rules'): + return + bind = op.get_bind() rows = bind.execute( sa.text('SELECT uuid, use_pipeline_uuid, pipeline_routing_rules, event_bindings FROM bots') diff --git a/src/langbot/pkg/persistence/alembic/versions/0011_drop_legacy_bot_routing.py b/src/langbot/pkg/persistence/alembic/versions/0011_drop_legacy_bot_routing.py new file mode 100644 index 000000000..6fc9e0067 --- /dev/null +++ b/src/langbot/pkg/persistence/alembic/versions/0011_drop_legacy_bot_routing.py @@ -0,0 +1,55 @@ +"""drop legacy bot pipeline routing columns + +Revision ID: 0011_drop_legacy_bot_routing +Revises: 0010_merge_mcp_resource_agent_heads +Create Date: 2026-07-01 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = '0011_drop_legacy_bot_routing' +down_revision = '0010_merge_mcp_resource_agent_heads' +branch_labels = None +depends_on = None + + +def _table_exists(table_name: str) -> bool: + return table_name in sa.inspect(op.get_bind()).get_table_names() + + +def _column_exists(table_name: str, column_name: str) -> bool: + if not _table_exists(table_name): + return False + return column_name in {column['name'] for column in sa.inspect(op.get_bind()).get_columns(table_name)} + + +def upgrade() -> None: + if not _table_exists('bots'): + return + existing_columns = {column['name'] for column in sa.inspect(op.get_bind()).get_columns('bots')} + columns_to_drop = [ + column_name + for column_name in ('use_pipeline_name', 'use_pipeline_uuid', 'pipeline_routing_rules') + if column_name in existing_columns + ] + if not columns_to_drop: + return + + with op.batch_alter_table('bots', schema=None) as batch_op: + for column_name in columns_to_drop: + batch_op.drop_column(column_name) + + +def downgrade() -> None: + if not _table_exists('bots'): + return + + with op.batch_alter_table('bots', schema=None) as batch_op: + if not _column_exists('bots', 'use_pipeline_name'): + batch_op.add_column(sa.Column('use_pipeline_name', sa.String(255), nullable=True)) + if not _column_exists('bots', 'use_pipeline_uuid'): + batch_op.add_column(sa.Column('use_pipeline_uuid', sa.String(255), nullable=True)) + if not _column_exists('bots', 'pipeline_routing_rules'): + batch_op.add_column(sa.Column('pipeline_routing_rules', sa.JSON(), nullable=False, server_default='[]')) diff --git a/src/langbot/pkg/platform/botmgr.py b/src/langbot/pkg/platform/botmgr.py index d539fbdd0..3846b277e 100644 --- a/src/langbot/pkg/platform/botmgr.py +++ b/src/langbot/pkg/platform/botmgr.py @@ -14,8 +14,6 @@ from ..core import app, entities as core_entities, taskmgr from ..discover import engine from ..entity.persistence import bot as persistence_bot -from ..entity.persistence import pipeline as persistence_pipeline - from ..entity.errors import platform as platform_errors from ..agent.runner.host_models import ( AgentBinding, @@ -32,6 +30,7 @@ import langbot_plugin.api.entities.builtin.provider.session as provider_session import langbot_plugin.api.entities.builtin.provider.message as provider_message import langbot_plugin.api.entities.events as plugin_events import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter from langbot_plugin.api.entities.builtin.agent_runner.event import ( @@ -74,26 +73,6 @@ class RuntimeBot: self.task_context = taskmgr.TaskContext() self.logger = logger - @staticmethod - def _match_operator(actual: str, operator: str, expected: str) -> bool: - """Evaluate a single operator condition.""" - if operator == 'eq': - return actual == expected - elif operator == 'neq': - return actual != expected - elif operator == 'contains': - return expected in actual - elif operator == 'not_contains': - return expected not in actual - elif operator == 'starts_with': - return actual.startswith(expected) - elif operator == 'regex': - try: - return bool(re.search(expected, actual)) - except re.error: - return False - return False - PIPELINE_DISCARD = '__discard__' PIPELINE_DISCARD_DISPLAY_NAME = 'Discarded' EVENT_DATA_MAX_STRING_BYTES = 512 @@ -229,12 +208,7 @@ class RuntimeBot: if isinstance(event_filter, dict) ) - def _resolve_eba_event_binding( - self, - event: platform_events.EBAEvent, - event_type: str, - ) -> dict[str, typing.Any] | None: - """Resolve the highest priority Bot event binding for a platform event.""" + def _get_event_bindings(self) -> list[dict[str, typing.Any]]: raw_bindings = self.bot_entity.event_bindings or [] if isinstance(raw_bindings, str): try: @@ -242,11 +216,18 @@ class RuntimeBot: except json.JSONDecodeError: raw_bindings = [] if not isinstance(raw_bindings, list): - return None + return [] + return [binding for binding in raw_bindings if isinstance(binding, dict)] + def _resolve_eba_event_binding( + self, + event: platform_events.EBAEvent, + event_type: str, + ) -> dict[str, typing.Any] | None: + """Resolve the highest priority Bot event binding for a platform event.""" matched: list[tuple[int, int, dict[str, typing.Any]]] = [] - for index, binding in enumerate(raw_bindings): - if not isinstance(binding, dict) or not binding.get('enabled', True): + for index, binding in enumerate(self._get_event_bindings()): + if not binding.get('enabled', True): continue event_pattern = str(binding.get('event_pattern') or '') @@ -265,6 +246,29 @@ class RuntimeBot: matched.sort(key=lambda item: (item[0], item[1]), reverse=True) return matched[0][2] + def get_pipeline_target_for_event_type(self, event_type: str = 'message.received') -> str | None: + """Return the first Pipeline target configured for an event type.""" + matched: list[tuple[int, int, str]] = [] + for index, binding in enumerate(self._get_event_bindings()): + if not binding.get('enabled', True): + continue + if binding.get('target_type') != 'pipeline': + continue + target_uuid = str(binding.get('target_uuid') or '') + if not target_uuid: + continue + if not self._match_event_pattern(event_type, str(binding.get('event_pattern') or '')): + continue + priority = int(binding.get('priority') or 0) + order = int(binding.get('order', index)) + matched.append((priority, -order, target_uuid)) + + if not matched: + return None + + matched.sort(key=lambda item: (item[0], item[1]), reverse=True) + return matched[0][2] + @staticmethod def _safe_model_dump(model: typing.Any) -> dict[str, typing.Any]: if model is None: @@ -451,6 +455,56 @@ class RuntimeBot: return None, None, metadata + @staticmethod + def _extract_message_id(message_chain: platform_message.MessageChain) -> str: + for component in message_chain: + if isinstance(component, platform_message.Source): + value = getattr(component, 'id', '') + return str(value) if value is not None else '' + return '' + + def _legacy_message_to_eba_event( + self, + event: platform_events.FriendMessage | platform_events.GroupMessage, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, + ) -> platform_events.MessageReceivedEvent: + if isinstance(event, platform_events.GroupMessage): + group = platform_entities.UserGroup( + id=event.group.id, + name=event.group.name, + ) + return platform_events.MessageReceivedEvent( + message_id=self._extract_message_id(event.message_chain), + message_chain=event.message_chain, + sender=platform_entities.User( + id=event.sender.id, + nickname=event.sender.member_name, + ), + chat_type=platform_entities.ChatType.GROUP, + chat_id=event.group.id, + group=group, + timestamp=event.time or time.time(), + bot_uuid=self.bot_entity.uuid, + adapter_name=adapter.__class__.__name__, + source_platform_object=event.source_platform_object, + ) + + return platform_events.MessageReceivedEvent( + message_id=self._extract_message_id(event.message_chain), + message_chain=event.message_chain, + sender=platform_entities.User( + id=event.sender.id, + nickname=event.sender.nickname, + remark=event.sender.remark, + ), + chat_type=platform_entities.ChatType.PRIVATE, + chat_id=event.sender.id, + timestamp=event.time or time.time(), + bot_uuid=self.bot_entity.uuid, + adapter_name=adapter.__class__.__name__, + source_platform_object=event.source_platform_object, + ) + @classmethod def _build_agent_input(cls, event: platform_events.EBAEvent) -> AgentInput: text = None @@ -635,6 +689,22 @@ class RuntimeBot: platform_message.MessageChain([platform_message.Plain(text=final_text)]), ) + async def _handle_platform_event( + self, + event: platform_events.EBAEvent, + adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, + ) -> None: + event.bot_uuid = self.bot_entity.uuid + plugin_event = self._eba_event_to_plugin_event(event) + + if plugin_event is not None: + try: + await self.ap.plugin_connector.emit_event(plugin_event) + except Exception: + await self.logger.error(f'Failed to dispatch platform event to plugins: {traceback.format_exc()}') + + await self._dispatch_eba_event_to_agent(event, adapter) + async def _dispatch_eba_event_to_agent( self, event: platform_events.EBAEvent, @@ -644,8 +714,7 @@ class RuntimeBot: event_binding = self._resolve_eba_event_binding(event, event_type) if event_binding is None: - if isinstance(event, platform_events.MessageReceivedEvent): - await self._dispatch_eba_message_to_pipeline(event, adapter) + await self.logger.info(f'Platform event {event_type} ignored: no event route matched') return target_type = event_binding.get('target_type') @@ -708,64 +777,6 @@ class RuntimeBot: f'Failed to deliver Agent output for EBA event {event_type}: {traceback.format_exc()}' ) - def resolve_pipeline_uuid( - self, - launcher_type: str, - launcher_id: str, - message_text: str, - message_element_types: list[str] | None = None, - ) -> tuple[str | None, bool]: - """Resolve pipeline UUID based on routing rules. - - Rules are evaluated in order; first match wins. - Falls back to use_pipeline_uuid if no rule matches. - - Rule types: - - launcher_type: session type ("person" / "group") - - launcher_id: session / group id - - message_content: message text content - - message_has_element: message contains element of given type - (Image, Voice, File, Forward, Face, At, AtAll, Quote) - Operators: eq (has), neq (doesn't have) - - Operators: eq, neq, contains, not_contains, starts_with, regex - - When pipeline_uuid is ``__discard__``, the message should be - silently dropped by the caller. - - Returns: - tuple: (pipeline_uuid, routed_by_rule) - routed_by_rule is True - when a routing rule matched, False when falling back to default. - """ - rules = self.bot_entity.pipeline_routing_rules or [] - element_type_set = set(message_element_types or []) - - for rule in rules: - rule_type = rule.get('type') - operator = rule.get('operator', 'eq') - rule_value = rule.get('value', '') - target_uuid = rule.get('pipeline_uuid') - if not rule_type or not target_uuid: - continue - - if rule_type == 'launcher_type': - if self._match_operator(launcher_type, operator, rule_value): - return target_uuid, True - elif rule_type == 'launcher_id': - if self._match_operator(str(launcher_id), operator, str(rule_value)): - return target_uuid, True - elif rule_type == 'message_content': - if self._match_operator(message_text, operator, rule_value): - return target_uuid, True - elif rule_type == 'message_has_element': - has_element = rule_value in element_type_set - if operator == 'eq' and has_element: - return target_uuid, True - elif operator == 'neq' and not has_element: - return target_uuid, True - - return self.bot_entity.use_pipeline_uuid, False - async def _record_discarded_message( self, launcher_type: provider_session.LauncherTypes, @@ -932,21 +943,21 @@ class RuntimeBot: event: platform_events.FriendMessage, adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): - await self._handle_legacy_message_event( - event, - adapter, - pipeline_uuid_override=websocket_pipeline_uuid(event, adapter), - ) + pipeline_uuid = websocket_pipeline_uuid(event, adapter) + if pipeline_uuid: + await self._handle_legacy_message_event(event, adapter, pipeline_uuid_override=pipeline_uuid) + return + await self._handle_platform_event(self._legacy_message_to_eba_event(event, adapter), adapter) async def on_group_message( event: platform_events.GroupMessage, adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): - await self._handle_legacy_message_event( - event, - adapter, - pipeline_uuid_override=websocket_pipeline_uuid(event, adapter), - ) + pipeline_uuid = websocket_pipeline_uuid(event, adapter) + if pipeline_uuid: + await self._handle_legacy_message_event(event, adapter, pipeline_uuid_override=pipeline_uuid) + return + await self._handle_platform_event(self._legacy_message_to_eba_event(event, adapter), adapter) self.adapter.register_listener(platform_events.FriendMessage, on_friend_message) self.adapter.register_listener(platform_events.GroupMessage, on_group_message) @@ -957,20 +968,11 @@ class RuntimeBot: adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): try: - # Resolve pipeline name + pipeline_id = self.get_pipeline_target_for_event_type('message.received') or '' pipeline_name = '' - if self.bot_entity.use_pipeline_uuid: - try: - pipeline_result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.select(persistence_pipeline.LegacyPipeline.name).where( - persistence_pipeline.LegacyPipeline.uuid == self.bot_entity.use_pipeline_uuid - ) - ) - pipeline_row = pipeline_result.first() - if pipeline_row: - pipeline_name = pipeline_row[0] - except Exception: - pass + if pipeline_id: + pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_id) + pipeline_name = pipeline.get('name', '') if pipeline else '' await self.ap.monitoring_service.record_feedback( feedback_id=event.feedback_id, @@ -979,7 +981,7 @@ class RuntimeBot: inaccurate_reasons=event.inaccurate_reasons, bot_id=self.bot_entity.uuid, bot_name=self.bot_entity.name, - pipeline_id=self.bot_entity.use_pipeline_uuid or '', + pipeline_id=pipeline_id, pipeline_name=pipeline_name, session_id=event.session_id, message_id=event.message_id, @@ -999,16 +1001,7 @@ class RuntimeBot: event: platform_events.EBAEvent, adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter, ): - event.bot_uuid = self.bot_entity.uuid - plugin_event = self._eba_event_to_plugin_event(event) - - if plugin_event is not None: - try: - await self.ap.plugin_connector.emit_event(plugin_event) - except Exception: - await self.logger.error(f'Failed to dispatch EBA event to plugins: {traceback.format_exc()}') - - await self._dispatch_eba_event_to_agent(event, adapter) + await self._handle_platform_event(event, adapter) self.adapter.register_listener(platform_events.EBAEvent, on_eba_event) diff --git a/src/langbot/pkg/platform/sources/websocket_adapter.py b/src/langbot/pkg/platform/sources/websocket_adapter.py index c34027792..f910950f7 100644 --- a/src/langbot/pkg/platform/sources/websocket_adapter.py +++ b/src/langbot/pkg/platform/sources/websocket_adapter.py @@ -77,6 +77,9 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) stream_enabled: bool = pydantic.Field(default=True, exclude=True) """是否启用流式输出""" + current_pipeline_uuid: str = pydantic.Field(default='', exclude=True) + """Pipeline currently associated with the active WebSocket exchange.""" + def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs): super().__init__( config=config, @@ -90,6 +93,18 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) self.bot_account_id = 'websocketbot' self.outbound_message_queue = asyncio.Queue() self.stream_enabled = True + self.current_pipeline_uuid = '' + + def _resolve_pipeline_uuid( + self, + message_source: platform_events.MessageEvent | None = None, + target_id: str = '', + ) -> str: + if message_source is not None: + event_pipeline_uuid = getattr(message_source, '_langbot_pipeline_uuid', '') + if isinstance(event_pipeline_uuid, str) and event_pipeline_uuid: + return event_pipeline_uuid + return self.current_pipeline_uuid or target_id async def send_message( self, @@ -103,8 +118,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) target_id 可能是 launcher_id(如 websocket_xxx)或 pipeline_uuid。 我们需要尝试两种方式来确保消息能够送达。 """ - # 获取当前的 pipeline_uuid - pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid + pipeline_uuid = self._resolve_pipeline_uuid(target_id=str(target_id)) session_type = 'group' if target_type == 'group' else 'person' # 选择会话 @@ -152,8 +166,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) else self.websocket_person_session ) - # 从message_source获取pipeline_uuid和connection_id - pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid + pipeline_uuid = self._resolve_pipeline_uuid(message_source) session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person' # 生成新的消息ID @@ -200,7 +213,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) else self.websocket_person_session ) - pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid + pipeline_uuid = self._resolve_pipeline_uuid(message_source) session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person' message_list = session.get_message_list(pipeline_uuid) stream_message_indexes = session.get_stream_message_indexes(pipeline_uuid) @@ -380,6 +393,7 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) When provided, its identity is used for logging and session tracking. """ pipeline_uuid = connection.pipeline_uuid + self.current_pipeline_uuid = pipeline_uuid session_type = connection.session_type # 获取stream参数,默认为True @@ -449,11 +463,6 @@ class WebSocketAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter) object.__setattr__(event, '_langbot_pipeline_uuid', pipeline_uuid) - # 设置流水线UUID (proxy bot always needs it for reply_message routing) - self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid = pipeline_uuid - if owner_bot is not None: - owner_bot.bot_entity.use_pipeline_uuid = pipeline_uuid - # 异步触发事件处理 # Use owner_bot's listeners if available, otherwise fall back to proxy bot listeners = ( diff --git a/web/src/app/home/agents/components/AgentCreateContent.tsx b/web/src/app/home/agents/components/AgentCreateContent.tsx index fe379e125..654aaa041 100644 --- a/web/src/app/home/agents/components/AgentCreateContent.tsx +++ b/web/src/app/home/agents/components/AgentCreateContent.tsx @@ -84,8 +84,8 @@ export default function AgentCreateContent({ { kind: 'agent', icon: Bot, - title: t('agents.agentOrchestration'), - description: t('agents.agentOrchestrationDescription'), + title: t('agents.agentType'), + description: t('agents.agentTypeDescription'), badge: t('agents.allEvents'), }, { diff --git a/web/src/app/home/agents/components/AgentFormComponent.tsx b/web/src/app/home/agents/components/AgentFormComponent.tsx index abfabfc75..eb8636d7b 100644 --- a/web/src/app/home/agents/components/AgentFormComponent.tsx +++ b/web/src/app/home/agents/components/AgentFormComponent.tsx @@ -148,7 +148,7 @@ export default function AgentFormComponent({ const sections: SectionItem[] = [ { label: t('agents.basicInfo'), name: 'basic', icon: Info }, { label: t('agents.runnerSettings'), name: 'runner', icon: Brain }, - { label: t('agents.eventCapability'), name: 'events', icon: FileJson2 }, + { label: t('agents.advanced'), name: 'events', icon: FileJson2 }, ]; const currentRunner = (form.watch('runner') as Record)?.id; @@ -450,9 +450,9 @@ export default function AgentFormComponent({ {activeSection === 'events' && ( - {t('agents.eventCapability')} + {t('agents.bindableEvents')} - {t('agents.eventCapabilityDescription')} + {t('agents.bindableEventsDescription')} diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index 7d7b72dc7..92e307436 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -444,13 +444,13 @@ export default function BotForm({ - {/* Card 2: Event Orchestration (edit mode only) */} + {/* Card 2: Event Routing (edit mode only) */} {initBotId && ( - {t('bots.eventOrchestration')} + {t('bots.eventRouting')} - {t('bots.eventOrchestrationDescription')} + {t('bots.eventRoutingDescription')} diff --git a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx index 19004790e..c8c4217ec 100644 --- a/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx +++ b/web/src/app/home/bots/components/bot-form/EventBindingsEditor.tsx @@ -59,7 +59,7 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Input } from '@/components/ui/input'; -import { EventBinding, Agent, AgentKind } from '@/app/infra/entities/api'; +import { EventBinding, Agent } from '@/app/infra/entities/api'; export const PIPELINE_DISCARD = '__discard__'; @@ -496,18 +496,6 @@ function BindingCardContent({ const isExpanded = expandedIds.has(id); const filterCount = (binding.filters as FilterRow[] | undefined)?.length ?? 0; const pipelineAllowed = isMessageEventPattern(binding.event_pattern); - const targetType = binding.target_type || 'agent'; - - function getTargetOptions(kind: AgentKind): Agent[] { - return agentOptions.filter((agent) => { - if (agent.kind !== kind) return false; - if (kind === 'pipeline') - return isMessageEventPattern(binding.event_pattern); - if (kind === 'agent') - return agentSupportsEventPattern(agent, binding.event_pattern); - return true; - }); - } return (
diff --git a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx index ccfe07e58..472b3507b 100644 --- a/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx +++ b/web/src/app/home/components/home-sidebar/SidebarDataContext.tsx @@ -29,7 +29,7 @@ export interface SidebarEntityItem { debug?: boolean; // Set when this item appears in the unified extensions list extensionType?: 'plugin' | 'mcp' | 'skill'; - // Agent-specific: distinguishes Agent orchestration from legacy Pipeline + // Agent-specific: distinguishes Agent processors from Pipelines kind?: 'agent' | 'pipeline'; } diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts index 7601af5bf..f52e39ab7 100644 --- a/web/src/app/infra/entities/api/index.ts +++ b/web/src/app/infra/entities/api/index.ts @@ -228,34 +228,12 @@ export interface Bot { enable?: boolean; adapter: string; adapter_config: object; - use_pipeline_name?: string; - use_pipeline_uuid?: string; - pipeline_routing_rules?: PipelineRoutingRule[]; event_bindings?: EventBinding[]; created_at?: string; updated_at?: string; adapter_runtime_values?: object; } -export type RoutingRuleOperator = - | 'eq' - | 'neq' - | 'contains' - | 'not_contains' - | 'starts_with' - | 'regex'; - -export interface PipelineRoutingRule { - type: - | 'launcher_type' - | 'launcher_id' - | 'message_content' - | 'message_has_element'; - operator: RoutingRuleOperator; - value: string; - pipeline_uuid: string; -} - export interface EventBinding { id?: string; event_pattern: string; diff --git a/web/src/app/wizard/page.tsx b/web/src/app/wizard/page.tsx index a6655e403..69b6c9cf7 100644 --- a/web/src/app/wizard/page.tsx +++ b/web/src/app/wizard/page.tsx @@ -478,7 +478,17 @@ export default function WizardPage() { adapter: existingBot.adapter, adapter_config: existingBot.adapter_config, enable: existingBot.enable, - use_pipeline_uuid: pipelineResp.uuid, + event_bindings: [ + { + event_pattern: 'message.received', + target_type: 'pipeline', + target_uuid: pipelineResp.uuid, + filters: [], + priority: 0, + enabled: true, + description: '', + }, + ], }); setCurrentStep(3); diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index cd889f91e..a3b0a327c 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -331,8 +331,7 @@ const enUS = { getBotConfigError: 'Failed to get bot configuration: ', saveSuccess: 'Saved successfully', saveError: 'Save failed: ', - createSuccess: - 'Created successfully. Please enable or modify the bound pipeline', + createSuccess: 'Created successfully. Please configure event routing', createError: 'Creation failed: ', deleteSuccess: 'Deleted successfully', deleteError: 'Delete failed: ', @@ -363,25 +362,25 @@ const enUS = { routingConnection: 'Routing & Connection', routingConnectionDescription: 'Bind the pipeline that processes messages for this bot', - eventOrchestration: 'Event Orchestration', - eventOrchestrationDescription: - 'Bind different handling logic to different bot events. Pipelines only support message events.', - eventBindings: 'Event Bindings', - addEventBinding: 'Add Event Binding', + eventRouting: 'Event Routing', + eventRoutingDescription: + 'Choose which processor handles each event received by this bot. Edit the processing logic on the Agent page. Pipelines only support message events.', + eventBindings: 'Event Routes', + addEventBinding: 'Add Route', eventPattern: 'Event', eventPatternPlaceholder: 'Select event', targetType: 'Target Type', - target: 'Handling Logic', - targetAgent: 'Agent Orchestration', + target: 'Processor', + targetAgent: 'Agent', targetPipeline: 'Pipeline', targetDiscard: 'Discard', - selectTarget: 'Select handling logic', - searchTarget: 'Search…', - noTargetFound: 'No results found', + selectTarget: 'Select processor', + searchTarget: 'Search processors…', + noTargetFound: 'No compatible processors found', priority: 'Priority', enabled: 'Enabled', eventBindingDescriptionPlaceholder: 'Rule description', - noEventBindings: 'No event bindings', + noEventBindings: 'No event routes', unsupportedPipelineEvent: 'Pipelines can only be used for message.* events', disable: 'Disable', enable: 'Enable', @@ -529,14 +528,13 @@ const enUS = { }, agents: { title: 'Agent', - description: - 'Manage Agent orchestrations and Pipelines, then bind them to bot events', - create: 'Create Agent', - editAgent: 'Edit Agent Orchestration', + description: 'Create reusable processors and use them in bot event routing', + create: 'Create Processor', + editAgent: 'Edit Agent', selectFromSidebar: 'Select an Agent or Pipeline from the sidebar', - agentOrchestration: 'Agent Orchestration', - agentOrchestrationDescription: - 'Event-first handling logic for messages, group members, friends, feedback, and other platform events.', + agentType: 'Agent', + agentTypeDescription: + 'Use a runner to handle messages, group members, friends, feedback, and other platform events.', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: 'Pipeline', @@ -549,12 +547,13 @@ const enUS = { basicInfo: 'Basic Information', basicInfoDescription: 'Set the name, icon, description and enabled state', runnerSettings: 'Runner', - eventCapability: 'Event Capability', - eventCapabilityDescription: - 'Declare which events this Agent orchestration can be bound to. Use one event pattern per line; * and namespace.* are supported.', - supportedEvents: 'Supported Events', + advanced: 'Advanced', + bindableEvents: 'Bindable Event Range', + bindableEventsDescription: + 'Limit which bot event routes can select this Agent. The default is suitable for most cases.', + supportedEvents: 'Event Range', supportedEventsDescription: - 'Examples: *, message.received, group.*. Pipelines are fixed to message.*.', + 'Use one event pattern per line, for example *, message.received, group.*. Pipelines are fixed to message.*.', enabled: 'Enable Agent', enabledDescription: 'When disabled, this Agent should not be selected by event routing.', @@ -566,11 +565,10 @@ const enUS = { saveError: 'Save failed: ', deleteSuccess: 'Deleted successfully', deleteError: 'Delete failed: ', - deleteConfirmation: - 'Are you sure you want to delete this Agent orchestration?', + deleteConfirmation: 'Are you sure you want to delete this Agent?', dangerZone: 'Danger Zone', dangerZoneDescription: 'Irreversible and destructive actions', - deleteAgentAction: 'Delete this Agent orchestration', + deleteAgentAction: 'Delete this Agent', deleteAgentHint: 'Once deleted, events bound to it can no longer be executed.', noRunnerMetadata: 'No AgentRunner metadata is currently available.', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index db4830f27..abcc92fc5 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -342,8 +342,7 @@ const esES = { getBotConfigError: 'Error al obtener la configuración del Bot: ', saveSuccess: 'Guardado correctamente', saveError: 'Error al guardar: ', - createSuccess: - 'Creado correctamente. Por favor, activa o modifica el Pipeline vinculado', + createSuccess: 'Creado correctamente. Configura el enrutamiento de eventos', createError: 'Error al crear: ', deleteSuccess: 'Eliminado correctamente', deleteError: 'Error al eliminar: ', @@ -374,6 +373,9 @@ const esES = { routingConnection: 'Enrutamiento y conexión', routingConnectionDescription: 'Vincula el Pipeline que procesa los mensajes de este Bot', + eventRouting: 'Enrutamiento de eventos', + eventRoutingDescription: + 'Elige qué procesador maneja cada evento recibido por este Bot. Edita la lógica de procesamiento en la página Agent. Los Pipelines solo admiten eventos de mensaje.', routingRules: 'Reglas de enrutamiento condicional', routingRulesDescription: 'Las reglas se evalúan en orden; la primera coincidencia enruta a su pipeline. Si ninguna coincide, se usa el pipeline predeterminado.', @@ -484,13 +486,13 @@ const esES = { agents: { title: 'Agent', description: - 'Gestiona orquestaciones de Agent y Pipelines, y vincúlalos a eventos de bot', - create: 'Crear Agent', - editAgent: 'Editar orquestación de Agent', + 'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot', + create: 'Crear procesador', + editAgent: 'Editar Agent', selectFromSidebar: 'Selecciona un Agent o Pipeline desde la barra lateral', - agentOrchestration: 'Orquestación de Agent', - agentOrchestrationDescription: - 'Lógica de procesamiento orientada a eventos para mensajes, miembros de grupo, amigos, retroalimentación y otros eventos de plataforma.', + agentType: 'Agent', + agentTypeDescription: + 'Usa un runner para procesar mensajes, miembros de grupo, amigos, retroalimentación y otros eventos de plataforma.', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: 'Pipeline', @@ -504,12 +506,13 @@ const esES = { basicInfoDescription: 'Establece el nombre, icono, descripción y estado de habilitación', runnerSettings: 'Runner', - eventCapability: 'Capacidad de eventos', - eventCapabilityDescription: - 'Declara a qué eventos puede vincularse esta orquestación de Agent. Un patrón de evento por línea; se admiten * y namespace.*.', - supportedEvents: 'Eventos admitidos', + advanced: 'Avanzado', + bindableEvents: 'Rango de eventos vinculables', + bindableEventsDescription: + 'Limita qué rutas de eventos del bot pueden seleccionar este Agent. El valor predeterminado sirve para la mayoría de los casos.', + supportedEvents: 'Rango de eventos', supportedEventsDescription: - 'Ejemplos: *, message.received, group.*. Los Pipelines están fijos en message.*.', + 'Usa un patrón de evento por línea, por ejemplo *, message.received, group.*. Los Pipelines están fijos en message.*.', enabled: 'Habilitar Agent', enabledDescription: 'Cuando está deshabilitado, este Agent no debe ser seleccionado por el enrutamiento de eventos.', @@ -521,11 +524,10 @@ const esES = { saveError: 'Error al guardar: ', deleteSuccess: 'Eliminado correctamente', deleteError: 'Error al eliminar: ', - deleteConfirmation: - '¿Estás seguro de que deseas eliminar esta orquestación de Agent?', + deleteConfirmation: '¿Estás seguro de que deseas eliminar este Agent?', dangerZone: 'Zona de peligro', dangerZoneDescription: 'Acciones irreversibles y destructivas', - deleteAgentAction: 'Eliminar esta orquestación de Agent', + deleteAgentAction: 'Eliminar este Agent', deleteAgentHint: 'Una vez eliminado, los eventos vinculados a él ya no podrán ejecutarse.', noRunnerMetadata: diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index e6d601cec..1ac873ade 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -337,8 +337,7 @@ const jaJP = { getBotConfigError: 'ボット設定の取得に失敗しました:', saveSuccess: '保存に成功しました', saveError: '保存に失敗しました:', - createSuccess: - '作成が完了しました。有効化するか、パイプラインの設定を行ってください', + createSuccess: '作成が完了しました。イベントルーティングを設定してください', createError: '作成に失敗しました:', deleteSuccess: '削除に成功しました', deleteError: '削除に失敗しました:', @@ -369,23 +368,23 @@ const jaJP = { routingConnection: 'ルーティングと接続', routingConnectionDescription: 'このボットのメッセージを処理するパイプラインを紐付け', - eventOrchestration: 'イベント編成', - eventOrchestrationDescription: - 'このボットのイベントごとに異なる処理ロジックを紐付けます。Pipeline はメッセージイベントのみ対応します。', - eventBindings: 'イベントバインディング', - addEventBinding: 'イベントバインディングを追加', + eventRouting: 'イベントルーティング', + eventRoutingDescription: + 'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。処理ロジックは Agent ページで編集します。Pipeline はメッセージイベントのみ対応します。', + eventBindings: 'イベントルート', + addEventBinding: 'ルートを追加', eventPattern: 'イベント', eventPatternPlaceholder: 'イベントを選択', targetType: 'ターゲットタイプ', - target: '処理ロジック', - targetAgent: 'Agent 編成', + target: 'プロセッサー', + targetAgent: 'Agent', targetPipeline: 'Pipeline', targetDiscard: '破棄', - selectTarget: '処理ロジックを選択', + selectTarget: 'プロセッサーを選択', priority: '優先度', enabled: '有効', eventBindingDescriptionPlaceholder: 'ルール説明', - noEventBindings: 'イベントバインディングはありません', + noEventBindings: 'イベントルートはありません', unsupportedPipelineEvent: 'Pipeline は message.* イベントにのみ使用できます', eventCustom: 'カスタムイベント', @@ -516,14 +515,15 @@ const jaJP = { }, agents: { title: 'Agent', - description: 'Agent 編成と Pipeline を管理し、ボットのイベントに紐付けます', - create: 'Agent を作成', - editAgent: 'Agent 編成を編集', + description: + '再利用可能なプロセッサーを作成し、ボットのイベントルーティングで使用します', + create: 'プロセッサーを作成', + editAgent: 'Agent を編集', selectFromSidebar: 'サイドバーから Agent または Pipeline を選択してください', - agentOrchestration: 'Agent 編成', - agentOrchestrationDescription: - 'メッセージ、グループメンバー、友だち、フィードバックなどのプラットフォームイベント向けの処理ロジックです。', + agentType: 'Agent', + agentTypeDescription: + 'Runner を使ってメッセージ、グループメンバー、友だち、フィードバックなどのプラットフォームイベントを処理します。', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: 'パイプライン', @@ -536,12 +536,13 @@ const jaJP = { basicInfo: '基本情報', basicInfoDescription: '名前、アイコン、説明、有効状態を設定します', runnerSettings: 'Runner', - eventCapability: 'イベント能力', - eventCapabilityDescription: - 'この Agent 編成をどのイベントに紐付けられるかを宣言します。1 行に 1 つのイベントパターンを指定し、* と namespace.* を利用できます。', - supportedEvents: '対応イベント', + advanced: '詳細', + bindableEvents: '紐付け可能なイベント範囲', + bindableEventsDescription: + 'この Agent を選択できるボットイベントルートの範囲を制限します。通常は既定値のままで問題ありません。', + supportedEvents: 'イベント範囲', supportedEventsDescription: - '例: *、message.received、group.*。Pipeline は message.* 固定です。', + '1 行に 1 つのイベントパターンを指定します。例: *、message.received、group.*。Pipeline は message.* 固定です。', enabled: 'Agent を有効化', enabledDescription: '無効化すると、この Agent はイベントルーティングで選択されません。', @@ -553,10 +554,10 @@ const jaJP = { saveError: '保存に失敗しました:', deleteSuccess: '削除に成功しました', deleteError: '削除に失敗しました:', - deleteConfirmation: 'この Agent 編成を削除してもよろしいですか?', + deleteConfirmation: 'この Agent を削除してもよろしいですか?', dangerZone: '危険ゾーン', dangerZoneDescription: '元に戻せない操作', - deleteAgentAction: 'この Agent 編成を削除', + deleteAgentAction: 'この Agent を削除', deleteAgentHint: '削除すると、紐付けられたイベントは実行できなくなります。', noRunnerMetadata: '現在利用可能な AgentRunner メタデータはありません。', }, diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index bd7aef80b..f359d8964 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -341,8 +341,7 @@ const ruRU = { getBotConfigError: 'Не удалось получить конфигурацию бота: ', saveSuccess: 'Успешно сохранено', saveError: 'Ошибка сохранения: ', - createSuccess: - 'Успешно создано. Пожалуйста, включите или измените привязанный конвейер', + createSuccess: 'Успешно создано. Настройте маршрутизацию событий', createError: 'Ошибка создания: ', deleteSuccess: 'Успешно удалено', deleteError: 'Ошибка удаления: ', @@ -373,6 +372,9 @@ const ruRU = { routingConnection: 'Маршрутизация и подключение', routingConnectionDescription: 'Привяжите конвейер, обрабатывающий сообщения для этого бота', + eventRouting: 'Маршрутизация событий', + eventRoutingDescription: + 'Выберите, какой обработчик будет получать каждое событие этого бота. Логика обработки редактируется на странице Agent. Pipeline поддерживает только события сообщений.', routingRules: 'Правила условной маршрутизации', routingRulesDescription: 'Правила проверяются по порядку; первое совпадение направляет в соответствующий конвейер. При отсутствии совпадений используется конвейер по умолчанию.', @@ -482,13 +484,13 @@ const ruRU = { agents: { title: 'Agent', description: - 'Управляйте оркестровками Agent и Pipeline, привязывая их к событиям бота', - create: 'Создать Agent', - editAgent: 'Редактировать оркестровку Agent', + 'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота', + create: 'Создать обработчик', + editAgent: 'Редактировать Agent', selectFromSidebar: 'Выберите Agent или Pipeline на боковой панели', - agentOrchestration: 'Оркестровка Agent', - agentOrchestrationDescription: - 'Логика обработки сообщений, участников групп, друзей, обратной связи и других событий платформы.', + agentType: 'Agent', + agentTypeDescription: + 'Используйте runner для обработки сообщений, участников групп, друзей, обратной связи и других событий платформы.', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: 'Pipeline', @@ -501,12 +503,13 @@ const ruRU = { basicInfo: 'Основная информация', basicInfoDescription: 'Задайте имя, иконку, описание и статус активации', runnerSettings: 'Runner', - eventCapability: 'Возможности событий', - eventCapabilityDescription: - 'Объявите, к каким событиям может быть привязана эта оркестровка Agent. Один шаблон события в строке; поддерживаются * и namespace.*.', - supportedEvents: 'Поддерживаемые события', + advanced: 'Дополнительно', + bindableEvents: 'Диапазон привязываемых событий', + bindableEventsDescription: + 'Ограничьте, какие маршруты событий бота могут выбирать этот Agent. Обычно достаточно значения по умолчанию.', + supportedEvents: 'Диапазон событий', supportedEventsDescription: - 'Примеры: *, message.received, group.*. Pipeline фиксирован на message.*.', + 'Один шаблон события в строке, например *, message.received, group.*. Pipeline фиксирован на message.*.', enabled: 'Включить Agent', enabledDescription: 'При отключении этот Agent не должен выбираться маршрутизацией событий.', @@ -518,10 +521,10 @@ const ruRU = { saveError: 'Ошибка сохранения: ', deleteSuccess: 'Успешно удалено', deleteError: 'Ошибка удаления: ', - deleteConfirmation: 'Вы уверены, что хотите удалить эту оркестровку Agent?', + deleteConfirmation: 'Вы уверены, что хотите удалить этот Agent?', dangerZone: 'Опасная зона', dangerZoneDescription: 'Необратимые и деструктивные действия', - deleteAgentAction: 'Удалить эту оркестровку Agent', + deleteAgentAction: 'Удалить этот Agent', deleteAgentHint: 'После удаления события, привязанные к ней, больше не смогут выполняться.', noRunnerMetadata: 'Метаданные AgentRunner в данный момент недоступны.', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index d5ae07dd1..119b18b61 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -328,7 +328,7 @@ const thTH = { getBotConfigError: 'ไม่สามารถดึงการกำหนดค่า Bot ได้: ', saveSuccess: 'บันทึกสำเร็จ', saveError: 'บันทึกล้มเหลว: ', - createSuccess: 'สร้างสำเร็จ กรุณาเปิดใช้งานหรือแก้ไข Pipeline ที่ผูกไว้', + createSuccess: 'สร้างสำเร็จ กรุณากำหนดเส้นทางเหตุการณ์', createError: 'สร้างล้มเหลว: ', deleteSuccess: 'ลบสำเร็จ', deleteError: 'ลบล้มเหลว: ', @@ -359,6 +359,9 @@ const thTH = { routingConnection: 'การกำหนดเส้นทางและการเชื่อมต่อ', routingConnectionDescription: 'ผูก Pipeline ที่ประมวลผลข้อความสำหรับ Bot นี้', + eventRouting: 'การกำหนดเส้นทางเหตุการณ์', + eventRoutingDescription: + 'เลือกตัวประมวลผลที่จะจัดการแต่ละเหตุการณ์ที่ Bot นี้ได้รับ แก้ไขตรรกะการประมวลผลในหน้า Agent และ Pipeline รองรับเฉพาะเหตุการณ์ข้อความ', routingRules: 'กฎการกำหนดเส้นทางตามเงื่อนไข', routingRulesDescription: 'กฎจะถูกประเมินตามลำดับ การจับคู่แรกจะกำหนดเส้นทางไปยัง Pipeline ที่เกี่ยวข้อง หากไม่ตรงกันจะใช้ Pipeline เริ่มต้นด้านบน', @@ -467,14 +470,13 @@ const thTH = { }, agents: { title: 'Agent', - description: - 'จัดการการประสาน Agent และ Pipeline แล้วเชื่อมกับเหตุการณ์ของบอท', - create: 'สร้าง Agent', - editAgent: 'แก้ไขการประสาน Agent', + description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท', + create: 'สร้างตัวประมวลผล', + editAgent: 'แก้ไข Agent', selectFromSidebar: 'เลือก Agent หรือ Pipeline จากแถบด้านข้าง', - agentOrchestration: 'การประสาน Agent', - agentOrchestrationDescription: - 'ตรรกะการประมวลผลที่เน้นเหตุการณ์สำหรับข้อความ สมาชิกกลุ่ม เพื่อน ฟีดแบ็ก และเหตุการณ์แพลตฟอร์มอื่นๆ', + agentType: 'Agent', + agentTypeDescription: + 'ใช้ runner เพื่อประมวลผลข้อความ สมาชิกกลุ่ม เพื่อน ฟีดแบ็ก และเหตุการณ์แพลตฟอร์มอื่นๆ', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: 'Pipeline', @@ -487,12 +489,13 @@ const thTH = { basicInfo: 'ข้อมูลพื้นฐาน', basicInfoDescription: 'ตั้งชื่อ ไอคอน คำอธิบาย และสถานะการเปิดใช้งาน', runnerSettings: 'Runner', - eventCapability: 'ความสามารถด้านเหตุการณ์', - eventCapabilityDescription: - 'ประกาศว่าการประสาน Agent นี้สามารถเชื่อมกับเหตุการณ์ใดได้บ้าง หนึ่งรูปแบบเหตุการณ์ต่อบรรทัด รองรับ * และ namespace.*', - supportedEvents: 'เหตุการณ์ที่รองรับ', + advanced: 'ขั้นสูง', + bindableEvents: 'ช่วงเหตุการณ์ที่ผูกได้', + bindableEventsDescription: + 'จำกัดว่าเส้นทางเหตุการณ์ของบอทใดสามารถเลือก Agent นี้ได้ ค่าเริ่มต้นเหมาะกับกรณีส่วนใหญ่', + supportedEvents: 'ช่วงเหตุการณ์', supportedEventsDescription: - 'ตัวอย่าง: *, message.received, group.* Pipeline ถูกกำหนดไว้ที่ message.*', + 'หนึ่งรูปแบบเหตุการณ์ต่อบรรทัด เช่น *, message.received, group.* Pipeline ถูกกำหนดไว้ที่ message.*', enabled: 'เปิดใช้งาน Agent', enabledDescription: 'เมื่อปิดใช้งาน Agent นี้จะไม่ถูกเลือกโดยการกำหนดเส้นทางเหตุการณ์', @@ -504,10 +507,10 @@ const thTH = { saveError: 'บันทึกล้มเหลว: ', deleteSuccess: 'ลบสำเร็จ', deleteError: 'ลบล้มเหลว: ', - deleteConfirmation: 'คุณแน่ใจหรือว่าต้องการลบการประสาน Agent นี้?', + deleteConfirmation: 'คุณแน่ใจหรือว่าต้องการลบ Agent นี้?', dangerZone: 'โซนอันตราย', dangerZoneDescription: 'การดำเนินการที่ไม่สามารถย้อนกลับและทำลายข้อมูล', - deleteAgentAction: 'ลบการประสาน Agent นี้', + deleteAgentAction: 'ลบ Agent นี้', deleteAgentHint: 'เมื่อลบแล้ว เหตุการณ์ที่เชื่อมกับมันจะไม่สามารถดำเนินการต่อได้', noRunnerMetadata: 'ขณะนี้ไม่มีข้อมูลเมตา AgentRunner ที่พร้อมใช้งาน', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index 4f90b89fe..12481ed26 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -337,8 +337,7 @@ const viVN = { getBotConfigError: 'Lấy cấu hình Bot thất bại: ', saveSuccess: 'Lưu thành công', saveError: 'Lưu thất bại: ', - createSuccess: - 'Tạo thành công. Vui lòng bật hoặc sửa đổi Pipeline đã liên kết', + createSuccess: 'Tạo thành công. Vui lòng cấu hình định tuyến sự kiện', createError: 'Tạo thất bại: ', deleteSuccess: 'Xóa thành công', deleteError: 'Xóa thất bại: ', @@ -369,6 +368,9 @@ const viVN = { routingConnection: 'Định tuyến & Kết nối', routingConnectionDescription: 'Liên kết Pipeline xử lý tin nhắn cho Bot này', + eventRouting: 'Định tuyến sự kiện', + eventRoutingDescription: + 'Chọn bộ xử lý sẽ nhận từng sự kiện của Bot này. Chỉnh sửa logic xử lý trong trang Agent. Pipeline chỉ hỗ trợ sự kiện tin nhắn.', routingRules: 'Quy tắc định tuyến có điều kiện', routingRulesDescription: 'Các quy tắc được đánh giá theo thứ tự; kết quả khớp đầu tiên sẽ định tuyến đến pipeline tương ứng. Nếu không khớp, pipeline mặc định ở trên sẽ được sử dụng.', @@ -478,13 +480,13 @@ const viVN = { agents: { title: 'Agent', description: - 'Quản lý dàn dựng Agent và Pipeline, sau đó gắn chúng vào sự kiện của bot', - create: 'Tạo Agent', - editAgent: 'Chỉnh sửa dàn dựng Agent', + 'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot', + create: 'Tạo bộ xử lý', + editAgent: 'Chỉnh sửa Agent', selectFromSidebar: 'Chọn một Agent hoặc Pipeline từ thanh bên', - agentOrchestration: 'Dàn dựng Agent', - agentOrchestrationDescription: - 'Logic xử lý hướng sự kiện cho tin nhắn, thành viên nhóm, bạn bè, phản hồi và các sự kiện nền tảng khác.', + agentType: 'Agent', + agentTypeDescription: + 'Dùng runner để xử lý tin nhắn, thành viên nhóm, bạn bè, phản hồi và các sự kiện nền tảng khác.', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: 'Pipeline', @@ -497,12 +499,13 @@ const viVN = { basicInfo: 'Thông tin cơ bản', basicInfoDescription: 'Đặt tên, biểu tượng, mô tả và trạng thái kích hoạt', runnerSettings: 'Runner', - eventCapability: 'Khả năng sự kiện', - eventCapabilityDescription: - 'Khai báo những sự kiện mà dàn dựng Agent này có thể được gắn vào. Mỗi dòng một mẫu sự kiện; hỗ trợ * và namespace.*.', - supportedEvents: 'Sự kiện được hỗ trợ', + advanced: 'Nâng cao', + bindableEvents: 'Phạm vi sự kiện có thể gắn', + bindableEventsDescription: + 'Giới hạn những tuyến sự kiện bot có thể chọn Agent này. Mặc định phù hợp với hầu hết trường hợp.', + supportedEvents: 'Phạm vi sự kiện', supportedEventsDescription: - 'Ví dụ: *, message.received, group.*. Pipeline cố định ở message.*.', + 'Mỗi dòng một mẫu sự kiện, ví dụ *, message.received, group.*. Pipeline cố định ở message.*.', enabled: 'Kích hoạt Agent', enabledDescription: 'Khi bị tắt, Agent này sẽ không được định tuyến sự kiện chọn.', @@ -514,10 +517,10 @@ const viVN = { saveError: 'Lưu thất bại: ', deleteSuccess: 'Xóa thành công', deleteError: 'Xóa thất bại: ', - deleteConfirmation: 'Bạn có chắc muốn xóa dàn dựng Agent này không?', + deleteConfirmation: 'Bạn có chắc muốn xóa Agent này không?', dangerZone: 'Vùng nguy hiểm', dangerZoneDescription: 'Hành động không thể hoàn tác và mang tính phá hủy', - deleteAgentAction: 'Xóa dàn dựng Agent này', + deleteAgentAction: 'Xóa Agent này', deleteAgentHint: 'Sau khi xóa, các sự kiện đã gắn vào nó sẽ không thể thực thi được nữa.', noRunnerMetadata: 'Hiện chưa có siêu dữ liệu AgentRunner khả dụng.', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 6d3a1e1ee..4f32e36ba 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -317,7 +317,7 @@ const zhHans = { getBotConfigError: '获取机器人配置失败:', saveSuccess: '保存成功', saveError: '保存失败:', - createSuccess: '创建成功 请启用或修改绑定流水线', + createSuccess: '创建成功,请配置事件路由', createError: '创建失败:', deleteSuccess: '删除成功', deleteError: '删除失败:', @@ -347,25 +347,25 @@ const zhHans = { basicInfoDescription: '设置机器人名称和描述', routingConnection: '路由与连接', routingConnectionDescription: '绑定处理此机器人消息的流水线', - eventOrchestration: '事件编排', - eventOrchestrationDescription: - '为此机器人不同事件绑定不同处理逻辑。Pipeline 仅支持消息事件。', - eventBindings: '事件绑定', - addEventBinding: '添加事件绑定', + eventRouting: '事件路由', + eventRoutingDescription: + '选择此机器人收到不同事件时交给哪个处理器。具体处理逻辑在 Agent 页面编辑,Pipeline 仅支持消息事件。', + eventBindings: '事件路由', + addEventBinding: '添加路由', eventPattern: '事件', eventPatternPlaceholder: '选择事件', targetType: '目标类型', - target: '处理逻辑', - targetAgent: 'Agent 编排', + target: '处理器', + targetAgent: 'Agent', targetPipeline: 'Pipeline', targetDiscard: '丢弃', - selectTarget: '选择处理逻辑', - searchTarget: '搜索处理逻辑…', - noTargetFound: '未找到匹配项', + selectTarget: '选择处理器', + searchTarget: '搜索处理器…', + noTargetFound: '未找到兼容处理器', priority: '优先级', enabled: '启用', eventBindingDescriptionPlaceholder: '规则说明', - noEventBindings: '暂无事件绑定', + noEventBindings: '暂无事件路由', unsupportedPipelineEvent: 'Pipeline 仅可用于 message.* 事件', disable: '禁用', enable: '启用', @@ -509,13 +509,12 @@ const zhHans = { }, agents: { title: 'Agent', - description: '管理 Agent 编排与 Pipeline,并将它们绑定到机器人事件', - create: '创建 Agent', - editAgent: '编辑 Agent 编排', + description: '创建可复用的处理器,并在机器人事件路由中使用', + create: '创建处理器', + editAgent: '编辑 Agent', selectFromSidebar: '从侧边栏选择一个 Agent 或 Pipeline', - agentOrchestration: 'Agent 编排', - agentOrchestrationDescription: - '面向平台事件的处理逻辑,可用于消息、群成员、好友、反馈等事件。', + agentType: 'Agent', + agentTypeDescription: '通过运行器处理消息、群成员、好友、反馈等平台事件。', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: '流水线', @@ -528,12 +527,13 @@ const zhHans = { basicInfo: '基础信息', basicInfoDescription: '设置名称、图标、描述和启用状态', runnerSettings: '运行器', - eventCapability: '事件能力', - eventCapabilityDescription: - '声明此 Agent 编排可被绑定到哪些事件。每行一个事件模式,支持 * 与 namespace.*。', - supportedEvents: '支持的事件', + advanced: '高级', + bindableEvents: '可绑定事件范围', + bindableEventsDescription: + '限制此 Agent 可被机器人事件路由选择的事件范围。通常保持默认即可。', + supportedEvents: '事件范围', supportedEventsDescription: - '例如 *、message.received、group.*。Pipeline 固定仅支持 message.*。', + '每行一个事件模式,例如 *、message.received、group.*。Pipeline 固定仅支持 message.*。', enabled: '启用 Agent', enabledDescription: '禁用后,此 Agent 不应被事件路由选中。', nameRequired: '名称不能为空', @@ -544,10 +544,10 @@ const zhHans = { saveError: '保存失败:', deleteSuccess: '删除成功', deleteError: '删除失败:', - deleteConfirmation: '你确定要删除这个 Agent 编排吗?', + deleteConfirmation: '你确定要删除这个 Agent 吗?', dangerZone: '危险区域', dangerZoneDescription: '不可逆的操作', - deleteAgentAction: '删除此 Agent 编排', + deleteAgentAction: '删除此 Agent', deleteAgentHint: '删除后,绑定到它的事件将无法继续执行。', noRunnerMetadata: '当前没有可用的 AgentRunner 元数据。', }, diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index e136c0036..423faedf6 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -317,7 +317,7 @@ const zhHant = { getBotConfigError: '取得機器人設定失敗:', saveSuccess: '儲存成功', saveError: '儲存失敗:', - createSuccess: '建立成功 請啟用或修改綁定流程線', + createSuccess: '建立成功,請設定事件路由', createError: '建立失敗:', deleteSuccess: '刪除成功', deleteError: '刪除失敗:', @@ -347,6 +347,9 @@ const zhHant = { basicInfoDescription: '設定機器人名稱和描述', routingConnection: '路由與連接', routingConnectionDescription: '綁定處理此機器人訊息的流程線', + eventRouting: '事件路由', + eventRoutingDescription: + '選擇此機器人收到不同事件時交給哪個處理器。具體處理邏輯在 Agent 頁面編輯,Pipeline 僅支援訊息事件。', routingRules: '條件路由規則', routingRulesDescription: '按順序匹配,命中第一條規則後路由到對應流程線;都不匹配時使用上方預設流程線', @@ -452,13 +455,12 @@ const zhHant = { }, agents: { title: 'Agent', - description: '管理 Agent 編排與 Pipeline,並將它們綁定到機器人事件', - create: '建立 Agent', - editAgent: '編輯 Agent 編排', + description: '建立可重用的處理器,並在機器人事件路由中使用', + create: '建立處理器', + editAgent: '編輯 Agent', selectFromSidebar: '從側邊欄選擇一個 Agent 或 Pipeline', - agentOrchestration: 'Agent 編排', - agentOrchestrationDescription: - '面向平台事件的處理邏輯,可用於訊息、群成員、好友、回饋等事件。', + agentType: 'Agent', + agentTypeDescription: '透過執行器處理訊息、群成員、好友、回饋等平台事件。', pipelineType: 'Pipeline', kindBadgeAgent: 'Agent', kindBadgePipeline: '流水線', @@ -471,12 +473,13 @@ const zhHant = { basicInfo: '基本資訊', basicInfoDescription: '設定名稱、圖示、描述和啟用狀態', runnerSettings: '執行器', - eventCapability: '事件能力', - eventCapabilityDescription: - '宣告此 Agent 編排可被綁定到哪些事件。每行一個事件模式,支援 * 與 namespace.*。', - supportedEvents: '支援的事件', + advanced: '進階', + bindableEvents: '可綁定事件範圍', + bindableEventsDescription: + '限制此 Agent 可被機器人事件路由選擇的事件範圍。通常保持預設即可。', + supportedEvents: '事件範圍', supportedEventsDescription: - '例如 *、message.received、group.*。Pipeline 固定僅支援 message.*。', + '每行一個事件模式,例如 *、message.received、group.*。Pipeline 固定僅支援 message.*。', enabled: '啟用 Agent', enabledDescription: '停用後,此 Agent 不應被事件路由選中。', nameRequired: '名稱不能為空', @@ -487,10 +490,10 @@ const zhHant = { saveError: '儲存失敗:', deleteSuccess: '刪除成功', deleteError: '刪除失敗:', - deleteConfirmation: '你確定要刪除這個 Agent 編排嗎?', + deleteConfirmation: '你確定要刪除這個 Agent 嗎?', dangerZone: '危險區域', dangerZoneDescription: '不可逆的操作', - deleteAgentAction: '刪除此 Agent 編排', + deleteAgentAction: '刪除此 Agent', deleteAgentHint: '刪除後,綁定到它的事件將無法繼續執行。', noRunnerMetadata: '目前沒有可用的 AgentRunner 中繼資料。', },