Skip to content

Commit 75effc8

Browse files
committed
feat: 增强文件发送功能并添加方法名大小写不敏感支持
添加文件类型检测库filetype,改进文件发送方法支持多种格式 实现方法名映射表支持大小写不敏感调用 优化消息段间空格分隔处理逻辑
1 parent 85cfc56 commit 75effc8

3 files changed

Lines changed: 174 additions & 33 deletions

File tree

OneBotAdapter/Core.py

Lines changed: 168 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import tempfile
88
import uuid
9+
import filetype
910
from fastapi import WebSocket, WebSocketDisconnect
1011
from typing import Dict, List, Optional, Union
1112
from 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适配器已关闭")

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "ErisPulse-OneBot11Adapter"
3-
version = "3.6.0"
3+
version = "3.6.1"
44
description = "ErisPulse的OneBotV11协议适配模块,异步的OneBot触发器"
55
readme = "README.md"
6-
requires-python = ">=3.9"
6+
requires-python = ">=3.10"
77
license = { file = "LICENSE" }
88
authors = [ { name = "wsu2059q", email = "wsu2059@qq.com" } ]
99
dependencies = [
10-
10+
"filetype>=1.2.0"
1111
]
1212

1313
[project.urls]
@@ -18,4 +18,4 @@ onebot11 = "OneBotAdapter:OneBotAdapter"
1818
qq = "OneBotAdapter:OneBotAdapter"
1919

2020
[tool.setuptools]
21-
packages = ["OneBotAdapter"]
21+
packages = ["OneBotAdapter"]

test/test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ async def _execute_test(self, test_num: int) -> Optional[Any]:
308308
elif test_num == 12:
309309
file_data = self._read_file(self.config.doc_file)
310310
if file_data:
311-
return await self.adapter.To("group", group_id).File(file_data)
311+
return await self.adapter.To("group", group_id).File(file_data, self.config.doc_file)
312312
return await self.adapter.To("group", group_id).File(self.config.file_url)
313313

314314
# 13. 发送文件(URL)
@@ -526,4 +526,4 @@ async def main():
526526

527527

528528
if __name__ == "__main__":
529-
asyncio.run(main())
529+
asyncio.run(main())

0 commit comments

Comments
 (0)