nonebot-bison/docs/dev/README.md
Azide f35c6c2fd3
♻️ 迁移到 uv 并重写 Dockerfile (#667)
Co-authored-by: BalconyJH <balconyjh@gmail.com>
Co-authored-by: suyiiyii <suyiiyii@gmail.com>
2025-01-03 17:55:06 +08:00

393 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
prev: /usage/install
next: /dev/cookie
---
# 基本开发须知
## 语言以及工具
1. 本项目使用了`python3.10`的特性进行开发,所以请确保你的 Python 版本>=3.10
2. 本项目使用 uv 进行依赖管理,请确保开发之前已经进行过`uv sync`,运行时在`uv venv`的环境中进行运行
3. 本项目使用的 node 项目管理工具是 pnpm
::: tip 参考
可以参考[安装 - 直接运行](../usage/install.md#直接运行)中的内容
:::
## 前端
本项目使用了前端,如果单独 clone 仓库本身,里面是**不包含**编译过的前端的,请使用`pnpm i && pnpm build`进行前端的构建。
如果想要开发前端,推荐的步骤是:
1.`.env.dev`中添加`BISON_OUTER_URL`配置项
```env
BISON_OUTER_URL="http://localhost:3000/bison/"
```
2. 运行 bot
```bash
uv run nb run
```
3. 运行前端:
```bash
cd admin-frontend
pnpm start
```
::: warning
请在开发前端的时候删除项目**根目录**中的`node_modules`,否则编译和运行的时候可能会出现奇怪的问题。
:::
## 文档
文档的相关部分在`docs`目录中,可以在项目根目录执行`pnpm docs:dev`预览文件更改效果。
## 代码格式
本项目使用了 pre-commit 来进行代码美化和格式化。在`uv venv`状态下执行`pre-commit install`来安装 git hook可自动在 commit 时
格式化代码。
## 适配新网站
本插件需要你的帮助!只需要会写简单的爬虫,就能给本插件适配新的网站。
::: danger
Nonebot 项目使用了全异步的处理方式所以你需要对异步Python asyncio 的机制有一定了解,当然,
依葫芦画瓢也是足够的
:::
## 基本概念
- `nonebot_bison.post`: 可以理解为推送内容,其中包含需要发送的文字,图片,链接,平台信息等,分为:
- `nonebot_bison.post.Post`: 推送内容格式,传入需要发送的内容由 Theme 模块处理
- 详细的介绍可参见[生成 bison 的推送文本](#生成-bison-的推送文本)
- `nonebot_bison.types.RawPost`: 从站点/平台中爬到的单条信息
- `nonebot_bison.types.Target`: 目标账号Bilibili微博等社交媒体中的账号
- `nonebot_bison.types.Category`: 信息分类,例如视频,动态,图文,文章等
- `nonebot_bison.types.Tag`: 信息标签,例如微博中的超话或者 hashtag
- `nonebot_bison.theme.Theme`: 用于渲染`nonebot_bison.post.Post`的模块,可以理解为一个模板引擎,生成可发送的消息
## 快速上手
上车!我们走
先明确需要适配的站点类型,先明确两个问题:
### 我要发送什么样的推送
- `nonebot_bison.platform.platform.NewMessage` 最常见的类型,每次爬虫向特定接口爬取一个消息列表,
与之前爬取的信息对比,过滤出新的消息,再根据用户自定义的分类和标签进行过滤,最后处理消息,把
处理过后的消息发送给用户
例如微博Bilibili
- `nonebot_bison.platform.platform.StatusChange` 每次爬虫获取一个状态,在状态改变时发布推送
例如:游戏开服提醒,主播上播提醒
- `nonebot_bison.platform.platform.SimplePost` 与`NewMessage`相似,但是不过滤之前发过的
,每次发送全部消息
例如:每日榜单定时发送
### 这个平台是否有账号的概念
- 有账号的概念
例如B 站用户动态,微博用户动态,网易云电台更新
- 没有账号的概念
例如:游戏公告,教务处公告
## 实现方法
现在你需要在`nonebot_bison/platform`下新建一个 `.py` 文件,
在里面新建一个类,继承推送类型的基类,重载一些关键的函数,然后……就完成了~(??)~,不需要修改别的东西了。
### 不同类型 Platform 的实现适配以及逻辑
- `nonebot_bison.platform.platform.NewMessage`
需要实现:
- `async get_sub_list(Target) -> list[RawPost]`
- `get_id(RawPost)`
- `get_date(RawPost)` (可选)
::: details 大致流程
1. 调用`get_sub_list`拿到 RawPost 列表
2. 调用`get_id`判断是否重复,如果没有重复就说明是新的 RawPost
3. 如果有`get_category`和`get_date`,则调用判断 RawPost 是否满足条件
4. 调用`parse`生成正式推文
:::
参考[nonebot_bison.platform.Weibo](https://github.com/MountainDash/nonebot-bison/blob/v0.9.1/nonebot_bison/platform/weibo.py)
- `nonebot_bison.platform.platform.StatusChange`
需要实现:
- `async get_status(Target) -> Any`
- `compare_status(Target, old_status, new_status) -> list[RawPost]`
:::details 大致流程
1. `get_status`获取当前状态
2. 传入`compare_status`比较前状态
3. 通过则进入`parser`生成 Post
:::
参考[nonenot_bison.platform.AkVersion](https://github.com/MountainDash/nonebot-bison/blob/v0.9.1/nonebot_bison/platform/arknights.py#L122)
- `nonebot_bison.platform.platform.SimplePost`
需要实现:
- `async get_sub_list(Target) -> list[RawPost]`
- `get_date(RawPost)` (可选)
::: details 大致流程
1. 调用`get_sub_list`拿到 RawPost 列表
2. 如果有`get_category`和`get_date`,则调用判断 RawPost 是否满足条件
3. 调用`parse`生成正式推文
:::
### 公共方法/成员
任何一种订阅类型需要实现的方法/成员如下:
- `schedule_type`, `schedule_kw` 调度的参数,本质是使用 apscheduler 的[trigger 参数](https://apscheduler.readthedocs.io/en/3.x/userguide.html?highlight=trigger#choosing-the-right-scheduler-job-store-s-executor-s-and-trigger-s)`schedule_type`可以是`date`,`interval`和`cron`
`schedule_kw`是对应的参数,一个常见的配置是`schedule_type=interval`, `schedule_kw={'seconds':30}`
- `is_common` 是否常用,如果被标记为常用,那么和机器人交互式对话添加订阅时,会直接出现在选择列表中,否则
需要输入`全部`才会出现。
- `enabled` 是否启用
- `name` 平台的正式名称,例如`微博`
- `has_target` 平台是否有“帐号”
- `category` 平台的发布内容分类,例如 B 站包括专栏,视频,图文动态,普通动态等,如果不包含分类功能则设为`{}`
- `enable_tag` 平台发布内容是否带 Tag例如微博
- `platform_name` 唯一的,英文的识别标识,比如`weibo`
- `async get_target_name(Target) -> Optional[str]` 通常用于获取帐号的名称,如果平台没有帐号概念,可以直接返回平台的`name`
- `get_tags(RawPost) -> Optional[Collection[Tag]]` (可选)从 RawPost 中提取 Tag
- `get_category(RawPos) -> Optional[Category]` (可选)从 RawPost 中提取 Category
- `async parse(RawPost) -> Post` 将获取到的 RawPost 处理成 Post
- `async parse_target(str) -> Target` (可选)定制化处理传入用户输入的 Target 字符串,返回 Target一般是把用户的主页链接解析为 Target如果输入本身就是 Target则直接返回 Target
- `parse_target_promot` (可选)在要求用户输入 Target 的时候显示的提示文字
- `default_theme` (可选)默认的渲染主题,如果用户没有指定渲染主题,则优先使用这个主题进行渲染,不显式覆盖则为`basic`
- `use_batch` (可选)是否使用批量获取,如果使用批量获取,那么会调用`batch_get_sub_list`,否则调用`get_sub_list`
### 特有的方法/成员
- `async get_sub_list(Target) -> list[RawPost]` 输入一个`Target`,输出一个`RawPost`的 list
- 对于`nonebot_bison.platform.platform.NewMessage`
`get_sub_list(Target) -> list[RawPost]` 用于获取对应 Target 的 RawPost 列表,与上一次`get_sub_list`获取的列表比较,过滤出新的 RawPost
- 对于`nonebot_bison.platform.platform.SimplePost`
`get_sub_list` 用于获取对应 Target 的 RawPost 列表,但不会与上次获取的结果进行比较,而是直接进行发送
- `async def batch_get_sub_list(list[Target]) -> list[list[RawPost]]` (可选)输入一个`Target`的 list输出一个`RawPost`的 list 的 list用于批量获取 RawPost
- 其他类似`get_sub_list`,但是可以一次性获取多个 Target 的 RawPost
- `get_id(RawPost) -> Any` 输入一个`RawPost`,从`RawPost`中获取一个唯一的 ID这个 ID 会用来判断这条`RawPost`是不是之前收到过
- `get_date(RawPost) -> Optional[int]` 输入一个`RawPost`,如果可以从`RawPost`中提取出发文的时间,返回发文时间的 timestamp否则返回`None`
- `async get_status(Target) -> Any`
- 对于`nonebot_bison.platform.platform.StatusChange`
`get_status`用于获取对应 Target 当前的状态,随后将获取的状态作为参数`new_status`传入`compare_status`中
- `compare_status(self, target: Target, old_status, new_status) -> list[RawPost]`
- 对于`nonebot_bison.platform.platform.StatusChange`
`compare_status` 用于比较储存的`old_status`与新传入的`new_status`,并返回发生变更的 RawPost 列表
### 单元测试
当然我们非常希望你对自己适配的平台写一些单元测试
你可以参照`tests/platforms/test_*.py`中的内容对单元测试进行编写。
为保证多次运行测试的一致性,可以 mock http 的响应,测试的内容应包括[获取 RawPost](https://github.com/MountainDash/nonebot-bison/blob/v0.9.1/tests/platforms/test_weibo.py#L43),处理成 Post
,测试分类以及提取 tag 等,当然最好和 rsshub 做一个交叉验证。
## 一些例子
例如要适配微博,我希望 bot 搬运新的消息,所以微博的类应该这样实现:
```python
class Weibo(NewMessage):
categories = {
1: "转发",
2: "视频",
3: "图文",
4: "文字",
}
enable_tag = True
platform_name = "weibo"
name = "新浪微博"
enabled = True
is_common = True
schedule_type = "interval"
schedule_kw = {"seconds": 3}
has_target = True
async def get_target_name(self, target: Target) -> Optional[str]:
#获取 Target 对应的用户名
...
async def get_sub_list(self, target: Target) -> list[RawPost]:
#获取对应 Target 的 RawPost 列表,会与上一次 get_sub_list 获取的列表比较,过滤出新的 RawPost
...
def get_id(self, post: RawPost) -> Any:
#获取可以标识每个 Rawpost 的,不与之前 RawPost 重复的 id用于过滤出新的 RawPost
...
def get_date(self, raw_post: RawPost) -> float:
#获取 RawPost 的发布时间,若 bot 过滤出的新 RawPost 发布时间与当前时间差超过 2 小时,该 RawPost 将被忽略,可以返回 None
...
def get_tags(self, raw_post: RawPost) -> Optional[list[Tag]]:
#获取RawPost中包含的微博话题#xxx#中的内容)
...
def get_category(self, raw_post: RawPost) -> Category:
#获取该 RawPost 在该类定义 categories 的具体分类 (转发?视频?图文?...)
...
async def parse(self, raw_post: RawPost) -> Post:
#将需要 bot 推送的 RawPost 处理成正式推送的 Post
...
```
## 生成 bison 的推送文本
### 什么是`nonebot_bison.post`
可以认为`nonebot_bison.post`是最终要交付给 bison 的 Theme 模块渲染,最终推送到群内的内容。
`parse`函数的工作就是将`nonebot_bison.types.RawPost`中的数据相应传入`nonebot_bison.post.Post`中
经过`parse`函数处理过后的报文应该返回属于`nonebot_bison.post`下的某个类。
目前 bison 所支持的类有:
- `nonebot_bison.post.Post`
### 什么是`nonebot_bison.post.Post`
最通用的 Post理论上包含所有常用的数据
```python
class Post(AbstractPost):
platform: "Platform"
"""来源平台"""
content: str
"""文本内容"""
title: str | None = None
"""标题"""
images: list[str | bytes | Path | BytesIO] | None = None
"""图片列表"""
timestamp: int | None = None
"""发布/获取时间戳"""
url: str | None = None
"""来源链接"""
avatar: str | bytes | Path | BytesIO | None = None
"""发布者头像"""
nickname: str | None = None
"""发布者昵称"""
description: str | None = None
"""发布者个性签名等"""
repost: "Post | None" = None
"""转发的 Post"""
```
额外参数 (AbstractPost)
- 使用`compress`参数将所有消息压缩为一条进行发送。
- 使用`extra_msg`可以携带额外的消息进行发送。
可参考[Post 的用法](https://github.com/MountainDash/nonebot-bison/blob/v0.9.1/nonebot_bison/platform/arknights.py#L240)
## 制作主题
### 什么是主题
主题是用于渲染`nonebot_bison.post.Post`的模块,可以理解为一个模板引擎,生成可发送的消息。
RawPost 通过`Platform.parse`函数处理成 Post然后通过`Theme.render`函数渲染成可发送的消息。
### 主题的注册
Bison 在启动时会尝试注册所有在`nonebot_bison/theme/themes`下的主题,如果你的主题在这个目录下,并指定了 `__theme_meta__`,那么它会被自动注册。
若配置项`BISON_USE_BROWSER=false`,则在注册的主题需要浏览器渲染,即`need_browser`字段为`True`时,会发出注册警告
同时,你也可以手动调用`nonebot_bison.theme.theme_manager.register`来注册主题
::: tip 另一种加载方式
理论上你自己的 Theme 可以创建在别的位置,甚至作为一个插件
这样的话想要注册这个 Theme就需要在插件里这样做
```python
from nonebot_bison.theme import theme_manager
from .path.to.your.theme import ATheme
theme_manager.register(ATheme())
```
:::
### 主题的实现
主题需要继承`nonebot_bison.theme.Theme`,并实现`render`函数
在某个 Platform 获取到 Post 之后,会根据 主题渲染规则,将 Post 传入对应的 Theme 中,然后调用`render`函数,将 Post 渲染成可发送的消息。
::: info 主题渲染规则
```python
def get_priority_themes(self) -> list[str]:
"""获取渲染所使用的 theme 名列表,按照优先级排序"""
themes_by_priority: list[str] = []
# 最先使用用户指定的 theme
if user_theme := self.get_config_theme():
themes_by_priority.append(user_theme)
# 然后使用平台默认的 theme
if self.platform.default_theme not in themes_by_priority:
themes_by_priority.append(self.platform.default_theme)
# 最后使用最基础的 theme
if "basic" not in themes_by_priority:
themes_by_priority.append("basic")
return themes_by_priority
```
:::
在获取到可渲染的主题列表后,会按照列表中的顺序依次调用`render`函数。
如果某个主题渲染失败,会继续调用下一个主题,直到渲染成功或者没有主题可用。
### 例子
想要创建一个主题,首先需要在`nonebot_bison/theme/themes`目录下创建一个新的目录,比如`mytheme`
然后在`mytheme`目录下创建一个`__init__.py`文件
接下来创建一个`build.py`文件,用于生成主题
在文件中写入:
```python
from typing import TYPE_CHECKING, Literal
from nonebot_bison.theme import Theme
if TYPE_CHECKING:
from nonebot_bison.post import Post
class MyTheme(Theme):
name: Literal["mytheme"] = "mytheme"
# 可选,该主题渲染是否需要浏览器
# need_browser: bool = ...
async def render(self, post: "Post") -> list[MessageSegmentFactory]:
...
```
在`render`函数中,将传入的 post 中的数据用你所希望的方式渲染成 MessageSegmentFactory就完成了一个主题的制作
然后在`__init__.py`中注册这个主题:
```python
from .build import MyTheme
__theme_meta__ = MyTheme()
```
这样就完成了一个主题的创建Bison 会在启动时自动加载这个主题。