简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

ErisPulse 发送方法规范

本文档定义了 ErisPulse 适配器中 Send 类发送方法的命名规范、参数规范和反向转换要求。

0. 关键词约定

本文档中的 必须(MUST)、应当(SHOULD)、可以(MAY) 按以下语义解释(参照 RFC 2119):

关键词 语义 违反后果
必须 强制要求,框架行为/跨平台一致性依赖它 适配器视为不符合标准,模块代码可能无法工作
应当 强烈推荐;除非有充分理由,否则遵循 偏离时须在适配器文档中说明原因与替代行为
可以 可选项,按平台能力自行决定 无

1. 标准方法命名

所有发送方法使用 大驼峰命名法(PascalCase),首字母大写。

1.1 标准发送方法

方法名 说明 参数类型 实现要求
Text 发送文本消息 str 必须
Image 发送图片 str | bytes 必须(基类已内置,见 §6.4)
Voice 发送语音 str | bytes 必须(基类已内置;平台不支持语音时按 §2.1.5 降级)
Video 发送视频 str | bytes 必须(基类已内置;平台不支持视频时按 §2.1.5 降级)
File 发送文件 str | bytes,filename: str | None = None 必须(基类已内置)
At @用户/群组 str (user_id) 修饰方法,按需
Face 发送表情 str (emoji) 可以
Reply 回复消息 str (message_id) 修饰方法,按需
Forward 转发消息 str (message_id) 可以
Markdown 发送 Markdown 消息 str 可以
HTML 发送 HTML 消息 str 可以
Card 发送卡片消息 dict 可以

标准方法(Text/Image/Voice/Video/File)由基类 SendDSL 内置并默认委托 Raw_ob12,适配器无需重复实现即可获得类型签名;仅当平台需要特殊逻辑时才覆盖单个方法(见 §6.4)。

1.2 链式修饰方法

方法名 说明 参数类型
At @用户(可多次调用) str (user_id)
AtAll @全体成员 无
Reply 回复消息 str (message_id)

1.3 协议方法

方法名 说明 是否必须
Raw_ob12 发送 OneBot12 格式消息段 必须

Raw_ob12 是必须实现的方法。这是适配器的核心职责之一:接收 OneBot12 标准消息段并将其转换为平台原生 API 调用。Raw_ob12 是反向转换(OneBot12 → 平台)的统一入口,确保模块可以不依赖平台特有方法,直接使用标准消息段发送消息。

未重写 Raw_ob12 时的行为:基类默认实现会记录 error 级别日志并返回标准错误响应格式(status: "failed", retcode: 10002),提示适配器开发者必须实现此方法。

1.4 推荐的扩展命名约定

适配器如需支持发送非 OneBot12 格式的原始数据(如平台特定 JSON、XML 等),推荐使用以下命名约定:

推荐方法名 说明
Raw_json 发送任意 JSON 数据
Raw_xml 发送任意 XML 数据

注意:这些方法不是基类提供的默认方法,也不强制要求实现。它们仅作为命名约定,适配器可根据需要自行定义。如果适配器不支持这些格式,则无需定义。

消息构建器(MessageBuilder):ErisPulse 提供了 MessageBuilder 工具类,用于方便地构建 OneBot12 消息段列表,配合 Raw_ob12 使用。详见 消息构建器 章节。

2. 参数规范详解

2.1 媒体消息发送协议(Image / Voice / Video / File)

本节是媒体发送的统一协议标准:模块以同一份代码调用四个媒体方法,适配器负责把 file 参数的各种形态转换为平台原生上传/发送行为。

2.1.1 file 参数的合法形态

形态 示例 适配器要求
HTTP(S) URL https://example.com/image.jpg 必须接受
本地文件路径 /path/to/file.jpg、C:\path\to\file.jpg 必须接受
二进制数据 b"\x89PNG..." 必须接受
file:// URI file:///path/to/file.jpg 应当接受(可转发为本地路径处理)
Base64 字符串 / Data URI iVBORw0KGgo=...、data:image/png;base64,... 应当接受(与 OneBot12 生态惯例兼容)

适配器必须在三种必须形态上行为一致——模块无论传 URL、路径还是 bytes, 收到的都是同一条消息。平台无法直接使用某形态时(如平台 API 不支持引用外部 URL), 由适配器自行下载/读取后上传,不得要求模块换形态重试。

2.1.2 形态判定顺序

