Add reliable reading progress, verified backup and restore, reading tools, offline downloads, source quality checks, personal credential status, OPDS serving and verified WebDAV backup uploads.
35 KiB
来源插件系统设计
状态:内置来源、插件、配置 UI、章节富文本/图片与自动追更已实现
适用接口:/api/v1,来源插件协议 1
更新日期:2026-09-20
本文定义 reader 对本地书库、OPDS 书库和远程书源的统一扩展架构。目标是让核心程序只维护一套书架、阅读、缓存和任务逻辑;内置 local、opds 两种来源;远程书源以独立插件开发、安装和升级。
本文区分目标架构和当前交付。local、OPDS 下载、Node 来源插件、配置界面、持久化章节出版物、富文本/图片缓存、手动刷新和服务端自动追更已经接通。外部来源通过通用进程协议接入。P1 已增加用户主动预下载与配额检查,自动缓存淘汰仍未实现。交付状态见 §13,调用示例见 API 参考。
1. 目标与范围
1.1 目标
- 通过同一套来源接口支持本地书、OPDS 目录和远程章节书源。
- 新增远程站点时只安装或更新插件,不修改核心路由、书架和阅读器。
- 将来源协议复杂性封装在深模块内:调用方只理解发现、详情和获取内容。
- 让文件内容和在线章节都进入现有
manifest / toc / assets阅读协议。 - 插件崩溃、超时或禁用时,不破坏书架、阅读进度和已经缓存的内容。
- 插件升级保持来源引用和章节身份稳定,并支持可验证的状态迁移。
- 通过能力声明、凭据隔离、网络权限和独立进程降低插件风险。
1.2 完整版本目标范围
包括:
- 宿主插件注册表、安装/启用/禁用/升级流程。
- 内置
local:扫描BOOKS_DIR,复用现有格式处理器。 - 内置
opds:支持 OPDS 1.x Atom、OPDS 2 JSON、导航、分页、搜索(若目录提供)和文件获取。 - 外部 Node.js 进程插件及版本化 RPC。
- 远程书源的搜索、详情、目录、章节正文和图片资源。
- 宿主统一缓存、下载任务、限流、错误码和日志。
- 来源配置 JSON Schema 和凭据引用。
不包括:
- 插件直接替换阅读器 UI 或注入任意前端代码。
- 不可信插件的完整操作系统级沙箱。首个切片只运行管理员明确安装的受信任插件;需要容器或系统沙箱时另行实现。
- 自动把不同来源的版本合并为同一作品并迁移进度。先保留独立版本,后续可增加可选
workId。
2. 术语与设计原则
| 术语 | 定义 |
|---|---|
| 插件包(Plugin package) | 可安装、校验、升级的代码与声明,例如 reader.source.example。 |
| 来源类型(Source type) | 一套来源行为和配置规范,例如 local、opds、demo-chapters。 |
| 来源实例(Source instance) | 一个具体配置,例如 /books、某个 OPDS 地址或一个站点账号。 |
| 来源条目(Source entry) | 来源目录中的书,以 (sourceInstanceId, externalId) 标识。 |
| 出版物(Publication) | 可阅读的具体版本,拥有自己的目录、章节/文件和定位体系。 |
| 资源(Resource) | 文件、章节正文、图片、字体等可读取内容。 |
| 宿主(Host) | reader 服务端,负责插件调度和用户数据。 |
设计遵循 docs/architecture.md 的数据边界:BOOKS_DIR 是用户书库,是否可写由挂载权限决定;DATA_DIR 存数据库、下载、缓存、扫描记录和插件状态。插件不直接访问宿主数据库,也不把任意路径或远程 URL 交给客户端。
来源接口是外部 seam。local、opds 和远程插件是该 seam 上的 adapter;宿主将鉴权、任务、缓存、资源流和错误语义放在接口之后,调用方不需要知道每个来源的协议细节。
3. 总体架构
flowchart TB
UI[书库/搜索/书架] --> API[来源 HTTP API]
API --> Host[SourceHost]
Host --> Registry[PluginRegistry]
Registry --> BuiltinRunner[BuiltinRunner]
Registry --> ProcessRunner[ProcessRunner]
BuiltinRunner --> Local[local adapter]
BuiltinRunner --> OPDS[opds adapter]
ProcessRunner --> Remote[外部来源插件]
Host --> Runtime[Runtime:HTTP/凭据/缓存/任务/日志]
Host --> Content[PublicationContent:manifest/toc/assets]
Content --> Reader[现有阅读器]
Local --> Content
OPDS --> Content
Remote --> Content
3.1 模块职责
| 模块 | 职责 | 不负责 |
|---|---|---|
PluginRegistry |
发现插件包、校验 manifest、选择版本、记录状态 | 不执行来源业务 |
SourceHost |
将来源调用包装成统一接口;管理实例、能力、超时、任务 | 不解析具体网站规则 |
BuiltinRunner |
在宿主进程内运行受控的 local 和 opds adapter |
不加载外部任意代码 |
ProcessRunner |
启动和监督外部插件进程,执行 RPC、取消和资源流 | 不拥有书架或用户数据 |
Runtime |
HTTP、凭据、命名空间存储、缓存、限流、日志、取消 | 不定义来源协议 |
PublicationContent |
将文件或章节绑定转换为统一阅读协议 | 不负责搜索和目录发现 |
JobManager |
下载、扫描、刷新目录、离线缓存、重试和进度 | 不修改插件内部状态 |
Book/Shelf |
持久化 publication、书架、进度、笔记 | 不根据书名猜测来源身份 |
4. 来源接口
接口以 server/src/sources/types.ts 为准。以下摘录首个切片的 SDK 契约;可选能力通过 capabilities 声明,注册表拒绝声明能力却缺少对应方法的提供者。
interface SourceProvider {
readonly descriptor: SourceDescriptor;
validateConfig?(config: unknown): void | Promise<void>;
browse?(ctx: SourceContext, request: BrowseRequest): Promise<CatalogPage>;
search?(ctx: SourceContext, request: SearchRequest): Promise<CatalogPage>;
detail(ctx: SourceContext, entryRef: string): Promise<CatalogEntry>;
acquire(ctx: SourceContext, request: AcquireRequest): Promise<Acquisition>;
openFile?(ctx: SourceContext,
acquisition: Extract<Acquisition, { kind: 'file' }>): Promise<ResourceResponse>;
getManifest?(ctx: SourceContext, publicationRef: string): Promise<ManifestSnapshot>;
readResource?(ctx: SourceContext, request: ResourceRequest): Promise<ResourceResponse>;
}
SourceProvider 的接口保持小而稳定。适配器内部可以实现规则解析、登录流程、分页协议和站点兼容逻辑;宿主只依赖这些方法的输入、输出、取消、超时和错误不变量。
4.1 目录和条目
interface SourceDescriptor {
id: string;
label: string;
version: string;
capabilities: SourceCapability[];
configSchema?: unknown;
}
interface CatalogPage {
items: CatalogEntry[];
navigation?: NavigationEntry[];
nextCursor?: string;
title?: string;
}
interface NavigationEntry {
ref: string;
title: string;
kind?: 'catalog' | 'search' | 'collection';
}
interface CatalogEntry {
ref: string;
title: string;
authors?: string[];
description?: string;
language?: string;
coverUrl?: string;
options?: AcquisitionOption[];
metadata?: Record<string, unknown>;
}
entryRef、游标和导航引用由来源解释。宿主只做不透明保存和回传,不拼接 URL,也不把它当作数据库主键。
4.2 内容获取
type Acquisition =
| { kind: 'ready'; publicationId: string }
| { kind: 'file'; acquisitionRef: string; mediaType: string }
| { kind: 'chapters'; publicationRef: string }
| { kind: 'action-required'; action: AcquisitionAction };
Acquisition 是提供者返回给宿主的内容绑定。local 可以返回已经存在的 publicationId;远程提供者返回文件或章节引用,由宿主分配稳定身份并处理获取流程。任何 ready ID 都必须由宿主验证确实存在且调用用户有权访问,不能直接信任外部插件的声明。后续引入持久化任务时,HTTP 响应可新增 pending/jobId,它不是首个切片的插件返回值。
文件内容和章节内容由宿主内部统一为:
interface PublicationContent {
manifest(ctx: ReadContext): Promise<ManifestSnapshot>;
toc(ctx: ReadContext): Promise<TocEntry[]>;
asset(ctx: ReadContext, ref: string): Promise<ResourceResponse>;
}
SDK 的 ResourceResponse 支持 mediaType、size、data、text 和 stream。内置 OPDS 使用流下载;进程插件暂只支持章节,acquire.file 会被拒绝。进程资源通过有大小上限的 text/base64 JSON 传输;入库章节接受 UTF-8 纯文本或 HTML/XHTML,HTML 内插图通过 reader-res:<ref> 引用同一插件的二进制资源。宿主清洗并内嵌图片后才返回给阅读器。来源开发预览始终返回 JSON。
4.3 能力
首个切片的能力枚举:
browse
search
detail
acquire.file
acquire.chapters
content.manifest
content.resource
content.update
来源级能力决定 UI 是否显示入口;条目级 options 决定本书是否支持某种格式、下载或试读。当前来源接口的能力缺失返回 SOURCE_UNSUPPORTED;完整错误分类目标见 §8.2。
5. 插件包与 SDK
5.1 manifest
{
"id": "reader.source.example",
"name": "示例远程书源",
"version": "1.0.0",
"apiVersion": 1,
"runtime": "node",
"entry": "dist/main.js",
"sourceTypes": [
{
"id": "example",
"label": "示例书源",
"capabilities": ["search", "detail", "acquire.chapters", "content.manifest", "content.resource"],
"configSchema": { "type": "object", "properties": {} }
}
],
"permissions": {
"network": { "domains": ["example.com", "*.example.com"] },
"storage": true,
"credentials": true
}
}
目标安装流程校验插件 ID、版本、宿主 apiVersion、入口文件、来源类型唯一性、JSON Schema 和权限。首个切片的 manifest 校验集中在 registry.ts,进程启动另行检查支持的协议版本和入口路径。语义化版本范围、包内容哈希、签名和完整权限 schema 校验属于后续包管理工作。
5.2 宿主能力
SDK 的目标能力如下。独立进程尚未实现宿主能力的反向 RPC;已实现的凭据注入会在每次调用时,按用户、来源实例、credentialKeys 和 permissions.credentials 选择允许传递的键,放入 context.credentials。这不等于以下全部运行约束都已强制生效:
| 能力 | 规则 |
|---|---|
http |
超时、重定向检查、Cookie、限流和允许域名均由宿主执行。 |
credentials |
按用户和来源实例隔离;插件只能通过引用读取,日志自动脱敏。 |
storage |
插件/来源实例命名空间 KV,带 schema 版本;不能访问 SQLite。 |
cache |
由宿主控制配额和淘汰;缓存键含来源、账号和资源身份。 |
logger |
结构化日志、级别和脱敏由宿主控制。 |
signal |
每次调用都带取消信号、截止时间和最大响应大小。 |
宿主不提供前端 DOM 和数据库接口。首个切片中的受信任 Node 进程仍可使用 Node 的网络、文件和子进程能力,独立进程只提供故障隔离;系统沙箱落地后才可以强制限制这些直接调用。
5.3 RPC
首个切片采用一行一个 JSON 的 stdio JSON-RPC 2.0;stdout 只输出协议消息,插件日志写 stderr:
request = { jsonrpc: "2.0", id, method,
params: { sourceType, context: { instance, userId, credentials? }, ... } }
response = { jsonrpc: "2.0", id, result }
error = { jsonrpc: "2.0", id, error: { code, message, data? } }
宿主每次来源调用预算为 60 秒,同一来源实例最多并发 2 次,宿主共 8 次,超限返回 429 RATE_LIMITED。插件单次 RPC 预算为 30 秒,每条 JSON 消息上限为 2 MiB(包括 base64 和协议字段)。所有 RPC(包括详情、加入书架、正文与扩展页面/操作/任务)超时或取消时,只结束当前请求:宿主发送 $/cancelRequest,清理该请求的计时器和监听器并忽略迟到响应,其它调用及后续请求继续使用同一进程。RPC 超时或 TimeoutError 信号返回 PLUGIN_TIMEOUT,主动取消返回 PLUGIN_CANCELLED。声明 search.session 的 searchCancel 控制请求不占用普通调用槽位,宿主等待确认最多 5 秒;取消或超时只结束等待,不再向其发送取消通知,让迟到的停止操作仍有机会清理搜索会话。插件应优先处理停止请求,并为断开的搜索设置空闲回收机制。
取消通知是尽力而为的协作机制,插件应按请求 ID 中断对应的网络、浏览器和解析任务;不处理通知的插件仍可能继续执行已超时的操作。超时不表示操作尚未生效,宿主不自动重复 acquire 等可能有副作用的调用。进程自身崩溃、输入管道损坏、协议错误或响应超过消息上限时,宿主仍会终止 worker,全部在途调用失败;进入 failed 后可由管理员重新启用。若插件的事件循环阻塞或内部队列被未取消任务占据,其后续调用仍可能超时,需要插件自身修复或管理员重新启用。
6. 内置来源
6.1 local
- 自动创建
local实例,配置固定为{},使用宿主BOOKS_DIR;首个切片不支持每个实例覆盖目录。文件管理写入是否可用取决于BOOKS_DIR的挂载权限。 - 由宿主扫描器负责增量检测,路径必须通过
resolveInside(),不跟随越界符号链接。 - 文件格式继续复用现有
server/src/indexer/formats处理器。 - 本地文件作为
Publication的file绑定;书籍身份继续遵循现有content_hash规则。 - 文件读取继续保持原接口的缺失错误语义(如
FILE_MISSING);本轮不改已有书架、进度及扫描清理规则。
6.2 opds
- 支持 OPDS 1.x Atom 和 OPDS 2 JSON;解析导航、分页、搜索入口、封面和 acquisition link。
- 不假设每个链接都可下载。根据
rel、type和条目状态产生AcquisitionOption。 - 首个切片支持公开或 Basic 认证的 EPUB、PDF、TXT、CBZ 下载;借阅、购买、试读等无法直接获取的关系返回
action-required。 - 来源地址、用户名、附加下载 origin 由管理员配置并共享;密码通过凭据接口按用户和来源隔离,以 AES-256-GCM 加密保存。密码不写在普通配置中。
- 导航和 Feed 请求限制在配置地址的 origin,附加下载 origin 必须列入
allowedOrigins;每次重定向重新校验,跨 origin 不转发 Basic 凭据。此策略允许管理员配置内网 OPDS,不等于系统级网络隔离。 - 下载同步完成后返回
ready,没有后台任务 ID。临时文件写入DATA_DIR/acquired,单文件最大 256 MiB;完成后计算真实 SHA-256、解析格式、原子改名并事务写入books、acquired_files、source_acquisitions,加入调用用户书架。超限、取消和入库失败清理本次文件;现有书籍元数据保持不变。 - 下载后的 EPUB/PDF 走与
local相同的格式解析和阅读路径。
6.3 外部远程书源
目标上插件负责搜索、详情、目录快照、正文清洗和图片引用;宿主负责缓存、限流、任务、权限和阅读协议。插件返回 chapters 绑定后,宿主获取目录快照、分配稳定 book ID 并加入调用用户书架;HTTP 返回 ready/publicationId,阅读器继续使用 /books/:id/manifest|toc|assets。
入库阅读支持 UTF-8 text/plain、text/html 和 application/xhtml+xml。纯文本由客户端转义排版;HTML 经 sanitize-html 白名单处理,只保留段落、标题、强调、列表、表格等静态标签。删除脚本、样式、表单、外链和 SVG。插图须声明 reader-res:<插件资源引用>,宿主调用同一来源 readResource,校验 PNG/JPEG/GIF/WebP 类型与文件签名,再内嵌 data URL。整章(含图片)原子缓存,任一资源失败不写半份缓存。禁用或卸载插件后已缓存正文和插图仍可读。
刷新目录通过 POST /books/:id/refresh 或订阅调度触发;阅读器目录面板以稳定 href 恢复当前位置。失败保留旧目录,界面在目标章节可读后才替换视图。chapter_subscriptions 持久保存开关、15~10080 分钟间隔、检查时间、失败次数和新增章提示。每分钟调度一次,单轮最多 20 本顺序检查,共用来源并发限制;手动与自动检查同书合并在途调用。失败退避,暂停来源、隐藏书籍、禁用账号时跳过,关闭浏览器不影响检查。新目录不强行替换正在阅读的旧快照,也不预下载正文。
7. 数据模型
实现使用增量表,保留已有 books、book_files、user_books 和 reading_progress,没有重写旧书 ID:
| 表 | 当前用途 |
|---|---|
source_instances |
共享来源配置:id/plugin_id/source_type/name/config_json/enabled/created_at。 |
source_credentials |
(source_id, user_id, key) 对应的加密凭据。 |
acquired_files |
book_id 对应的 DATA_DIR 相对文件路径和大小,与扫描目录分离。 |
source_acquisitions |
(source_id, user_id, entry_ref, option_id) 到 book_id 的映射。 |
chapter_publications |
按用户隔离的章节出版物、来源引用、当前 revision 和目录快照。 |
chapter_snapshots |
出版物历次目录快照;刷新后保留旧 revision。 |
chapter_resources |
每个 revision 的章节引用、已缓存正文、media_type 和正文 SHA-256;HTML 正文包含校验后的内嵌图片。旧缓存增量迁移为纯文本。 |
chapter_subscriptions |
每书追更开关、间隔、检查时间、失败退避、新增章数及配置 generation。 |
plugin_storage |
宿主 KV,按来源和用户命名空间保存;外部进程尚无反向访问接口。 |
installed_plugins |
插件 ID、已放置的包目录名及启用状态。 |
来源实例由管理员配置,对登录用户共享。凭据、获取记录和书架属于当前用户;普通用户的来源列表不返回 config。同一插件条目可能因账号返回不同正文,因此章节出版物和缓存也按用户隔离,不与他人共享 book ID。禁用来源不删除已下载文件和章节缓存。
当前章节关系如下:
books (format=chapters)
└─ chapter_publications (book_id, user_id, source_id, publication_ref, revision)
└─ chapter_snapshots (book_id, revision, snapshot_json)
└─ chapter_resources (book_id, revision, chapter_id, provider_ref, body?)
├─ user_books
├─ reading_progress
└─ notes
约束:
- 章节 book ID 由
(source_instance_id, user_id, publication_ref)派生;唯一约束与之对应。反复获取和重新上架沿用同一个 ID。 external_ref、OPDS URL、章节序号都不是publicationId。books.content_hash_kind是增量字段,旧数据默认file。章节使用manifest,content_hash为规范化目录的真实 SHA-256,不冒充整书文件哈希,也不参与文件去重。- 章节必须有稳定 ID。
href由该 ID 派生,顺序单独保存;插章不会让旧进度指向另一章。资源的resourceRef另含目录 revision,客户端用 href 保存位置,用 resourceRef 取正文和缓存。 - revision 包含目录内容和插件的可选
version。即使目录不变,正文修订时插件也必须改变version;否则宿主继续复用已缓存正文。 - 同一个 resourceRef 的缓存内容不变。旧 revision 已缓存的章节可继续读;旧 revision 未缓存的章节返回
409 CHAPTER_SNAPSHOT_EXPIRED,避免取到上游当前正文后冒充旧版。刷新期间发生的资源请求也遵守此规则。 - 来源失效或插件禁用后,已缓存出版物仍可读;卸载插件默认保留用户数据。现阶段历史快照和正文尚无总配额及自动淘汰策略。
- 初期不同来源版本不自动合并进度;未来的作品聚合只提供迁移建议。
- 插件状态写入自己的 namespace,状态迁移失败时保持旧版本或回滚。
8. 生命周期与故障语义
8.1 插件生命周期
发现 → 校验 → 安装 → 启用 → 健康检查 → 运行
↓
禁用 / 故障
当前安装流程:管理员在 Web 输入 npm 包名或上传 .tgz,显式确认信任代码。服务端在 DATA_DIR/plugins 中创建独立依赖目录,使用 npm 安装并禁用生命周期脚本,校验真实路径、manifest、入口和来源类型,保存安装记录并自动启用。同 ID 的不同版本自动进入更新流程;相同版本返回中文重复安装提示。失败清理本次目录,不影响已有插件。预部署目录加载接口仍保留,重复加载不会自动覆盖。启动时自动恢复启用的插件。启用、禁用、卸载和更新切换串行执行,禁用先注销提供者再终止进程;卸载移除安装记录,保留包文件、来源实例和已有用户数据。内置 reader.local、reader.opds 不允许卸载或替换。
已实现的在线更新流程:
- 下载到独立安装目录,校验包、manifest、API 版本、路径和来源接口。
- 检查已有来源实例使用的类型和能力是否仍存在,并用新版
validateConfig校验每个实例的原配置(包括停用实例)。校验失败保留旧版本。 - 若旧插件仍有在途 RPC 或管理/后台写入操作,返回
PLUGIN_BUSY,由管理员停止搜索或等待后重试。 - 在同一次同步切换中更换提供者及安装记录;数据库写入或注册失败则恢复原提供者。成功后停止旧进程并启用新版。
- 保留来源 ID、配置、凭据、订阅、插件数据目录、书架及缓存;旧包目录也保留。重启按新的安装记录恢复。
宿主不会执行插件数据迁移,也不对插件自行写入的数据提供快照回滚。插件的配置校验应无副作用,版本间需要保持自己的数据格式兼容。签名校验、自动迁移和一键版本回滚尚未实现;管理员可再次上传兼容的其它版本切换代码。
插件禁用不删除来源实例、publication、缓存和进度。删除数据是单独的显式操作。
8.2 统一错误
AUTH_REQUIRED 需要登录或凭据过期
RATE_LIMITED 来源限流
SOURCE_CHANGED 站点结构或规则失效
RESOURCE_GONE 远程或本地资源不存在
CAPABILITY_UNSUPPORTED 当前条目不支持该能力
PLUGIN_UNAVAILABLE 插件未运行或崩溃
TIMEOUT 宿主截止时间已到
QUOTA_EXCEEDED 缓存或下载配额不足
该列表是完整版本的错误分类目标。当前 HTTP 响应保持 { "error": { "code", "message" } },具体已实现错误码以 API 参考 为准,例如 SOURCE_TIMEOUT、PLUGIN_TIMEOUT、RATE_LIMITED。retryAfter/details 未在本轮通用错误响应中实现。
9. 安全与资源边界
以下区分当前强制约束与后续安全工作;不能把 manifest 权限声明当作已生效的系统策略。
- 外部插件默认独立进程运行;插件进程崩溃不能让宿主退出。
- 内置 OPDS 的网络请求经过宿主校验:允许 origin、重定向目标、Basic 凭据转发、超时和响应大小;普通 OPDS 链接不能跳到未配置的 origin。
- 外部 Node 插件当前是受信任进程,仍拥有本机 Node 的网络、文件和子进程权限;manifest 权限声明和
SourceContext可选能力尚未构成 OS 级网络沙箱。管理员只能安装信任的包。 - 插件凭据不出现在普通配置、前端 DTO 和日志中。
- 资源预览只返回 JSON;阅读正文纯文本转义或 HTML 白名单清洗,并带
nosniff、禁止脚本和外网资源的 CSP。图片经来源通道获取后内嵌,不让浏览器直接访问书源网站。 - 本地和已获取文件路径通过宿主
resolveInside()校验;外部插件直接读写路径的能力目前依赖管理员信任,系统沙箱属于后续工作。 - 硬限制:宿主调用及整章加载 60 秒、插件 RPC 30 秒、JSON 2 MiB、文件获取 256 MiB;目录最多 10,000 项及 2 MiB,原始正文 2 MiB;最多 32 个不同图片、每图 2 MiB,内嵌后整章最多 8 MiB。图片仍受 RPC base64 开销限制。CPU/内存和域名权限的系统级隔离属于后续工作。
- 插件安装及来源实例配置需要管理员权限;实例配置共享,凭据按用户隔离。
manifest 中的权限声明用于宿主授权和展示,不能单独视为安全沙箱;运行不可信插件需要容器或操作系统策略。
10. 当前 HTTP 接口
均位于 /api/v1 并遵循现有认证和错误格式:
GET /plugins 插件及版本状态
POST /plugins 安装 npm 包(package)或加载预部署目录(folder)
POST /plugins/upload 上传 .tgz,安装并启用
PATCH /plugins/:id 启用/禁用(enabled)
DELETE /plugins/:id 卸载注册(保留包文件和数据)
GET /sources/types 当前已注册来源类型
GET /sources 来源实例列表
POST /sources 创建来源实例
PATCH /sources/:id 仅修改 enabled
PUT /sources/:id/credentials/:key 设置当前用户凭据
GET /sources/:id/browse 浏览目录
POST /sources/:id/search Streamable HTTP 搜索(SSE results/done/error 事件)
GET /sources/:id/entries?ref=... 详情
POST /sources/:id/acquire 获取内容,body 传 entryRef
GET /sources/:id/publications/:publicationRef/manifest
GET /sources/:id/publications/:publicationRef/resource?ref=...
POST /books/:id/refresh 获取并提交新的章节目录快照
HTTP 返回宿主 DTO,凭据不回显。插件和来源配置管理需管理员权限;浏览、获取和个人凭据需登录。PATCH /sources/:id 更新名称/config/启停,config 变更清除所有用户的该来源凭据;有在途来源调用时拒绝变更 config。DELETE 只移除无获取记录的非本地来源。Web #/sources 根据类型 schema 绘制配置表单,普通成员只有浏览、凭据和个人追更操作。插件可声明 credentialKeys 与 credentials 权限,宿主只按调用用户向进程传递明确声明的键。Web 输入 npm 包名或上传安装包均支持更新同 ID 的已安装插件。
11. 分阶段实施
阶段一:内容 seam 与内置 local
- 抽取
PublicationContent,让现有/manifest、/toc、/assets不再直接依赖book_files → absPath。 - 将现有扫描和格式处理包装为
localadapter。 - 保留旧
bookId、进度、笔记和书架语义,补充 publication 映射。 - 验收:已有 EPUB/TXT/PDF/漫画书籍、阅读进度和离线缓存行为不变。
阶段二:SourceHost 与内置 OPDS
- 实现来源注册、实例配置、能力 DTO 和
JobManager。 - 实现 OPDS 1.x/2.x 解析、分页、详情和 EPUB/PDF 下载。
- 下载文件进入
DATA_DIR,复用 local 的内容解析链路。 - 验收:OPDS 书可浏览、搜索(若服务支持)、下载、阅读和重启恢复;失败任务可重试。
阶段三:插件管理与 ProcessRunner
- 定义 manifest、SDK、RPC、插件目录和生命周期。
- 增加一个最小章节插件,验证搜索→详情→目录→正文→图片完整链路。
- 增加超时、取消、进程崩溃恢复、错误映射和插件状态迁移。
- 验收:插件无需修改核心代码即可新增来源;禁用插件不影响已缓存内容。
阶段四:远程书源生态
- 实现配置表单、凭据动作、章节缓存和目录刷新。
- 开发独立来源插件,并使用通用协议验证接入。
- 建立契约测试样例:分页、登录、限流、规则失效、插章和图片资源。
- 验收:导入一个书源配置即可创建来源实例;更新规则只升级插件/配置,不改阅读器。
12. 验证与兼容策略
每个 adapter 和插件都必须通过宿主契约测试:
- 能力声明与实际方法一致。
- 所有调用支持取消、超时和稳定错误码。
- 外部引用保持可重复解析;章节 ID 在刷新后稳定。
- 资源流不会被 JSON 编码截断或超出大小限制。
- 凭据和缓存按用户/来源实例隔离。
- 插件崩溃、禁用、升级失败时,书架和已缓存 publication 仍可访问。
宿主阅读 API 继续遵循现有长期兼容策略:为章节增加 revision/resourceRef 字段和刷新端点,已有文件资源形状保持不变。数据库迁移只新增表和带默认值的列;章节使用明确标记的目录哈希,不伪造整书文件哈希。设备端章节目录、正文、书架和进度缓存按服务地址与用户隔离;升级时恢复已有会话可一次性迁移旧书架、进度和待同步操作,新账号登录不认领旧缓存。
13. 本轮实施状态与后续工作
已交付来源管理 UI、章节富文本/插图与追更订阅;外部来源通过独立 Node.js 进程接入。后续条目不能视为已交付。
| 工作 | 本轮状态 | 说明 |
|---|---|---|
| 来源契约 | 已实现并测试 | server/src/sources/types.ts,包括目录、获取、章节和资源。 |
| 来源注册表及 manifest 校验 | 已实现并测试 | 验证能力与方法一致,校验插件 DTO、章节 ID 和顺序。 |
| OPDS 解析、浏览、搜索、获取 | 已实现并测试 | OPDS 1.x/2.x、相对链接、OpenSearch、分页、Basic 认证和获取关系。 |
| OPDS 文件下载入库 | 已实现并测试 | 同步下载,真实哈希、256 MiB 上限、失败清理、幂等复用、当前用户上架;复用现有阅读接口。 |
| SourceHost、来源实例及凭据 | 已实现 | 实例由管理员配置共享,密码按来源/用户加密,调用预算 60 秒;并发限额每实例 2、全宿主 8。 |
| 来源管理与配置 UI | 已实现并测试 | 创建、编辑、启停、删除空来源、浏览/搜索、获取及个人凭据。 |
| 内置 local 与本地内容 seam | 已实现并测试 | 保留现有 book ID、书架及阅读接口,集中本地与下载文件解析。 |
| 受信任 Node ProcessRunner | 已实现并测试 | JSON-RPC、30 秒调用预算、所有请求的超时/取消隔离、停止请求隔离、异常退出、2 MiB 消息上限。 |
| 插件安装、启停、卸载与启动恢复 | 已实现并测试 | Web 安装 npm 包或上传 .tgz;失败清理本次目录,卸载保留数据和包文件。 |
| 最小外部来源示例 | 已实现并测试 | examples/plugins/demo-chapters,提供静态示例目录和正文,不联网。 |
| 插件安装 UI | 已实现 | 输入 npm 包名或上传 .tgz 后安装/更新并启用;保留来源配置和数据,失败保留原版本;支持启停、重启、卸载。一键版本回滚仍后续。 |
| 后台下载任务、断点恢复、JobManager | 后续 | 当前完整文件会持久化,下载过程本身没有后台任务及恢复机制。 |
| 外部章节写入书架与阅读 | 已实现并测试 | 私有稳定 book ID、现有 manifest/items/toc/assets、书架、继续阅读和进度。 |
| 目录快照与服务端章节缓存 | 已实现并测试 | 稳定 href、revision 资源引用、已读缓存、失败保留旧目录、禁用/卸载/重启后缓存可读。 |
| Web 章节阅读与设备离线缓存 | 已实现 | 目录与已读章节按服务器和用户持久缓存;断网或服务端 5xx 时回退;授权及资源不存在错误不回退。 |
| 手动刷新目录 | 已实现 | 阅读器目录面板入口,恢复当前章节位置,记录检查状态。 |
| 远程富文本与图片 | 已实现并测试 | 白名单 HTML、受控栅格图片、服务端与设备整章缓存;设备仅已读内容可离线。 |
| 整书预缓存、缓存配额与淘汰 | 后续 | 保留历史目录与缓存,无自动清理配额。 |
| 自动追更 | 已实现并测试 | 用户按书订阅,服务端定时检查、持久状态、失败退避与新增章提示。 |
| 声明凭据注入 | 已实现并测试 | 每次调用仅传递获准声明键,按用户和来源隔离。 |
| 反向宿主能力 RPC、系统权限隔离和缓存配额 | 后续 | 类型预留不等于运行策略已生效。 |
| 容器/操作系统沙箱 | 后续 | 首个版本仅允许运行管理员信任的插件。 |
验证覆盖来源、插件进程、管理接口、OPDS 下载、用户隔离、并发限额、参数校验与既有本地阅读回归。章节端到端测试使用真实 Node 进程和可变目录 fixture,覆盖获取幂等、插章定位、版本缓存、旧快照拒绝、刷新失败、禁用/卸载/扫描/重启、媒体类型不一致拒绝及 HTML 清洗;核心测试另覆盖旧缓存迁移、请求与刷新并发及资源边界。原生规则插件测试使用真实 Node 进程和本地 HTTP fixture,覆盖 npm 安装、按用户 Cookie、插章后按 URL 定位、图片缓存和追更;包测试另覆盖真实 Chromium 与脚本兼容对象,未验证特定公网 OPDS 服务或第三方采集站点。
回归入口:在 server 和 web 分别执行 npm test;两端另提供 npm run typecheck 和 npm run build。测试结果以对应提交的 CI 输出为准。界面评审记录见 书源界面和书库与阅读界面。
通用配置页、任务、筛选与换源
已实现 manifest.extensions.pages/tasks、独立数据目录、声明式页面、通用后台任务、search.filters 与 content.alternatives。详情、责任分配和失败语义见扩展设计。这些扩展不依赖具体插件 ID。
0.3 实例配置作用域
来源实例可声明独立配置页面与后台任务,同一插件的多个实例互不共享配置。页面的 Tab、表单和输出由通用 DTO 定义,见扩展协议。
P2 接入体验补充(2026-09-24)
来源页按 descriptor 展示能力声明,不把声明当作站点可用性保证。个人凭据页显示配置状态和最近访问结果,用户可以编辑、保留或删除指定字段;不回显秘密。
插件遇到未登录、登录过期、人工验证时应分别抛出 AUTH_REQUIRED、AUTH_EXPIRED、VERIFICATION_REQUIRED(也可作为搜索部分失败项返回)。不要将登录页面当作成功正文返回。宿主不会识别任意站点的登录页面或自动绕过验证码;实际登录、验证码和网站适配仍由插件负责。未提供会话过期时间时,界面不估算过期时间。
换源的质量检测复用 acquire / getManifest / readResource,可能拉取内嵌图片;不要在这些读取调用中加入不可重入的副作用。检测结果是一次快照,确认换源时会重新读取并验证。宿主拒绝空白正文;正文正确性、缺段与广告识别仍须插件验证。