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:
Junyan Qin
2026-06-26 16:26:38 +08:00
committed by huanghuoguoguo
parent 9a0081c4d2
commit 7d801cfb68
14 changed files with 2498 additions and 972 deletions
@@ -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