@@ -857,29 +857,122 @@ def _resolve_send_path(
857857 guild_id : Optional [str ] = None ,
858858 ) -> tuple [str , bool , str ]:
859859 """Return (api_path, use_msg_seq, seq_key)."""
860- if message_type == "dm" and guild_id :
860+ if message_type == "dm" :
861+ if not guild_id :
862+ raise ValueError ("QQ guild DM route requires guild_id" )
861863 return (
862864 f"/dms/{ guild_id } /messages" ,
863865 False ,
864866 "" ,
865867 )
866- if message_type == "group" and group_openid :
868+ if message_type == "group" :
869+ if not group_openid :
870+ raise ValueError ("QQ group route requires group_openid" )
867871 return (
868872 f"/v2/groups/{ group_openid } /messages" ,
869873 True ,
870874 "group" ,
871875 )
872- if message_type == "guild" and channel_id :
876+ if message_type == "guild" :
877+ if not channel_id :
878+ raise ValueError ("QQ guild route requires channel_id" )
873879 return (
874880 f"/channels/{ channel_id } /messages" ,
875881 False ,
876882 "" ,
877883 )
878- # c2c or fallback
884+ if message_type == "c2c" :
885+ if not sender_id :
886+ raise ValueError ("QQ C2C route requires user_openid" )
887+ return (
888+ f"/v2/users/{ sender_id } /messages" ,
889+ True ,
890+ "c2c" ,
891+ )
892+ raise ValueError (f"Unsupported QQ message_type: { message_type } " )
893+
894+ # ------------------------------------------------------------------
895+ # Session / route helpers
896+ # ------------------------------------------------------------------
897+
898+ def resolve_session_id (
899+ self ,
900+ sender_id : str ,
901+ channel_meta : Optional [Dict [str , Any ]] = None ,
902+ ) -> str :
903+ """Return a session ID scoped to one QQ conversation."""
904+ meta = channel_meta or {}
905+ message_type = str (meta .get ("message_type" ) or "c2c" )
906+ if message_type == "group" :
907+ group_openid = str (meta .get ("group_openid" ) or "unknown" )
908+ return f"qq:group:{ group_openid } "
909+ if message_type == "guild" :
910+ channel_id = str (meta .get ("channel_id" ) or "unknown" )
911+ return f"qq:guild:{ channel_id } "
912+ if message_type == "dm" :
913+ guild_id = str (meta .get ("guild_id" ) or "unknown" )
914+ return f"qq:dm:{ guild_id } "
915+ return f"qq:c2c:{ sender_id or 'unknown' } "
916+
917+ @staticmethod
918+ def _route_meta_from_handle (to_handle : str ) -> Dict [str , str ]:
919+ """Decode a QQ session ID or direct handle into routing metadata."""
920+ handle = (to_handle or "" ).strip ()
921+ routes = (
922+ ("qq:c2c:" , "c2c" , "sender_id" ),
923+ ("qq:group:" , "group" , "group_openid" ),
924+ ("qq:guild:" , "guild" , "channel_id" ),
925+ ("qq:dm:" , "dm" , "guild_id" ),
926+ ("qq:" , "c2c" , "sender_id" ),
927+ ("group:" , "group" , "group_openid" ),
928+ ("channel:" , "guild" , "channel_id" ),
929+ )
930+ for prefix , message_type , target_key in routes :
931+ if handle .startswith (prefix ):
932+ target = handle .removeprefix (prefix )
933+ if target == "unknown" :
934+ return {"message_type" : message_type }
935+ return {
936+ "message_type" : message_type ,
937+ target_key : target ,
938+ }
939+ return {}
940+
941+ def _normalize_route_meta (
942+ self ,
943+ to_handle : str ,
944+ meta : Optional [Dict [str , Any ]],
945+ ) -> Dict [str , Any ]:
946+ """Merge durable route data encoded in ``to_handle`` into metadata."""
947+ route_meta = dict (meta or {})
948+ encoded_route = self ._route_meta_from_handle (to_handle )
949+ if encoded_route :
950+ route_meta .update (encoded_route )
951+ else :
952+ route_meta .setdefault ("message_type" , "c2c" )
953+ if to_handle :
954+ route_meta .setdefault ("sender_id" , to_handle )
955+ return route_meta
956+
957+ def to_handle_from_target (self , * , user_id : str , session_id : str ) -> str :
958+ """Return a durable QQ session handle for proactive sends."""
959+ return session_id or f"qq:c2c:{ user_id } "
960+
961+ def get_to_handle_from_request (self , request : Any ) -> str :
962+ """Return the request session ID so replies retain their route."""
963+ session_id = getattr (request , "session_id" , "" ) or ""
964+ user_id = getattr (request , "user_id" , "" ) or ""
965+ return session_id or f"qq:c2c:{ user_id } "
966+
967+ def get_on_reply_sent_args (
968+ self ,
969+ request : Any ,
970+ to_handle : str ,
971+ ) -> tuple :
972+ """Report the original QQ user and isolated session to the callback."""
879973 return (
880- f"/v2/users/{ sender_id } /messages" ,
881- True ,
882- "c2c" ,
974+ getattr (request , "user_id" , "" ) or "" ,
975+ getattr (request , "session_id" , "" ) or "" ,
883976 )
884977
885978 async def _dispatch_text (
@@ -1093,12 +1186,12 @@ async def send(
10931186 meta : Optional [Dict [str , Any ]] = None ,
10941187 ) -> None :
10951188 """Send one text via QQ HTTP API.
1096- Routes by meta or to_handle (group:/channel:/openid) .
1189+ Routes by metadata or a conversation-scoped QQ session handle .
10971190 """
10981191 if not self .enabled or not text .strip ():
10991192 return
11001193 text = text .strip ()
1101- meta = meta or {}
1194+ meta = self . _normalize_route_meta ( to_handle , meta )
11021195 use_markdown = _as_bool (
11031196 meta .get ("markdown_enabled" , self ._markdown_enabled ),
11041197 )
@@ -1108,21 +1201,12 @@ async def send(
11081201 logger .info (
11091202 "qq send: stripped URL content for API compatibility" ,
11101203 )
1111- message_type = meta .get ("message_type" )
1204+ message_type = str ( meta .get ("message_type" ) or "c2c " )
11121205 msg_id = meta .get ("message_id" )
11131206 sender_id = meta .get ("sender_id" ) or to_handle
11141207 channel_id = meta .get ("channel_id" )
11151208 group_openid = meta .get ("group_openid" )
11161209 guild_id = meta .get ("guild_id" )
1117- if message_type is None :
1118- if to_handle .startswith ("group:" ):
1119- message_type = "group"
1120- group_openid = to_handle [6 :]
1121- elif to_handle .startswith ("channel:" ):
1122- message_type = "guild"
1123- channel_id = to_handle [8 :]
1124- else :
1125- message_type = "c2c"
11261210 try :
11271211 token = await self ._get_access_token_async ()
11281212 except Exception :
@@ -1326,14 +1410,19 @@ def build_agent_request_from_native(self, native_payload: Any) -> Any:
13261410 if attachments :
13271411 media_parts = self ._parse_qq_attachments (attachments )
13281412 content_parts = list (content_parts ) + media_parts
1329- session_id = self .resolve_session_id (sender_id , meta )
1330- return self .build_agent_request_from_user_content (
1413+ session_id = payload .get ("session_id" ) or self .resolve_session_id (
1414+ sender_id ,
1415+ meta ,
1416+ )
1417+ request = self .build_agent_request_from_user_content (
13311418 channel_id = channel_id ,
13321419 sender_id = sender_id ,
13331420 session_id = session_id ,
13341421 content_parts = content_parts ,
13351422 channel_meta = meta ,
13361423 )
1424+ request .channel_meta = meta
1425+ return request
13371426
13381427 # ------------------------------------------------------------------
13391428 # Instant acknowledgment
@@ -1573,6 +1662,7 @@ async def on_event_message_completed(
15731662 send_meta : Dict [str , Any ],
15741663 ) -> None :
15751664 """Render card-flagged events via the card handler; else default."""
1665+ send_meta = self ._normalize_route_meta (to_handle , send_meta )
15761666 if await self ._card_handler .try_send_card_for_event (
15771667 to_handle ,
15781668 event ,
@@ -2041,7 +2131,7 @@ async def send_content_parts(
20412131
20422132 body = "\n " .join (text_parts ).strip () if text_parts else ""
20432133
2044- meta = meta or {}
2134+ meta = self . _normalize_route_meta ( to_handle , meta )
20452135 message_type = meta .get ("message_type" , "c2c" )
20462136 msg_id = meta .get ("message_id" )
20472137
@@ -2105,7 +2195,7 @@ async def send_media(
21052195 if not self .enabled :
21062196 return
21072197
2108- meta = meta or {}
2198+ meta = self . _normalize_route_meta ( to_handle , meta )
21092199 (
21102200 message_type ,
21112201 sender_id ,
0 commit comments