66import os
77import tempfile
88import uuid
9+ import filetype
910from fastapi import WebSocket , WebSocketDisconnect
1011from typing import Dict , List , Optional , Union
1112from dataclasses import dataclass
@@ -36,6 +37,28 @@ class OneBotAdapter(sdk.BaseAdapter):
3637 class Send (sdk .BaseAdapter .Send ):
3738 """消息发送DSL实现"""
3839
40+ # 方法名映射表(全小写 -> 实际方法名)
41+ _METHOD_MAP = {
42+ # 消息发送方法
43+ "text" : "Text" ,
44+ "image" : "Image" ,
45+ "voice" : "Voice" ,
46+ "video" : "Video" ,
47+ "face" : "Face" ,
48+ "file" : "File" ,
49+
50+ # 批量和其他方法
51+ "recall" : "Recall" ,
52+
53+ # 原始消息和转换
54+ "raw_ob12" : "Raw_ob12" ,
55+
56+ # 链式修饰方法
57+ "at" : "At" ,
58+ "atall" : "AtAll" ,
59+ "reply" : "Reply" ,
60+ }
61+
3962 def __init__ (self , adapter , target_type = None , target_id = None , account_id = None ):
4063 super ().__init__ (adapter , target_type , target_id , account_id )
4164 self ._at_user_ids = [] # @的用户列表
@@ -44,10 +67,21 @@ def __init__(self, adapter, target_type=None, target_id=None, account_id=None):
4467
4568 def __getattr__ (self , name ):
4669 """
47- 处理未定义的发送方法
70+ 处理未定义的发送方法(支持大小写不敏感)
4871
49- 当调用不存在的消息类型方法时,发送文本提示
72+ 当调用不存在的消息类型方法时:
73+ 1. 通过映射表查找对应的方法
74+ 2. 如果找到则调用该方法
75+ 3. 如果找不到,则发送文本提示不支持
5076 """
77+ name_lower = name .lower ()
78+
79+ # 查找映射
80+ if name_lower in self ._METHOD_MAP :
81+ actual_method_name = self ._METHOD_MAP [name_lower ]
82+ return getattr (self , actual_method_name )
83+
84+ # 方法不存在,返回文本提示
5185 def unsupported_method (* args , ** kwargs ):
5286 # 格式化参数信息
5387 params_info = []
@@ -70,6 +104,34 @@ def unsupported_method(*args, **kwargs):
70104
71105 return unsupported_method
72106
107+ def _get_msg_type_by_filetype (self , file : Union [str , bytes ]) -> str :
108+ """
109+ 根据文件内容或路径识别文件类型,返回对应的消息段类型
110+
111+ :param file: 文件内容(bytes)或路径(str)
112+ :return: 消息段类型(image/record/video)
113+ """
114+ try :
115+ if isinstance (file , bytes ):
116+ kind = filetype .guess (file )
117+ else :
118+ kind = filetype .guess (file )
119+ except Exception :
120+ kind = None
121+
122+ if kind is None :
123+ return "image" # 默认作为图片尝试
124+
125+ # 根据MIME类型判断
126+ if kind .mime .startswith ('image/' ):
127+ return "image"
128+ elif kind .mime .startswith ('audio/' ):
129+ return "record"
130+ elif kind .mime .startswith ('video/' ):
131+ return "video"
132+ else :
133+ return "image" # 未知类型默认作为图片
134+
73135 def _build_message_array (self , message : Union [str , List [Dict ]]) -> List [Dict ]:
74136 """
75137 构建消息数组,包含链式修饰
@@ -112,9 +174,51 @@ def _build_message_array(self, message: Union[str, List[Dict]]) -> List[Dict]:
112174 "data" : {"text" : message }
113175 })
114176 else :
115- message_list .extend (message )
177+ # 添加消息段数组,并在需要的地方插入空格分隔
178+ for segment in message :
179+ message_list .append (segment )
180+
181+ # 在相邻的文本段之间添加空格
182+ self ._insert_text_separators (message_list )
116183
117184 return message_list
185+
186+ def _insert_text_separators (self , message_list : List [Dict ]):
187+ """
188+ 在相邻的文本消息段之间插入空格分隔
189+
190+ :param message_list: 消息段数组
191+ """
192+ result = []
193+ for i , segment in enumerate (message_list ):
194+ seg_type = segment .get ("type" , "" )
195+
196+ # 添加当前段
197+ result .append (segment )
198+
199+ # 检查是否需要在当前段和下一段之间添加空格
200+ if i < len (message_list ) - 1 :
201+ next_seg = message_list [i + 1 ]
202+ next_type = next_seg .get ("type" , "" )
203+
204+ # 在特定类型的消息段之间添加空格
205+ # 当前段和下一段都是文本
206+ if seg_type == "text" and next_type == "text" :
207+ result .append ({"type" : "text" , "data" : {"text" : " " }})
208+ # 当前段是 @,下一段是文本(且文本不以空格开头)
209+ elif seg_type == "at" and next_type == "text" :
210+ next_text = next_seg .get ("data" , {}).get ("text" , "" )
211+ if next_text and not next_text .startswith (" " ):
212+ result .append ({"type" : "text" , "data" : {"text" : " " }})
213+ # 当前段是文本(且不以空格结尾),下一段是 @
214+ elif seg_type == "text" and next_type == "at" :
215+ current_text = segment .get ("data" , {}).get ("text" , "" )
216+ if current_text and not current_text .endswith (" " ):
217+ result .append ({"type" : "text" , "data" : {"text" : " " }})
218+
219+ # 替换原数组
220+ message_list .clear ()
221+ message_list .extend (result )
118222
119223 def _send (self , message_array : List [Dict ]):
120224 """
@@ -126,6 +230,9 @@ def _send(self, message_array: List[Dict]):
126230 # 添加链式修饰
127231 if self ._at_user_ids or self ._at_all or self ._reply_message_id :
128232 message_array = self ._build_message_array (message_array )
233+ else :
234+ # 没有链式修饰时,也需要添加分隔符
235+ self ._insert_text_separators (message_array )
129236
130237 return asyncio .create_task (
131238 self ._adapter .call_api (
@@ -178,6 +285,9 @@ def Raw_ob12(self, message, **kwargs):
178285 # 添加链式修饰
179286 if self ._at_user_ids or self ._at_all or self ._reply_message_id :
180287 ob11_message = self ._build_message_array (ob11_message )
288+ else :
289+ # 没有链式修饰时,也需要添加分隔符
290+ self ._insert_text_separators (ob11_message )
181291
182292 return asyncio .create_task (
183293 self ._adapter .call_api (
@@ -230,35 +340,66 @@ def Recall(self, message_id: Union[str, int]):
230340
231341 def File (self , file : Union [str , bytes ], filename : str = "file.dat" ):
232342 """
233- 发送文件(通用接口)
343+ 发送文件
234344
235- 注意:OneBot11 标准没有独立的文件消息段类型
236- 此方法会根据文件类型自动选择合适的发送方式:
237- - 图片文件:使用 Image
238- - 音频文件:使用 Voice
239- - 视频文件:使用 Video
240- - 其他文件:尝试发送(可能不被支持)
345+ 使用 OneBot11 的 file 消息段发送文件
241346
242- :param file: 文件内容(bytes)或 URL(str)
347+ :param file: 文件内容(bytes)或路径/ URL(str)
243348 :param filename: 文件名
244349 :return: asyncio.Task
245350 """
246- # 判断文件类型
247- if isinstance (file , str ):
248- # URL 方式,无法判断类型,尝试作为普通文本发送
249- return self ._send ([{"type" : "text" , "data" : {"text" : f"[文件] { file } " }}])
250-
251- # 根据 filename 判断类型
252- filename_lower = filename .lower ()
253- if any (ext in filename_lower for ext in ['.jpg' , '.jpeg' , '.png' , '.gif' , '.bmp' , '.webp' ]):
254- return self .Image (file , filename )
255- elif any (ext in filename_lower for ext in ['.mp3' , '.amr' , '.wav' , '.ogg' , '.flac' , '.m4a' ]):
256- return self .Voice (file , filename )
257- elif any (ext in filename_lower for ext in ['.mp4' , '.avi' , '.mov' , '.mkv' , '.flv' , '.wmv' ]):
258- return self .Video (file , filename )
351+ if isinstance (file , bytes ):
352+ # bytes 类型,优先尝试 base64 方式
353+ try :
354+ b64_data = base64 .b64encode (file ).decode ('utf-8' )
355+ return self ._send ([{
356+ "type" : "file" ,
357+ "data" : {
358+ "file" : f"base64://{ b64_data } " ,
359+ "name" : filename
360+ }
361+ }])
362+ except Exception as e :
363+ self ._adapter .logger .warning (f"Base64发送文件失败: { str (e )} " )
364+ # 创建临时文件
365+ temp_dir = os .path .join (tempfile .gettempdir (), "onebot_media" )
366+ os .makedirs (temp_dir , exist_ok = True )
367+ unique_filename = f"{ uuid .uuid4 ().hex } _{ filename } "
368+ filepath = os .path .join (temp_dir , unique_filename )
369+
370+ try :
371+ with open (filepath , "wb" ) as f :
372+ f .write (file )
373+
374+ task = self ._send ([{
375+ "type" : "file" ,
376+ "data" : {
377+ "file" : filepath ,
378+ "name" : filename
379+ }
380+ }])
381+
382+ # 延迟删除,确保发送完成
383+ async def delayed_cleanup ():
384+ await asyncio .sleep (1 )
385+ try :
386+ os .remove (filepath )
387+ except Exception :
388+ pass
389+ asyncio .create_task (delayed_cleanup ())
390+
391+ return task
392+ except Exception :
393+ pass
259394 else :
260- # 其他类型,尝试发送
261- return self ._send ([{"type" : "text" , "data" : {"text" : f"[文件] { filename } " }}])
395+ # 路径或 URL,直接发送
396+ return self ._send ([{
397+ "type" : "file" ,
398+ "data" : {
399+ "file" : file ,
400+ "name" : filename
401+ }
402+ }])
262403
263404 def _convert_ob12_to_ob11 (self , message : List [Dict ]) -> List [Dict ]:
264405 """
@@ -805,4 +946,4 @@ async def shutdown(self):
805946 self .logger .error (f"关闭session失败: { str (e )} " )
806947 self .sessions .clear ()
807948
808- self .logger .info ("OneBot11适配器已关闭" )
949+ self .logger .info ("OneBot11适配器已关闭" )
0 commit comments