mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-26 23:44:19 +00:00
feat(eba): consolidate event bindings, fix command.tsx pointer-events
- Replace legacy pipeline binding card + RoutingRulesEditor with unified EventBindingsEditor; remove use_pipeline_uuid/pipeline_routing_rules from bot form schema and API update handler - Add _augment_event_data() to botmgr for filter virtual fields (message_text, message_element_types, chat_type) - Add alembic migration 0009: migrate use_pipeline_uuid and pipeline_routing_rules into event_bindings on first run - Fix command.tsx: data-[disabled] -> data-[disabled=true] so cmdk 1.x items (data-disabled=false) are not pointer-events:none - EventBindingsEditor: onSelect on CommandItems, filter conditions panel, disabled bindings section, dnd reorder - i18n: add filter/condition keys for zh-Hans and en-US - Update tests to match new bot service behavior Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -190,17 +190,6 @@ class BotService:
|
||||
# TODO: 检查配置信息格式
|
||||
bot_data['uuid'] = str(uuid.uuid4())
|
||||
|
||||
# bind the most recently updated pipeline if any exist
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline)
|
||||
.order_by(persistence_pipeline.LegacyPipeline.updated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
pipeline = result.first()
|
||||
if pipeline is not None:
|
||||
bot_data['use_pipeline_uuid'] = pipeline.uuid
|
||||
bot_data['use_pipeline_name'] = pipeline.name
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(bot_data))
|
||||
|
||||
bot = await self.get_bot(bot_data['uuid'])
|
||||
@@ -219,18 +208,10 @@ class BotService:
|
||||
if 'event_bindings' in update_data:
|
||||
update_data['event_bindings'] = await self._normalize_event_bindings(update_data.get('event_bindings'))
|
||||
|
||||
# set use_pipeline_name
|
||||
if 'use_pipeline_uuid' in update_data:
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||
persistence_pipeline.LegacyPipeline.uuid == update_data['use_pipeline_uuid']
|
||||
)
|
||||
)
|
||||
pipeline = result.first()
|
||||
if pipeline is not None:
|
||||
update_data['use_pipeline_name'] = pipeline.name
|
||||
else:
|
||||
raise Exception('Pipeline not found')
|
||||
# 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)
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_bot.Bot).values(update_data).where(persistence_bot.Bot.uuid == bot_uuid)
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
"""migrate use_pipeline_uuid and pipeline_routing_rules into event_bindings
|
||||
|
||||
Revision ID: 0009_migrate_routing_to_event_bindings
|
||||
Revises: 0008_agent_product_surface
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = '0009_migrate_routing_to_event_bindings'
|
||||
down_revision = '0008_agent_product_surface'
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _rule_to_filters(rule: dict) -> list[dict] | None:
|
||||
"""Convert a pipeline_routing_rule to event_binding filters (best effort).
|
||||
|
||||
Rules that don't map cleanly (message_content, message_has_element) are
|
||||
skipped — callers should handle None as "cannot migrate".
|
||||
"""
|
||||
rule_type = rule.get('type')
|
||||
operator = rule.get('operator', 'eq')
|
||||
value = rule.get('value', '')
|
||||
|
||||
if rule_type == 'launcher_type':
|
||||
if value == 'group':
|
||||
return [{'field': 'group', 'operator': 'neq', 'value': None}]
|
||||
if value == 'person':
|
||||
return [{'field': 'group', 'operator': 'eq', 'value': None}]
|
||||
elif rule_type == 'launcher_id':
|
||||
return [{'field': 'chat_id', 'operator': operator, 'value': value}]
|
||||
|
||||
return None # message_content / message_has_element: no clean mapping
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
rows = bind.execute(
|
||||
sa.text('SELECT uuid, use_pipeline_uuid, pipeline_routing_rules, event_bindings FROM bots')
|
||||
).fetchall()
|
||||
|
||||
for bot_uuid, use_pipeline_uuid, routing_rules_raw, event_bindings_raw in rows:
|
||||
try:
|
||||
existing = (
|
||||
json.loads(event_bindings_raw) if isinstance(event_bindings_raw, str) else (event_bindings_raw or [])
|
||||
)
|
||||
except Exception:
|
||||
existing = []
|
||||
|
||||
if existing:
|
||||
continue # already has event_bindings — skip
|
||||
|
||||
try:
|
||||
routing_rules = (
|
||||
json.loads(routing_rules_raw) if isinstance(routing_rules_raw, str) else (routing_rules_raw or [])
|
||||
)
|
||||
except Exception:
|
||||
routing_rules = []
|
||||
|
||||
new_bindings: list[dict] = []
|
||||
base_priority = len(routing_rules)
|
||||
|
||||
for i, rule in enumerate(routing_rules):
|
||||
target_uuid = rule.get('pipeline_uuid', '')
|
||||
if not target_uuid:
|
||||
continue
|
||||
filters = _rule_to_filters(rule)
|
||||
if filters is None:
|
||||
continue
|
||||
new_bindings.append(
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'event_pattern': 'message.*',
|
||||
'target_type': 'pipeline',
|
||||
'target_uuid': target_uuid,
|
||||
'filters': filters,
|
||||
'priority': base_priority - i,
|
||||
'enabled': True,
|
||||
'description': f'Migrated from routing rule ({rule.get("type")})',
|
||||
'order': i,
|
||||
}
|
||||
)
|
||||
|
||||
if use_pipeline_uuid:
|
||||
new_bindings.append(
|
||||
{
|
||||
'id': str(uuid.uuid4()),
|
||||
'event_pattern': 'message.*',
|
||||
'target_type': 'pipeline',
|
||||
'target_uuid': use_pipeline_uuid,
|
||||
'filters': [],
|
||||
'priority': 0,
|
||||
'enabled': True,
|
||||
'description': 'Migrated from default pipeline binding',
|
||||
'order': len(new_bindings),
|
||||
}
|
||||
)
|
||||
|
||||
if new_bindings:
|
||||
bind.execute(
|
||||
sa.text('UPDATE bots SET event_bindings = :b WHERE uuid = :u'),
|
||||
{'b': json.dumps(new_bindings, ensure_ascii=False), 'u': bot_uuid},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass # not reversible
|
||||
@@ -192,6 +192,25 @@ class RuntimeBot:
|
||||
return False
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _augment_event_data(
|
||||
cls,
|
||||
event_data: dict[str, typing.Any],
|
||||
) -> dict[str, typing.Any]:
|
||||
"""Inject virtual computed fields to simplify common filter patterns."""
|
||||
message_chain = event_data.get('message_chain')
|
||||
if isinstance(message_chain, list):
|
||||
text_parts = [
|
||||
comp.get('text', '') for comp in message_chain if isinstance(comp, dict) and comp.get('type') == 'Plain'
|
||||
]
|
||||
event_data['message_text'] = ''.join(text_parts)
|
||||
event_data['message_element_types'] = [
|
||||
comp.get('type', '') for comp in message_chain if isinstance(comp, dict)
|
||||
]
|
||||
if 'group' in event_data:
|
||||
event_data['chat_type'] = 'group' if event_data.get('group') is not None else 'person'
|
||||
return event_data
|
||||
|
||||
@classmethod
|
||||
def _match_event_filters(
|
||||
cls,
|
||||
@@ -203,7 +222,7 @@ class RuntimeBot:
|
||||
if not isinstance(filters, list):
|
||||
return False
|
||||
|
||||
event_data = cls._safe_model_dump(event)
|
||||
event_data = cls._augment_event_data(cls._safe_model_dump(event))
|
||||
return all(
|
||||
cls._match_event_filter(event_data, event_filter)
|
||||
for event_filter in filters
|
||||
@@ -854,11 +873,8 @@ class RuntimeBot:
|
||||
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
|
||||
)
|
||||
pipeline_uuid = None
|
||||
routed_by_rule = False
|
||||
else:
|
||||
pipeline_uuid = pipeline_uuid_override
|
||||
routed_by_rule = routed_by_event_binding
|
||||
|
||||
Reference in New Issue
Block a user