1111from ..logger import EventLogger
1212from langbot .libs .wecom_ai_bot_api .wecombotevent import WecomBotEvent
1313from langbot .libs .wecom_ai_bot_api .api import WecomBotClient
14+ from langbot .libs .wecom_ai_bot_api .ws_client import WecomBotWsClient
1415
1516
1617DEFAULT_PULL_PENDING_PLACEHOLDER = 'AI 正在思考中,请稍候'
@@ -194,12 +195,13 @@ async def target2yiri(event: WecomBotEvent):
194195
195196
196197class WecomBotAdapter (abstract_platform_adapter .AbstractMessagePlatformAdapter ):
197- bot : WecomBotClient
198+ bot : typing . Union [ WecomBotClient , WecomBotWsClient ]
198199 bot_account_id : str
199200 message_converter : WecomBotMessageConverter = WecomBotMessageConverter ()
200201 event_converter : WecomBotEventConverter = WecomBotEventConverter ()
201202 config : dict
202203 bot_uuid : str = None
204+ _ws_mode : bool = False
203205
204206 @staticmethod
205207 def _get_int_config (config : dict , key : str , default : int , min_value : int , max_value : int ) -> int :
@@ -211,52 +213,60 @@ def _get_int_config(config: dict, key: str, default: int, min_value: int, max_va
211213 return max (min_value , min (max_value , value ))
212214
213215 def __init__ (self , config : dict , logger : EventLogger ):
214- enable_webhook = config .get ('enable-webhook' , True )
215- if not enable_webhook :
216- raise Exception ('WecomBot websocket mode is not supported in this branch yet. Please enable webhook mode.' )
217-
218- required_keys = ['Token' , 'EncodingAESKey' , 'Corpid' , 'BotId' ]
219- missing_keys = [key for key in required_keys if not config .get (key )]
220- if missing_keys :
221- raise Exception (f'WecomBot webhook mode missing config: { missing_keys } ' )
216+ enable_webhook = config .get ('enable-webhook' , False )
217+ normalized_config = dict (config )
222218
223- pull_poll_timeout_ms = self ._get_int_config (config , 'PullPollTimeoutMs' , 500 , 50 , 2000 )
224- pull_stream_max_lifetime_ms = self ._get_int_config (config , 'PullStreamMaxLifetimeMs' , 300000 , 1000 , 600000 )
225- pending_placeholder_enabled = config .get ('PullPendingPlaceholderEnabled' , False )
226- pending_placeholder_delay_ms = self ._get_int_config (config , 'PullPendingPlaceholderDelayMs' , 3000 , 0 , 10000 )
227- pending_placeholder = config .get ('PullPendingPlaceholder' , DEFAULT_PULL_PENDING_PLACEHOLDER )
219+ if not enable_webhook :
220+ bot = WecomBotWsClient (
221+ bot_id = config ['BotId' ],
222+ secret = config ['Secret' ],
223+ logger = logger ,
224+ encoding_aes_key = config .get ('EncodingAESKey' , '' ),
225+ )
226+ ws_mode = True
227+ else :
228+ required_keys = ['Token' , 'EncodingAESKey' , 'Corpid' ]
229+ missing_keys = [key for key in required_keys if key not in config or not config [key ]]
230+ if missing_keys :
231+ raise Exception (f'WecomBot webhook mode missing config: { missing_keys } ' )
232+
233+ pull_poll_timeout_ms = self ._get_int_config (config , 'PullPollTimeoutMs' , 500 , 50 , 2000 )
234+ pull_stream_max_lifetime_ms = self ._get_int_config (config , 'PullStreamMaxLifetimeMs' , 300000 , 1000 , 600000 )
235+ pending_placeholder_enabled = config .get ('PullPendingPlaceholderEnabled' , False )
236+ pending_placeholder_delay_ms = self ._get_int_config (config , 'PullPendingPlaceholderDelayMs' , 3000 , 0 , 10000 )
237+ pending_placeholder = config .get ('PullPendingPlaceholder' , DEFAULT_PULL_PENDING_PLACEHOLDER )
238+
239+ normalized_config ['PullPollTimeoutMs' ] = pull_poll_timeout_ms
240+ normalized_config ['PullStreamMaxLifetimeMs' ] = pull_stream_max_lifetime_ms
241+ normalized_config ['PullPendingPlaceholderEnabled' ] = pending_placeholder_enabled
242+ normalized_config ['PullPendingPlaceholderDelayMs' ] = pending_placeholder_delay_ms
243+ normalized_config ['PullPendingPlaceholder' ] = pending_placeholder
244+
245+ effective_placeholder_delay = pending_placeholder_delay_ms / 1000 if pending_placeholder_enabled else 0
246+ effective_placeholder = pending_placeholder if pending_placeholder_enabled else ''
247+
248+ bot = WecomBotClient (
249+ Token = config ['Token' ],
250+ EnCodingAESKey = config ['EncodingAESKey' ],
251+ Corpid = config ['Corpid' ],
252+ logger = logger ,
253+ unified_mode = True ,
254+ stream_poll_timeout = pull_poll_timeout_ms / 1000 ,
255+ stream_max_lifetime = pull_stream_max_lifetime_ms / 1000 ,
256+ pending_placeholder = effective_placeholder ,
257+ pending_placeholder_delay = effective_placeholder_delay ,
258+ )
259+ ws_mode = False
228260
229- normalized_config = dict (config )
230- normalized_config ['enable-webhook' ] = True
231- normalized_config ['PullPollTimeoutMs' ] = pull_poll_timeout_ms
232- normalized_config ['PullStreamMaxLifetimeMs' ] = pull_stream_max_lifetime_ms
233- normalized_config ['PullPendingPlaceholderEnabled' ] = pending_placeholder_enabled
234- normalized_config ['PullPendingPlaceholderDelayMs' ] = pending_placeholder_delay_ms
235- normalized_config ['PullPendingPlaceholder' ] = pending_placeholder
236-
237- # 如果未开启首字等待占位,则将延迟设为0且占位文案设为空
238- effective_placeholder_delay = pending_placeholder_delay_ms / 1000 if pending_placeholder_enabled else 0
239- effective_placeholder = pending_placeholder if pending_placeholder_enabled else ''
240-
241- bot = WecomBotClient (
242- Token = config ['Token' ],
243- EnCodingAESKey = config ['EncodingAESKey' ],
244- Corpid = config ['Corpid' ],
245- logger = logger ,
246- unified_mode = True ,
247- stream_poll_timeout = pull_poll_timeout_ms / 1000 ,
248- stream_max_lifetime = pull_stream_max_lifetime_ms / 1000 ,
249- pending_placeholder = effective_placeholder ,
250- pending_placeholder_delay = effective_placeholder_delay ,
251- )
252- bot_account_id = config ['BotId' ]
261+ bot_account_id = config .get ('BotId' , '' )
253262
254263 super ().__init__ (
255264 config = normalized_config ,
256265 logger = logger ,
257266 bot = bot ,
258267 bot_account_id = bot_account_id ,
259268 )
269+ self ._ws_mode = ws_mode
260270
261271 async def reply_message (
262272 self ,
@@ -265,7 +275,15 @@ async def reply_message(
265275 quote_origin : bool = False ,
266276 ):
267277 content = await self .message_converter .yiri2target (message )
268- await self .bot .set_message (message_source .source_platform_object .message_id , content )
278+ if self ._ws_mode :
279+ event = message_source .source_platform_object
280+ req_id = event .get ('req_id' , '' )
281+ if req_id :
282+ await self .bot .reply_text (req_id , content )
283+ else :
284+ await self .bot .set_message (event .message_id , content )
285+ else :
286+ await self .bot .set_message (message_source .source_platform_object .message_id , content )
269287
270288 async def reply_message_chunk (
271289 self ,
@@ -275,30 +293,22 @@ async def reply_message_chunk(
275293 quote_origin : bool = False ,
276294 is_final : bool = False ,
277295 ):
278- """将流水线增量输出写入企业微信 stream 会话。
279-
280- Args:
281- message_source: 流水线提供的原始消息事件。
282- bot_message: 当前片段对应的模型元信息(未使用)。
283- message: 需要回复的消息链。
284- quote_origin: 是否引用原消息(企业微信暂不支持)。
285- is_final: 标记当前片段是否为最终回复。
286-
287- Returns:
288- dict: 包含 `stream` 键,标识写入是否成功。
289-
290- Example:
291- 在流水线 `reply_message_chunk` 调用中自动触发,无需手动调用。
292- """
293- # 转换为纯文本(智能机器人当前协议仅支持文本流)
294296 content = await self .message_converter .yiri2target (message )
295297 msg_id = message_source .source_platform_object .message_id
296- # 将片段推送到 WecomBotClient 中的队列,返回值用于判断是否走降级逻辑
297- success = await self .bot .push_stream_chunk (msg_id , content , is_final = is_final )
298- if not success and is_final :
299- # 未命中流式队列时使用旧有 set_message 兜底
300- await self .bot .set_message (msg_id , content )
301- return {'stream' : success }
298+
299+ if self ._ws_mode :
300+ success = await self .bot .push_stream_chunk (msg_id , content , is_final = is_final )
301+ if not success and is_final :
302+ event = message_source .source_platform_object
303+ req_id = event .get ('req_id' , '' )
304+ if req_id :
305+ await self .bot .reply_text (req_id , content )
306+ return {'stream' : success }
307+ else :
308+ success = await self .bot .push_stream_chunk (msg_id , content , is_final = is_final )
309+ if not success and is_final :
310+ await self .bot .set_message (msg_id , content )
311+ return {'stream' : success }
302312
303313 async def is_stream_output_supported (self ) -> bool :
304314 """智能机器人侧默认开启流式能力。
@@ -315,7 +325,11 @@ async def create_message_card(self, message_id: str, event) -> bool:
315325 return False
316326
317327 async def send_message (self , target_type , target_id , message ):
318- pass
328+ if self ._ws_mode :
329+ content = await self .message_converter .yiri2target (message )
330+ await self .bot .send_message (target_id , content )
331+ else :
332+ pass
319333
320334 def register_listener (
321335 self ,
@@ -344,29 +358,25 @@ def set_bot_uuid(self, bot_uuid: str):
344358 self .bot_uuid = bot_uuid
345359
346360 async def handle_unified_webhook (self , bot_uuid : str , path : str , request ):
347- """处理统一 webhook 请求。
348-
349- Args:
350- bot_uuid: Bot 的 UUID
351- path: 子路径(如果有的话)
352- request: Quart Request 对象
353-
354- Returns:
355- 响应数据
356- """
361+ if self ._ws_mode :
362+ return None
357363 return await self .bot .handle_unified_webhook (request )
358364
359365 async def run_async (self ):
360- # 统一 webhook 模式下,不启动独立的 Quart 应用
361- # 保持运行但不启动独立端口
366+ if self ._ws_mode :
367+ await self .bot .connect ()
368+ else :
362369
363- async def keep_alive ():
364- while True :
365- await asyncio .sleep (1 )
370+ async def keep_alive ():
371+ while True :
372+ await asyncio .sleep (1 )
366373
367- await keep_alive ()
374+ await keep_alive ()
368375
369376 async def kill (self ) -> bool :
377+ if self ._ws_mode :
378+ await self .bot .disconnect ()
379+ return True
370380 return False
371381
372382 async def unregister_listener (
0 commit comments