--- 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 会在启动时自动加载这个主题。