From 19967c8a731a2069e9ca1ff04503b8bbf82f8c25 Mon Sep 17 00:00:00 2001 From: fdc310 <2213070223@qq.com> Date: Wed, 28 Oct 2026 02:23:55 +0800 Subject: [PATCH] Restore LangBot branding on DingTalk A2UI cards --- src/langbot/libs/dingtalk_api/a2ui.py | 31 ++++++++++++++++++- .../platform/adapters/dingtalk/interaction.py | 4 ++- src/langbot/pkg/platform/sources/dingtalk.py | 3 ++ .../platform/test_dingtalk_adapter.py | 2 ++ .../unit_tests/platform/test_dingtalk_api.py | 15 +++++++++ .../platform/test_dingtalk_eba_adapter.py | 2 ++ 6 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/langbot/libs/dingtalk_api/a2ui.py b/src/langbot/libs/dingtalk_api/a2ui.py index d352d85bf..d6f5739c8 100644 --- a/src/langbot/libs/dingtalk_api/a2ui.py +++ b/src/langbot/libs/dingtalk_api/a2ui.py @@ -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(), ] diff --git a/src/langbot/pkg/platform/adapters/dingtalk/interaction.py b/src/langbot/pkg/platform/adapters/dingtalk/interaction.py index 3f9e71765..a36e89375 100644 --- a/src/langbot/pkg/platform/adapters/dingtalk/interaction.py +++ b/src/langbot/pkg/platform/adapters/dingtalk/interaction.py @@ -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 diff --git a/src/langbot/pkg/platform/sources/dingtalk.py b/src/langbot/pkg/platform/sources/dingtalk.py index e70ef5b8f..e1ca4f7f8 100644 --- a/src/langbot/pkg/platform/sources/dingtalk.py +++ b/src/langbot/pkg/platform/sources/dingtalk.py @@ -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 diff --git a/tests/unit_tests/platform/test_dingtalk_adapter.py b/tests/unit_tests/platform/test_dingtalk_adapter.py index af7de7364..a5fb28382 100644 --- a/tests/unit_tests/platform/test_dingtalk_adapter.py +++ b/tests/unit_tests/platform/test_dingtalk_adapter.py @@ -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(): diff --git a/tests/unit_tests/platform/test_dingtalk_api.py b/tests/unit_tests/platform/test_dingtalk_api.py index 8d467bc59..f7a5b7824 100644 --- a/tests/unit_tests/platform/test_dingtalk_api.py +++ b/tests/unit_tests/platform/test_dingtalk_api.py @@ -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( diff --git a/tests/unit_tests/platform/test_dingtalk_eba_adapter.py b/tests/unit_tests/platform/test_dingtalk_eba_adapter.py index 2b1bfd08b..280a4f48e 100644 --- a/tests/unit_tests/platform/test_dingtalk_eba_adapter.py +++ b/tests/unit_tests/platform/test_dingtalk_eba_adapter.py @@ -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'},