14 KiB
QQBot Plugin Platform 彻底解耦设计
背景
第三期 QQBot 插件平台已经完成目录收敛、worker runtime、定时任务和内置插件包化,但当前代码仍没有做到架构真实解耦。plugin-platform 仍直接认识四个具体插件:
src/modules/qqbot/plugin-platform/infrastructure/integration/package/builtin-plugin-package-loader.service.ts直接 importbangdream、ff14-market、fflogs、repeater的入口、类型和内部 helper,并在平台侧拼出每个插件的专用 host/options。src/modules/qqbot/plugin-platform/infrastructure/integration/runtime/builtin-plugin-worker.thread.ts直接 import 四个插件,在 worker 内按pluginKeyswitch 创建具体插件实例和配置 key 列表。QqbotBuiltinPluginWorkerRuntimeFactoryService依赖QqbotBuiltinPluginPackageLoaderService,workerData 只传pluginKey,迫使 worker 继续硬编码插件包路径。- 现有结构测试只禁止
plugin-platform/applicationimport 插件,没有覆盖infrastructure/integration和 worker thread;部分 DI/loader 测试还在固定旧耦合。
这类实现本质上仍是“平台内置插件转接层”,不满足第三期“统一插件平台、线上安装、热插拔、worker 隔离”的目标。用户已确认采用方案 A:强门禁彻底解耦,不保留平台到具体插件的转接行为。
已确认决策
plugin-platform不得 import、switch、factory、配置白名单或 host-call 分支任何具体插件。- 内置插件只是预安装的普通插件包,和线上安装插件走同一 manifest、installation、runtime descriptor、worker entry 协议。
- 平台只认识插件包协议:
plugin.json、安装路径、入口文件、运行时声明、operation/event/task metadata、权限和 host capability。 - 具体插件的业务适配全部放回各自插件包。BangDream、FF14 Market、FFLogs、Repeater 可以有自己的 package-local adapter,但不能由平台创建。
- 旧
QqbotBuiltinPluginPackageLoaderService和builtin-plugin-worker.thread.ts里的具体插件 import/switch 必须删除或改名为完全通用实现。 - 测试必须先补硬门禁,让当前耦合明确失败;后续任何新增插件都不能要求改
plugin-platform源码。
目标
- 平台源码中不再出现四个具体插件包的 import、路径、factory、switch 或 config key 专用逻辑。
- worker 通过
pluginRoot + manifest.entry动态加载插件入口,而不是按pluginKeyimport 包。 - registry、operation executor、event dispatcher、task runner 只依赖 manifest 和 installation runtime descriptor。
- host bridge 变成 capability-based 通用协议,不包含
bangdreamRequestJson、ff14WorldCatalog这类插件专用方法。 - 四个现有插件能力保持一致:BangDream 15 个 operation 和 Bestdori 定时任务、FF14 Market 2 个 operation、FFLogs 1 个 operation、Repeater message event 不退化。
- Admin/API 既有插件管理、命令测试、定时任务和 worker 超时恢复语义不退化。
非目标
- 不重新设计整个第三期 QQBot 模块。
- 不重写 Admin 页面。
- 不改变 NapCat 登录链路。
- 不做线上插件市场功能扩展。
- 不为快速通过保留平台侧兼容 shim、具体插件 import map、具体插件 key switch 或纯 re-export 转接层。
- 不改变现有 operation key、handlerName、alias、权限码和 command SQL linkage。
边界规则
Plugin Platform 允许依赖
src/modules/qqbot/plugin-platform/** 可以依赖:
- 插件 manifest 类型、manifest parser、manifest validator。
- 插件 installation/version/task/runtime event 实体和 repository。
- worker driver、queue、BullMQ、runtime evidence、HTTP/storage/dictionary/send 等宿主服务。
- 通用插件 SDK/host contract。
- 受控插件根目录配置和安装路径白名单。
Plugin Platform 禁止依赖
src/modules/qqbot/plugin-platform/** 禁止:
@/modules/qqbot/plugins/**import。- 拼接
src/modules/qqbot/plugins/bangdream、ff14-market、fflogs、repeater这类具体路径。 switch (pluginKey)或if (pluginKey === 'bangdream')这类具体插件分支。createBangDreamPlugin、createFf14MarketPlugin、createFflogsPlugin、createRepeaterPlugin这类具体 factory。BANGDREAM_TSUGU_ENV_KEYS、FF14 world helper、FFLogs world resolver 等插件内部常量/helper。bangdreamRequestJson、bangdreamRequestBuffer或其他插件命名 host call。
允许平台扫描一个受控目录下的 */plugin.json。扫描行为只能基于 manifest 文件存在与否,不能把具体插件 key 写入平台源码。
目标架构
插件包来源
新增通用 package source,替代 built-in loader:
plugin-platform/infrastructure/integration/package/
plugin-package-source.service.ts
plugin-package-manifest-reader.service.ts
plugin-package-path-policy.service.ts
职责:
- 从一个或多个受控 root 扫描
*/plugin.json。 - 读取并校验 manifest。
- 生成
PluginPackageDescriptor:
{
pluginKey: manifest.pluginKey,
version: manifest.version,
packageRoot: <受控安装或内置包根目录>,
entry: manifest.entry,
manifest,
sourceType: 'builtin' | 'installed',
}
内置插件 root 是一个平台配置项,例如 src/modules/qqbot/plugins 对应开发态源码目录,或构建后等价目录。平台可以知道“内置插件包目录根”,但不能知道根目录下有哪些具体插件。
Runtime Descriptor
worker runtime 不再只接收 pluginKey。启动 worker 时传入完整 descriptor:
{
installationId,
pluginKey,
packageRoot,
entry,
manifest,
runtimeOptions,
configSnapshot,
}
packageRoot + entry 是唯一代码加载入口。pluginKey 只用于日志、状态、权限和 runtime event 归属,不用于选择具体代码分支。
Worker Entry Loader
worker thread 改为通用加载器:
- 校验
packageRoot在 path policy 允许范围内。 - resolve
entry,确认入口仍在packageRoot内。 - 使用动态 import 加载入口。
- 校验入口导出
createPlugin函数。 - 调用统一协议:
createPlugin({ manifest, host, runtime, normalizeError, now })。 - 从返回对象读取
operations、events、tasks、activate、dispose、healthCheck。
worker thread 不再 import 任何具体插件包,也不再保存 config key switch。
通用 Host Bridge
平台提供 QqbotPluginHostBridge,向插件暴露 capability-based host:
getConfig(key)/getConfigMany(keys):读取插件配置快照。getDictByKey(key)/getDictItemsByKey(key):读取平台字典。relationTree(key):读取字典关系树。requestJson(request)/requestBuffer(request):受控外部 HTTP。readAssetFile(path)/readJsonFile(path)/readExcelRows(path):读取插件声明资产或缓存文件。writeJsonFile(path, value)/renameFile(source, target):写入插件私有 storage。sendText(context, text):走 QQBot send queue。emitRuntimeEvent(event):记录插件运行事件。sleep(ms):受控等待。warn(message, context):结构化 warning。
host call 名称必须是通用能力名,不带插件 key 或业务名。插件如果需要 Bestdori、Universalis、FFLogs GraphQL、world resolver 等业务语义,必须在插件包内部通过这些通用能力组合出来。
插件入口协议
每个插件 src/index.ts 是唯一入口,只导出 createPlugin。统一入参:
{
manifest,
host,
runtime,
normalizeError,
now,
}
统一返回值:
{
pluginKey,
operations?,
events?,
tasks?,
activate?,
dispose?,
healthCheck?,
}
平台不关心插件内部如何从 host 组装业务 client。BangDream 可以在自己的入口里把通用 host 映射为 configReader、dictionaryReader、io;FFLogs 可以在自己的包内实现 world resolver;Repeater 可以在自己的包内组装 event API。
配置 key 来源
删除 getConfigKeysForPlugin()。worker 需要预加载配置时,配置 key 必须来自 manifest:
configSchema.properties的 key。- 或 manifest
runtime.configKeys。
如果某插件配置无法静态声明,入口必须使用 async host 配置读取,不能让平台新增插件 key 分支。
Registry 和 Lifecycle
QqbotPluginRegistryService、QqbotEventPluginRegistryService 和 task scheduler 只从 installation/runtime summary 获得能力:
- 平台启动时扫描 package descriptor。
- 同步或修复 built-in installation/version/manifest 元数据。
- 对 enabled installation 启动 worker。
- worker load/activate 后返回 operation/event/task runtime summaries。
- registry 只注册 active summaries,不持有具体插件实例。
- 禁用、卸载、升级时通过 installation 状态驱动 registry 移除或替换。
旧的 loadCommandPlugins()、loadEventPlugins() 和 loadBuiltinManifests() 不再返回具体插件实例;manifest 同步和 runtime 启动分离。
插件包侧调整
BangDream
BangDream 入口负责把通用 host 映射为自身需要的 configReader、dictionaryReader、io 和 Bestdori/cache adapter。Bestdori 主数据同步 task 仍属于 BangDream 包内部,只通过 host HTTP/storage 能力读写缓存。
BangDream 包不得要求平台提供 BangDreamRuntimeIo 或 BANGDREAM_TSUGU_ENV_KEYS。需要的配置 key 写入 manifest。
FF14 Market
FF14 world catalog、region/data center/world 解析留在插件包内。平台只提供字典、relation tree 和 HTTP 能力。
FFLogs
FFLogs 不再通过平台导入 FF14 helper 解析世界。它可以在包内复用 FF14 插件暴露的纯数据文件,或通过通用字典 host 解析 known world;选择必须在插件包内完成。
Repeater
Repeater event API 留在插件包内。平台只负责把 message event 交给 active event runtime,并通过 host send queue 发送文本。
测试策略
必须先写 RED 测试,让当前耦合失败:
- 扩展
test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts,扫描整个src/modules/qqbot/plugin-platform/**/*.ts,禁止具体插件 import、路径、factory、switch 和 config key switch。 - 更新
test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts,不再期待 runtime factory 依赖QqbotBuiltinPluginPackageLoaderService;改为期待通用 package source / host bridge / runtime descriptor。 - 替换
test/modules/qqbot/plugin-platform/builtin-plugin-package-loader.spec.ts,使用临时 demo plugin package 验证 manifest 扫描和 descriptor 生成,不再传bangdream。 - 新增 worker generic loading 测试:临时插件入口通过 manifest entry 动态加载,worker source 不含具体插件 import/switch。
- 新增 package entry contract 测试:四个现有插件均可通过同一
createPlugin({ manifest, host, runtime, normalizeError, now })fake host 创建 runtime。 - 保留并复跑 BangDream package entry、Repeater plugin、plugin task manifest、worker timeout/recovery 相关测试,证明行为没有退化。
实现完成后的验证:
pnpm exec jest --runInBand --runTestsByPath跑插件平台结构、lifecycle、loader、worker、task、四个插件 entry 的聚焦用例。pnpm run typecheck。- changed-file ESLint 或聚焦 lint。
pnpm run build,因为 worker 动态 import 和构建路径容易漂移。- 本地接口 smoke 覆盖
/qqbot/plugin-platform/*、/qqbot/plugin/operation/page、/qqbot/command/test和插件定时任务页面相关 API。
完成标准
rg "@/modules/qqbot/plugins|src/modules/qqbot/plugins" src/modules/qqbot/plugin-platform不再命中具体插件 import 或具体插件路径。rg "QqbotBuiltinPluginPackageLoaderService|BUILTIN_PLUGIN_KEYS|getConfigKeysForPlugin|createBangDreamPlugin|createFf14MarketPlugin|createFflogsPlugin|createRepeaterPlugin" src/modules/qqbot/plugin-platform不再命中旧耦合实现。- workerData 包含
packageRoot、entry、manifest,worker 通过动态 import 加载入口。 plugin-platform源码中不包含pluginKey === 'bangdream'、case 'ff14-market'、case 'fflogs'、case 'repeater'等具体插件分支。- 四个内置插件仍从各自
plugin.json同步 operation/event/task metadata。 - BangDream Bestdori 同步 task、命令执行、图片渲染、Repeater event、FF14/FFLogs 查询不因解耦退化。
- 新增结构测试能防止后续重新引入平台到具体插件的耦合。
风险和控制
- 动态 import 在源码态和构建态路径不同:通过 package path policy 和 build 后 smoke 覆盖。
- BangDream 当前入口参数和其他插件不完全一致:允许 package-local adapter,但 adapter 必须在 BangDream 包内。
- 同步配置 key 从代码 switch 迁入 manifest 可能遗漏:用四个插件 entry contract 测试和 worker runtime 测试覆盖。
- 旧 direct registry 行为可能影响命令测试:先用 runtime summary 保持 active registry 语义,再移除 direct plugin instance。
- FFLogs world resolver 依赖 FF14 helper:迁回 FFLogs 包内或改用通用字典 host,禁止平台代为转接。
Spec 自检
- 无空白占位:本文没有空段落、临时标记或未定字段。
- 范围明确:本设计只覆盖 QQBot Plugin Platform 到具体插件的解耦,不扩展 Admin/NapCat/线上市场。
- 边界明确:平台允许处理插件包协议,禁止认识具体插件实现。
- 验收可验证:完成标准均可通过源码扫描、聚焦测试、typecheck/build 和本地 smoke 验证。