mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-04 06:38:05 +08:00
docs(4.0): 更新 API 参考与开发者文档,新增获取 SDK 页
- API 参考按实际路由重写,修正认证方式、请求方法与设置向导端点 - 数据库指南改为异步 Database 与 MIGRATIONS - E2E 指南改为指向 e2e/README.md - 插件快速上手补上安装 SDK 轮子的步骤,ab-plugin dev 加 --config-dir - 发布流程改为手动推送标签触发
This commit is contained in:
@@ -1,394 +1,146 @@
|
||||
# 数据库开发指南
|
||||
|
||||
本指南介绍 AutoBangumi 中的数据库架构、模型和操作。
|
||||
本文说明 AutoBangumi 后端的数据层:引擎、`Database` 门面、仓储、会话约定和 schema 迁移。代码在 `backend/src/module/database/`。
|
||||
|
||||
## 概述
|
||||
|
||||
AutoBangumi 使用 **SQLite** 作为数据库,使用 **SQLModel**(Pydantic + SQLAlchemy 混合)作为 ORM。数据库文件位于 `data/data.db`。
|
||||
|
||||
### 架构
|
||||
- 数据库是 SQLite 文件 `data/data.db`,ORM 是 SQLModel(Pydantic + SQLAlchemy)。
|
||||
- 整个应用只用异步引擎 `sqlite+aiosqlite`。每个连接设置 `PRAGMA journal_mode=WAL`、`busy_timeout=5000` 和 `foreign_keys=ON`:多个连接可以并发写,冲突的写入会等待而不是立刻报 `database is locked`。
|
||||
- 外键约束是生效的。删除被引用的行之前,要先处理引用它的行(例如 `BangumiDatabase.delete_all` 先删种子记录)。
|
||||
|
||||
```
|
||||
module/database/
|
||||
├── engine.py # SQLAlchemy 引擎配置
|
||||
├── combine.py # Database 类、迁移、会话管理
|
||||
├── bangumi.py # 番剧(动画订阅)操作
|
||||
├── rss.py # RSS 订阅源操作
|
||||
├── torrent.py # 种子跟踪操作
|
||||
└── user.py # 用户认证操作
|
||||
├── __init__.py # Database、get_db(FastAPI 依赖)
|
||||
├── engine.py # async_engine、async_session_factory、连接 PRAGMA
|
||||
├── combine.py # Database:一个会话加全部仓储
|
||||
├── migrations.py # MIGRATIONS 表与迁移执行
|
||||
├── bangumi.py、rss.py、torrent.py、movie.py、user.py、auth.py、
|
||||
├── inbox.py、llm_credential.py、rename_operation.py、aria2.py、
|
||||
└── plugin_kv.py、passkey.py # 各表的仓储
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
## Database 与会话
|
||||
|
||||
### Database 类
|
||||
|
||||
`combine.py` 中的 `Database` 类是主入口点。它继承自 SQLModel 的 `Session`,并提供对所有子数据库的访问:
|
||||
`Database` 是异步上下文管理器。它创建一个 `AsyncSession`,并把仓储挂在自己身上;退出 `async with` 块时关闭会话。
|
||||
|
||||
```python
|
||||
from module.database import Database
|
||||
|
||||
with Database() as db:
|
||||
# 访问子数据库
|
||||
bangumis = db.bangumi.search_all()
|
||||
rss_items = db.rss.search_active()
|
||||
torrents = db.torrent.search_all()
|
||||
async with Database() as db:
|
||||
rules = await db.bangumi.search_all()
|
||||
feeds = await db.rss.search_all()
|
||||
```
|
||||
|
||||
### 子数据库类
|
||||
| 属性 | 仓储 | 表 |
|
||||
| --- | --- | --- |
|
||||
| `db.bangumi` | `BangumiDatabase` | `bangumi` |
|
||||
| `db.rss` | `RSSDatabase` | `rssitem` |
|
||||
| `db.torrent` | `TorrentDatabase` | `torrent` |
|
||||
| `db.movie` | `MovieDatabase` | `movie` |
|
||||
| `db.user` | `UserDatabase` | `user` |
|
||||
| `db.auth` | `AuthDatabase` | `auth_session`、`api_token` |
|
||||
| `db.inbox` | `InboxDatabase` | 通知中心 |
|
||||
| `db.llm_credential` | `LLMCredentialDatabase` | LLM 订阅凭据 |
|
||||
| `db.rename_operation` | `RenameOperationDatabase` | `rename_operation`(重命名冲突与进度) |
|
||||
| `db.aria2` | `Aria2GidDatabase` | `aria2_gid` |
|
||||
| `db.plugin_kv` | `PluginKVDatabase` | `plugin_kv`(插件的 `ctx.kv`) |
|
||||
|
||||
| 类 | 模型 | 用途 |
|
||||
|-------|-------|---------|
|
||||
| `BangumiDatabase` | `Bangumi` | 动画订阅规则 |
|
||||
| `RSSDatabase` | `RSSItem` | RSS 订阅源 |
|
||||
| `TorrentDatabase` | `Torrent` | 已下载种子跟踪 |
|
||||
| `UserDatabase` | `User` | 认证 |
|
||||
`PasskeyDatabase` 不挂在 `Database` 上,需要时用 `PasskeyDatabase(db.session)` 构造。
|
||||
|
||||
## 模型
|
||||
`Database` 还转发几个会话方法:`add`(同步,只是入队)、`commit`、`rollback`、`refresh`、`close`。`begin_write()` 在 SQLite 上执行 `BEGIN IMMEDIATE`,用于「先读后写且须保持不变量」的场景,必须在本会话的第一次访问之前调用。
|
||||
|
||||
### Bangumi 模型
|
||||
### 会话约定
|
||||
|
||||
动画订阅的核心模型:
|
||||
**每次操作一个会话。** 不要把会话或 `Database` 存到比一次请求或一轮循环活得更久的对象上。`AppContext` 不持有会话。
|
||||
|
||||
- 路由用依赖注入:
|
||||
|
||||
```python
|
||||
from fastapi import Depends
|
||||
from module.database import Database, get_db
|
||||
|
||||
@router.get("/example")
|
||||
async def example(db: Database = Depends(get_db)):
|
||||
return await db.bangumi.search_all()
|
||||
```
|
||||
|
||||
- 定时循环与服务在每次执行时自己开:`async with Database() as db:`。
|
||||
- 服务在构造函数里接收依赖:`RSSEngine(db)`、`TorrentManager(db)`。服务内部用 `self.db.<仓储>`;只需要一个仓储的调用方直接用 `db.<仓储>`。
|
||||
- 引擎设置了 `expire_on_commit=False`,提交后对象的属性仍可读,但对象不再与新会话关联。需要修改时,在新的会话里重新查询。
|
||||
|
||||
## 仓储
|
||||
|
||||
仓储接收一个 `AsyncSession`,方法都是 `async def`,大多在方法内部提交。新增查询时:
|
||||
|
||||
- 放进对应表的仓储,不要在路由或服务里直接写 SQL。
|
||||
- 批量查询用 `in_` 一次取回,不要在循环里逐条查。
|
||||
- 软删除:`bangumi` 和 `movie` 有 `deleted` 字段。「停用」规则(`disable_rule`)只把 `deleted` 置为 `True`;`delete_one` 才真正删除行。查询活动规则时要过滤 `deleted`。
|
||||
|
||||
### 多下载器
|
||||
|
||||
4.0 支持多个下载器实例(配置 `plugins.instances`)。`bangumi`、`movie`、`rssitem` 和 `torrent` 都有 `downloader_id` 列:
|
||||
|
||||
- 规则与订阅上为空表示「使用默认实例」(`plugins.slots.downloader`)。
|
||||
- 种子记录上是种子实际所在的实例。从 3.3 升级的存量种子为 `default`。
|
||||
|
||||
## Schema 迁移
|
||||
|
||||
迁移在 `migrations.py` 的 `MIGRATIONS` 元组中,以表驱动的方式执行。数据库里的 `schema_version` 表记录已应用的版本。
|
||||
|
||||
```python
|
||||
class Bangumi(SQLModel, table=True):
|
||||
id: int # 主键
|
||||
official_title: str # 显示名称(如"无职转生")
|
||||
title_raw: str # 用于种子匹配的原始标题(有索引)
|
||||
season: int = 1 # 季度编号
|
||||
episode_offset: int = 0 # 集数编号调整
|
||||
season_offset: int = 0 # 季度编号调整
|
||||
rss_link: str # 逗号分隔的 RSS 订阅源 URL
|
||||
filter: str # 排除过滤器(如 "720,\\d+-\\d+")
|
||||
poster_link: str # TMDB 海报 URL
|
||||
save_path: str # 下载目标路径
|
||||
rule_name: str # qBittorrent RSS 规则名称
|
||||
added: bool = False # 规则是否已添加到下载器
|
||||
deleted: bool = False # 软删除标志(有索引)
|
||||
archived: bool = False # 用于已完结系列(有索引)
|
||||
needs_review: bool = False # 检测到偏移不匹配
|
||||
needs_review_reason: str # 需要审核的原因
|
||||
suggested_season_offset: int # 建议的季度偏移
|
||||
suggested_episode_offset: int # 建议的集数偏移
|
||||
air_weekday: int # 放送日(0=周日,6=周六)
|
||||
Migration(
|
||||
26,
|
||||
"add downloader_id to bangumi, movie, rssitem and torrent",
|
||||
(...SQL 语句...),
|
||||
already_applied=..., # 守卫:该迁移是否已生效
|
||||
guarded_statements=(...), # 可选:逐条语句的守卫
|
||||
)
|
||||
```
|
||||
|
||||
### RSSItem 模型
|
||||
启动时 AB 先执行 `SQLModel.metadata.create_all`,再执行 `run_migrations()`:
|
||||
|
||||
RSS 订阅源:
|
||||
1. 读取 `schema_version`,跳过版本号不大于它的迁移。
|
||||
2. 对每个待应用的迁移,重新检查数据库结构,再调用守卫:已生效(例如新库已由 `create_all` 按模型建表)时只记录版本号。
|
||||
3. 否则在一个 SAVEPOINT 中执行它的语句。任何一条失败,该迁移回滚,异常向上抛出,整个迁移事务回滚,AB 启动失败。AB 不会在只迁移了一半的 schema 上继续运行。
|
||||
4. 有迁移被执行时,最后按模型的默认值填充存量行中的 `NULL`(`fill_null_with_defaults_conn`):跳过主键、`default_factory` 字段和默认值为 `None` 的可选字段。
|
||||
|
||||
### 添加迁移
|
||||
|
||||
1. 先修改 `module/models/` 中的模型。新库由 `create_all` 直接得到新结构。
|
||||
2. 在 `MIGRATIONS` 末尾追加一个 `Migration`,版本号为上一个加 1。`CURRENT_SCHEMA_VERSION` 由列表推出,不要手动修改。
|
||||
3. 提供 `already_applied` 守卫:`column_exists(表, 列)`、`table_exists(表)`、`index_exists(表, 索引)`,多个条件用 `all_checks(...)` 组合。一个迁移改多张表、每张表可能处于不同状态时,用 `guarded_statements` 给每条语句单独的守卫。
|
||||
4. 在 `backend/src/test/` 中为迁移加测试:从旧 schema 开始,执行迁移,检查结果。
|
||||
|
||||
```python
|
||||
class RSSItem(SQLModel, table=True):
|
||||
id: int # 主键
|
||||
name: str # 显示名称
|
||||
url: str # 订阅源 URL(唯一,有索引)
|
||||
aggregate: bool = True # 是否解析种子
|
||||
parser: str = "mikan" # 解析器类型:mikan、dmhy、nyaa
|
||||
enabled: bool = True # 启用标志
|
||||
connection_status: str # "healthy" 或 "error"
|
||||
last_checked_at: str # ISO 时间戳
|
||||
last_error: str # 最后一次错误消息
|
||||
Migration(
|
||||
27,
|
||||
"add note to bangumi",
|
||||
("ALTER TABLE bangumi ADD COLUMN note TEXT DEFAULT ''",),
|
||||
column_exists("bangumi", "note"),
|
||||
)
|
||||
```
|
||||
|
||||
### Torrent 模型
|
||||
|
||||
跟踪已下载的种子:
|
||||
|
||||
```python
|
||||
class Torrent(SQLModel, table=True):
|
||||
id: int # 主键
|
||||
name: str # 种子名称(有索引)
|
||||
url: str # 种子/磁力链接 URL(唯一,有索引)
|
||||
rss_id: int # 来源 RSS 订阅源 ID
|
||||
bangumi_id: int # 关联的番剧 ID(可为空)
|
||||
qb_hash: str # qBittorrent 信息哈希(有索引)
|
||||
downloaded: bool = False # 下载完成
|
||||
```
|
||||
|
||||
## 常用操作
|
||||
|
||||
### BangumiDatabase
|
||||
|
||||
```python
|
||||
with Database() as db:
|
||||
# 创建
|
||||
db.bangumi.add(bangumi) # 单条插入
|
||||
db.bangumi.add_all(bangumi_list) # 批量插入(去重)
|
||||
|
||||
# 读取
|
||||
db.bangumi.search_all() # 所有记录(缓存,5分钟 TTL)
|
||||
db.bangumi.search_id(123) # 按 ID 查询
|
||||
db.bangumi.match_torrent("torrent name") # 按 title_raw 匹配查找
|
||||
db.bangumi.not_complete() # 未完结系列
|
||||
db.bangumi.get_needs_review() # 标记需要审核的
|
||||
|
||||
# 更新
|
||||
db.bangumi.update(bangumi) # 更新单条记录
|
||||
db.bangumi.update_all(bangumi_list) # 批量更新
|
||||
|
||||
# 删除
|
||||
db.bangumi.delete_one(123) # 硬删除
|
||||
db.bangumi.disable_rule(123) # 软删除(deleted=True)
|
||||
```
|
||||
|
||||
### RSSDatabase
|
||||
|
||||
```python
|
||||
with Database() as db:
|
||||
# 创建
|
||||
db.rss.add(rss_item) # 单条插入
|
||||
db.rss.add_all(rss_items) # 批量插入(去重)
|
||||
|
||||
# 读取
|
||||
db.rss.search_all() # 所有订阅源
|
||||
db.rss.search_active() # 仅启用的订阅源
|
||||
db.rss.search_aggregate() # 启用且 aggregate=True
|
||||
|
||||
# 更新
|
||||
db.rss.update(id, rss_update) # 部分更新
|
||||
db.rss.enable(id) # 启用订阅源
|
||||
db.rss.disable(id) # 禁用订阅源
|
||||
db.rss.enable_batch([1, 2, 3]) # 批量启用
|
||||
db.rss.disable_batch([1, 2, 3]) # 批量禁用
|
||||
```
|
||||
|
||||
### TorrentDatabase
|
||||
|
||||
```python
|
||||
with Database() as db:
|
||||
# 创建
|
||||
db.torrent.add(torrent) # 单条插入
|
||||
db.torrent.add_all(torrents) # 批量插入
|
||||
|
||||
# 读取
|
||||
db.torrent.search_all() # 所有种子
|
||||
db.torrent.search_by_qb_hash(hash) # 按 qBittorrent 哈希查询
|
||||
db.torrent.search_by_url(url) # 按 URL 查询
|
||||
db.torrent.check_new(torrents) # 过滤掉已存在的
|
||||
|
||||
# 更新
|
||||
db.torrent.update_qb_hash(id, hash) # 设置 qb_hash
|
||||
```
|
||||
|
||||
## 缓存
|
||||
|
||||
### 番剧缓存
|
||||
|
||||
`search_all()` 的结果在模块级别缓存,TTL 为 5 分钟:
|
||||
|
||||
```python
|
||||
# bangumi.py 中的模块级缓存
|
||||
_bangumi_cache: list[Bangumi] | None = None
|
||||
_bangumi_cache_time: float = 0
|
||||
_BANGUMI_CACHE_TTL: float = 300.0 # 5 分钟
|
||||
|
||||
# 缓存失效
|
||||
def _invalidate_bangumi_cache():
|
||||
global _bangumi_cache, _bangumi_cache_time
|
||||
_bangumi_cache = None
|
||||
_bangumi_cache_time = 0
|
||||
```
|
||||
|
||||
**重要:** 缓存在以下操作时自动失效:
|
||||
- `add()`、`add_all()`
|
||||
- `update()`、`update_all()`
|
||||
- `delete_one()`、`delete_all()`
|
||||
- `archive_one()`、`unarchive_one()`
|
||||
- 任何 RSS 链接更新操作
|
||||
|
||||
### 会话分离
|
||||
|
||||
缓存的对象会从会话中**分离**,以防止 `DetachedInstanceError`:
|
||||
|
||||
```python
|
||||
for b in bangumis:
|
||||
self.session.expunge(b) # 从会话中分离
|
||||
```
|
||||
|
||||
## 迁移系统
|
||||
|
||||
### Schema 版本控制
|
||||
|
||||
迁移通过 `schema_version` 表跟踪:
|
||||
|
||||
```python
|
||||
CURRENT_SCHEMA_VERSION = 7
|
||||
|
||||
# 每个迁移:(版本号, 描述, [SQL 语句])
|
||||
MIGRATIONS = [
|
||||
(1, "add air_weekday column", [...]),
|
||||
(2, "add connection status columns", [...]),
|
||||
(3, "create passkey table", [...]),
|
||||
(4, "add archived column", [...]),
|
||||
(5, "rename offset to episode_offset", [...]),
|
||||
(6, "add qb_hash column", [...]),
|
||||
(7, "add suggested offset columns", [...]),
|
||||
]
|
||||
```
|
||||
|
||||
### 添加新迁移
|
||||
|
||||
1. 在 `combine.py` 中增加 `CURRENT_SCHEMA_VERSION`
|
||||
2. 在 `MIGRATIONS` 列表中添加迁移元组:
|
||||
|
||||
```python
|
||||
MIGRATIONS = [
|
||||
# ... 现有迁移 ...
|
||||
(
|
||||
8,
|
||||
"add my_new_column to bangumi",
|
||||
[
|
||||
"ALTER TABLE bangumi ADD COLUMN my_new_column TEXT DEFAULT NULL",
|
||||
],
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
3. 在 `run_migrations()` 中添加幂等性检查:
|
||||
|
||||
```python
|
||||
if "bangumi" in tables and version == 8:
|
||||
columns = [col["name"] for col in inspector.get_columns("bangumi")]
|
||||
if "my_new_column" in columns:
|
||||
needs_run = False
|
||||
```
|
||||
|
||||
4. 更新 `module/models/` 中对应的 Pydantic 模型
|
||||
|
||||
### 默认值回填
|
||||
|
||||
迁移后,`_fill_null_with_defaults()` 会根据模型默认值自动填充 NULL 值:
|
||||
|
||||
```python
|
||||
# 如果模型定义为:
|
||||
class Bangumi(SQLModel, table=True):
|
||||
my_field: bool = False
|
||||
|
||||
# 那么现有记录中的 NULL 值将被更新为 False
|
||||
```
|
||||
|
||||
## 性能模式
|
||||
|
||||
### 批量查询
|
||||
|
||||
`add_all()` 使用单个查询检查重复项,而不是 N 个查询:
|
||||
|
||||
```python
|
||||
# 高效:单个 SELECT
|
||||
keys_to_check = [(d.title_raw, d.group_name) for d in datas]
|
||||
conditions = [
|
||||
and_(Bangumi.title_raw == tr, Bangumi.group_name == gn)
|
||||
for tr, gn in keys_to_check
|
||||
]
|
||||
statement = select(Bangumi.title_raw, Bangumi.group_name).where(or_(*conditions))
|
||||
```
|
||||
|
||||
### 正则表达式匹配
|
||||
|
||||
`match_list()` 为所有标题匹配编译单个正则表达式模式:
|
||||
|
||||
```python
|
||||
# 编译一次,匹配多次
|
||||
sorted_titles = sorted(title_index.keys(), key=len, reverse=True)
|
||||
pattern = "|".join(re.escape(title) for title in sorted_titles)
|
||||
title_regex = re.compile(pattern)
|
||||
|
||||
# 每个种子 O(1) 查找而不是 O(n)
|
||||
for torrent in torrent_list:
|
||||
match = title_regex.search(torrent.name)
|
||||
```
|
||||
|
||||
### 索引列
|
||||
|
||||
以下列具有索引以实现快速查找:
|
||||
|
||||
| 表 | 列 | 索引类型 |
|
||||
|-------|--------|------------|
|
||||
| `bangumi` | `title_raw` | 普通 |
|
||||
| `bangumi` | `deleted` | 普通 |
|
||||
| `bangumi` | `archived` | 普通 |
|
||||
| `rssitem` | `url` | 唯一 |
|
||||
| `torrent` | `name` | 普通 |
|
||||
| `torrent` | `url` | 唯一 |
|
||||
| `torrent` | `qb_hash` | 普通 |
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试数据库设置
|
||||
`backend/src/test/conftest.py` 提供内存数据库:
|
||||
|
||||
测试使用内存中的 SQLite 数据库:
|
||||
- `db_engine`:`sqlite+aiosqlite://` 加 `StaticPool`,每个测试新建并按模型建表。
|
||||
- `db_session`:绑定 `db_engine` 的 `AsyncSession`。
|
||||
|
||||
直接测试仓储时,用 `Database(engine=db_engine)`,或用 `db_session` 构造单个仓储:
|
||||
|
||||
```python
|
||||
# conftest.py
|
||||
@pytest.fixture
|
||||
def db_engine():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
SQLModel.metadata.create_all(engine)
|
||||
yield engine
|
||||
engine.dispose()
|
||||
from module.database import Database
|
||||
from module.database.bangumi import BangumiDatabase
|
||||
from test.factories import make_bangumi
|
||||
|
||||
@pytest.fixture
|
||||
def db_session(db_engine):
|
||||
with Session(db_engine) as session:
|
||||
yield session
|
||||
async def test_add_bangumi(db_engine):
|
||||
async with Database(engine=db_engine) as db:
|
||||
assert await db.bangumi.add(make_bangumi())
|
||||
|
||||
async def test_search(db_session):
|
||||
repo = BangumiDatabase(db_session)
|
||||
assert await repo.search_all() == []
|
||||
```
|
||||
|
||||
### 工厂函数
|
||||
|
||||
使用工厂函数创建测试数据:
|
||||
|
||||
```python
|
||||
from test.factories import make_bangumi, make_torrent, make_rss_item
|
||||
|
||||
def test_bangumi_search():
|
||||
bangumi = make_bangumi(title_raw="Test Title", season=2)
|
||||
# ... 测试逻辑
|
||||
```
|
||||
|
||||
## 设计说明
|
||||
|
||||
### 无外键
|
||||
|
||||
SQLite 外键强制默认是禁用的。关系(如 `Torrent.bangumi_id`)在应用程序逻辑中管理,而不是通过数据库约束。
|
||||
|
||||
### 软删除
|
||||
|
||||
`Bangumi.deleted` 标志启用软删除。面向用户的数据查询应按 `deleted=False` 过滤:
|
||||
|
||||
```python
|
||||
statement = select(Bangumi).where(Bangumi.deleted == false())
|
||||
```
|
||||
|
||||
### 种子标记
|
||||
|
||||
种子在 qBittorrent 中使用 `ab:{bangumi_id}` 标记,用于重命名操作时的偏移查找。这使得无需数据库查询即可快速识别番剧。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### DetachedInstanceError
|
||||
|
||||
如果您从不同的会话访问缓存的对象:
|
||||
|
||||
```python
|
||||
# 错误:在新会话中访问缓存的对象
|
||||
bangumis = db.bangumi.search_all() # 已缓存
|
||||
with Database() as new_db:
|
||||
new_db.session.add(bangumis[0]) # 错误!
|
||||
|
||||
# 正确:对象已分离,可独立工作
|
||||
bangumis = db.bangumi.search_all()
|
||||
bangumis[0].title_raw = "New Title" # 可以,但不会持久化
|
||||
```
|
||||
|
||||
### 缓存过期
|
||||
|
||||
如果手动 SQL 更新绕过了 ORM,请使缓存失效:
|
||||
|
||||
```python
|
||||
from module.database.bangumi import _invalidate_bangumi_cache
|
||||
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text("UPDATE bangumi SET ..."))
|
||||
conn.commit()
|
||||
|
||||
_invalidate_bangumi_cache() # 重要!
|
||||
```
|
||||
后端的 pytest 已开启 `asyncio_mode = "auto"`,异步测试函数不需要额外的装饰器。运行:`cd backend && uv run pytest src/test/test_xxx.py -v`。
|
||||
|
||||
@@ -1,145 +1,62 @@
|
||||
# E2E Integration Test Guide
|
||||
# E2E 测试指南
|
||||
|
||||
End-to-end tests that exercise the full AutoBangumi workflow against real
|
||||
Docker services (qBittorrent + mock RSS server).
|
||||
端到端测试用生产构建的 AutoBangumi 镜像,对接本地的、确定性的上游服务(模拟 RSS、TMDB、播放器与 qBittorrent)。测试不访问任何公网服务。Docker Compose 在回环地址上发布随机端口,服务之间走内部网络。
|
||||
|
||||
## Prerequisites
|
||||
完整说明(各条测试线、诊断产物、添加模拟场景、更新 qBittorrent 镜像摘要)在仓库的 [`e2e/README.md`](https://github.com/EstrellaXD/Auto_Bangumi/blob/main/e2e/README.md)。本页只给出本地运行的最短路径。
|
||||
|
||||
- **Docker** with `docker compose` (v2)
|
||||
- **uv** for Python dependency management
|
||||
- Ports **7892**, **18080**, **18888** must be free
|
||||
## 准备
|
||||
|
||||
## Quick Start
|
||||
- Docker Engine 与 Compose v2
|
||||
- Python 3.13 与 `uv`
|
||||
- Node.js 20 与 pnpm 9.11
|
||||
|
||||
```bash
|
||||
# 1. Build the mock RSS server image
|
||||
cd backend/src/test/e2e
|
||||
docker build -f Dockerfile.mock-rss -t ab-mock-rss .
|
||||
|
||||
# 2. Start test infrastructure
|
||||
docker compose -f docker-compose.test.yml up -d --wait
|
||||
|
||||
# 3. Verify services are healthy
|
||||
docker compose -f docker-compose.test.yml ps
|
||||
|
||||
# 4. Run E2E tests
|
||||
cd backend && uv run pytest -m e2e -v --tb=long
|
||||
|
||||
# 5. Cleanup
|
||||
docker compose -f backend/src/test/e2e/docker-compose.test.yml down -v
|
||||
uv sync --directory backend --locked --group dev
|
||||
pnpm --dir webui install --frozen-lockfile
|
||||
pnpm --dir webui exec playwright install --with-deps chromium webkit firefox
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Host machine
|
||||
├── pytest (test runner)
|
||||
│ └── Drives HTTP requests to AutoBangumi at localhost:7892
|
||||
├── AutoBangumi subprocess
|
||||
│ ├── Isolated config/ and data/ in temp directory
|
||||
│ └── Uses mock downloader (no real qB coupling during setup)
|
||||
├── qBittorrent container (localhost:18080)
|
||||
│ └── linuxserver/qbittorrent:latest
|
||||
└── Mock RSS server container (localhost:18888)
|
||||
└── Serves static XML fixtures from fixtures/
|
||||
```
|
||||
|
||||
## Test Phases
|
||||
|
||||
| Phase | Tests | What It Validates |
|
||||
|-------|-------|-------------------|
|
||||
| 1. Setup Wizard | `test_01` - `test_06` | First-run detection, mock downloader, setup completion, 403 guard |
|
||||
| 2. Authentication | `test_10` - `test_13` | Login, cookie-based JWT, token refresh, logout |
|
||||
| 3. Configuration | `test_20` - `test_22` | Config CRUD, password masking |
|
||||
| 4. RSS Management | `test_30` - `test_32` | Add, list, delete RSS feeds |
|
||||
| 5. Program Lifecycle | `test_40` - `test_41` | Status check, restart |
|
||||
| 6. Downloader | `test_50` - `test_51` | Mock downloader health, direct qB connectivity |
|
||||
| 7. Cleanup | `test_90` | Logout |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Mock Downloader for Setup
|
||||
|
||||
The setup wizard's `_validate_url()` blocks private/loopback IPs (SSRF
|
||||
protection). Since the Docker qBittorrent instance is on `localhost`, the
|
||||
setup wizard's "test downloader" endpoint would reject it. Instead:
|
||||
|
||||
1. Setup uses `downloader_type: "mock"` (bypasses URL validation)
|
||||
2. Config can be updated to point to real qBittorrent after auth
|
||||
3. Direct qBittorrent connectivity is tested independently (`test_51`)
|
||||
|
||||
### DEV_VERSION Auth Bypass
|
||||
|
||||
When running from source, `VERSION == "DEV_VERSION"` which bypasses JWT
|
||||
validation (`get_current_user` returns `"dev_user"` unconditionally). Tests
|
||||
document this behavior: login/refresh/logout endpoints still work, but
|
||||
unauthenticated access is also allowed. In production builds, test_13
|
||||
would expect HTTP 401.
|
||||
|
||||
### CWD-Based Isolation
|
||||
|
||||
AutoBangumi resolves all paths relative to the working directory:
|
||||
- `config/` - config files, JWT secret, setup sentinel
|
||||
- `data/` - SQLite database, posters, logs
|
||||
|
||||
The `ab_process` fixture creates a temp directory with these subdirs and
|
||||
runs `main.py` from there, ensuring complete isolation from any existing
|
||||
installation.
|
||||
|
||||
### qBittorrent Password Extraction
|
||||
|
||||
Recent `linuxserver/qbittorrent` images generate a random temporary
|
||||
password on first start. The `qb_password` fixture polls `docker logs`
|
||||
until it finds the line:
|
||||
|
||||
```
|
||||
A temporary password is provided for this session: XXXXXXXX
|
||||
```
|
||||
|
||||
## Debugging Failures
|
||||
|
||||
### AutoBangumi won't start
|
||||
## 构建测试镜像
|
||||
|
||||
```bash
|
||||
# Check if port 7892 is in use
|
||||
lsof -i :7892
|
||||
|
||||
# Run manually to see startup logs
|
||||
cd /tmp/test-workdir && uv run python /path/to/backend/src/main.py
|
||||
pnpm --dir webui run build
|
||||
python3 e2e/scripts/build_test_image.py \
|
||||
--version 3.3.999-e2e.1 \
|
||||
--image auto-bangumi:e2e \
|
||||
--dist webui/dist
|
||||
```
|
||||
|
||||
### qBittorrent issues
|
||||
构建脚本在临时的 Docker 上下文里放入 WebUI 产物和生成的版本模块,不会写入源码树。
|
||||
|
||||
## 运行
|
||||
|
||||
每次运行都用新的项目名和工作目录。重用工作目录会同时重用设置状态,首次运行的覆盖就失效了。
|
||||
|
||||
```bash
|
||||
docker logs ab-test-qbittorrent
|
||||
docker exec ab-test-qbittorrent curl -s http://localhost:18080
|
||||
export LANE=runtime
|
||||
export AB_E2E_PROJECT="ab-e2e-local-$LANE-$$"
|
||||
export AB_E2E_WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ab-e2e-$LANE.XXXXXX")"
|
||||
export AB_E2E_APP_IMAGE=auto-bangumi:e2e
|
||||
|
||||
# 运行时 API 与进程测试
|
||||
python3 e2e/scripts/stack.py run \
|
||||
--profile browser \
|
||||
--project-name "$AB_E2E_PROJECT" \
|
||||
--work-dir "$AB_E2E_WORK_DIR" \
|
||||
-- uv run --directory backend pytest src/test/e2e/runtime -m e2e -q
|
||||
```
|
||||
|
||||
### Mock RSS server issues
|
||||
| 测试线 | `--` 之后的命令 | `--profile` |
|
||||
| --- | --- | --- |
|
||||
| 运行时 | `uv run --directory backend pytest src/test/e2e/runtime -m e2e -q` | `browser` |
|
||||
| Chromium 桌面 | `pnpm --dir webui run test:e2e:chromium --retries=0` | `browser` |
|
||||
| WebKit 移动端 | `pnpm --dir webui run test:e2e:webkit --retries=0` | `browser` |
|
||||
| 真实 qBittorrent 与打包镜像 | `python3 e2e/scripts/run_downloader_lane.py` | `downloader` |
|
||||
|
||||
```bash
|
||||
docker logs ab-test-mock-rss
|
||||
curl http://localhost:18888/health
|
||||
curl http://localhost:18888/rss/mikan.xml
|
||||
```
|
||||
- 普通的 `pytest` 会跳过 E2E 用例,必须显式传入 `-m e2e`。
|
||||
- `stack.py run` 在拆除 Compose 之前收集诊断信息,写到 `$AB_E2E_WORK_DIR/artifacts`。Playwright 的截图与视频在 `webui/test-results`,HTML 报告在 `webui/playwright-report`。
|
||||
- 停止一个栈:`python3 e2e/scripts/stack.py stop --profile browser --project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"`。
|
||||
|
||||
### Test infrastructure stuck
|
||||
## CI
|
||||
|
||||
```bash
|
||||
# Force cleanup
|
||||
docker compose -f backend/src/test/e2e/docker-compose.test.yml down -v --remove-orphans
|
||||
```
|
||||
|
||||
## Adding New Test Scenarios
|
||||
|
||||
1. Add new test methods to `TestE2EWorkflow` in definition order
|
||||
2. Use `api_client` for HTTP requests (cookies persist across tests)
|
||||
3. Use `e2e_state` dict to share data between tests
|
||||
4. For new RSS fixtures, add XML files to `fixtures/` directory
|
||||
5. Keep test names ordered: `test_XX_description` where XX reflects the phase
|
||||
|
||||
### Adding a new fixture feed
|
||||
|
||||
1. Create `backend/src/test/e2e/fixtures/your_feed.xml`
|
||||
2. Access via `http://localhost:18888/rss/your_feed.xml`
|
||||
3. Rebuild the mock RSS image: `docker compose ... build mock-rss`
|
||||
`.github/workflows/e2e.yml` 在每个 PR 上运行运行时线、浏览器线(Chromium、WebKit 移动端)和真实下载器线。`e2e-nightly.yml` 每晚运行 Firefox 线,并把浏览器线和下载器线各独立运行两遍。端点结构、参数校验等更快的契约测试仍在 `backend/src/test/test_api_*.py` 中。
|
||||
|
||||
@@ -128,7 +128,12 @@ AutoBangumi 使用"分支开发,主干发布"模式。
|
||||
|
||||
## 发布流程
|
||||
|
||||
目前,发布是在开发团队手动合并特定的"发布 PR"后自动触发的。
|
||||
发布只由维护者手动推送版本标签触发,合并 PR 本身不会发布。
|
||||
|
||||
- **beta**:在开发分支上打 `X.Y.Z-beta.N` 形式的标签(如 `4.0.0-beta.1`)。CI 构建 Docker 镜像,镜像标签为该版本号与 `dev-latest`,并创建预发布的 GitHub Release。
|
||||
- **正式版**:开发分支经 PR 合并到 `main` 后,在 `main` 的合并提交上打 `X.Y.Z` 形式的标签。CI 校验标签格式,并确认该提交在 `main` 上,然后构建镜像(标签为版本号与 `latest`),创建 GitHub Release,发布说明取自 `docs/changelog/<X.Y>.md`。
|
||||
|
||||
每个 Release 的附件包括 WebUI、在线更新包、插件 SDK 轮子与插件开发 skill(见 [获取 SDK](/dev/plugins/sdk))。
|
||||
|
||||
Bug 修复 PR 通常会快速发布,一般在一周内。
|
||||
|
||||
|
||||
@@ -8,23 +8,25 @@
|
||||
|
||||
## 五分钟上手
|
||||
|
||||
1. 安装 SDK 与命令行。轮子随每个 4.0 beta / 正式版发布在 [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases) 的附件中(文件名 `autobangumi_sdk-<版本>-py3-none-any.whl`),不发布到 PyPI:
|
||||
1. 安装 SDK 与命令行。轮子随每个 4.0 beta / 正式版发布在 [GitHub Releases](https://github.com/EstrellaXD/Auto_Bangumi/releases) 的附件中(文件名 `autobangumi_sdk-<SDK 版本>-py3-none-any.whl`),不发布到 PyPI。详见 [获取 SDK](/dev/plugins/sdk)。
|
||||
|
||||
```bash
|
||||
uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
2. 生成骨架并测试:
|
||||
2. 生成骨架并测试。骨架的 `pyproject.toml` 依赖 `autobangumi-sdk`,而 PyPI 上没有这个包,所以先用 `uv add` 让它指向下载的轮子:
|
||||
|
||||
```bash
|
||||
ab-plugin new my-rename --kind rename # 也可选 notifier、search
|
||||
cd my-rename && uv run pytest # 骨架自带契约测试
|
||||
cd my-rename
|
||||
uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # 换成轮子的实际路径
|
||||
uv run pytest # 骨架自带契约测试
|
||||
```
|
||||
|
||||
3. 链接到本机的 AutoBangumi 并热重载(先启动过一次 AB,使 `config/` 下有配置文件):
|
||||
3. 链接到本机的 AutoBangumi 并热重载。先启动过一次 AB,使它的配置目录下有配置文件;`--config-dir` 指向这个目录(源码运行时为 `backend/src/config`):
|
||||
|
||||
```bash
|
||||
ab-plugin dev .
|
||||
ab-plugin dev . --config-dir /path/to/autobangumi/config
|
||||
```
|
||||
|
||||
重启 AB 一次后,修改插件目录里的文件会自动重载。
|
||||
@@ -34,6 +36,8 @@
|
||||
|
||||
**基础**
|
||||
|
||||
- [获取 SDK](/dev/plugins/sdk):GitHub Release 中的轮子与插件开发 skill
|
||||
|
||||
- [核心概念](/dev/plugins/concepts):`Plugin`、`ctx`、三种扩展声明、清单、隔离与熔断
|
||||
- [配置表单](/dev/plugins/config-forms):`config_model` 如何变成 WebUI 表单
|
||||
- [事件](/dev/plugins/events):系统事件、整理事件、自定义事件
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
- 本插件之前为同一集创建的链接,在版本升级(新版本替换旧种子,规范文件名不变)后被原子替换为指向新文件的链接。
|
||||
- 已链接的文件再次收到事件时不做任何事;插件在自己的键值存储里记录它创建过的目标路径。
|
||||
- **删除种子不会删除媒体库中的链接。**
|
||||
- 启用前已经下载的文件不会自动处理。在插件的设置分区点「补链已有文件」,或调用 `POST /api/v1/plugins/hardlink/backfill`:遍历 `source_root` 下的 `.mp4` / `.mkv` / `.ass` / `.srt`,返回 `{"linked", "exists", "conflict", "failed"}` 计数。
|
||||
- 启用前已经下载的文件不会自动处理。在插件的设置分区点「链接已有文件」,或调用 `POST /api/v1/plugins/hardlink/backfill`:遍历 `source_root` 下的 `.mp4` / `.mkv` / `.ass` / `.srt`,返回 `{"linked", "exists", "conflict", "failed"}` 计数。
|
||||
|
||||
::: tip Docker
|
||||
硬链接不能跨文件系统。在 Docker 中,请把下载目录与媒体库放在同一块盘上,并以**同一个挂载点**映射进 AB 容器(例如把 `/mnt/media` 整体挂载为 `/media`,下载目录与媒体库都在其下)。两个独立挂载的目录即使在同一块盘上,也会被视为不同文件系统,链接会退化为 `cross_device` 指定的行为。下载器运行在另一个容器里、看到的路径与 AB 不同时,用 `path_map` 做映射。
|
||||
|
||||
@@ -7,7 +7,7 @@ uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl
|
||||
# 或在项目里:uv add ./autobangumi_sdk-0.5.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
轮子在 4.0 beta / 正式版的 GitHub Release 附件中,不发布到 PyPI。契约测试需要 pytest:`uv add --dev pytest`(或安装 `autobangumi-sdk[test]`)。
|
||||
轮子在 4.0 beta / 正式版的 GitHub Release 附件中,不发布到 PyPI,见 [获取 SDK](/dev/plugins/sdk)。契约测试需要 pytest:`new` 生成的 `pyproject.toml` 已在 `dev` 依赖组里带上 pytest;自己建的项目用 `uv add --dev pytest`(或安装 `autobangumi-sdk[test]`)。
|
||||
|
||||
## new
|
||||
|
||||
@@ -28,6 +28,8 @@ ab-plugin new <id> [--kind rename|notifier|search] [--dir .]
|
||||
|
||||
`id` 只能由小写字母、数字和连字符组成。目录已存在时拒绝执行。没有下载器骨架:下载器要连接真实后端,一个能直接通过契约的骨架没有意义。
|
||||
|
||||
生成的 `pyproject.toml` 依赖 `autobangumi-sdk`。这个包不在 PyPI 上,运行 `uv run pytest` 前先在插件目录里执行一次 `uv add <轮子路径>`,uv 会把依赖指向本地轮子。
|
||||
|
||||
## validate
|
||||
|
||||
```bash
|
||||
@@ -42,7 +44,7 @@ ab-plugin validate [path]
|
||||
ab-plugin pack [path] [-o dist]
|
||||
```
|
||||
|
||||
先校验,再打成 `dist/<id>-<版本>.zip`。zip 内容在 zip 根(与签名目录的包布局一致)。排除 `tests/`、`pyproject.toml`、`uv.lock`、缓存和 `dist/`。文件顺序与时间戳固定:同样的内容得到同样的 sha256。
|
||||
先校验,再打成 `dist/<id>-<版本>.zip`。zip 内容在 zip 根(与签名目录的包布局一致)。排除 `tests/`、`pyproject.toml`、`uv.lock`、`dist/`、`.venv`、`.git`、`node_modules`、各类缓存和 `.pyc`。文件顺序与时间戳固定:同样的内容得到同样的 sha256。
|
||||
|
||||
## dev
|
||||
|
||||
@@ -50,7 +52,9 @@ ab-plugin pack [path] [-o dist]
|
||||
ab-plugin dev [path] [--config-dir config]
|
||||
```
|
||||
|
||||
- 把插件目录软链到 `<config-dir>/plugins/local/<id>`。
|
||||
- 先校验,再把插件目录软链到 `<config-dir>/plugins/local/<id>`。
|
||||
- `--config-dir` 默认是当前目录下的 `config`。在插件目录里运行时,请指向 AB 的配置目录(源码运行时为 `backend/src/config`)。
|
||||
- 软链接指向插件目录的绝对路径,AB 必须能用同一路径访问它(例如源码运行的 AB)。AB 在 Docker 中运行时,请把插件目录复制或挂载到容器的 `config/plugins/local/<id>/`,再在 设置 → 插件 中开启「允许未签名插件」并启用它。
|
||||
- 在宿主配置文件(先找 `config_dev.json`,再找 `config.json`)里写入 `plugins.dev_mode`、`plugins.allow_unsigned` 和 `plugins.enabled.<id>`。
|
||||
- 配置文件不存在时拒绝执行:请先启动一次 AutoBangumi。
|
||||
- 运行中的 AB 不会重读配置文件,需要重启一次。之后,`dev_mode` 每秒检查一次本地插件目录,文件变化后自动重载该插件。上次加载失败的插件也被监听,修好源码后自动恢复。
|
||||
@@ -61,8 +65,9 @@ ab-plugin dev [path] [--config-dir config]
|
||||
```bash
|
||||
ab-plugin new my-notify --kind notifier
|
||||
cd my-notify
|
||||
uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl # 只需一次
|
||||
uv run pytest # 契约测试
|
||||
ab-plugin dev . # 链接并开启热重载,重启 AB 一次
|
||||
ab-plugin dev . --config-dir /path/to/autobangumi/config # 链接并开启热重载,重启 AB 一次
|
||||
# 改代码 → AB 自动重载 → 在 WebUI 里验证
|
||||
ab-plugin pack .
|
||||
```
|
||||
|
||||
@@ -49,4 +49,4 @@ class MyPlugin(Plugin[Options]):
|
||||
|
||||
## 超出表单的界面
|
||||
|
||||
表单满足不了时(图表、操作按钮、列表),插件可以提供前端组件,见 [前端挂载点](/dev/plugins/frontend-slots)。内置 `hardlink` 的「补链已有文件」按钮就是一个 `settings.section` 组件。
|
||||
表单满足不了时(图表、操作按钮、列表),插件可以提供前端组件,见 [前端挂载点](/dev/plugins/frontend-slots)。内置 `hardlink` 的「链接已有文件」按钮就是一个 `settings.section` 组件。
|
||||
|
||||
@@ -21,10 +21,12 @@ cp -r examples/plugins/ntfy-notifier config/plugins/local/
|
||||
|
||||
## 运行示例的测试
|
||||
|
||||
在仓库根目录运行,脚本逐个校验示例的清单并运行它的测试(与 CI 相同):
|
||||
|
||||
```bash
|
||||
cd examples/plugins/ntfy-notifier && uv run pytest
|
||||
# 或一次跑完全部示例(在仓库根目录):
|
||||
scripts/test_example_plugins.sh
|
||||
```
|
||||
|
||||
脚本在 `backend/` 的环境里运行,SDK 以可编辑方式装在那里。示例的 `pyproject.toml` 依赖 `autobangumi-sdk`,PyPI 上没有这个包;把示例复制到仓库外单独测试时,先在示例目录里 `uv add <轮子路径>`,再 `uv run pytest`。
|
||||
|
||||
刷新 Jellyfin / Emby / Plex 媒体库不需要写插件:内置插件 `media-server-refresh` 已经支持。
|
||||
|
||||
@@ -21,5 +21,5 @@ class Alerts(Plugin):
|
||||
|
||||
- 只影响外部推送。通知中心里的文案由前端按 `i18n()` 渲染,不受影响。
|
||||
- 钩子对每个启用的渠道各调用一次;`channel` 是渠道类型,如 `telegram`。
|
||||
- 钩子出错或超时时沿用上一步的文案,并计入熔断;返回值类型不对时整条消息回退到默认文案。
|
||||
- 钩子出错、超时或返回的不是 `RenderedMessage` 时,AB 沿用上一步的文案,并计入熔断。后面的钩子照常执行。
|
||||
- 「新集数」通知暂不经过该钩子,仍使用通知渠道里配置的单集模板。
|
||||
|
||||
80
docs/dev/plugins/sdk.md
Normal file
80
docs/dev/plugins/sdk.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 获取 SDK
|
||||
|
||||
插件 SDK 不发布到 PyPI。每个 4.0 beta 与正式版的 [GitHub Release](https://github.com/EstrellaXD/Auto_Bangumi/releases) 都附带两个给插件作者的文件:
|
||||
|
||||
| 文件 | 内容 |
|
||||
| --- | --- |
|
||||
| `autobangumi_sdk-<SDK 版本>-py3-none-any.whl` | `ab_sdk` 包与 `ab-plugin` 命令行 |
|
||||
| `autobangumi-plugin-skill-<AB 版本>.zip` | 给 AI 编码助手使用的插件开发 skill |
|
||||
|
||||
轮子文件名里是 SDK 版本(`ab_sdk.SDK_VERSION`,如 `0.5.0`),不是 AB 的版本号。请从目标 AB 版本的 Release 下载轮子,两者的 SDK 版本一致。插件清单里的 `sdk` 范围(如 `">=0.5,<1"`)必须包含这个版本。
|
||||
|
||||
## 安装轮子
|
||||
|
||||
轮子要求 Python 3.13 或更高,依赖 `pydantic`、`httpx` 与 `packaging`,不需要安装 AutoBangumi。
|
||||
|
||||
只用命令行时,装成全局工具:
|
||||
|
||||
```bash
|
||||
uv tool install ./autobangumi_sdk-0.5.0-py3-none-any.whl
|
||||
ab-plugin --help
|
||||
```
|
||||
|
||||
写插件和跑测试时,把轮子加为插件项目的依赖:
|
||||
|
||||
```bash
|
||||
cd my-plugin
|
||||
uv add ../autobangumi_sdk-0.5.0-py3-none-any.whl
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
`uv add` 在 `pyproject.toml` 的 `[tool.uv.sources]` 里记下轮子的本地路径。`ab-plugin pack` 不打包 `pyproject.toml` 与 `uv.lock`,这个路径不会进入插件包。`ab-plugin new` 生成的 `pyproject.toml` 已经依赖 `autobangumi-sdk`,但没有指定来源,所以在新骨架里也要先执行一次 `uv add`。
|
||||
|
||||
契约测试需要 pytest。骨架的 `dev` 依赖组已带上 pytest;也可以安装可选依赖 `autobangumi-sdk[test]`。
|
||||
|
||||
升级 SDK 时,下载新 Release 的轮子,再执行一次 `uv tool install --force <新轮子>` 或 `uv add <新轮子>`。
|
||||
|
||||
## 在 AB 仓库里开发
|
||||
|
||||
SDK 的源码在 `backend/src/ab_sdk`,打包配置在 `backend/sdk/pyproject.toml`。`backend` 的 `dev` 依赖组以可编辑方式安装它,所以在仓库里不需要轮子:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv sync --group dev
|
||||
uv run ab-plugin validate ../examples/plugins/ntfy-notifier
|
||||
```
|
||||
|
||||
自己构建轮子(与 CI 相同的命令):
|
||||
|
||||
```bash
|
||||
uv build --wheel backend/sdk --out-dir sdk-dist
|
||||
```
|
||||
|
||||
## 插件开发 skill
|
||||
|
||||
skill 是一组 Markdown 说明,AI 编码助手读取它们来写、测试和打包 AB 插件。zip 内是 `autobangumi-plugin/` 目录:
|
||||
|
||||
```
|
||||
autobangumi-plugin/
|
||||
├── SKILL.md # 工作流程、扩展点一览、常见错误
|
||||
└── references/
|
||||
├── extension-points.md # 每个扩展点的签名与字段
|
||||
├── frontend.md # 前端挂载点
|
||||
└── testing.md # 契约测试与 create_plugin
|
||||
```
|
||||
|
||||
把它解压到助手的 skills 目录。以 Claude Code 为例,个人 skills 目录是 `~/.claude/skills/`:
|
||||
|
||||
```bash
|
||||
unzip autobangumi-plugin-skill-<AB 版本>.zip -d ~/.claude/skills/
|
||||
```
|
||||
|
||||
其它助手请按它们各自的 skills 目录放置。skill 的源码在仓库的 `skills/autobangumi-plugin/`。
|
||||
|
||||
## CI 如何构建
|
||||
|
||||
`.github/workflows/build.yml` 的 `release` 任务只在维护者推送版本标签时运行:
|
||||
|
||||
1. `uv build --wheel backend/sdk --out-dir sdk-dist` 构建轮子,版本取自 `ab_sdk.SDK_VERSION`。
|
||||
2. 把 `skills/autobangumi-plugin` 压缩为 `autobangumi-plugin-skill-<AB 版本>.zip`。
|
||||
3. 把两者与 WebUI、在线更新包等文件一起附到该版本的 GitHub Release。beta(预发布)与正式版都带这两个文件。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 签名与分发
|
||||
|
||||
插件有三种来源,信任级别不同:
|
||||
插件有四种来源,信任级别不同:
|
||||
|
||||
| 来源 | 位置 | 签名 | 如何启用 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -49,11 +49,7 @@ AB 从 GitHub release `plugins` 下载 `catalog.json`。目录包含:
|
||||
|
||||
签名目录的插件只能依赖标准库和 AB 已有的包,不做 pip 安装。需要第三方纯 Python 库时放进 `vendor/`。
|
||||
|
||||
::: warning WebUI 尚无目录界面
|
||||
4.0 预览期间,目录的浏览和安装只有 API,设置 → 插件 页还没有对应的按钮。
|
||||
:::
|
||||
|
||||
API:
|
||||
在 WebUI 中,**设置 → 插件** 的「插件目录」区域可以浏览目录,并安装、更新与卸载插件。API:
|
||||
|
||||
| 请求 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -86,4 +82,4 @@ API:
|
||||
|
||||
## 发布 SDK
|
||||
|
||||
SDK 以轮子发布在 4.0 beta / 正式版的 GitHub Release 附件里,由 CI 用 `uv build --wheel backend/sdk` 构建。同一个 Release 还附带 `autobangumi-plugin-skill-<版本>.zip`:给 AI 编码助手使用的插件开发 skill,解压到助手的 skills 目录即可。
|
||||
SDK 轮子与插件开发 skill 由 CI 构建,附在每个 4.0 beta / 正式版的 GitHub Release 上,维护者不需要手动发布。下载和安装见 [获取 SDK](/dev/plugins/sdk)。
|
||||
|
||||
Reference in New Issue
Block a user