mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-07-26 15:51:26 +00:00
Update docs and other details
This commit is contained in:
28
docs/Context.md
Normal file
28
docs/Context.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 统一消息上下文
|
||||
|
||||
消息上下文是本程序中最重要的概念,也是贯穿整个程序执行流程的一个对象,无论命令、过滤器或是其它插件类型,都需要使用这个对象,在代码中常以 `ctx_msg`、`ctx` 等形式出现,它是字典类型,其中保存了当前(目前正在处理的这条)消息的上报类型、消息内容、消息类型、发送者、接受者、消息源名称等等重要信息。消息源适配器的主要工作,就是将不同消息源的上报数据统一成下方所定义的消息上下文的格式。
|
||||
|
||||
下面定义一个统一化之后的消息上下文对象应当符合的形式:
|
||||
|
||||
| 字段名 | 是否必须(是/否/建议) | 数据类型 | 支持的值 | 说明 |
|
||||
| ---------------------------- | ------------ | ---- | --------------------------- | ---------------------------------------- |
|
||||
| `raw_ctx` | 是 | dict | - | 消息源上报过来的原始数据 |
|
||||
| `post_type` | 是 | str | `message` | 上报类型,可以随意设置,但此字段值非 `message` 的消息上下文将会被某一层过滤器过滤掉 |
|
||||
| `time` | 建议 | int | - | 消息、事件等发生的时间戳 |
|
||||
| `msg_id` | 建议 | str | - | 消息的唯一 ID |
|
||||
| `msg_type` | 是 | str | `private`、`group`、`discuss` | 消息类型,标识私聊消息或群组消息等 |
|
||||
| `format` | 是 | str | `text`、`media` 等 | 可随意填写,但实际上能够直接理解的只有 `text`,对于 `media` 格式,只有语音识别过滤器对 Mojo-Webxin 进行了单独支持 |
|
||||
| `content` | 是 | str | - | 消息内容文本 |
|
||||
| `receiver` | 建议 | str | - | 接受者显示名(当有备注名时为备注名,否则为昵称,见表格下方注释 1) |
|
||||
| `receiver_name` | 否 | str | - | 接受者昵称 |
|
||||
| `receiver_id`/`receiver_tid` | 建议 | str | - | 接受者 ID(分别是固定 ID 和 临时 ID,见注释 2) |
|
||||
| `sender` | 建议 | str | - | 发送者显示名(当有备注名时为备注名,否则为昵称) |
|
||||
| `sender_name` | 否 | str | - | 发送者昵称 |
|
||||
| `sender_id`/`sender_tid` | 是 | str | - | 发送者 ID |
|
||||
| `group` | 建议 | str | - | 来源群组显示名 |
|
||||
| `group_id`/`group_tid` | 是(当为群组消息时) | str | - | 来源群组 ID |
|
||||
| `discuss` | 建议 | str | - | 来源讨论组显示名 |
|
||||
| `discuss_id`/`discuss_tid` | 是(当为讨论组消息时) | str | - | 来源讨论组 ID |
|
||||
|
||||
- 注释 1:消息的接受者通常情况下表示当前消息源中登录的账号。
|
||||
- 注释 2:所有 `xxx_id` 和 `xxx_tid` 分别表示固定 ID 和临时 ID:固定 ID(`xxx_id`)表示重新登录不会变的 ID,通常即为该消息平台的账号(微信 ID、QQ 号);临时 ID(`xxx_tid`)表示在消息源的此次登录中不会变的 ID,但下次登录可能同一个用户的 ID 和上次不同,对于某些平台(如微信),可能有时完全无法获取到固定 ID,此时临时 ID 将成为发送消息时的重要依据。当没有(或无法获取)固定 ID 时,可将固定 ID 置空,如果同时也没有临时 ID,将意味着程序可能无法回复消息(因为没有任何能够唯一标记消息来源的值),当有固定 ID 没有临时 ID 时,应直接将临时 ID 设置为和固定 ID 相同。
|
9
docs/Message_Sources.md
Normal file
9
docs/Message_Sources.md
Normal file
@ -0,0 +1,9 @@
|
||||
# 消息源列表
|
||||
|
||||
「消息源」在文档的某些位置可能还称为「消息平台」「聊天平台客户端」「消息源客户端」「机器人前端」等。比如网上很多开源的 SmartQQ 封装或网页微信封装,我们这里就称他们为「消息源」,他们的功能通常是模拟登录账号、维护登录状态、上报接收到的消息、通过某种方式调用接口发送消息等。通过适配器,本程序可以支持多种消息源,下面是目前所支持的消息源列表:
|
||||
|
||||
| 消息源名称 | 官网/项目地址 | 配置文件中定义时必填项 |
|
||||
| -------------------- | ---------------------------------------- | ----------------- |
|
||||
| mojo_webqq | https://github.com/sjdy521/Mojo-Webqq | `api_url`:API 根地址 |
|
||||
| mojo_weixin | https://github.com/sjdy521/Mojo-Weixin | `api_url`:API 根地址 |
|
||||
| coolq_http_api(即将支持) | https://github.com/richardchien/coolq-http-api | |
|
152
docs/Write_Adapter.md
Normal file
152
docs/Write_Adapter.md
Normal file
@ -0,0 +1,152 @@
|
||||
# 编写消息源适配器
|
||||
|
||||
消息源适配器是用来在消息源和本程序之间进行数据格式的一类程序,相当于一个驱动程序,通过不同的驱动程序,本程序便可以接入多种聊天平台。后文中简称为「适配器」。
|
||||
|
||||
通常情况下一个消息源需要能够支持通过 HTTP 来上报消息和调用操作,才能够便于开发适配器,不过实际上如果有需求,你也可以直接在适配器中对程序的 HTTP 服务端进行请求,例如某些直接以模块形式给出的消息平台客户端,通过回调函数来通知事件,此时你可以在这个事件的回调函数中,手动请求本程序的上报地址并发送相应的数据。但这不在此文的讨论范围之内,这属于另一类适配器,与本程序无直接关联。
|
||||
|
||||
我们这里讨论在本程序接收到 HTTP 上报消息之后、及内部逻辑中产生了对适配器的接口调用之后,需要将上报数据转换成本程序能够识别的数据格式,或将本程序中发出的接口调用转换成消息源客户端能够识别的接口调用,例如我们调用 `adapter.send_private_message`,相对应的适配器将会在内部通过 HTTP 请求这个消息源客户端的用来发送私聊消息的接口。
|
||||
|
||||
为了形象的理解,你可能需要去参考已有的那些适配器的代码。
|
||||
|
||||
## 写法
|
||||
|
||||
其实写起来非常简单,就和那些 Web 框架的 Handler 一样,继承一个基类,实现某几个固定的函数接口即可,这里需要继承的是 `msg_src_adapter.py` 中的 `Adapter` 类,此基类中已经实现了一些通用的、或默认的逻辑,对于像 `unitize_context`(上报数据统一化)、`send_private_message`(发送私聊消息)、`get_sender_group_role`(获取发送者在群组中的身份)等等接口,通常需要在子类中进行具体的、差异化的操作。
|
||||
|
||||
我们直接以 Mojo-Webqq 的适配器 `msg_src_adapters/mojo_webqq.py` 为例,代码如下(可能不是最新):
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
from msg_src_adapter import Adapter, as_adapter, ConfigurationError
|
||||
|
||||
|
||||
@as_adapter(via='mojo_webqq')
|
||||
class MojoWebqqAdapter(Adapter):
|
||||
def __init__(self, config: dict):
|
||||
super().__init__(config)
|
||||
if not config.get('api_url'):
|
||||
raise ConfigurationError
|
||||
self.api_url = config['api_url']
|
||||
|
||||
def unitize_context(self, ctx_msg: dict):
|
||||
new_ctx = {'raw_ctx': ctx_msg, 'post_type': ctx_msg['post_type'], 'via': ctx_msg['via'],
|
||||
'login_id': ctx_msg['login_id']}
|
||||
if new_ctx['post_type'] != 'receive_message':
|
||||
return new_ctx
|
||||
new_ctx['post_type'] = 'message' # Just handle 'receive_message', and make 'post_type' 'message'
|
||||
new_ctx['time'] = ctx_msg['time']
|
||||
new_ctx['msg_id'] = str(ctx_msg['id'])
|
||||
new_ctx['msg_type'] = ctx_msg['type'].split('_')[0]
|
||||
new_ctx['msg_type'] = 'private' if new_ctx['msg_type'] == 'friend' else new_ctx['msg_type']
|
||||
new_ctx['format'] = 'text'
|
||||
new_ctx['content'] = ctx_msg['content']
|
||||
|
||||
new_ctx['receiver'] = ctx_msg.get('receiver', '')
|
||||
new_ctx['receiver_name'] = (requests.get(self.api_url + '/get_user_info').json() or {}).get('name', '')
|
||||
new_ctx['receiver_id'] = str(ctx_msg.get('receiver_uid', ''))
|
||||
new_ctx['receiver_tid'] = str(ctx_msg.get('receiver_id', ''))
|
||||
|
||||
new_ctx['sender'] = ctx_msg.get('sender', '')
|
||||
friend = list(filter(
|
||||
lambda f: f.get('uid') == ctx_msg['sender_uid'],
|
||||
requests.get(self.api_url + '/get_friend_info').json() or []
|
||||
))
|
||||
new_ctx['sender_name'] = friend[0].get('name', '') if friend else ''
|
||||
new_ctx['sender_id'] = str(ctx_msg.get('sender_uid', ''))
|
||||
new_ctx['sender_tid'] = str(ctx_msg.get('sender_id', ''))
|
||||
|
||||
if new_ctx['msg_type'] == 'group':
|
||||
new_ctx['group'] = ctx_msg.get('group', '')
|
||||
new_ctx['group_id'] = str(ctx_msg.get('group_uid', ''))
|
||||
new_ctx['group_tid'] = str(ctx_msg.get('group_id', ''))
|
||||
|
||||
if new_ctx['msg_type'] == 'discuss':
|
||||
new_ctx['discuss'] = ctx_msg.get('discuss', '')
|
||||
new_ctx['discuss_tid'] = str(ctx_msg.get('discuss_id', ''))
|
||||
|
||||
return new_ctx
|
||||
|
||||
def get_login_info(self, ctx_msg: dict):
|
||||
json = requests.get(self.api_url + '/get_user_info').json()
|
||||
if json:
|
||||
json['user_tid'] = json.get('id')
|
||||
json['user_id'] = json.get('uid')
|
||||
json['nickname'] = json.get('name')
|
||||
return json
|
||||
|
||||
def _get_group_info(self):
|
||||
return requests.get(self.api_url + '/get_group_info').json()
|
||||
|
||||
def get_sender_group_role(self, ctx_msg: dict):
|
||||
groups = list(filter(
|
||||
lambda g: str(g.get('id')) == ctx_msg['raw_ctx'].get('group_id'),
|
||||
self._get_group_info() or []
|
||||
))
|
||||
if len(groups) <= 0 or 'member' not in groups[0]:
|
||||
# This is strange, not likely happens
|
||||
return 'member'
|
||||
members = list(filter(
|
||||
lambda m: str(m.get('id')) == ctx_msg['raw_ctx'].get('sender_id'),
|
||||
groups[0].get('member')
|
||||
))
|
||||
if len(members) <= 0:
|
||||
# This is strange, not likely happens
|
||||
return 'member'
|
||||
return members[0].get('role', 'member')
|
||||
|
||||
def send_private_message(self, target: dict, content: str):
|
||||
params = None
|
||||
if target.get('user_id'):
|
||||
params = {'uid': target.get('user_id')}
|
||||
elif target.get('user_tid'):
|
||||
params = {'id': target.get('user_tid')}
|
||||
|
||||
if params:
|
||||
params['content'] = content
|
||||
requests.get(self.api_url + '/send_friend_message', params=params)
|
||||
|
||||
def send_group_message(self, target: dict, content: str):
|
||||
params = None
|
||||
if target.get('group_id'):
|
||||
params = {'uid': target.get('group_id')}
|
||||
elif target.get('group_tid'):
|
||||
params = {'id': target.get('group_tid')}
|
||||
|
||||
if params:
|
||||
params['content'] = content
|
||||
requests.get(self.api_url + '/send_group_message', params=params)
|
||||
|
||||
def send_discuss_message(self, target: dict, content: str):
|
||||
params = None
|
||||
if target.get('discuss_tid'):
|
||||
params = {'id': target.get('discuss_tid')}
|
||||
|
||||
if params:
|
||||
params['content'] = content
|
||||
requests.get(self.api_url + '/send_discuss_message', params=params)
|
||||
```
|
||||
|
||||
代码逻辑上很简单,首先调用 `@as_adapter(via='mojo_webqq')` 把类注册为适配器,`via` 就是配置文件里定义消息源时候要填的那个 `via`,同时也是上报消息路径里的那个 `via`。初始化函数里面要求配置文件中的消息源定义里必须有 `api_url`。
|
||||
|
||||
`unitize_context` 函数是用来统一上报消息上下文的,这个上下文(Context)是一个字典类型,在整个程序中起核心作用,此函数需要将消息源发送来的数据转换成本程序能够理解的格式,也就是对字段进行翻译,需要翻译成一个统一的格式,这个格式见 [统一消息上下文](https://cczu-dev.github.io/xiaokai-bot/#/Context)。
|
||||
|
||||
其它的函数就是对调用操作的翻译,例如把 `send_group_message` 的调用翻译成对 `self.api_url + '/send_group_message'` 的 HTTP 请求。
|
||||
|
||||
### 消息发送目标的定义
|
||||
|
||||
由于发送消息使用一个统一接口,插件中调用时并不知道是哪个适配器接收到调用,所以发送消息的目标同样是需要统一的,也即 `send_message` 函数的 `target` 参数,此参数应当和消息上下文兼容,也就是说,当调用发送消息的接口时,直接把消息上下文传入,就应当能正确发送到此消息上下文所在的语境(比如和某个用户的私聊消息或某个群组中)。
|
||||
|
||||
它主要应当接受如下字段:
|
||||
|
||||
| 字段名 | 说明 |
|
||||
| -------------------------- | ---------------------------------------- |
|
||||
| `user_id`/`user_tid` | 消息要发送的对象(私聊用户)的 ID |
|
||||
| `group_id`/`group_tid` | 要发送的群组 ID |
|
||||
| `discuss_id`/`discuss_tid` | 要发送的讨论组 ID |
|
||||
| `content` | 消息内容,通常是 str 类型,目前所有适配器只支持发送文本消息(str 类型) |
|
||||
|
||||
以上所有 `xxx_id` 和 `xxx_tid` 分别表示固定 ID 和临时 ID,这和消息上下文中的定义一样,即,固定 ID(`xxx_id`)表示重新登录不会变的 ID,通常即为该消息平台的账号(微信 ID、QQ 号),临时 ID(`xxx_tid`)表示在消息源的此次登录中不会变的 ID,但下次登录可能同一个用户的 ID 和上次不同,对于某些平台(如微信),可能有时完全无法获取到固定 ID,此时临时 ID 将成为发送消息时的重要依据。
|
||||
|
||||
### 其它
|
||||
|
||||
对于需要对群组中用户身份进行区分的情况,例如某些命令只允许群组管理员运行,要实现 `get_sender_group_role` 函数,此函数返回的成员身份应为 `member`、`admin`、`owner` 三者之一。
|
@ -35,7 +35,9 @@ def list_all(args_text, ctx_msg):
|
||||
|
||||
这样可以在保持高级用户可以通过简洁的方式调用命令的同时,避免不同命令仓库下同名命令都被调用的问题(因为在默认情况下命令中心在调用命令时,不同仓库中的同名命令都会被依次调用)。
|
||||
|
||||
16.12.29 注:由于微信限制,无法获取到群组中成员的身份(普通成员还是管理员或群主),因此这里的对群组的限制在微信上不起效果,超级用户限制在能够获取到发送者微信 ID 的情况下有效。
|
||||
16.12.29 注:由于 Mojo-Weixin 无法获取到群组中成员的身份(普通成员还是管理员或群主),因此这里的对群组的限制在微信上不起效果,超级用户限制在能够获取到发送者微信 ID 的情况下有效。
|
||||
|
||||
17.2.15 注:如果使用 Mojo-Webqq 作为消息源,现在也无法获取群组中的成员身份了,因此造成对群成员身份有要求的命令在此种情况下也无法使用。
|
||||
|
||||
## 命令中心 Command Hub
|
||||
|
||||
@ -75,7 +77,9 @@ Source 表示命令的来源(由谁发出),Target 表示命令将对谁产
|
||||
|
||||
至于如何获取 Source 和 Target,可用 `little_shit.py` 中的 `get_source` 和 `get_target` 函数,当然,如果你对默认的行为感到不满意,也可以自己去实现不一样的区分方法。
|
||||
|
||||
16.12.29 注:在支持了微信之后,此处有所变化,由于微信消息的限制,有时候无法获得发送者的微信 ID,而群组甚至没有一个固定 ID,因此,这里对 Source 和 Target 做一个精确定义:Source 是一个用来表示当前消息的发送者的唯一值,但重新登录后可能变化,并且每次获取,一定可以获取到;Target 是一个用来表示当前消息所产生的效果需要作用的对象,这个值是永久(或至少长期)不变的,如果当前的消息语境下不存在这样的值,则为 None。
|
||||
16.12.29 注:在支持了 Mojo-Weixin 消息源之后,此处有所变化,由于一些限制,有时候无法获得发送者的微信 ID,而群组甚至没有一个固定 ID,因此,这里对 Source 和 Target 做一个精确定义:Source 是一个用来表示当前消息的发送者的唯一值,但重新登录后可能变化,并且每次获取,一定可以获取到;Target 是一个用来表示当前消息所产生的效果需要作用的对象,这个值是永久(或至少长期)不变的,如果当前的消息语境下不存在这样的值,则为 None。
|
||||
|
||||
17.2.15 注:在使用了适配器模式后,`get_source` 和 `get_target` 函数的实现实际上已经移到了 `msg_src_adapter.py` 中的 `Adapter` 基类。
|
||||
|
||||
## 交互式命令 Interactive Command
|
||||
|
||||
|
@ -37,11 +37,10 @@ def _interceptor(ctx_msg):
|
||||
|
||||
## 现有的几个重要过滤器
|
||||
|
||||
| 文件 | 优先级 | 作用 | 备注 |
|
||||
| ------------------------------------- | ----- | ---------------------------------------- | -------------------------------------- |
|
||||
| unitize_context_message_10000.py | 10000 | 对来自不同平台(QQ、微信)的消息上下文进行统一化,以避免耦合 | 不建议添加比它优先级更高的过滤器 |
|
||||
| message_logger_1000.py | 1000 | 把收到的消息打印在标准输出 | 不建议添加比它优先级更高的过滤器 |
|
||||
| intercept_some_message_formats_100.py | 100 | 拦截某些不支持的消息类型,对于文本消息,会把 `content` 字段复制到 `text` 字段 | 如果要自己编写插件,这里可以按需修改 |
|
||||
| speech_recognition_90.py | 90 | 对语音消息进行语音识别(仅私聊消息),并把识别出的文字放到 `text` 字段,并标记 `from_voice` 字段为 True | 如果不需要可以删掉 |
|
||||
| split_at_xiaokai_50.py | 50 | 分离群组和讨论组中消息开头的 `@CCZU 小开`,并更新 `text` 字段为剩余部分 | 也就是说通过此过滤器的消息,就是确定用户的意图就是和这个 bot 说话的消息 |
|
||||
| command_dispatcher_0.py | 0 | 识别消息中的命令,并进行相应的调用 | |
|
||||
| 文件 | 优先级 | 作用 | 备注 |
|
||||
| ------------------------------------- | ---- | ---------------------------------------- | -------------------------------------- |
|
||||
| message_logger_1000.py | 1000 | 把收到的消息打印在标准输出 | 不建议添加比它优先级更高的过滤器 |
|
||||
| intercept_some_message_formats_100.py | 100 | 拦截某些不支持的消息类型,对于文本消息,会把 `content` 字段复制到 `text` 字段 | 如果要自己编写插件,这里可以按需修改 |
|
||||
| speech_recognition_90.py | 90 | 对语音消息进行语音识别(仅私聊消息),并把识别出的文字放到 `text` 字段,并标记 `from_voice` 字段为 True | 此过滤器只对 Mojo-Weixin 消息源生效,如果不需要可以删掉 |
|
||||
| split_at_xiaokai_50.py | 50 | 分离群组和讨论组中消息开头的 `@CCZU 小开`,并更新 `text` 字段为剩余部分 | 也就是说通过此过滤器的消息,就是确定用户的意图就是和这个 bot 说话的消息 |
|
||||
| command_dispatcher_0.py | 0 | 识别消息中的命令,并进行相应的调用 | |
|
@ -9,16 +9,25 @@ self.$config = {
|
||||
title: '首页', path: '/'
|
||||
},
|
||||
{
|
||||
title: '编写插件', type: 'dropdown',
|
||||
title: '消息源列表', path: '/Message_Sources'
|
||||
},
|
||||
{
|
||||
title: '开发', type: 'dropdown',
|
||||
items: [
|
||||
{
|
||||
title: '过滤器', path: '/Write_Filter'
|
||||
title: '统一消息上下文', path: '/Context'
|
||||
},
|
||||
{
|
||||
title: '命令', path: '/Write_Command'
|
||||
title: '编写消息源适配器', path: '/Write_Adapter'
|
||||
},
|
||||
{
|
||||
title: '自然语言处理器', path: '/Write_NLProcessor'
|
||||
title: '编写过滤器', path: '/Write_Filter'
|
||||
},
|
||||
{
|
||||
title: '编写命令', path: '/Write_Command'
|
||||
},
|
||||
{
|
||||
title: '编写自然语言处理器', path: '/Write_NLProcessor'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user