From c1284be8fcedc212197bcd8b3f32f527c45ec3d1 Mon Sep 17 00:00:00 2001 From: leonoxo Date: Thu, 29 Oct 2026 14:54:31 +0800 Subject: [PATCH] fix(line): decouple webhook processing lifecycle and use Push API for proactive sends (#2721) - webhook handler: run listener in a tracked background task so the HTTP ACK is not coupled to the Agent run lifetime (prevents in-flight cancellation when the request scope ends); exceptions are logged - send_message: implement LINE Push API path (text + image) instead of the empty stub, separated from the reply-token path Verified in production on a self-hosted v4.11.0 deployment (LangBot community edition, rootless Docker). Co-authored-by: leonoxo --- src/langbot/pkg/platform/sources/line.py | 48 ++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/langbot/pkg/platform/sources/line.py b/src/langbot/pkg/platform/sources/line.py index d2d36519b..014059b94 100644 --- a/src/langbot/pkg/platform/sources/line.py +++ b/src/langbot/pkg/platform/sources/line.py @@ -18,7 +18,15 @@ from ...utils import bounded_executor from linebot.v3 import WebhookHandler from linebot.v3.exceptions import InvalidSignatureError -from linebot.v3.messaging import Configuration, ApiClient, MessagingApi, ReplyMessageRequest, TextMessage, ImageMessage +from linebot.v3.messaging import ( + Configuration, + ApiClient, + MessagingApi, + PushMessageRequest, + ReplyMessageRequest, + TextMessage, + ImageMessage, +) from linebot.v3.webhooks import ( MessageEvent, TextMessageContent, @@ -223,7 +231,27 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ) async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain): - pass + content_list = await self.message_converter.yiri2target(message, self.api_client) + + for content in content_list: + if content['type'] == 'text': + await asyncio.to_thread( + self.bot.push_message_with_http_info, + PushMessageRequest( + to=target_id, + messages=[TextMessage(text=content['content'])], + ), + ) + elif content['type'] == 'image': + # LINE ImageMessage requires original_content_url and preview_image_url + image_url = content['image'] + await asyncio.to_thread( + self.bot.push_message_with_http_info, + PushMessageRequest( + to=target_id, + messages=[ImageMessage(original_content_url=image_url, preview_image_url=image_url)], + ), + ) async def reply_message( self, @@ -306,11 +334,23 @@ class LINEAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter): ) return quart.Response('Invalid signature', status=400) - # 处理事件 + # 处理事件(P1 熱修復:背景任務脫鉤,webhook 立即 ACK,agent run 不受請求生命週期取消) if events and len(events) > 0: lb_event = await self.event_converter.target2yiri(events[0], self.api_client) if lb_event.__class__ in self.listeners: - await self.listeners[lb_event.__class__](lb_event, self) + listener = self.listeners[lb_event.__class__] + + async def _run_background_event(): + try: + await listener(lb_event, self) + except Exception: + await self.logger.error(f'Error in LINE background event task: {traceback.format_exc()}') + + task = asyncio.create_task(_run_background_event()) + if not hasattr(self, '_background_tasks'): + self._background_tasks = set() + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) return {'code': 200, 'message': 'ok'} except Exception: