diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index c5c6236d..b3dd83d7 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -32,6 +32,7 @@ module.exports = {
'nl-processor',
'tuling',
'notice-and-request',
+ 'scheduler',
'whats-next',
]
}
@@ -46,6 +47,7 @@ module.exports = {
'session',
'permission',
'decorator',
+ 'scheduler',
'logging',
'configuration',
'argparse',
diff --git a/docs/advanced/scheduler.md b/docs/advanced/scheduler.md
new file mode 100644
index 00000000..14b105e4
--- /dev/null
+++ b/docs/advanced/scheduler.md
@@ -0,0 +1 @@
+# 计划任务
diff --git a/docs/configurations.md b/docs/configurations.md
index dbd6152b..062fb5b4 100644
--- a/docs/configurations.md
+++ b/docs/configurations.md
@@ -29,3 +29,5 @@ sidebar: auto
## `SESSION_RUNNING_EXPRESSION`
## `SHORT_MESSAGE_MAX_LENGTH`
+
+## `APSCHEDULER_CONFIG`
diff --git a/docs/guide/code/awesome-bot-6/awesome/plugins/group_admin.py b/docs/guide/code/awesome-bot-6/awesome/plugins/group_admin.py
new file mode 100644
index 00000000..9ad1bae6
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/awesome/plugins/group_admin.py
@@ -0,0 +1,21 @@
+from none import on_request, RequestSession
+from none 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-6/awesome/plugins/scheduler.py b/docs/guide/code/awesome-bot-6/awesome/plugins/scheduler.py
new file mode 100644
index 00000000..b706c0a1
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/awesome/plugins/scheduler.py
@@ -0,0 +1,16 @@
+from datetime import datetime
+
+import none
+import pytz
+from aiocqhttp.exceptions import Error as CQHttpError
+
+
+@none.scheduler.scheduled_job('cron', hour='*')
+async def _():
+ bot = none.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-6/awesome/plugins/tuling.py b/docs/guide/code/awesome-bot-6/awesome/plugins/tuling.py
new file mode 100644
index 00000000..0134186c
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/awesome/plugins/tuling.py
@@ -0,0 +1,87 @@
+import json
+from typing import Optional
+
+import aiohttp
+from aiocqhttp.message import escape
+from none import on_command, CommandSession
+from none import on_natural_language, NLPSession, NLPResult
+from none.helpers import context_id
+
+# 定义无法获取图灵回复时的「表达(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:
+ # 如果调用失败,或者它返回的内容我们目前处理不了,发送无法获取图灵回复时的「表达」
+ # session.send_expr() 内部会调用 none.expression.render()
+ # 该函数会将一个「表达」渲染成一个字符串消息
+ await session.send_expr(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-6/awesome/plugins/weather/__init__.py b/docs/guide/code/awesome-bot-6/awesome/plugins/weather/__init__.py
new file mode 100644
index 00000000..a1502420
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/awesome/plugins/weather/__init__.py
@@ -0,0 +1,43 @@
+from none import on_command, CommandSession
+from none import on_natural_language, NLPSession, NLPResult
+from jieba import posseg
+
+from .data_source import get_weather_of_city
+
+
+@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-6/awesome/plugins/weather/data_source.py b/docs/guide/code/awesome-bot-6/awesome/plugins/weather/data_source.py
new file mode 100644
index 00000000..cc7f8329
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/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-6/bot.py b/docs/guide/code/awesome-bot-6/bot.py
new file mode 100644
index 00000000..df6f9cd2
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/bot.py
@@ -0,0 +1,11 @@
+from os import path
+
+import none
+
+import config
+
+if __name__ == '__main__':
+ none.init(config)
+ none.load_plugins(path.join(path.dirname(__file__), 'awesome', 'plugins'),
+ 'awesome.plugins')
+ none.run()
diff --git a/docs/guide/code/awesome-bot-6/config.py b/docs/guide/code/awesome-bot-6/config.py
new file mode 100644
index 00000000..262e4ccd
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/config.py
@@ -0,0 +1,10 @@
+from none.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-6/requirements.txt b/docs/guide/code/awesome-bot-6/requirements.txt
new file mode 100644
index 00000000..090c6372
--- /dev/null
+++ b/docs/guide/code/awesome-bot-6/requirements.txt
@@ -0,0 +1,4 @@
+none-bot>=0.2.1
+jieba
+aiohttp
+pytz
\ No newline at end of file
diff --git a/docs/guide/scheduler.md b/docs/guide/scheduler.md
new file mode 100644
index 00000000..2b5d7507
--- /dev/null
+++ b/docs/guide/scheduler.md
@@ -0,0 +1,50 @@
+# 添加计划任务
+
+实际应用中还经常会有定时执行任务的需求,为了方便这类需求的开发,NoneBot 可选地包含了计划任务功能。
+
+::: tip 提示
+本章的完整代码可以在 [awesome-bot-6](https://github.com/richardchien/none-bot/tree/master/docs/guide/code/awesome-bot-6) 查看。
+:::
+
+## 安装 APScheduler
+
+计划任务功能在 NoneBot 中是可选功能,只有当同时安装了 [APScheduler](https://github.com/agronholm/apscheduler) 时,才会启用。
+
+使用下面命令安装 APScheduler:
+
+```bash
+pip install apscheduler
+```
+
+安装成功之后就可以通过 `none.scheduler` 访问 [`AsyncIOScheduler`](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/asyncio.html#apscheduler.schedulers.asyncio.AsyncIOScheduler) 对象。
+
+## 定时发送消息
+
+这里以一个整点报时的功能为例,来介绍定时任务的使用。
+
+新建文件 `awesome/plugins/scheduler.py`,编写代码如下:
+
+```python {8}
+from datetime import datetime
+
+import none
+import pytz
+from aiocqhttp.exceptions import Error as CQHttpError
+
+
+@none.scheduler.scheduled_job('cron', hour='*')
+async def _():
+ bot = none.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
+```
+
+这里最主要的就是第 8 行,`none.scheduler.scheduled_job()` 是一个装饰器,第一个参数是触发器类型(这里是 `cron`,表示使用 [Cron](https://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html#module-apscheduler.triggers.cron)) 类型的触发参数)。这里 `hour='*'` 表示每小时都执行,`minute` 和 `second` 不填时默认为 `0`,也就是说装饰器所装饰的这个函数会在每小时的第一秒被执行。
+
+除了 `cron`,还有两种触发器类型 `interval` 和 `date`。例如,你可以使用 `none.scheduler.scheduled_job('interval', minutes=10)` 来每十分钟执行一次任务。
+
+限于篇幅,这里无法给出太详细的接口介绍,`none.scheduler` 是一个 APScheduler 的 `AsyncIOScheduler` 对象,因此关于它的更多使用方法,可以参考 [APScheduler 的官方文档](https://apscheduler.readthedocs.io/en/latest/userguide.html)。