Restore LangBot branding on DingTalk A2UI cards

This commit is contained in:
fdc310
2026-10-28 02:23:55 +08:00
parent 21307f9b5d
commit 19967c8a73
6 changed files with 55 additions and 2 deletions
+30 -1
View File
@@ -9,12 +9,40 @@ import typing
A2UI_CATALOG_ID = 'https://dingtalk.com/card/a2ui/catalogs/public/catalog.json'
A2UI_MESSAGE_VERSION = 'v1.0'
A2UI_PROTOCOL_VERSION = '1.0'
LANGBOT_LOGO_URL = 'https://langbot.app/logo-blue.png'
LANGBOT_CARD_URL = 'https://langbot.app?utm_source=dingtalk&utm_medium=bot_card&utm_campaign=langbot'
def binding(path: str) -> dict[str, str]:
return {'path': path}
def brand_components() -> list[dict[str, typing.Any]]:
return [
{
'id': 'langbot-brand',
'component': 'Row',
'children': ['langbot-brand-icon', 'langbot-brand-link'],
'align': 'center',
'gap': 4,
},
{
'id': 'langbot-brand-icon',
'component': 'Image',
'url': LANGBOT_LOGO_URL,
'variant': 'icon',
'fit': 'contain',
'previewEnabled': False,
},
{
'id': 'langbot-brand-link',
'component': 'Link',
'text': 'LangBot',
'action': {'functionCall': {'call': 'openUrl', 'args': {'url': LANGBOT_CARD_URL}}},
},
]
def serialize_messages(messages: typing.Iterable[dict | str]) -> list[str]:
serialized = [
message if isinstance(message, str) else json.dumps(message, ensure_ascii=False, separators=(',', ':'))
@@ -95,7 +123,7 @@ def text_card_components() -> list[dict[str, typing.Any]]:
{
'id': 'root',
'component': 'Column',
'children': ['content'],
'children': ['content', 'langbot-brand'],
'gap': 8,
},
{
@@ -103,6 +131,7 @@ def text_card_components() -> list[dict[str, typing.Any]]:
'component': 'Markdown',
'content': binding('/content'),
},
*brand_components(),
]
@@ -7,7 +7,7 @@ import time
import typing
import uuid
from langbot.libs.dingtalk_api.a2ui import binding, create_surface_messages, update_surface_messages
from langbot.libs.dingtalk_api.a2ui import brand_components, binding, create_surface_messages, update_surface_messages
from langbot.libs.dingtalk_api.dingtalkevent import DingTalkEvent
from langbot_plugin.api.entities.builtin.platform import events as platform_events
@@ -150,6 +150,8 @@ def _build_interaction_messages(
)
children.append(button_id)
children.append('langbot-brand')
components.extend(brand_components())
components.insert(0, {'id': 'root', 'component': 'Column', 'children': children, 'gap': 12})
return {'form': form}, components, field_types
@@ -1,4 +1,5 @@
from langbot.libs.dingtalk_api.a2ui import (
brand_components,
binding,
create_surface_messages,
create_text_card_messages,
@@ -519,6 +520,8 @@ def _dingtalk_a2ui_form(
]
)
children.append(button_id)
children.append('langbot-brand')
components.extend(brand_components())
return data, components
@@ -53,6 +53,8 @@ def test_dingtalk_dify_input_card_uses_a2ui_binding_and_required_check():
assert field['value'] == {'path': '/form/value'}
assert button['action']['event']['context']['input'] == {'path': '/form/value'}
assert button['checks'][0]['condition']['call'] == 'required'
assert components[0]['children'][-1] == 'langbot-brand'
assert any(component['id'] == 'langbot-brand-link' for component in components)
def test_dingtalk_extract_select_from_builtin_result_dict():
@@ -31,6 +31,21 @@ def _response(result=None):
return type('Response', (), {'status_code': 200, 'json': lambda self: payload})()
def test_text_card_keeps_brand_when_content_updates():
components = create_text_card_messages('surface-id', 'hello')[2]['updateComponents']['components']
by_id = {component['id']: component for component in components}
assert by_id['root']['children'] == ['content', 'langbot-brand']
assert by_id['langbot-brand']['children'] == ['langbot-brand-icon', 'langbot-brand-link']
assert by_id['langbot-brand-icon']['url'] == 'https://langbot.app/logo-blue.png'
assert by_id['langbot-brand-icon']['previewEnabled'] is False
assert by_id['langbot-brand-link']['action']['functionCall'] == {
'call': 'openUrl',
'args': {'url': 'https://langbot.app?utm_source=dingtalk&utm_medium=bot_card&utm_campaign=langbot'},
}
assert update_text_card_messages('surface-id', 'done')[0]['updateDataModel']['path'] == '/content'
@pytest.mark.asyncio
async def test_create_ai_card_uses_group_robot_delivery_model():
post = AsyncMock(
@@ -342,6 +342,8 @@ async def test_dingtalk_interaction_delivery_and_callback_event():
components = kwargs['a2ui_messages'][2]['updateComponents']['components']
button = next(component for component in components if component['component'] == 'Button')
assert button['action']['event']['context']['actionId'] == 'lbi:callback-token:a:0'
assert components[0]['children'][-1] == 'langbot-brand'
assert any(component['id'] == 'langbot-brand-link' for component in components)
native = dingtalk_card_callback_event(
content={'actionId': 'lbi:callback-token:a:0'},