diff --git a/README.md b/README.md index 188aa905..d44226ae 100644 --- a/README.md +++ b/README.md @@ -6,46 +6,74 @@ [![QQ](https://img.shields.io/badge/qq-1647869577-orange.svg)](#) [![WeChat](https://img.shields.io/badge/wechat-cczu__xiaokai-brightgreen.svg)](#) -基于 [sjdy521/Mojo-Webqq](https://github.com/sjdy521/Mojo-Webqq) 和 [sjdy521/Mojo-Weixin](https://github.com/sjdy521/Mojo-Weixin) 实现的自动处理 QQ 和微信消息的机器人,支持自定义插件。 +用 Python 编写的即时聊天平台机器人,通过适配器模式支持使用多种 bot 框架/平台作为消息源(目前支持 Mojo-Webqq、Mojo-Weixin),支持自定义插件。 -## 快速开始 +请注意区分此程序和其它模拟登录或封装接口的聊天平台**客户端**,此程序不负责登录或维护即时聊天平台的账号的状态,而只负责收到消息之后对消息的分析、处理、回复等逻辑,本程序通过适配器来与所支持的聊天平台客户端进行通讯,通常包括上报数据的统一化、调用接口获取额外信息、发送消息等,而这些聊天平台客户端(很多时候它们的项目名称也是「某某 bot」,相当于机器人的前端)需要你自行运行。 -### 部署 +## 如何运行 -推荐使用 Docker 部署,因为基本可以一键开启,如果你想手动运行,也可以参考第二个小标题「手动部署」。 +### 预备 -#### 使用 Docker +首先你需要了解如何运行你需要的消息源。以 Mojo-Weixin 为例,查看它的 [官方使用文档](https://github.com/sjdy521/Mojo-Weixin#如何使用) 来了解如何运行,其它消息源基本类似。 + +注意消息源必须已有相应的消息源适配器,消息源的概念解释及目前支持的消息源见 [消息源列表](https://cczu-dev.github.io/xiaokai-bot/#/Message_Sources)。 + +### 配置 + +复制 `config.sample.py` 为 `config.py`,然后修改 `config.py` 中的 `message_sources` 字段,定义你需要的消息源,例如: + +```python +{ + 'via': 'mojo_weixin', + 'login_id': 'your_login_id', + 'superuser_id': 'your_superuser_id', + 'api_url': 'http://127.0.0.1:5001/openwx', +} +``` + +上面的定义了一个 Mojo-Weixin 消息源,登录号是 `your_login_id`,超级用户 ID 是 `your_superuser_id`,Mojo-Weixin API 地址是 `http://127.0.0.1:5001/openwx`,`via` 和 `login_id` 是必须的,其它字段根据不同消息源适配器可能略有不同,具体请查看 [消息源列表](https://cczu-dev.github.io/xiaokai-bot/#/Message_Sources)。 + +与此同时,当你决定了本 bot 程序要运行的 IP 和端口之后,要把相应的上报 URL 填写到消息源程序的配置参数中,上报 URL 格式必须为 `http://your_host:your_port//`,这里可以见到 `via` 和 `login_id`,即为之前定义消息源时必填的项,用来唯一确定一个消息来源。比如如果你使用 Mojo-Weixin 登录一个 bot,微信号为 `my_bot`,而本 bot 程序跑在 `127.0.0.1` 的 `8888` 端口,那么你需要在 Mojo-Weixin 的参数中设置 `post_url` 为 `http://127.0.0.1:8888/mojo_weixin/my_bot`。 + +### 运行 + +推荐使用 Docker 运行,因为基本可以一键开启,如果你想手动运行,也可以参考第二个小标题「手动运行」。 + +#### 使用 Docker 运行 本仓库根目录下的 `docker-compose.yml` 即为 Docker Compose 的配置文件,直接跑就行(某些功能可能需要自行修改一下 `docker-compose.yml` 里的环境变量,例如如果要使用天气功能,需要在里面填上你的和风天气 API KEY)。如果你想对镜像进行修改,可以自行更改 Dockerfile 来构建或者继承已经构建好的镜像。 #### 手动运行 -首先需要运行 sjdy521/Mojo-Webqq 或 sjdy521/Mojo-Webqq,具体见它们的 GitHub 仓库的使用教程。然后运行: - ```sh -pip install -r requirements.txt -python app.py +pip3 install -r requirements.txt +python3 app.py ``` -注意要求 Python 3.x。 - 你可以通过设置环境变量来控制程序的某些行为,请参考 `docker-compose.yml` 文件中的最后一个容器的环境变量设置。 -## 使用 +## 如何使用 + +如果不是出于修改程序以适应自己的需求的目的,建议直接使用已经跑起来的小开 bot 即可,使用文档见 [如何使用 CCZU 小开机器人](http://fenkipedia.cn/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8CCZU%E5%B0%8F%E5%BC%80%E6%9C%BA%E5%99%A8%E4%BA%BA)。而如果是自行修改,那么使用方式就由你自己的插件决定了。 + +下面是一个示例的使用截图: ![](https://ww3.sinaimg.cn/large/006tNbRwgw1fb4a75bp2dj30ku1nsaey.jpg) ## 局限性 -由于 QQ 的限制,现有下列问题: +这里不讨论消息源客户端的局限性,那不是后端所负责的范围。只讨论本程序(聊天机器人后端)的局限性: -- 可能无法连续在线较长时间,因此需要频繁重启服务(大约一到两天一次) -- 无法处理临时消息 -- 无法接受图片、语音消息等非文字消息 -- 单条消息无法发送很长的内容 -- 有时候群消息会被屏蔽,私聊消息则正常 +- 直接忽略了所有事件类型的上报,比如好友请求、群请求,只接受消息类型 +- 目前只能处理文字消息(微信语音消息会通过语音识别转成文字) -目前看来微信相比 QQ 要更稳定一些,并且也可以接收图片、语音、视频等,不过有时候需要多次扫码才能登录成功。 +## 配置文件 + +本程序的配置文件(`config.py`)非常简单,重要的配置只有消息源定义、默认命令等,还有一些对标记的定义,如命令开始标记、命令名与参数分割标记等,基本上都是字面义,通过字段名即可明白,这里不再给出具体的文档。 + +## 消息源适配器 + +简称「适配器」,用来在消息源和本程序之间进行数据格式的转换,相当于一个驱动程序,通过不同的驱动程序,本程序便可以接入多种聊天平台。用户可以自行开发适配器来适配尚未支持的消息源,见 [编写消息源适配器](https://cczu-dev.github.io/xiaokai-bot/#/Write_Adapter)。 ## 插件 @@ -63,4 +91,4 @@ python app.py ### 自然语言处理器 -程序默认的 fallback 命令是 `natural_language.process`,也即自然语言处理命令,这个命令会通过消息的分词结果寻找注册了相应关键词的 NL 处理器并调用它们,得到一个有可能的等价命令列表,然后选择其中置信度最高且超过 60 的命令作为最佳识别结果执行。如果没有超过 60 的命令,则调用另一个 fallback 命令(`config.py` 中 `fallback_command_after_nl_processors` 指定,默认为 `ai.tuling123`)。 \ No newline at end of file +程序默认的 fallback 命令是 `natural_language.process`,也即自然语言处理命令,这个命令会通过消息的分词结果寻找注册了相应关键词的 NL 处理器并调用它们,得到一个有可能的等价命令列表,然后选择其中置信度最高且超过 60 的命令作为最佳识别结果执行。如果没有超过 60 的命令,则调用另一个 fallback 命令(`config.py` 中 `fallback_command_after_nl_processors` 指定,默认为 `ai.tuling123`)。 diff --git a/config.py b/config.sample.py similarity index 80% rename from config.py rename to config.sample.py index 3855e87a..439b067d 100644 --- a/config.py +++ b/config.sample.py @@ -8,16 +8,16 @@ config = { 'message_sources': [ { - 'via': 'mojo_weixin', - 'login_id': 'rcdevtest', - 'superuser_id': 'richard_chien_0', - 'api_url': 'http://127.0.0.1:5001/openwx', + 'via': 'mojo_webqq', + 'login_id': '12345678', + 'superuser_id': '23456789', + 'api_url': 'http://127.0.0.1:5000/openqq', }, { - 'via': 'mojo_webqq', - 'login_id': '3281334718', - 'superuser_id': '1002647525', - 'api_url': 'http://127.0.0.1:5000/openqq', + 'via': 'mojo_weixin', + 'login_id': 'your_login_id', + 'superuser_id': 'your_superuser_id', + 'api_url': 'http://127.0.0.1:5001/openwx', } ] } diff --git a/docker-compose.yml b/docker-compose.yml index 73fd3e99..e15f7d20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,31 +1,5 @@ version: '2' services: - mojo-webqq-api: - image: sjdy521/mojo-webqq - container_name: mojo-webqq-api - networks: - - my-net - expose: - - '5000' - volumes: - - /tmp:/tmp - environment: - - MOJO_WEBQQ_PLUGIN_OPENQQ_PORT=5000 - - MOJO_WEBQQ_PLUGIN_OPENQQ_POST_API=http://xiaokai-bot:8888/qq/ - restart: always - mojo-weixin-api: - image: sjdy521/mojo-weixin - container_name: mojo-weixin-api - networks: - - my-net - expose: - - '5001' - volumes: - - /tmp:/tmp - environment: - - MOJO_WEIXIN_PLUGIN_OPENQQ_PORT=5001 - - MOJO_WEIXIN_PLUGIN_OPENQQ_POST_API=http://xiaokai-bot:8888/wx/ - restart: always xiaokai-bot: image: richardchien/xiaokai-bot container_name: xiaokai-bot @@ -40,10 +14,6 @@ services: - TURING123_API_KEY=YOUR_API_KEY - HOST=0.0.0.0 - PORT=8888 - - QQ_API_URL=http://mojo-webqq-api:5000/openqq - - QQ_SUPER_USER=12345678 - - WX_API_URL=http://mojo-weixin-api:5001/openwx - - WX_SUPER_USER=abcdedfh - BAIDU_FANYI_APP_ID=YOUR_APP_ID - BAIDU_FANYI_API_KEY=YOUR_API_KEY - SPEECH_RECOGNITION_SERVICE=baidu|bing diff --git a/docs/Context.md b/docs/Context.md new file mode 100644 index 00000000..5cf181ed --- /dev/null +++ b/docs/Context.md @@ -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 相同。 diff --git a/docs/Message_Sources.md b/docs/Message_Sources.md new file mode 100644 index 00000000..e5374203 --- /dev/null +++ b/docs/Message_Sources.md @@ -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 | | diff --git a/docs/Write_Adapter.md b/docs/Write_Adapter.md new file mode 100644 index 00000000..7b27a27c --- /dev/null +++ b/docs/Write_Adapter.md @@ -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` 三者之一。 \ No newline at end of file diff --git a/docs/Write_Command.md b/docs/Write_Command.md index 0060ad93..c37be8b1 100644 --- a/docs/Write_Command.md +++ b/docs/Write_Command.md @@ -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 diff --git a/docs/Write_Filter.md b/docs/Write_Filter.md index 7785ea51..4ab21951 100644 --- a/docs/Write_Filter.md +++ b/docs/Write_Filter.md @@ -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 | 识别消息中的命令,并进行相应的调用 | | \ No newline at end of file +| 文件 | 优先级 | 作用 | 备注 | +| ------------------------------------- | ---- | ---------------------------------------- | -------------------------------------- | +| 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 | 识别消息中的命令,并进行相应的调用 | | \ No newline at end of file diff --git a/docs/config.js b/docs/config.js index 258c71be..3278f95a 100644 --- a/docs/config.js +++ b/docs/config.js @@ -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' } ] } diff --git a/filters/speech_recognition_90.py b/filters/speech_recognition_90.py index 828551cb..978897e3 100644 --- a/filters/speech_recognition_90.py +++ b/filters/speech_recognition_90.py @@ -1,5 +1,7 @@ """ This filter recognizes speech in voice message and stores it in 'text' field of context message. + +NOTE! This filter is only for Mojo-Weixin platform. """ import re @@ -59,7 +61,7 @@ def _recognize_bing(wav_path, api_key, language='zh-CN'): @as_filter(priority=90) def _filter(ctx_msg): - if ctx_msg.get('format') == 'media' and ctx_msg.get('media_type') == 'voice': + if ctx_msg.get('format') == 'media' and ctx_msg['raw_ctx'].get('media_type') == 'voice': m = re.match('\[语音\]\(([/_A-Za-z0-9]+\.mp3)\)', ctx_msg.get('content')) if m: core.echo('正在识别语音内容,请稍等……', ctx_msg)