From 9d138a059555ebec24bfe748f47859933d993202 Mon Sep 17 00:00:00 2001 From: Richard Chien Date: Mon, 21 Jan 2019 15:25:45 +0800 Subject: [PATCH] Add docs for writing "usage" command --- docs/.vuepress/config.js | 1 + .../awesome/plugins/group_admin.py | 21 +++++ .../awesome/plugins/scheduler.py | 16 ++++ .../awesome-bot-7/awesome/plugins/tuling.py | 93 +++++++++++++++++++ .../awesome-bot-7/awesome/plugins/usage.py | 20 ++++ .../awesome/plugins/weather/__init__.py | 50 ++++++++++ .../awesome/plugins/weather/data_source.py | 2 + docs/guide/code/awesome-bot-7/bot.py | 11 +++ docs/guide/code/awesome-bot-7/config.py | 10 ++ .../guide/code/awesome-bot-7/requirements.txt | 4 + docs/guide/command.md | 2 +- docs/guide/scheduler.md | 2 +- docs/guide/usage.md | 76 +++++++++++++++ 13 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 docs/guide/code/awesome-bot-7/awesome/plugins/group_admin.py create mode 100644 docs/guide/code/awesome-bot-7/awesome/plugins/scheduler.py create mode 100644 docs/guide/code/awesome-bot-7/awesome/plugins/tuling.py create mode 100644 docs/guide/code/awesome-bot-7/awesome/plugins/usage.py create mode 100644 docs/guide/code/awesome-bot-7/awesome/plugins/weather/__init__.py create mode 100644 docs/guide/code/awesome-bot-7/awesome/plugins/weather/data_source.py create mode 100644 docs/guide/code/awesome-bot-7/bot.py create mode 100644 docs/guide/code/awesome-bot-7/config.py create mode 100644 docs/guide/code/awesome-bot-7/requirements.txt create mode 100644 docs/guide/usage.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 7a42add8..3ceb83d7 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -48,6 +48,7 @@ module.exports = { 'notice-and-request', 'calling-cqhttp-api', 'scheduler', + 'usage', 'whats-next', ] } diff --git a/docs/guide/code/awesome-bot-7/awesome/plugins/group_admin.py b/docs/guide/code/awesome-bot-7/awesome/plugins/group_admin.py new file mode 100644 index 00000000..ad3c4791 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/awesome/plugins/group_admin.py @@ -0,0 +1,21 @@ +from nonebot import on_request, RequestSession +from nonebot import on_notice, NoticeSession + + +# 将函数注册为群请求处理器 +@on_request('group') +async def _(session: RequestSession): + # 判断验证信息是否符合要求 + if session.ctx['comment'] == '暗号': + # 验证信息正确,同意入群 + await session.approve() + return + # 验证信息错误,拒绝入群 + await session.reject('请说暗号') + + +# 将函数注册为群成员增加通知处理器 +@on_notice('group_increase') +async def _(session: NoticeSession): + # 发送欢迎消息 + await session.send('欢迎新朋友~') diff --git a/docs/guide/code/awesome-bot-7/awesome/plugins/scheduler.py b/docs/guide/code/awesome-bot-7/awesome/plugins/scheduler.py new file mode 100644 index 00000000..b23b6487 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/awesome/plugins/scheduler.py @@ -0,0 +1,16 @@ +from datetime import datetime + +import nonebot +import pytz +from aiocqhttp.exceptions import Error as CQHttpError + + +@nonebot.scheduler.scheduled_job('cron', hour='*') +async def _(): + bot = nonebot.get_bot() + now = datetime.now(pytz.timezone('Asia/Shanghai')) + try: + await bot.send_group_msg(group_id=672076603, + message=f'现在{now.hour}点整啦!') + except CQHttpError: + pass diff --git a/docs/guide/code/awesome-bot-7/awesome/plugins/tuling.py b/docs/guide/code/awesome-bot-7/awesome/plugins/tuling.py new file mode 100644 index 00000000..a4b41d35 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/awesome/plugins/tuling.py @@ -0,0 +1,93 @@ +import json +from typing import Optional + +import aiohttp +from aiocqhttp.message import escape +from nonebot import on_command, CommandSession +from nonebot import on_natural_language, NLPSession, NLPResult +from nonebot.helpers import context_id, render_expression + +__plugin_name__ = '智能聊天' +__plugin_usage__ = r""" +智能聊天 + +直接跟我聊天即可~ +""".strip() + +# 定义无法获取图灵回复时的「表达(Expression)」 +EXPR_DONT_UNDERSTAND = ( + '我现在还不太明白你在说什么呢,但没关系,以后的我会变得更强呢!', + '我有点看不懂你的意思呀,可以跟我聊些简单的话题嘛', + '其实我不太明白你的意思……', + '抱歉哦,我现在的能力还不能够明白你在说什么,但我会加油的~' +) + + +# 注册一个仅内部使用的命令,不需要 aliases +@on_command('tuling') +async def tuling(session: CommandSession): + # 获取可选参数,这里如果没有 message 参数,命令不会被中断,message 变量会是 None + message = session.get_optional('message') + + # 通过封装的函数获取图灵机器人的回复 + reply = await call_tuling_api(session, message) + if reply: + # 如果调用图灵机器人成功,得到了回复,则转义之后发送给用户 + # 转义会把消息中的某些特殊字符做转换,以避免 酷Q 将它们理解为 CQ 码 + await session.send(escape(reply)) + else: + # 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」 + # 这里的 render_expression() 函数会将一个「表达」渲染成一个字符串消息 + await session.send(render_expression(EXPR_DONT_UNDERSTAND)) + + +@on_natural_language +async def _(session: NLPSession): + # 以置信度 60.0 返回 tuling 命令 + # 确保任何消息都在且仅在其它自然语言处理器无法理解的时候使用 tuling 命令 + return NLPResult(60.0, 'tuling', {'message': session.msg_text}) + + +async def call_tuling_api(session: CommandSession, text: str) -> Optional[str]: + # 调用图灵机器人的 API 获取回复 + + if not text: + return None + + url = 'http://openapi.tuling123.com/openapi/api/v2' + + # 构造请求数据 + payload = { + 'reqType': 0, + 'perception': { + 'inputText': { + 'text': text + } + }, + 'userInfo': { + 'apiKey': session.bot.config.TULING_API_KEY, + 'userId': context_id(session.ctx, use_hash=True) + } + } + + group_unique_id = context_id(session.ctx, mode='group', use_hash=True) + if group_unique_id: + payload['userInfo']['groupId'] = group_unique_id + + try: + # 使用 aiohttp 库发送最终的请求 + async with aiohttp.ClientSession() as sess: + async with sess.post(url, json=payload) as response: + if response.status != 200: + # 如果 HTTP 响应状态码不是 200,说明调用失败 + return None + + resp_payload = json.loads(await response.text()) + if resp_payload['results']: + for result in resp_payload['results']: + if result['resultType'] == 'text': + # 返回文本类型的回复 + return result['values']['text'] + except (aiohttp.ClientError, json.JSONDecodeError, KeyError): + # 抛出上面任何异常,说明调用失败 + return None diff --git a/docs/guide/code/awesome-bot-7/awesome/plugins/usage.py b/docs/guide/code/awesome-bot-7/awesome/plugins/usage.py new file mode 100644 index 00000000..825a57f9 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/awesome/plugins/usage.py @@ -0,0 +1,20 @@ +import nonebot +from nonebot import on_command, CommandSession + + +@on_command('usage', aliases=['使用帮助', '帮助', '使用方法']) +async def _(session: CommandSession): + # 获取设置了名称的插件列表 + plugins = list(filter(lambda p: p.name, nonebot.get_loaded_plugins())) + + arg = session.current_arg_text.strip().lower() + if not arg: + # 如果用户没有发送参数,则发送功能列表 + await session.send( + '我现在支持的功能有:\n\n' + '\n'.join(p.name for p in plugins)) + return + + # 如果发了参数则发送相应命令的使用帮助 + for p in plugins: + if p.name.lower() == arg: + await session.send(p.usage) diff --git a/docs/guide/code/awesome-bot-7/awesome/plugins/weather/__init__.py b/docs/guide/code/awesome-bot-7/awesome/plugins/weather/__init__.py new file mode 100644 index 00000000..5802922d --- /dev/null +++ b/docs/guide/code/awesome-bot-7/awesome/plugins/weather/__init__.py @@ -0,0 +1,50 @@ +from nonebot import on_command, CommandSession +from nonebot import on_natural_language, NLPSession, NLPResult +from jieba import posseg + +from .data_source import get_weather_of_city + +__plugin_name__ = '天气' +__plugin_usage__ = r""" +天气查询 + +天气 [城市名称] +""" + + +@on_command('weather', aliases=('天气', '天气预报', '查天气')) +async def weather(session: CommandSession): + city = session.get('city', prompt='你想查询哪个城市的天气呢?') + weather_report = await get_weather_of_city(city) + await session.send(weather_report) + + +@weather.args_parser +async def _(session: CommandSession): + stripped_arg = session.current_arg_text.strip() + if session.current_key: + session.args[session.current_key] = stripped_arg + elif stripped_arg: + session.args['city'] = stripped_arg + + +# on_natural_language 装饰器将函数声明为一个自然语言处理器 +# keywords 表示需要响应的关键词,类型为任意可迭代对象,元素类型为 str +# 如果不传入 keywords,则响应所有没有被当作命令处理的消息 +@on_natural_language(keywords=('天气',)) +async def _(session: NLPSession): + # 去掉消息首尾的空白符 + stripped_msg_text = session.msg_text.strip() + # 对消息进行分词和词性标注 + words = posseg.lcut(stripped_msg_text) + + city = None + # 遍历 posseg.lcut 返回的列表 + for word in words: + # 每个元素是一个 pair 对象,包含 word 和 flag 两个属性,分别表示词和词性 + if word.flag == 'ns': + # ns 词性表示地名 + city = word.word + + # 返回处理结果,三个参数分别为置信度、命令名、命令会话的参数 + return NLPResult(90.0, 'weather', {'city': city}) diff --git a/docs/guide/code/awesome-bot-7/awesome/plugins/weather/data_source.py b/docs/guide/code/awesome-bot-7/awesome/plugins/weather/data_source.py new file mode 100644 index 00000000..cc7f8329 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/awesome/plugins/weather/data_source.py @@ -0,0 +1,2 @@ +async def get_weather_of_city(city: str) -> str: + return f'{city}的天气是……' diff --git a/docs/guide/code/awesome-bot-7/bot.py b/docs/guide/code/awesome-bot-7/bot.py new file mode 100644 index 00000000..263537a6 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/bot.py @@ -0,0 +1,11 @@ +from os import path + +import nonebot + +import config + +if __name__ == '__main__': + nonebot.init(config) + nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'), + 'awesome.plugins') + nonebot.run() diff --git a/docs/guide/code/awesome-bot-7/config.py b/docs/guide/code/awesome-bot-7/config.py new file mode 100644 index 00000000..a96493d0 --- /dev/null +++ b/docs/guide/code/awesome-bot-7/config.py @@ -0,0 +1,10 @@ +from nonebot.default_config import * + +HOST = '0.0.0.0' +PORT = 8080 + +SUPERUSERS = {12345678} +COMMAND_START.add('') +NICKNAME = {'小明', '明明'} + +TULING_API_KEY = '' diff --git a/docs/guide/code/awesome-bot-7/requirements.txt b/docs/guide/code/awesome-bot-7/requirements.txt new file mode 100644 index 00000000..97198cfe --- /dev/null +++ b/docs/guide/code/awesome-bot-7/requirements.txt @@ -0,0 +1,4 @@ +nonebot>=1.0.0 +jieba +aiohttp +pytz \ No newline at end of file diff --git a/docs/guide/command.md b/docs/guide/command.md index 0cbd5de0..150f845f 100644 --- a/docs/guide/command.md +++ b/docs/guide/command.md @@ -43,7 +43,7 @@ import config if __name__ == '__main__': nonebot.init(config) nonebot.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'), - 'awesome.plugins') + 'awesome.plugins') nonebot.run() ``` diff --git a/docs/guide/scheduler.md b/docs/guide/scheduler.md index 0a2e80aa..d43eb680 100644 --- a/docs/guide/scheduler.md +++ b/docs/guide/scheduler.md @@ -13,7 +13,7 @@ 使用下面命令安装可选功能(会自动安装 APScheduler): ```bash -pip install nonebot[scheduler] +pip install "nonebot[scheduler]" ``` 安装成功之后就可以通过 `nonebot.scheduler` 访问 [`AsyncIOScheduler`](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/asyncio.html#apscheduler.schedulers.asyncio.AsyncIOScheduler) 对象。 diff --git a/docs/guide/usage.md b/docs/guide/usage.md new file mode 100644 index 00000000..fda2724b --- /dev/null +++ b/docs/guide/usage.md @@ -0,0 +1,76 @@ +# 编写使用帮助 + +经过前面的部分,我们已经给机器人编写了天气查询和图灵聊天插件,当然,你可能已经另外编写了更多具有个性化功能的插件。 + +现在,为了让用户能够更方便的使用,是时候编写一个使用帮助了。 + +::: tip 提示 +本章的完整代码可以在 [awesome-bot-7](https://github.com/richardchien/nonebot/tree/master/docs/guide/code/awesome-bot-7) 查看。 +::: + +## 给插件添加名称和用法 + +这里以天气查询和图灵聊天两个插件为例,分别在 `awesome/plugins/weather/__init__.py` 和 `awesome/plugins/tuling.py` 两个文件的开头,通过 `__plugin_name__` 和 `__plugin_usage__` 两个特殊变量设置插件的名称和使用方法,如下: + +```python +# awesome/plugins/weather/__init__.py + +# ... 各种 import + +__plugin_name__ = '天气' +__plugin_usage__ = r""" +天气查询 + +天气 [城市名称] +""" +``` + +```python +# awesome/plugins/tuling.py + +# ... 各种 import + +__plugin_name__ = '智能聊天' +__plugin_usage__ = r""" +智能聊天 + +直接跟我聊天即可~ +""".strip() +``` + +一旦使用 `__plugin_name__` 和 `__plugin_usage__` 特殊变量设置了插件的名称和使用方法,NoneBot 在加载插件时就能够读取到这些内容,并存放在已加载插件的数据结构中。 + +## 编写使用帮助命令 + +新建插件 `awesome/plugins/usage.py`,编写内容如下: + +```python {8,13-14,20} +import nonebot +from nonebot import on_command, CommandSession + + +@on_command('usage', aliases=['使用帮助', '帮助', '使用方法']) +async def _(session: CommandSession): + # 获取设置了名称的插件列表 + plugins = list(filter(lambda p: p.name, nonebot.get_loaded_plugins())) + + arg = session.current_arg_text.strip().lower() + if not arg: + # 如果用户没有发送参数,则发送功能列表 + await session.send( + '我现在支持的功能有:\n\n' + '\n'.join(p.name for p in plugins)) + return + + # 如果发了参数则发送相应命令的使用帮助 + for p in plugins: + if p.name.lower() == arg: + await session.send(p.usage) +``` + +这里高亮的内容是重点: + +- `nonebot.get_loaded_plugins()` 函数用于获取所有已经加载的插件,**注意,由于可能存在插件没有设置 `__plugin_name__` 变量的情况,插件的名称有可能为空**,因此建议过滤一下 +- 插件的 `name` 属性(`plugin.name`)用于获得插件模块的 `__plugin_name__` 特殊变量的值 +- 插件的 `usage` 属性(`plugin.usage`)用于获得插件模块的 `__plugin_usage__` 特殊变量的值 + +到这里,使用帮助命令就已经编写完成了。如果愿意,可以继续按照自己的思路实现相对应的自然语言处理器,以优化使用体验。