Skip to content

Commit c5a1dd5

Browse files
committed
fix(embed): isolate browser chat sessions
1 parent 0755bee commit c5a1dd5

6 files changed

Lines changed: 503 additions & 73 deletions

File tree

src/langbot/pkg/api/http/controller/groups/pipelines/embed.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from ... import group
2323
from ......utils import paths
24-
from ......platform.sources.websocket_manager import ws_connection_manager
24+
from ......platform.sources.websocket_manager import is_valid_session_id, ws_connection_manager
2525

2626
logger = logging.getLogger(__name__)
2727

@@ -203,11 +203,15 @@ async def get_embed_messages(bot_uuid: str, session_type: str) -> str:
203203
if session_type not in ['person', 'group']:
204204
return self.http_status(400, -1, 'session_type must be person or group')
205205

206+
session_id = quart.request.args.get('session_id', '')
207+
if not is_valid_session_id(session_id):
208+
return self.http_status(400, -1, 'Valid session_id is required')
209+
206210
websocket_adapter = self.ap.platform_mgr.websocket_proxy_bot.adapter
207211
if not websocket_adapter:
208212
return self.http_status(404, -1, 'WebSocket adapter not found')
209213

210-
messages = websocket_adapter.get_websocket_messages(pipeline_uuid, session_type)
214+
messages = websocket_adapter.get_websocket_messages(pipeline_uuid, session_type, session_id)
211215
return self.success(data={'messages': messages})
212216

213217
except Exception as e:
@@ -227,11 +231,15 @@ async def reset_embed_session(bot_uuid: str, session_type: str) -> str:
227231
if session_type not in ['person', 'group']:
228232
return self.http_status(400, -1, 'session_type must be person or group')
229233

234+
session_id = quart.request.args.get('session_id', '')
235+
if not is_valid_session_id(session_id):
236+
return self.http_status(400, -1, 'Valid session_id is required')
237+
230238
websocket_adapter = self.ap.platform_mgr.websocket_proxy_bot.adapter
231239
if not websocket_adapter:
232240
return self.http_status(404, -1, 'WebSocket adapter not found')
233241

234-
websocket_adapter.reset_session(pipeline_uuid, session_type)
242+
websocket_adapter.reset_session(pipeline_uuid, session_type, session_id)
235243
return self.success(data={'message': 'Session reset successfully'})
236244

237245
except Exception as e:
@@ -294,6 +302,11 @@ async def embed_websocket_connect(bot_uuid: str):
294302
)
295303
return
296304

305+
session_id = quart.websocket.args.get('session_id', '')
306+
if not is_valid_session_id(session_id):
307+
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Valid session_id is required'}))
308+
return
309+
297310
websocket_adapter = self.ap.platform_mgr.websocket_proxy_bot.adapter
298311
if not websocket_adapter:
299312
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'WebSocket adapter not found'}))
@@ -304,6 +317,7 @@ async def embed_websocket_connect(bot_uuid: str):
304317
websocket=quart.websocket._get_current_object(),
305318
pipeline_uuid=pipeline_uuid,
306319
session_type=session_type,
320+
session_id=session_id,
307321
metadata={'user_agent': quart.websocket.headers.get('User-Agent', '')},
308322
)
309323

src/langbot/pkg/platform/sources/websocket_adapter.py

Lines changed: 116 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
1414
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
1515
from ...core import app
16-
from .websocket_manager import ws_connection_manager, WebSocketConnection
16+
from .websocket_manager import WebSocketConnection, is_valid_session_id, ws_connection_manager
1717

1818
logger = logging.getLogger(__name__)
1919

@@ -91,6 +91,59 @@ def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventL
9191
self.outbound_message_queue = asyncio.Queue()
9292
self.stream_enabled = True
9393

