feat(agent): add event orchestration surface

This commit is contained in:
Junyan Qin
2026-06-23 23:23:09 +08:00
parent d7b97741e3
commit ed3598f8ac
35 changed files with 2983 additions and 142 deletions
+692 -118
View File
@@ -3,7 +3,10 @@ from __future__ import annotations
import asyncio
import json
import re
import time
import traceback
import typing
import uuid
import sqlalchemy
from ..core import app, entities as core_entities, taskmgr
@@ -14,14 +17,30 @@ 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,
AgentEventEnvelope,
BindingScope,
DeliveryPolicy,
ResourcePolicy,
StatePolicy,
)
from .logger import EventLogger
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.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 (
ActorContext,
SubjectContext,
RawEventRef,
)
from langbot_plugin.api.entities.builtin.agent_runner.input import AgentInput
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
class RuntimeBot:
@@ -77,6 +96,7 @@ class RuntimeBot:
PIPELINE_DISCARD = '__discard__'
PIPELINE_DISCARD_DISPLAY_NAME = 'Discarded'
EVENT_DATA_MAX_STRING_BYTES = 512
@staticmethod
def _eba_event_to_plugin_event(event: platform_events.EBAEvent) -> plugin_events.BaseEventModel | None:
@@ -103,6 +123,572 @@ class RuntimeBot:
return None
@staticmethod
def _match_event_pattern(event_type: str, pattern: str) -> bool:
if not event_type or not pattern:
return False
if pattern == '*':
return True
if pattern.endswith('.*'):
return event_type.startswith(f'{pattern[:-2]}.')
return event_type == pattern
@classmethod
def _is_message_event_type(cls, event_type: str) -> bool:
return cls._match_event_pattern(event_type, 'message.*')
@classmethod
def _agent_supports_event_type(
cls,
supported_patterns: list[str] | None,
event_type: str,
) -> bool:
return any(cls._match_event_pattern(event_type, pattern) for pattern in (supported_patterns or ['*']))
@staticmethod
def _get_nested_value(data: dict[str, typing.Any], path: str) -> typing.Any:
current: typing.Any = data
for key in path.split('.'):
if isinstance(current, dict):
current = current.get(key)
else:
current = getattr(current, key, None)
if current is None:
return None
return current
@classmethod
def _match_event_filter(
cls,
event_data: dict[str, typing.Any],
event_filter: dict[str, typing.Any],
) -> bool:
field = str(event_filter.get('field') or event_filter.get('path') or '').strip()
if not field:
return True
operator = str(event_filter.get('operator') or 'eq')
expected = event_filter.get('value')
actual = cls._get_nested_value(event_data, field)
if operator == 'eq':
return actual == expected
if operator == 'neq':
return actual != expected
if operator == 'contains':
if isinstance(actual, (list, tuple, set)):
return expected in actual
return str(expected) in str(actual or '')
if operator == 'not_contains':
if isinstance(actual, (list, tuple, set)):
return expected not in actual
return str(expected) not in str(actual or '')
if operator == 'starts_with':
return str(actual or '').startswith(str(expected))
if operator == 'regex':
try:
return bool(re.search(str(expected), str(actual or '')))
except re.error:
return False
return False
@classmethod
def _match_event_filters(
cls,
event: platform_events.EBAEvent,
filters: typing.Any,
) -> bool:
if not filters:
return True
if not isinstance(filters, list):
return False
event_data = cls._safe_model_dump(event)
return all(
cls._match_event_filter(event_data, event_filter)
for event_filter in filters
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."""
raw_bindings = self.bot_entity.event_bindings or []
if isinstance(raw_bindings, str):
try:
raw_bindings = json.loads(raw_bindings)
except json.JSONDecodeError:
raw_bindings = []
if not isinstance(raw_bindings, list):
return None
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):
continue
event_pattern = str(binding.get('event_pattern') or '')
if not self._match_event_pattern(event_type, event_pattern):
continue
if not self._match_event_filters(event, binding.get('filters')):
continue
priority = int(binding.get('priority') or 0)
order = int(binding.get('order', index))
matched.append((priority, -order, binding))
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:
return {}
if hasattr(model, 'model_dump'):
try:
return model.model_dump(mode='json')
except TypeError:
try:
return model.model_dump()
except Exception:
return {}
except Exception:
return {}
if isinstance(model, dict):
return model
return {}
@classmethod
def _compact_event_data(cls, event: platform_events.EBAEvent) -> dict[str, typing.Any]:
raw_event_data = cls._safe_model_dump(event)
compact: dict[str, typing.Any] = {}
for key, value in raw_event_data.items():
if key == 'source_platform_object' or key.startswith('_'):
continue
if value is None or isinstance(value, (bool, int, float)):
compact[key] = value
continue
if isinstance(value, str):
if len(value.encode('utf-8')) <= cls.EVENT_DATA_MAX_STRING_BYTES:
compact[key] = value
continue
if isinstance(value, (list, dict)):
try:
encoded = json.dumps(value, ensure_ascii=False)
except (TypeError, ValueError):
continue
if len(encoded.encode('utf-8')) <= cls.EVENT_DATA_MAX_STRING_BYTES:
compact[key] = value
return compact
@staticmethod
def _get_entity_id(entity: typing.Any) -> str | None:
entity_id = getattr(entity, 'id', None)
if entity_id is None and isinstance(entity, dict):
entity_id = entity.get('id')
if entity_id is None or entity_id == '':
return None
return str(entity_id)
@staticmethod
def _get_entity_name(entity: typing.Any) -> str | None:
if entity is None:
return None
if hasattr(entity, 'get_name'):
try:
name = entity.get_name()
if name:
return str(name)
except Exception:
pass
for attr in ('nickname', 'member_name', 'name', 'display_name'):
value = getattr(entity, attr, None)
if value:
return str(value)
if isinstance(entity, dict):
for attr in ('nickname', 'member_name', 'name', 'display_name'):
value = entity.get(attr)
if value:
return str(value)
return None
@classmethod
def _infer_actor_context(cls, event: platform_events.EBAEvent) -> ActorContext | None:
actor = getattr(event, 'sender', None) or getattr(event, 'member', None) or getattr(event, 'user', None)
actor_id = cls._get_entity_id(actor)
actor_name = cls._get_entity_name(actor)
if actor_id is None:
user_id = getattr(event, 'user_id', None)
if user_id:
actor_id = str(user_id)
if actor_id is None:
return None
return ActorContext(
actor_type='user',
actor_id=actor_id,
actor_name=actor_name,
metadata={},
)
@classmethod
def _infer_subject_context(cls, event: platform_events.EBAEvent) -> SubjectContext:
group = getattr(event, 'group', None)
if group is not None:
group_id = cls._get_entity_id(group)
return SubjectContext(
subject_type='group',
subject_id=group_id,
data={'group_name': cls._get_entity_name(group)},
)
message_id = getattr(event, 'message_id', None)
if message_id:
return SubjectContext(
subject_type='message',
subject_id=str(message_id),
data={},
)
feedback_id = getattr(event, 'feedback_id', None)
if feedback_id:
return SubjectContext(
subject_type='feedback',
subject_id=str(feedback_id),
data={'message_id': getattr(event, 'message_id', None)},
)
action = getattr(event, 'action', None)
if action:
return SubjectContext(
subject_type='platform_action',
subject_id=str(action),
data={},
)
return SubjectContext(
subject_type='event',
subject_id=getattr(event, 'type', None),
data={},
)
@staticmethod
def _session_to_reply_target(session_id: str | None) -> tuple[str | None, str | None]:
if not session_id or '_' not in session_id:
return None, None
target_type, target_id = session_id.split('_', 1)
if target_type == 'person':
target_type = 'person'
elif target_type == 'group':
target_type = 'group'
else:
return None, None
return target_type, target_id or None
@classmethod
def _infer_reply_target(
cls,
event: platform_events.EBAEvent,
) -> tuple[str | None, str | None, dict[str, typing.Any]]:
metadata: dict[str, typing.Any] = {}
group = getattr(event, 'group', None)
group_id = cls._get_entity_id(group)
if group_id:
metadata['group_id'] = group_id
return 'group', group_id, metadata
chat_id = getattr(event, 'chat_id', None)
chat_type = getattr(event, 'chat_type', None)
chat_type_value = getattr(chat_type, 'value', chat_type)
if chat_id:
metadata['chat_id'] = str(chat_id)
if chat_type_value == 'group':
return 'group', str(chat_id), metadata
return 'person', str(chat_id), metadata
session_target_type, session_target_id = cls._session_to_reply_target(getattr(event, 'session_id', None))
if session_target_type and session_target_id:
return session_target_type, session_target_id, metadata
raw_data = getattr(event, 'data', None)
if isinstance(raw_data, dict):
target_type = raw_data.get('target_type') or raw_data.get('chat_type')
target_id = (
raw_data.get('target_id')
or raw_data.get('chat_id')
or raw_data.get('group_id')
or raw_data.get('user_id')
)
if target_type and target_id:
return str(target_type), str(target_id), metadata
return None, None, metadata
@classmethod
def _build_agent_input(cls, event: platform_events.EBAEvent) -> AgentInput:
text = None
contents: list[dict[str, typing.Any]] = []
message_chain = getattr(event, 'message_chain', None)
if message_chain:
text_parts: list[str] = []
try:
for component in message_chain:
if isinstance(component, platform_message.Plain):
text_parts.append(component.text)
elif isinstance(component, platform_message.Image):
if component.url:
contents.append({'type': 'image_url', 'image_url': {'url': component.url}})
elif component.base64:
contents.append({'type': 'image_base64', 'image_base64': component.base64})
except TypeError:
text_parts.append(str(message_chain))
text = ''.join(text_parts) or str(message_chain)
if text is None:
feedback_content = getattr(event, 'feedback_content', None)
if feedback_content:
text = str(feedback_content)
elif getattr(event, 'action', None):
text = str(getattr(event, 'action'))
else:
text = str(getattr(event, 'type', 'event'))
if text:
contents.insert(0, {'type': 'text', 'text': text})
return AgentInput(
text=text,
contents=contents,
attachments=[],
)
def _eba_event_to_agent_envelope(
self,
event: platform_events.EBAEvent,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
) -> AgentEventEnvelope:
event_type = getattr(event, 'type', None) or event.__class__.__name__
event_time = getattr(event, 'timestamp', None) or time.time()
event_id = (
getattr(event, 'message_id', None) or getattr(event, 'feedback_id', None) or f'{event_type}:{uuid.uuid4()}'
)
target_type, target_id, target_metadata = self._infer_reply_target(event)
conversation_id = None
if target_type and target_id:
conversation_id = f'{target_type}_{target_id}'
elif getattr(event, 'session_id', None):
conversation_id = str(getattr(event, 'session_id'))
return AgentEventEnvelope(
event_id=f'platform:{self.bot_entity.uuid}:{event_id}',
event_type=event_type,
event_time=int(event_time) if isinstance(event_time, (int, float)) else None,
source='platform',
source_event_type=event_type,
bot_id=self.bot_entity.uuid,
workspace_id=None,
conversation_id=conversation_id,
thread_id=None,
actor=self._infer_actor_context(event),
subject=self._infer_subject_context(event),
input=self._build_agent_input(event),
delivery=DeliveryContext(
surface='platform',
reply_target={
'target_type': target_type,
'target_id': target_id,
'message_id': getattr(event, 'message_id', None),
**target_metadata,
},
supports_streaming=False,
supports_edit=False,
supports_reaction=False,
platform_capabilities={
'adapter': adapter.__class__.__name__,
'event_type': event_type,
},
),
raw_ref=RawEventRef(ref_id=str(event_id), storage_key=None),
data=self._compact_event_data(event),
)
@staticmethod
def _agent_product_to_binding(
agent: dict[str, typing.Any],
event_binding: dict[str, typing.Any],
event_type: str,
bot_uuid: str,
) -> AgentBinding | None:
config = agent.get('config') if isinstance(agent, dict) else None
if not isinstance(config, dict):
return None
runner = config.get('runner')
runner_id = None
if isinstance(runner, dict):
runner_id = runner.get('id')
runner_id = runner_id or agent.get('component_ref')
if not runner_id:
return None
runner_config_map = config.get('runner_config')
runner_config = {}
if isinstance(runner_config_map, dict):
runner_config = runner_config_map.get(runner_id) or {}
return AgentBinding(
binding_id=f'bot:{bot_uuid}:{event_binding.get("id") or uuid.uuid4()}',
scope=BindingScope(scope_type='bot', scope_id=bot_uuid),
event_types=[event_type],
runner_id=runner_id,
runner_config=runner_config,
resource_policy=ResourcePolicy(),
state_policy=StatePolicy(state_scopes=['conversation', 'actor', 'subject', 'runner']),
delivery_policy=DeliveryPolicy(enable_streaming=False, enable_reply=True),
enabled=True,
agent_id=agent.get('uuid'),
)
@staticmethod
def _provider_content_to_text(content: typing.Any) -> str:
if content is None:
return ''
if isinstance(content, str):
return content
if isinstance(content, list):
parts: list[str] = []
for item in content:
item_data = item.model_dump(mode='json') if hasattr(item, 'model_dump') else item
if isinstance(item_data, dict):
if item_data.get('type') == 'text' and item_data.get('text') is not None:
parts.append(str(item_data.get('text')))
elif item_data.get('text') is not None:
parts.append(str(item_data.get('text')))
elif item_data is not None:
parts.append(str(item_data))
return ''.join(parts)
return str(content)
@classmethod
def _provider_output_to_text(cls, result: provider_message.Message | provider_message.MessageChunk) -> str:
if getattr(result, 'all_content', None):
return str(getattr(result, 'all_content'))
return cls._provider_content_to_text(getattr(result, 'content', None))
async def _deliver_agent_outputs(
self,
envelope: AgentEventEnvelope,
outputs: list[provider_message.Message | provider_message.MessageChunk],
) -> None:
if not outputs or not envelope.delivery.reply_target:
return
reply_target = envelope.delivery.reply_target
target_type = reply_target.get('target_type')
target_id = reply_target.get('target_id')
if not target_type or not target_id:
return
final_text = ''
for output in outputs:
output_text = self._provider_output_to_text(output)
if isinstance(output, provider_message.Message):
final_text = output_text or final_text
elif output_text:
final_text = output_text
if not final_text:
return
await self.adapter.send_message(
str(target_type),
str(target_id),
platform_message.MessageChain([platform_message.Plain(text=final_text)]),
)
async def _dispatch_eba_event_to_agent(
self,
event: platform_events.EBAEvent,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
) -> None:
event_type = getattr(event, 'type', None) or event.__class__.__name__
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)
return
target_type = event_binding.get('target_type')
if target_type == 'discard':
if isinstance(event, platform_events.MessageReceivedEvent):
await self._dispatch_eba_message_to_pipeline(
event,
adapter,
pipeline_uuid=self.PIPELINE_DISCARD,
routed_by_event_binding=True,
)
return
await self.logger.info(f'EBA event {event_type} discarded by event binding')
return
if target_type == 'pipeline':
if not self._is_message_event_type(event_type):
await self.logger.warning(f'EBA event {event_type} ignored Pipeline target for non-message event')
return
await self._dispatch_eba_message_to_pipeline(
event,
adapter,
pipeline_uuid=event_binding.get('target_uuid'),
routed_by_event_binding=True,
)
return
if target_type != 'agent':
await self.logger.warning(f'EBA event {event_type} ignored unsupported target type {target_type}')
return
target_uuid = event_binding.get('target_uuid')
agent = await self.ap.agent_service.get_agent(target_uuid)
if not agent or agent.get('kind') != 'agent':
await self.logger.warning(f'EBA event {event_type} target agent not found: {target_uuid}')
return
if not agent.get('enabled', True):
await self.logger.info(f'EBA event {event_type} target agent disabled: {target_uuid}')
return
if not self._agent_supports_event_type(agent.get('supported_event_patterns'), event_type):
await self.logger.info(f'EBA event {event_type} target agent does not support this event: {target_uuid}')
return
binding = self._agent_product_to_binding(agent, event_binding, event_type, self.bot_entity.uuid)
if binding is None:
await self.logger.warning(f'EBA event {event_type} target agent has no runner: {target_uuid}')
return
envelope = self._eba_event_to_agent_envelope(event, adapter)
outputs: list[provider_message.Message | provider_message.MessageChunk] = []
try:
async for output in self.ap.agent_run_orchestrator.run(envelope, binding):
outputs.append(output)
except Exception:
await self.logger.error(f'Failed to run Agent for EBA event {event_type}: {traceback.format_exc()}')
return
try:
await self._deliver_agent_outputs(envelope, outputs)
except Exception:
await self.logger.error(
f'Failed to deliver Agent output for EBA event {event_type}: {traceback.format_exc()}'
)
def resolve_pipeline_uuid(
self,
launcher_type: str,
@@ -222,128 +808,115 @@ class RuntimeBot:
except Exception as e:
await self.logger.error(f'Failed to record discarded message: {e}')
async def _handle_legacy_message_event(
self,
event: platform_events.FriendMessage | platform_events.GroupMessage,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
pipeline_uuid_override: str | None = None,
routed_by_event_binding: bool = False,
) -> None:
is_group_message = isinstance(event, platform_events.GroupMessage)
launcher_kind = 'group' if is_group_message else 'person'
launcher_type = (
provider_session.LauncherTypes.GROUP if is_group_message else provider_session.LauncherTypes.PERSON
)
launcher_id = event.group.id if is_group_message else event.sender.id
sender_id = event.sender.id
image_components = [
component for component in event.message_chain if isinstance(component, platform_message.Image)
]
await self.logger.info(
f'{event.message_chain}',
images=image_components,
message_session_id=f'{launcher_kind}_{launcher_id}',
)
skip_pipeline = False
if hasattr(self.ap, 'webhook_pusher') and self.ap.webhook_pusher:
if is_group_message:
skip_pipeline = await self.ap.webhook_pusher.push_group_message(
event, self.bot_entity.uuid, adapter.__class__.__name__
)
else:
skip_pipeline = await self.ap.webhook_pusher.push_person_message(
event, self.bot_entity.uuid, adapter.__class__.__name__
)
if skip_pipeline:
await self.logger.info(f'Pipeline skipped for {launcher_kind} message due to webhook response')
return
if hasattr(adapter, 'get_launcher_id'):
custom_launcher_id = adapter.get_launcher_id(event)
if custom_launcher_id:
launcher_id = custom_launcher_id
if pipeline_uuid_override is None:
message_text = str(event.message_chain)
element_types = [comp.type for comp in event.message_chain]
pipeline_uuid, routed_by_rule = self.resolve_pipeline_uuid(
launcher_kind, launcher_id, message_text, element_types
)
else:
pipeline_uuid = pipeline_uuid_override
routed_by_rule = routed_by_event_binding
if pipeline_uuid == self.PIPELINE_DISCARD:
await self.logger.info(f'{launcher_kind.title()} message discarded by routing rule')
await self._record_discarded_message(
launcher_type,
launcher_id,
sender_id,
event,
event.message_chain,
)
return
await self.ap.msg_aggregator.add_message(
bot_uuid=self.bot_entity.uuid,
launcher_type=launcher_type,
launcher_id=launcher_id,
sender_id=sender_id,
message_event=event,
message_chain=event.message_chain,
adapter=adapter,
pipeline_uuid=pipeline_uuid,
routed_by_rule=routed_by_rule,
)
async def _dispatch_eba_message_to_pipeline(
self,
event: platform_events.EBAEvent,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
pipeline_uuid: str | None = None,
routed_by_event_binding: bool = False,
) -> None:
if not isinstance(event, platform_events.MessageReceivedEvent):
event_type = getattr(event, 'type', None) or event.__class__.__name__
await self.logger.warning(f'EBA event {event_type} cannot be dispatched to legacy Pipeline')
return
await self._handle_legacy_message_event(
event.to_legacy_event(),
adapter,
pipeline_uuid_override=pipeline_uuid,
routed_by_event_binding=routed_by_event_binding,
)
async def initialize(self):
async def on_friend_message(
event: platform_events.FriendMessage,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
):
image_components = [
component for component in event.message_chain if isinstance(component, platform_message.Image)
]
await self.logger.info(
f'{event.message_chain}',
images=image_components,
message_session_id=f'person_{event.sender.id}',
)
# Push to webhooks and check if pipeline should be skipped
skip_pipeline = False
if hasattr(self.ap, 'webhook_pusher') and self.ap.webhook_pusher:
skip_pipeline = await self.ap.webhook_pusher.push_person_message(
event, self.bot_entity.uuid, adapter.__class__.__name__
)
# Only add to query pool if no webhook requested to skip pipeline
if not skip_pipeline:
launcher_id = event.sender.id
if hasattr(adapter, 'get_launcher_id'):
custom_launcher_id = adapter.get_launcher_id(event)
if custom_launcher_id:
launcher_id = custom_launcher_id
message_text = str(event.message_chain)
element_types = [comp.type for comp in event.message_chain]
pipeline_uuid, routed_by_rule = self.resolve_pipeline_uuid(
'person', launcher_id, message_text, element_types
)
if pipeline_uuid == self.PIPELINE_DISCARD:
await self.logger.info('Person message discarded by routing rule')
await self._record_discarded_message(
provider_session.LauncherTypes.PERSON,
launcher_id,
event.sender.id,
event,
event.message_chain,
)
return
await self.ap.msg_aggregator.add_message(
bot_uuid=self.bot_entity.uuid,
launcher_type=provider_session.LauncherTypes.PERSON,
launcher_id=launcher_id,
sender_id=event.sender.id,
message_event=event,
message_chain=event.message_chain,
adapter=adapter,
pipeline_uuid=pipeline_uuid,
routed_by_rule=routed_by_rule,
)
else:
await self.logger.info('Pipeline skipped for person message due to webhook response')
await self._handle_legacy_message_event(event, adapter)
async def on_group_message(
event: platform_events.GroupMessage,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
):
image_components = [
component for component in event.message_chain if isinstance(component, platform_message.Image)
]
await self.logger.info(
f'{event.message_chain}',
images=image_components,
message_session_id=f'group_{event.group.id}',
)
# Push to webhooks and check if pipeline should be skipped
skip_pipeline = False
if hasattr(self.ap, 'webhook_pusher') and self.ap.webhook_pusher:
skip_pipeline = await self.ap.webhook_pusher.push_group_message(
event, self.bot_entity.uuid, adapter.__class__.__name__
)
# Only add to query pool if no webhook requested to skip pipeline
if not skip_pipeline:
launcher_id = event.group.id
if hasattr(adapter, 'get_launcher_id'):
custom_launcher_id = adapter.get_launcher_id(event)
if custom_launcher_id:
launcher_id = custom_launcher_id
message_text = str(event.message_chain)
element_types = [comp.type for comp in event.message_chain]
pipeline_uuid, routed_by_rule = self.resolve_pipeline_uuid(
'group', launcher_id, message_text, element_types
)
if pipeline_uuid == self.PIPELINE_DISCARD:
await self.logger.info('Group message discarded by routing rule')
await self._record_discarded_message(
provider_session.LauncherTypes.GROUP,
launcher_id,
event.sender.id,
event,
event.message_chain,
)
return
await self.ap.msg_aggregator.add_message(
bot_uuid=self.bot_entity.uuid,
launcher_type=provider_session.LauncherTypes.GROUP,
launcher_id=launcher_id,
sender_id=event.sender.id,
message_event=event,
message_chain=event.message_chain,
adapter=adapter,
pipeline_uuid=pipeline_uuid,
routed_by_rule=routed_by_rule,
)
else:
await self.logger.info('Pipeline skipped for group message due to webhook response')
await self._handle_legacy_message_event(event, adapter)
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
self.adapter.register_listener(platform_events.GroupMessage, on_group_message)
@@ -398,13 +971,14 @@ class RuntimeBot:
):
event.bot_uuid = self.bot_entity.uuid
plugin_event = self._eba_event_to_plugin_event(event)
if plugin_event is None:
return
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()}')
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)
self.adapter.register_listener(platform_events.EBAEvent, on_eba_event)