适配器实现媒体参数处理时,应当按以下顺序判定形态:

  1. bytes 类型 → 直接上传
  2. 字符串以 http:// / https:// 开头 → 按 URL 处理(直接引用或下载后上传,按平台能力)
  3. 字符串以 file:// 开头 → 剥离前缀按本地路径处理
  4. 其余字符串 → 按本地路径处理(存在则读取上传;不存在则返回标准错误响应)
def _resolve_media(self, file: "str | bytes") -> bytes:
    """形态判定与归一化(示例)"""
    if isinstance(file, (bytes, bytearray)):
        return bytes(file)
    if file.startswith(("http://", "https://")):
        return self._download(file)          # 平台不能引用 URL 时下载
    if file.startswith("file://"):
        file = file[len("file://"):]
    with open(file, "rb") as f:              # 本地路径
        return f.read()

2.1.3 File 的文件名语义

File 方法签名:File(file, filename=None)(filename 为可选参数,基类已内置)。

文件名推导顺序(适配器在未显式提供 filename 时按此生成):

  1. 显式 filename 参数(最高优先)
  2. URL 的 basename(如 https://host/a/b/report.pdf → report.pdf,须剥离 query string)
  3. 本地路径的 basename(如 /tmp/data/backup.zip → backup.zip)
  4. 平台默认生成(如 file_{timestamp};应当保留真实扩展名——扩展名影响平台侧的 类型识别与预览行为)

Image / Voice / Video 同样可以接受 filename(经消息段 data.filename 传递), 但仅 File 的文件名有跨平台语义保证。

2.1.4 平台限制的声明义务

各平台对媒体的大小上限、格式(MIME)、时长(音视频)等约束不同。适配器应当:

2.1.5 能力降级阶梯

平台不支持某个媒体类型时,按以下阶梯降级(遵循总纲"能力降级不报错"原则):

场景 降级行为
Voice 不支持语音消息 应当按 File(或平台近缘形态)发送;无法表达时返回 retcode=10002
Video 不支持视频消息 同上
媒体类型完全不支持(无文件能力) 返回 retcode=10002,message 注明不支持的数据类型
形态不支持(如无法处理 base64) 返回 retcode=10002,可以在 message 中提示模块改用 URL/bytes

禁止的行为:静默丢弃(无返回)、抛出异常、要求模块编写平台分支处理。

2.2 @用户参数规范

方法: At(修饰方法)

参数: user_id (str)

要求:

示例:

# 单个 @ 用户
Send.To("group", "g123").At("123456").Text("你好")

# 多个 @ 用户(链式调用)
send.To("group", "g123").At("123456").At("789012").Text("大家好")

2.3 回复消息参数规范

方法: Reply(修饰方法)

参数: message_id (str)

要求:

示例:

send.To("group", "g123").Reply("msg_123456").Text("收到")

3. 平台特有方法命名

不推荐在 Send 类中直接添加平台前缀方法。建议使用通用方法名或 Raw_{协议} 方法。

不推荐:

def YunhuForm(self, form_id: str):  # ❌ 不推荐
    pass

def TelegramSticker(self, sticker_id: str):  # ❌ 不推荐
    pass

推荐:

def Form(self, form_id: str):  # ✅ 通用方法名
    pass

def Sticker(self, sticker_id: str):  # ✅ 通用方法名
    pass

def Raw_ob12(self, message):  # ✅ 发送 OneBot12 格式
    pass

扩展方法要求:

4. 参数命名规范

参数名 说明 类型
text 文本内容 str
file 媒体内容(URL / 路径 / 二进制,见 §2.1.1) str / bytes
filename 文件名(File 可选,见 §2.1.3) str / None
user_id 用户 ID str / int
group_id 群组 ID str / int
message_id 消息 ID str
data 数据对象(如卡片数据) dict

5. 返回值规范


6. 反向转换规范(OneBot12 → 平台)

适配器不仅需要将平台原生事件转换为 OneBot12 格式(正向转换),还必须提供将 OneBot12 消息段转换回平台原生 API 调用的能力(反向转换)。反向转换的统一入口是 Raw_ob12 方法。

6.1 转换模型

正向转换(接收方向)                反向转换(发送方向)
─────────────────                ─────────────────
平台原生事件                       OneBot12 消息段列表
    │                                  │
    ▼                                  ▼
Converter.convert()               Send.Raw_ob12()
    │                                  │
    ▼                                  ▼
OneBot12 标准事件                  平台原生 API 调用
(含 {platform}_raw)             (返回标准响应格式)

核心对称性:正向转换保留原始数据在 {platform}_raw 中,反向转换接受 OneBot12 标准格式并还原为平台调用。

6.2 Raw_ob12 实现规范

Raw_ob12 接收 OneBot12 标准消息段列表,必须将其转换为平台原生 API 调用。

方法签名:

def Raw_ob12(self, message_segments: List[Dict]) -> asyncio.Task:
    """
    发送 OneBot12 标准消息段

    :param message_segments: OneBot12 消息段列表
        [
            {"type": "text", "data": {"text": "Hello"}},
            {"type": "image", "data": {"file": "https://..."}},
            {"type": "mention", "data": {"user_id": "123"}},
        ]
    :return: asyncio.Task,await 后返回标准响应格式
    """

实现要求:

  1. 必须处理所有标准消息段类型:至少支持 text、image、audio、video、file、mention、reply
  2. 必须处理平台扩展消息段:对于 {platform}_xxx 类型的消息段,转换为平台对应的原生调用
  3. 必须返回标准响应格式:遵循 API 响应标准
  4. 不支持的消息段应跳过并记录警告,不应抛出异常导致整条消息发送失败

6.3 消息段转换规则

6.3.1 标准消息段转换

适配器必须实现以下标准消息段的转换:

OneBot12 消息段 转换要求
text 直接使用 data.text
image data.file 按 §2.1 媒体协议处理(三必须形态 + 判定顺序)
audio 同 image 处理逻辑
video 同 image 处理逻辑
file 同 image 处理逻辑;文件名按 §2.1.3 推导顺序处理 data.filename
mention 转换为平台的 @用户 机制(如 Telegram 的 entities,云湖的 at_uid)
reply 转换为平台的回复引用机制
face 转换为平台的表情发送机制,不支持则跳过
location 转换为平台的位置发送机制,不支持则跳过

6.3.2 平台扩展消息段转换

对于带平台前缀的消息段,适配器应识别并转换:

def _convert_ob12_segments(self, segments: List[Dict]) -> Any:
    """将 OneBot12 消息段转换为平台原生格式"""
    platform_prefix = f"{self._platform_name}_"
    
    for segment in segments:
        seg_type = segment["type"]
        seg_data = segment["data"]
        
        if seg_type.startswith(platform_prefix):
            # 平台扩展消息段 → 平台原生调用
            self._handle_platform_segment(seg_type, seg_data)
        elif seg_type in self._standard_segment_handlers:
            # 标准消息段 → 平台等价操作
            self._standard_segment_handlers[seg_type](seg_data)
        else:
            # 未知消息段 → 记录警告并跳过
            logger.warning(f"不支持的消息段类型: {seg_type}")

6.3.3 复合消息段处理

一条消息可能包含多个消息段,适配器需要正确处理复合消息:

# 模块发送包含文本+图片+@用户 的消息
await send.Raw_ob12([
    {"type": "mention", "data": {"user_id": "123"}},
    {"type": "text", "data": {"text": "你好"}},
    {"type": "image", "data": {"file": "https://example.com/img.jpg"}}
])

处理策略:

6.4 Raw_ob12 与标准方法的关系

适配器的标准发送方法(Text、Image 等)**已由 SendDSL 基类内置实现并默认委托给 Raw_ob12**,适配器子类无需重复实现:

class Send(SendDSL):
    def Raw_ob12(self, message_segments: List[Dict]) -> asyncio.Task:
        """核心实现:OneBot12 消息段 → 平台 API(必须实现)"""
        return asyncio.create_task(self._send_ob12(message_segments))

    # Text/Image/Voice/Video/File 已从基类继承,自动委托 Raw_ob12
    # 如需平台特定逻辑,可覆盖单个方法:
    # def Text(self, text: str) -> asyncio.Task:
    #     return self.Raw_ob12([{"type": "text", "data": {"text": text}}])

好处:

6.5 实现示例

class YunhuSend(SendDSL):
    """云湖平台 Send 实现"""
    
    def Raw_ob12(self, message_segments: list) -> asyncio.Task:
        """OneBot12 消息段 → 云湖 API 调用"""
        return asyncio.create_task(self._do_send(message_segments))
    
    async def _do_send(self, segments: list) -> dict:
        """实际发送逻辑"""
        # 1. 解析修饰器状态
        at_users = self._at_users or []
        reply_to = self._reply_to
        at_all = self._at_all
        
        # 2. 转换消息段
        yunhu_elements = []
        for seg in segments:
            seg_type = seg["type"]
            seg_data = seg["data"]
            
            if seg_type == "text":
                yunhu_elements.append({"type": "text", "content": seg_data["text"]})
            elif seg_type == "image":
                yunhu_elements.append({"type": "image", "url": seg_data["file"]})
            elif seg_type == "mention":
                at_users.append(seg_data["user_id"])
            elif seg_type == "reply":
                reply_to = seg_data["message_id"]
            elif seg_type == "yunhu_form":
                # 平台扩展消息段
                yunhu_elements.append({"type": "form", "form_id": seg_data["form_id"]})
            else:
                logger.warning(f"云湖不支持的消息段: {seg_type}")
        
        # 3. 调用云湖 API
        response = await self._call_yunhu_api(yunhu_elements, at_users, reply_to, at_all)
        
        # 4. 返回标准响应格式
        return {
            "status": "ok" if response["code"] == 0 else "failed",
            "retcode": response["code"],
            "data": {"message_id": response.get("msg_id", ""), "time": int(time.time())},
            "message_id": response.get("msg_id", ""),
            "message": "",
            "yunhu_raw": response
        }

7. 方法发现

模块开发者可以通过 API 查询适配器支持的发送方法(不要在模块中硬编码某平台的方法 清单——各适配器的扩展方法随版本演进,以运行时发现为准):

from ErisPulse import adapter

# 列出所有发送方法
methods = adapter.list_sends("myplatform")
# ["Batch", "Form", "Image", "Recall", "Sticker", "Text", ...]

# 查看方法详情
info = adapter.send_info("myplatform", "Form")
# {
#     "name": "Form",
#     "parameters": [{"name": "form_id", "type": "str", ...}],
#     "return_type": "Awaitable[Any]",
#     "docstring": "发送云湖表单"
# }

9. 适配器开发注意事项

关于如何正确重写 BaseAdapter、Send、Request 的 __init__,详见 适配器开发入门 - __init__ 注意事项。



10. 适配器实现检查清单

发送方法

媒体发送协议

反向转换


11. 消息构建器(MessageBuilder)

MessageBuilder 是 ErisPulse 提供的消息段构建工具,配合 Raw_ob12 使用,简化 OneBot12 消息段的构建过程。

11.1 导入

from ErisPulse.Core import MessageBuilder
# 或
from ErisPulse.Core.Event import MessageBuilder

11.2 链式调用构建

# 构建包含文本、图片、@用户的消息
segments = (
    MessageBuilder()
    .mention("123456")
    .text("你好,看看这张图")
    .image("https://example.com/img.jpg")
    .reply("msg_789")
    .build()
)

# 发送
await adapter.Send.To("group", "456").Raw_ob12(segments)

11.3 快速构建单段

# 快速构建单个消息段(返回 list[dict],可直接传给 Raw_ob12)
await adapter.Send.To("user", "123").Raw_ob12(MessageBuilder.text("Hello"))
await adapter.Send.To("group", "456").Raw_ob12(MessageBuilder.image("https://..."))
await adapter.Send.To("group", "456").Raw_ob12(MessageBuilder.mention("123"))
await adapter.Send.To("group", "456").Raw_ob12(MessageBuilder.reply("msg_id"))
await adapter.Send.To("group", "456").Raw_ob12(MessageBuilder.at_all())

11.4 配合 Event.reply_ob12 使用

from ErisPulse.Core import MessageBuilder

@message()
async def handle(event: Event):
    await event.reply_ob12(
        MessageBuilder()
        .mention(event.get_user_id())
        .text("收到你的消息")
        .build()
    )

11.5 支持的消息段方法

方法 说明 data 字段
text(text) 文本 text
image(file) 图片 file
audio(file) 音频 file
video(file) 视频 file
file(file, filename=None) 文件 file, filename(可选)
mention(user_id, user_name=None) @用户 user_id, user_name(可选)
at(user_id, user_name=None) @用户(mention 的别名) 同 mention
reply(message_id) 回复 message_id
at_all() @全体成员 {}
custom(type, data) 自定义/平台扩展 自定义

11.6 工具方法

builder = MessageBuilder().text("基础内容")

# 复制(深拷贝)
msg1 = builder.copy().image("img1").build()
msg2 = builder.copy().image("img2").build()

# 清空
builder.clear().text("新内容").build()

# 判断是否为空
if builder:
    print(f"包含 {len(builder)} 个消息段")

12. 相关文档