94+
@staticmethod
95+
def _conversation_key(pipeline_uuid: str, session_id: str | None = None) -> str:
96+
"""Return the history key for a pipeline/client conversation."""
97+
return f'{pipeline_uuid}:{session_id}' if session_id else pipeline_uuid
98+
99+
@staticmethod
100+
def _parse_embed_target(target_id: str) -> tuple[str, str] | None:
101+
"""Extract pipeline and session identifiers from a stable embed launcher."""
102+
target_value = str(target_id)
103+
for prefix in ('websocket_', 'websocketgroup_'):
104+
if target_value.startswith(prefix):
105+
target = target_value[len(prefix) :]
106+
break
107+
else:
108+
return None
109+
if ':' not in target:
110+
return None
111+
pipeline_uuid, session_id = target.rsplit(':', 1)
112+
if not pipeline_uuid or not is_valid_session_id(session_id):
113+
return None
114+
return pipeline_uuid, session_id
115+
116+
@classmethod
117+
async def _get_connection_from_target(cls, target_id: str):
118+
"""Resolve a person or group WebSocket launcher to its connection."""
119+
target_value = str(target_id)
120+
for prefix in ('websocket_', 'websocketgroup_'):
121+
if target_value.startswith(prefix):
122+
target = target_value[len(prefix) :]
123+
break
124+
else:
125+
return None
126+
connection = await ws_connection_manager.get_connection(target)
127+
if connection is not None:
128+
return connection
129+
embed_target = cls._parse_embed_target(target_id)
130+
if embed_target is not None:
131+
pipeline_uuid, session_id = embed_target
132+
return await ws_connection_manager.get_connection_by_session_id(session_id, pipeline_uuid)
133+
return await ws_connection_manager.get_connection_by_session_id(target)
134+
135+
async def _get_message_context(self, message_source) -> tuple[str, str | None]:
136+
"""Resolve the originating pipeline and browser session for a reply."""
137+
sender = getattr(message_source, 'sender', None)
138+
sender_id = getattr(sender, 'id', '')
139+
connection = await self._get_connection_from_target(sender_id)
140+
if connection is not None:
141+
return connection.pipeline_uuid, connection.session_id
142+
embed_target = self._parse_embed_target(sender_id)
143+
if embed_target is not None:
144+
return embed_target
145+
return typing.cast(str, self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid), None
146+
94147
async def send_message(
95148
self,
96149
target_type: str,
@@ -103,15 +156,26 @@ async def send_message(
103156
target_id 可能是 launcher_id(如 websocket_xxx)或 pipeline_uuid。
104157
我们需要尝试两种方式来确保消息能够送达。
105158
"""
106-
# 获取当前的 pipeline_uuid
107-
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
159+
connection = await self._get_connection_from_target(target_id)
160+
if connection is not None:
161+
pipeline_uuid = connection.pipeline_uuid
162+
session_id = connection.session_id
163+
else:
164+
embed_target = self._parse_embed_target(target_id)
165+
if embed_target is not None:
166+
pipeline_uuid, session_id = embed_target
167+
else:
168+
pipeline_uuid = typing.cast(
169+
str,
170+
self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid,
171+
)
172+
session_id = None
108173
session_type = 'group' if target_type == 'group' else 'person'
174+
conversation_key = self._conversation_key(pipeline_uuid, session_id)
109175

110-
# 选择会话
111176
session = self.websocket_group_session if session_type == 'group' else self.websocket_person_session
112177

113-
# 生成唯一消息ID
114-
msg_id = len(session.get_message_list(pipeline_uuid)) + 1
178+
msg_id = len(session.get_message_list(conversation_key)) + 1
115179

116180
message_data = WebSocketMessage(
117181
id=msg_id,
@@ -122,10 +186,8 @@ async def send_message(
122186
is_final=True,
123187
)
124188

125-
# 保存到历史记录
126-
session.get_message_list(pipeline_uuid).append(message_data)
189+
session.get_message_list(conversation_key).append(message_data)
127190

128-
# 直接广播到当前pipeline的连接
129191
await ws_connection_manager.broadcast_to_pipeline(
130192
pipeline_uuid,
131193
{
@@ -134,6 +196,7 @@ async def send_message(
134196
'data': message_data.model_dump(),
135197
},
136198
session_type=session_type,
199+
session_id=session_id,
137200
)
138201

139202
return message_data.model_dump()
@@ -152,12 +215,11 @@ async def reply_message(
152215
else self.websocket_person_session
153216
)
154217

155-
# 从message_source获取pipeline_uuid和connection_id
156-
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
218+
pipeline_uuid, session_id = await self._get_message_context(message_source)
157219
session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
220+
conversation_key = self._conversation_key(pipeline_uuid, session_id)
158221

159-
# 生成新的消息ID
160-
msg_id = len(session.get_message_list(pipeline_uuid)) + 1
222+
msg_id = len(session.get_message_list(conversation_key)) + 1
161223

162224
message_data = WebSocketMessage(
163225
id=msg_id,
@@ -168,10 +230,8 @@ async def reply_message(
168230
is_final=True,
169231
)
170232

171-
# 保存到历史记录
172-
session.get_message_list(pipeline_uuid).append(message_data)
233+
session.get_message_list(conversation_key).append(message_data)
173234

174-
# 直接广播到所有该pipeline的连接,包含session_type信息
175235
await ws_connection_manager.broadcast_to_pipeline(
176236
pipeline_uuid,
177237
{
@@ -180,6 +240,7 @@ async def reply_message(
180240
'data': message_data.model_dump(),
181241
},
182242
session_type=session_type,
243+
session_id=session_id,
183244
)
184245

185246
return message_data.model_dump()
@@ -200,10 +261,11 @@ async def reply_message_chunk(
200261
else self.websocket_person_session
201262
)
202263

203-
pipeline_uuid = self.ap.platform_mgr.websocket_proxy_bot.bot_entity.use_pipeline_uuid
264+
pipeline_uuid, session_id = await self._get_message_context(message_source)
204265
session_type = 'group' if isinstance(message_source, platform_events.GroupMessage) else 'person'
205-
message_list = session.get_message_list(pipeline_uuid)
206-
stream_message_indexes = session.get_stream_message_indexes(pipeline_uuid)
266+
conversation_key = self._conversation_key(pipeline_uuid, session_id)
267+
message_list = session.get_message_list(conversation_key)
268+
stream_message_indexes = session.get_stream_message_indexes(conversation_key)
207269

208270
# Streaming messages in LangBot have a stable resp_message_id during the same assistant reply.
209271
# Use it as the primary key to avoid overwriting an old card from a previous reply.
@@ -247,7 +309,6 @@ async def reply_message_chunk(
247309
if message_is_final and resp_message_id:
248310
stream_message_indexes.pop(resp_message_id, None)
249311

250-
# 直接广播到所有该pipeline的连接,包含session_type信息
251312
await ws_connection_manager.broadcast_to_pipeline(
252313
pipeline_uuid,
253314
{
@@ -256,6 +317,7 @@ async def reply_message_chunk(
256317
'data': message_data.model_dump(),
257318
},
258319
session_type=session_type,
320+
session_id=session_id,
259321
)
260322

261323
return message_data.model_dump()
@@ -381,23 +443,19 @@ async def handle_websocket_message(
381443
"""
382444
pipeline_uuid = connection.pipeline_uuid
383445
session_type = connection.session_type
446+
conversation_key = self._conversation_key(pipeline_uuid, connection.session_id)
384447

385-
# 获取stream参数,默认为True
386448
self.stream_enabled = message_data.get('stream', True)
387449

388-
# 选择会话
389450
use_session = self.websocket_group_session if session_type == 'group' else self.websocket_person_session
390451

391-
# 解析消息链
392452
message_chain_obj = message_data.get('message', [])
393453

394-
# 处理图片组件:将path转换为base64
395454
await self._process_image_components(message_chain_obj)
396455

397456
message_chain = platform_message.MessageChain.model_validate(message_chain_obj)
398457

399-
# 生成消息ID
400-
message_id = len(use_session.get_message_list(pipeline_uuid)) + 1
458+
message_id = len(use_session.get_message_list(conversation_key)) + 1
401459

402460
# 保存用户消息
403461
user_message = WebSocketMessage(
@@ -409,9 +467,8 @@ async def handle_websocket_message(
409467
connection_id=connection.connection_id,
410468
is_final=True, # 用户消息始终是完整的,非流式
411469
)
412-
use_session.get_message_list(pipeline_uuid).append(user_message)
470+
use_session.get_message_list(conversation_key).append(user_message)
413471

414-
# 广播用户消息到所有连接(包括发送者),包含session_type信息
415472
await ws_connection_manager.broadcast_to_pipeline(
416473
pipeline_uuid,
417474
{
@@ -420,25 +477,27 @@ async def handle_websocket_message(
420477
'data': user_message.model_dump(),
421478
},
422479
session_type=session_type,
480+
session_id=connection.session_id,
423481
)
424482

425483
# 添加消息源
426484
message_chain.insert(0, platform_message.Source(id=message_id, time=datetime.now().timestamp()))
427485

428486
# 创建事件
487+
launcher_id = f'{pipeline_uuid}:{connection.session_id}' if connection.session_id else connection.connection_id
429488
if session_type == 'person':
430-
sender = platform_entities.Friend(
431-
id=f'websocket_{connection.connection_id}', nickname='User', remark='User'
432-
)
489+
sender = platform_entities.Friend(id=f'websocket_{launcher_id}', nickname='User', remark='User')
433490
event = platform_events.FriendMessage(
434491
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
435492
)
436493
else:
437494
group = platform_entities.Group(
438-
id='websocketgroup', name='Group', permission=platform_entities.Permission.Member
495+
id=f'websocketgroup_{launcher_id}' if connection.session_id else 'websocketgroup',
496+
name='Group',
497+
permission=platform_entities.Permission.Member,
439498
)
440499
sender = platform_entities.GroupMember(
441-
id=f'websocket_{connection.connection_id}',
500+
id=f'websocket_{launcher_id}',
442501
member_name='User',
443502
group=group,
444503
permission=platform_entities.Permission.Member,
@@ -468,22 +527,27 @@ async def handle_websocket_message(
468527
if event.__class__ in listeners:
469528
asyncio.create_task(listeners[event.__class__](event, callback_adapter))
470529

471-
def get_websocket_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]:
472-
"""获取消息历史"""
473-
if session_type == 'person':
474-
return [message.model_dump() for message in self.websocket_person_session.get_message_list(pipeline_uuid)]
475-
else:
476-
return [message.model_dump() for message in self.websocket_group_session.get_message_list(pipeline_uuid)]
477-
478-
def reset_session(self, pipeline_uuid: str, session_type: str):
479-
"""重置会话"""
480-
if session_type == 'person':
481-
if pipeline_uuid in self.websocket_person_session.message_lists:
482-
self.websocket_person_session.message_lists[pipeline_uuid] = []
483-
if pipeline_uuid in self.websocket_person_session.stream_message_indexes:
484-
self.websocket_person_session.stream_message_indexes[pipeline_uuid] = {}
485-
else:
486-
if pipeline_uuid in self.websocket_group_session.message_lists:
487-
self.websocket_group_session.message_lists[pipeline_uuid] = []
488-
if pipeline_uuid in self.websocket_group_session.stream_message_indexes:
489-
self.websocket_group_session.stream_message_indexes[pipeline_uuid] = {}
530+
def get_websocket_messages(
531+
self,
532+
pipeline_uuid: str,
533+
session_type: str,
534+
session_id: str | None = None,
535+
) -> list[dict]:
536+
"""Return history for one pipeline/client conversation."""
537+
conversation_key = self._conversation_key(pipeline_uuid, session_id)
538+
session = self.websocket_person_session if session_type == 'person' else self.websocket_group_session
539+
return [message.model_dump() for message in session.get_message_list(conversation_key)]
540+
541+
def reset_session(
542+
self,
543+
pipeline_uuid: str,
544+
session_type: str,
545+
session_id: str | None = None,
546+
):
547+
"""Reset one pipeline/client conversation."""
548+
conversation_key = self._conversation_key(pipeline_uuid, session_id)
549+
session = self.websocket_person_session if session_type == 'person' else self.websocket_group_session
550+
if conversation_key in session.message_lists:
551+
session.message_lists[conversation_key] = []
552+
if conversation_key in session.stream_message_indexes:
553+
session.stream_message_indexes[conversation_key] = {}

0 commit comments

Comments
 (0)