Compare commits

...

22 Commits

Author SHA1 Message Date
7edbb97e70 feat: 重建NapCat登录运行态持久化 2026-06-16 00:37:08 +08:00
69705281fa refactor: 收敛QQBot插件平台加载边界 2026-06-16 00:20:58 +08:00
9b14acf1bc refactor: 收敛QQBot插件包第三期结构 2026-06-15 18:58:11 +08:00
bb69427921 feat: 重建QQBot插件平台运行时 2026-06-15 16:20:53 +08:00
379e5fb608 refactor: 收敛QQBot核心模块边界 2026-06-15 15:52:26 +08:00
8ada857f57 test: 增加QQBot强收敛门禁 2026-06-15 15:17:48 +08:00
80691af285 test: 冻结QQBot重构能力矩阵 2026-06-15 15:11:25 +08:00
201fa13151 fix: 补齐QQBot插件分页与新设备二维码 2026-06-15 15:07:40 +08:00
8bcb5f78a9 docs: 规划QQBot强收敛与BangDream重写 2026-06-15 15:05:21 +08:00
7e0e2a0067 docs: 设计QQBot强收敛与BangDream重写 2026-06-15 14:54:02 +08:00
83cd0c3856 docs: 记录Admin全域瘦身证据 2026-06-15 12:39:55 +08:00
635f97a2c7 refactor: 收敛QQBot插件平台与现有插件 2026-06-15 12:18:53 +08:00
e17d1f89ab refactor: 收敛QQBot核心与NapCat模块 2026-06-15 12:09:15 +08:00
ff8e3908f4 refactor: 收敛Blog WordPress Asset模块 2026-06-15 11:12:57 +08:00
cca2f9e1b4 fix: 清理Admin模块旧根依赖 2026-06-15 10:44:56 +08:00
838fea68c8 refactor: 收敛Admin与平台配置模块 2026-06-15 10:31:40 +08:00
c3b0476dcc test: 补强架构收敛导入门禁 2026-06-15 10:09:15 +08:00
e66d3fd824 test: 增加架构收敛结构门禁 2026-06-15 10:02:43 +08:00
6724be84e7 docs: 建立架构收敛清理清单 2026-06-15 09:38:42 +08:00
418c9706d2 docs: 编写第三期架构收敛实施计划 2026-06-15 09:29:51 +08:00
510afe8dc7 docs: 明确第三期全模块瘦身门禁 2026-06-15 09:18:31 +08:00
e82edf51d5 docs: 设计第三期架构收敛批次 2026-06-15 09:02:23 +08:00
619 changed files with 12130 additions and 5370 deletions

10
API.md
View File

@ -449,7 +449,7 @@ pnpm qqbot-plugin install-local <packageFile>
### BangDream
插件 key`bangdream`。旧 `bangDream` 作为兼容别名仍可解析;当前源码根目录为 `src/modules/qqbot/plugins/bangDream`,不再使用旧 `tsugu`目录。
插件 key`bangdream`。旧 `bangDream` 作为兼容别名仍可解析;当前源码根目录为 `src/modules/qqbot/plugins/bangdream/src`,按第三期插件结构拆分为 `operations`、`domain/*`、`application`、`infrastructure/integration`、`infrastructure/storage`、`config`、`assets` 和 `theme`,不再使用旧 `tsugu` 子目录、宿主 builtins 包装层或纯转接目录。
| operation key | 命令 | 说明 |
| ----------------------------- | ------------- | ------------------------ |
@ -469,11 +469,11 @@ pnpm qqbot-plugin install-local <packageFile>
| `bangdream.cutoff.all` | `/ycxall` | 全档位预测线 |
| `bangdream.cutoff.recent` | `/lsycx` | 历史/近期档线 |
`registry/operation-registry.ts` 是 BangDream operation、handlerName、别名、冷却和说明的单一来源。新增或调整命令必须同步在线命令 SQL并跑 registry/command-SQL 测试。
`plugins/bangdream/plugin.json` 是 BangDream operation、handlerName、别名、权限、超时和说明的单一来源。新增或调整命令必须同步在线命令 SQL并跑 manifest/command-SQL 测试。
### FF14 Market
插件 key`ff14-market`。旧 `ff14Market` 作为兼容别名仍可解析。
插件 key`ff14-market`。旧 `ff14Market` 作为兼容别名仍可解析;源码按第三期插件结构拆分为 `operations`、`application`、`domain`、`infrastructure/integration` 和 `config`
| operation key | 说明 |
| ------------------- | ------------------------------------------ |
@ -486,6 +486,8 @@ pnpm qqbot-plugin install-local <packageFile>
插件 key`fflogs`。
源码按第三期插件结构拆分为 `operations`、`application`、`domain`、`infrastructure/integration`、`infrastructure/storage` 和 `config`
| operation key | 说明 |
| -------------------------- | --------------------------------------------------------------- |
| `fflogs.character.summary` | 查询 FFLogs 角色公开排名;传 `encounter` 时查询指定高难最近记录 |
@ -527,7 +529,7 @@ BangDream 图片 smoke
```powershell
.\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.song.search -Text "夏祭り" -OutFile ".kt-workspace/bangdream-smoke/song.jpg"
.\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text "310" -OutFile ".kt-workspace/bangdream-smoke/stage.jpg"
.\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text "310" -OutFile ".kt-workspace/bangdream-smoke/stage.jpg" -ExpectedImageCount 5
```
Jenkins/K8s 发布后还需要观察 rollout、新 Pod 日志,并跑真实运行态 smoke推送成功不等于发布完成。

View File

@ -23,8 +23,8 @@
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志和插件平台 |
| `modules/qqbot/plugin-platform` | QQBot 插件 manifest 校验、版本安装、运行事件、受控 SDK 和 CLI 脚手架 |
| `qqbot/plugins/bangDream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
| `qqbot/plugins/ff14Market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
| `qqbot/plugins/bangdream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
| `qqbot/plugins/ff14-market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
| `common` | 响应封装、异常过滤、请求日志、日期格式化、字典解码、Snowflake、工具服务 |
@ -160,8 +160,8 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow
- NapCat 设备身份按账号持久化到 `napcat_device_identity`同一账号重建容器会复用数据目录、hostname、machine-id 和 MAC并在 Docker run 中注入 `--hostname`、`--mac-address`、只读 `/etc/machine-id`,避免频繁重建被 QQ 判定为全新设备。
- NapCat 新设备验证走同一 scan session`CaptchaLogin` 返回 `needNewDevice` 后,后端继续调用 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`Admin/SSE 分开展示 `captchaUrl`、`newDeviceQrcode`、已扫码、确认中、验证成功、登录成功/失败等中文进度,不把 `jumpUrl` 当作唯一完成入口。
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000``QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,使掉线/被踢无需管理员打开列表页即可及时发现;检测到离线后先尝试 `ACCOUNT` 历史会话快速登录,再尝试账号保存的登录密码,仍失败时写入离线原因并复用 `super` 站内信告警;看门狗不自动进入扫码阶段。
- BangDream 当前源码根目录是 `src/modules/qqbot/plugins/bangDream`;不要恢复旧 `tsugu` 层级或旧大桶目录
- BangDream 在线命令以 `registry/operation-registry.ts` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 registry/command-SQL 测试。
- BangDream 当前源码根目录是 `src/modules/qqbot/plugins/bangdream/src`;按第三期插件结构放置真实职责代码:业务在 `domain/*`,编排在 `application`,操作在 `operations`,外部 API 在 `infrastructure/integration`,缓存/静态修正在 `infrastructure/storage`,字典和静态配置在 `config`,视觉渲染公共件在 `theme`;不要恢复旧 `tsugu` 层级、旧大桶目录、纯 re-export 转接文件或空 `.gitkeep` 目录壳
- BangDream 在线命令以 `plugins/bangdream/plugin.json` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 manifest/command-SQL 测试。
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
## 轻量验证
@ -184,6 +184,7 @@ BangDream 图片能力改动:
```powershell
.\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.song.search -Text "夏祭り" -OutFile ".kt-workspace/bangdream-smoke/song.jpg"
.\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text "310" -OutFile ".kt-workspace/bangdream-smoke/stage.jpg" -ExpectedImageCount 5
```
接口改动必须启动或复用本地服务,并真实调用一次对应接口。
@ -196,4 +197,4 @@ BangDream 图片能力改动:
| 一级来源 | 使用方式 | License |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/modules/qqbot/plugins/bangDream`,保留本地 `TSUGU-LICENSE` | MIT |
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/modules/qqbot/plugins/bangdream/src`,保留本地 `TSUGU-LICENSE` | MIT |

View File

@ -0,0 +1,22 @@
# Architecture Convergence Inventory
## Baseline
| Area | Current Evidence | Target Evidence |
| --- | --- | --- |
| API legacy roots | `src/admin`, `src/blog`, `src/minio`, `src/wordpress`, `src/qqbot` exist. | These paths do not exist. |
| API forbidden module imports | `src/modules/**` imports old roots. | No import from `@/admin`, `@/blog`, `@/minio`, `@/wordpress`, or `@/qqbot`. |
| Admin System | System pages and callers exist under `views/system` and `api/system`. | No duplicate caller/type/page state after cleanup. |
| Admin Blog/Asset | Blog, WordPress, and Asset callers/pages exist under `api/blog` and `views/blog`. | Shared list/upload/import state where behavior repeats. |
| Admin QQBot | QQBot callers/pages exist under `api/qqbot` and `views/qqbot`. | Core, Plugin Platform, and NapCat state boundaries are explicit. |
## Domain Decisions
| Domain | Deleted | Merged | Kept | Evidence |
| --- | --- | --- | --- | --- |
| Admin/Auth/Platform Config | `src/admin/example` if no active route evidence; empty old admin subdirectories | auth/user/role/menu/dept/component/dict/notice/system-log/timezone moved into `src/modules/admin/**`; duplicate old imports removed | route paths and public DTO class names kept | `rg '@/admin/' src test` returns no source hits; admin focused Jest passes |
| Blog/WordPress/Asset | old `src/blog`, `src/wordpress`, `src/minio`; legacy module wrappers after local imports replaced | MinIO internal files renamed into Asset module; Blog and WordPress controller/service/entity files moved under target modules | existing route decorators and response DTO behavior | `rg '@/blog/|@/wordpress/|@/minio/' src test` has no hits; Blog/WordPress/Asset focused tests pass |
| Runtime/Common | none yet | none yet | current files before migration | baseline only |
| QQBot Core/NapCat | old `src/qqbot/qqbot.module.ts`; old core and napcat roots after moves | account/command/config/connection/dashboard/dedupe/event/message/mqtt/permission/rule/send moved into `src/modules/qqbot/core`; NapCat login/container/persistence moved into `src/modules/qqbot/napcat` | route decorators, command parsing, send queue, device persistence, captcha, new-device, and manual QR semantics | core and NapCat focused Jest pass; `rg '@/qqbot/' src/modules/qqbot/core src/modules/qqbot/napcat` has no hits |
| Plugin Platform/Plugins | old `src/qqbot/plugin`; no additional plugin source files deleted because existing manifests/tests still reference them | registry/controller moved into `src/modules/qqbot/plugin-platform/**`; direct old-root imports removed | plugin HTTP routes, platform plugin keys, legacy key compatibility, BangDream file-cache HTTP client | architecture convergence and plugin platform focused Jest pass; plugin scan has no `@/admin/` or `@/qqbot/` imports, and FF14/FFLogs have no direct HTTP client hits |
| Admin UI | none; `ktTableDemo` has active `/system/ktTableDemo` route evidence and was kept | duplicated QQBot status/action rendering centralized under `views/qqbot/modules`; NapCat new-device labels covered in caller helper tests | System demo route behavior, Blog/WordPress/Asset pages, QQBot Core/Plugin/NapCat management boundaries | Admin focused Vitest and typecheck pass |

View File

@ -0,0 +1,78 @@
# QQBot Architecture Convergence Inventory
This inventory freezes the current QQBot behavior before the strong convergence rewrite. It is a baseline for preserving behavior while deleting old structure.
## Repository Baseline
- API branch: `dev-api-architecture-convergence-v3`.
- Admin branch: `dev-admin-architecture-convergence-v3`.
- API package manager: `pnpm@9.15.9`.
- Admin package manager: `pnpm@10.28.2`, Node engine `>=20.19.0`.
- Current plugin source root: `src/modules/qqbot/plugins`.
## Built-In Plugin Matrix
| Current Dir | Platform Key | Legacy Key | Runtime Entry | Operations | Events |
| --- | --- | --- | --- | --- | --- |
| `bangDream` | `bangdream` | `bangDream` | `qqbot-bangdream.plugin.ts` | 15 | 0 |
| `ff14Market` | `ff14-market` | `ff14Market` | `qqbot-ff14-market.plugin.ts` | 2 | 0 |
| `fflogs` | `fflogs` | none | `qqbot-fflogs.plugin.ts` | 1 | 0 |
| `repeater` | `repeater` | none | `qqbot-repeater.plugin.ts` | 0 | 1 |
## BangDream Operation Matrix
| Operation Key | Name | Handler | Aliases |
| --- | --- | --- | --- |
| `bangdream.song.search` | 查曲 | `searchSong` | 查曲, bd, bangdream, bandori, 邦邦, 邦邦查歌 |
| `bangdream.song.chart` | 查谱面 | `getSongChart` | 查谱面, 谱面, bd谱面 |
| `bangdream.song.random` | 随机曲 | `randomSong` | 随机曲, 随机, bd随机 |
| `bangdream.song.meta` | 查询分数表 | `getSongMeta` | 查询分数表, 查分数表, 查询分数榜, 查分数榜, bd分数表 |
| `bangdream.card.search` | 查卡 | `searchCard` | 查卡, 查卡牌, bd查卡 |
| `bangdream.card.illustration` | 查卡面 | `getCardIllustration` | 查卡面, 查卡插画, 查插画, bd卡面 |
| `bangdream.character.search` | 查角色 | `searchCharacter` | 查角色, bd角色 |
| `bangdream.event.search` | 查活动 | `searchEvent` | 查活动, bd活动 |
| `bangdream.event.stage` | 查试炼 | `getEventStage` | 查试炼, 查stage, 查舞台, 查festival, 查5v5 |
| `bangdream.player.search` | 查玩家 | `searchPlayer` | 查玩家, 查询玩家, bd玩家 |
| `bangdream.gacha.search` | 查卡池 | `searchGacha` | 查卡池, bd卡池 |
| `bangdream.gacha.simulate` | 抽卡模拟 | `simulateGacha` | 抽卡模拟, bd抽卡 |
| `bangdream.cutoff.detail` | ycx | `getCutoffDetail` | ycx, 预测线, 查档线, bd档线 |
| `bangdream.cutoff.all` | ycxall | `getCutoffAll` | ycxall, myycx, 全部档线 |
| `bangdream.cutoff.recent` | lsycx | `getCutoffRecent` | lsycx, 历史档线, 近期档线 |
## Other Plugin Capability Matrix
| Operation/Event Key | Plugin | Type | Handler | Aliases |
| --- | --- | --- | --- | --- |
| `ff14.item.resolve` | `ff14-market` | operation | `resolveItem` | 物品, item, ff14item |
| `ff14.market.price` | `ff14-market` | operation | `getPrice` | 查价, price, ff14price |
| `fflogs.character.summary` | `fflogs` | operation | `getCharacterSummary` | fflogs, logs, 查logs, 查log |
| `repeater.message` | `repeater` | event | `handleMessage` | message event |
## Current Route And Contract Surface
- QQBot core compatibility routes remain under `/qqbot/account`, `/qqbot/command`, `/qqbot/rule`, `/qqbot/message`, `/qqbot/send`, and `/qqbot/dashboard`.
- Plugin compatibility routes remain under `/qqbot/plugin/*`, including list, operation list, operation page, and health.
- Plugin platform management routes remain under `/qqbot/plugin-platform/*`.
- NapCat login routes remain under QQBot account/login compatibility endpoints and must continue to expose scan create, refresh, status, captcha submit, SSE events, and runtime status.
- Admin current QQBot callers are concentrated in `apps/web-antdv-next/src/api/qqbot/index.ts`; the target split is `index.ts`, `plugin.ts`, and `napcat.ts`.
## Command Seed Linkage
- Current online command seed is `sql/qqbot-init.sql`.
- Current BangDream command rows use legacy plugin key `bangDream` and `bangdream.*` operation keys.
- Current FF14 market command row uses legacy plugin key `ff14Market` and operation key `ff14.market.price`.
- Current FFLogs command row uses plugin key `fflogs` and operation key `fflogs.character.summary`.
- Repeater is event-driven and has no command row.
## Obsolete File Groups To Delete After Replacement
- Core feature buckets that remain outside the approved layers: `core/account`, `core/command`, `core/config`, `core/connection`, `core/dashboard`, `core/dedupe`, `core/event`, `core/message`, `core/mqtt`, `core/permission`, `core/rule`, `core/send`.
- Plugin Platform buckets that remain outside the approved layers: `plugin-platform/manifest`, `plugin-platform/persistence`, `plugin-platform/registry`, `plugin-platform/runtime`, `plugin-platform/sdk`, root controller/service/module files.
- Plugin directories that must be replaced by package keys: `plugins/bangDream`, `plugins/ff14Market`.
- Plugin service files that must not survive the rewrite as runtime code: `qqbot-bangdream.plugin.ts`, `qqbot-ff14-market.plugin.ts`, `qqbot-fflogs.plugin.ts`, `qqbot-repeater.plugin.ts`, and companion Nest service files.
- NapCat buckets that remain outside the approved layers: `napcat/device`, `napcat/container`, `napcat/login`, `napcat/integration`, root service files, and `napcat/persistence.ts`.
## Verification
- `test/modules/qqbot/architecture/qqbot-current-operation-matrix.spec.ts` locks this matrix against current registry, manifests, and `sql/qqbot-init.sql`.
- Later rewrite batches must update the test only when the new package structure proves the same behavior through manifest-driven runtime contracts.

File diff suppressed because it is too large Load Diff

View File

@ -1191,13 +1191,13 @@ git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加QQBot插件管
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangDream\plugin.json`
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangDream\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangDream\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangdream\plugin.json`
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangdream\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangdream\**`
- [x] **Step 1: Preserve business directories**
Keep business capabilities in `song`, `card`, `character`, `event`, `gacha`, `player`, `cutoff`, `catalog`, with cross-cutting code only in `application`, `registry`, `hook`, `provider`, `policy`, `theme`, `config`, `dictionary`, `search`, `shared`.
Keep business capabilities in `domain/song`, `domain/card`, `domain/character`, `domain/event`, `domain/gacha`, `domain/player`, `domain/cutoff`, and `domain/catalog`; keep orchestration in `application`, operations in `operations`, external APIs in `infrastructure/integration`, cache/static patch adapters in `infrastructure/storage`, dictionary/static config in `config`, and visual helpers in `theme`.
- [x] **Step 2: Convert operation registry to manifest metadata**
@ -1208,22 +1208,22 @@ Keep operation keys and aliases stable. `handlerName` becomes worker-internal.
Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/registry/operation-registry.spec.ts test/qqbot/plugins/bangDream/registry/command-sql.spec.ts test/modules/qqbot/plugins/bangDream
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangdream/manifest/operation-manifest.spec.ts test/qqbot/plugins/bangdream/manifest/command-sql.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
Expected: PASS. Event stage smoke keeps `imageCount=5`.
Actual note: BangDream moved to `src/modules/qqbot/plugins/bangDream` with the approved business and cross-cutting directory layout preserved. Added `plugin.json` with platform key `bangdream`; manifest operations are asserted against `BANGDREAM_OPERATION_REGISTRY` for stable operation keys, aliases, names, and worker-internal `handlerName`. Runtime registry now uses platform key `bangdream` as primary while resolving legacy `bangDream`; `/qqbot/plugin/*` local HTTP smoke now proves the controller returns platform keys and resolves legacy `bangDream` to `bangdream`. Verification passed via focused registry/command SQL Jest, plugin migration contract, QQBot core module contract path guard, plugin controller HTTP smoke, `pnpm run typecheck`, and `scripts/bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text 310`, which produced `imageCount=5`.
Actual note: BangDream moved to `src/modules/qqbot/plugins/bangdream/src` with third-phase plugin ownership: business code in `domain/*`, orchestration in `application`, operations in `operations`, external APIs in `infrastructure/integration`, cache/static patch adapters in `infrastructure/storage`, dictionary/static config in `config`, and visual helpers in `theme`. Added `plugin.json` with platform key `bangdream`; manifest operations are the single metadata source for stable operation keys, aliases, names, and worker-internal `handlerName`. Runtime registry now uses platform key `bangdream` as primary while resolving legacy `bangDream`; `/qqbot/plugin/*` local HTTP smoke now proves the controller returns platform keys and resolves legacy `bangDream` to `bangdream`. Verification passed via focused manifest/command SQL Jest, plugin migration contract, QQBot core module contract path guard, plugin controller HTTP smoke, `pnpm run typecheck`, and `scripts/bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text 310`, which produced `imageCount=5`.
### Task 6.2: Rewrite FF14 Market, FFLogs, and Repeater
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\ff14Market\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\ff14-market\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\fflogs\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\repeater\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\ff14Market\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\ff14-market\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\fflogs\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\repeater\**`
@ -1244,13 +1244,13 @@ Use host send queue and account binding. Do not bypass rate limit.
Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/qqbot/plugins/ff14Market/qqbot-ff14-worlds.spec.ts test/qqbot/plugins/fflogs/qqbot-fflogs-client.service.spec.ts test/qqbot/plugins/repeater/qqbot-repeater.plugin.spec.ts test/modules/qqbot/plugins/ff14Market test/modules/qqbot/plugins/fflogs test/modules/qqbot/plugins/repeater
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/qqbot/plugins/ff14-market/qqbot-ff14-worlds.spec.ts test/qqbot/plugins/fflogs/qqbot-fflogs-client.service.spec.ts test/qqbot/plugins/repeater/qqbot-repeater.plugin.spec.ts test/modules/qqbot/plugins/ff14-market test/modules/qqbot/plugins/fflogs test/modules/qqbot/plugins/repeater
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
Expected: PASS.
Actual note: FF14 Market, FFLogs, and Repeater moved to `src/modules/qqbot/plugins/*` and each now has a platform `plugin.json`. FF14 Market uses platform key `ff14-market` with legacy `ff14Market` compatibility and delegates outbound requests through `QqbotPluginHttpClientService`; FFLogs delegates outbound requests through the same host SDK and keeps credential references in manifest config schema without real secrets; Repeater remains event-plugin driven through host account binding and send queue services. Verification passed via focused plugin Jest, migration direct-HTTP scan for FF14/FFLogs, `/qqbot/plugin/list` and `/qqbot/plugin/operation/list` local HTTP smoke, and `pnpm run typecheck`; final focused Jest covered 9 suites / 27 tests.
Actual note: FF14 Market, FFLogs, and Repeater moved to `src/modules/qqbot/plugins/*` and each now has a platform `plugin.json` plus real third-phase package structure. FF14 Market splits operation binding into `operations`, use cases into `application`, world/market types into `domain`, external clients into `infrastructure/integration`, and config into `config`; FFLogs splits character-summary operation, application use case, GraphQL/OAuth client, config, and token cache; Repeater splits event handler, repeat policy/domain state, runtime config, application state, and host integration. Verification passed via focused plugin Jest, migration direct-HTTP scan for FF14/FFLogs, `/qqbot/plugin/list` and `/qqbot/plugin/operation/list` local HTTP smoke, and `pnpm run typecheck`; final focused Jest covered 9 suites / 27 tests.
### Task 6.3: Commit Batch 6

View File

@ -0,0 +1,949 @@
# QQBot Architecture Convergence And BangDream Rewrite Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the approved third-phase QQBot target architecture true in code: core, plugin-platform, napcat, built-in plugins, BangDream, Admin callers/pages, local validation, deployment observation, and online account smoke must all close.
**Architecture:** Execute as strict batch gates. Each batch starts with RED checks that fail on the current loose structure, then moves code into the approved boundaries, removes obsolete implementation, updates schema/docs/Admin, verifies locally, reviews, and commits only the scoped result.
**Tech Stack:** NestJS 11, TypeORM 0.3, MySQL, pnpm 9 API workspace, Vben Admin 5 / Vue 3 / TSX / Ant Design Vue, pnpm 10 Admin workspace, Jest, Vitest, Playwright/browser smoke, ktWorkflow MCP, Jenkins/K8s, NapCat WebUI and OneBot runtime.
---
## Success Boundary
This plan is complete only when all of these are true:
- `src/modules/qqbot/core/**`, `src/modules/qqbot/plugin-platform/**`, and `src/modules/qqbot/napcat/**` use the approved `contract/application/domain/infrastructure/{persistence,integration}/schema` structure.
- Core imports no concrete plugin implementation and registers no concrete plugin provider.
- Plugin Platform owns manifest validation, package install, lifecycle, worker runtime, operation registry, event dispatcher, SDK host adapters, runtime events, and account binding.
- Every built-in plugin uses the same package structure under `src/modules/qqbot/plugins/<pluginKey>/plugin.json` and `src/**`.
- BangDream is rebuilt as `plugins/bangdream`, old `bangDream` business code is removed, and the 15 approved operations remain behavior-compatible.
- FF14 Market, FFLogs, and Repeater are rebuilt as `ff14-market`, `fflogs`, and `repeater` packages with no Nest/Admin/Core/env/direct HTTP or direct fs dependencies.
- NapCat device identity, container runtime, login session, challenge, new-device flow, and cleanup failure semantics are recoverable from persistence.
- Admin callers and pages are split by QQBot core, Plugin Platform, and NapCat state domains.
- Local API/Admin tests, local real interface smoke, browser smoke, KT global review, Superpowers review, push/deploy observation, and online QQBot/NapCat smoke are recorded.
## Current Baseline
- API repo: `D:\MyFiles\KT\Node\kt-template-online-api`, branch `dev-api-architecture-convergence-v3`, package manager `pnpm@9.15.9`, no `.node-version`, no `engines`.
- Admin repo: `D:\MyFiles\KT\Vue\kt-template-admin`, branch `dev-admin-architecture-convergence-v3`, package manager `pnpm@10.28.2`, `.node-version=22.22.0`, `engines.node >=20.19.0`, `engines.pnpm >=10.0.0`.
- Root repo: `D:\MyFiles\KT`, branch `main`.
- Existing API dirty work contains the already verified plugin operation pagination and NapCat new-device QR fix:
- `package.json`
- `pnpm-lock.yaml`
- `src/modules/qqbot/napcat/integration/napcat-login-api.client.ts`
- `src/modules/qqbot/plugin-platform/qqbot-plugin.controller.ts`
- `src/modules/qqbot/plugins/bangdream/src/application/main-data-store.ts`
- `test/modules/qqbot/napcat/new-device-flow.spec.ts`
- `test/modules/qqbot/plugins/plugin-controller-http-smoke.spec.ts`
- Existing Admin dirty work contains the matching plugin pagination wrapper/page change:
- `apps/web-antdv-next/src/api/qqbot/index.ts`
- `apps/web-antdv-next/src/views/qqbot/plugin/list.tsx`
- `apps/web-antdv-next/src/api/qqbot/index.spec.ts`
- Existing root dirty work contains KT governance/task context:
- `AGENTS.md`
- `TASKS.md`
## Non-Negotiable Execution Rules
- Do not start broad implementation while API/Admin/root dirty state is ambiguous.
- Preserve verified prior fixes by committing them as a baseline or by carrying their tests and behavior into Batch 1 before deleting old files.
- Use `apply_patch` for manual edits.
- Do not revert user changes outside the explicit baseline cleanup scope.
- Use at most three background subagents for independent audits or reviews, each with a Context Packet under `.kt-workspace/subagents/`.
- Every structural rule below must have an executable test or scan, not a prose-only promise.
- Do not push until local closure and review pass. The user goal requires online closure, so the final release batch includes push, Jenkins/K8s observation, and online smoke after local evidence is clean.
- Destructive DB or online state writes require named source, target, backup path, rollback command, action SQL/scripts, and verification query before execution.
## Batch 0: Baseline Cleanliness And Evidence Freeze
### Task 0.1: Confirm repository state
**Files:** no edits.
- [ ] Run:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch
git -C D:\MyFiles\KT\Vue\kt-template-admin status --short --branch
git -C D:\MyFiles\KT status --short --branch
```
- [ ] Confirm the dirty sets match `Current Baseline`.
- [ ] Stop and inspect before edits if any extra dirty path appears.
### Task 0.2: Preserve verified pagination and new-device QR fixes
**Files:**
- API dirty files listed in `Current Baseline`
- Admin dirty files listed in `Current Baseline`
- Root `AGENTS.md`, `TASKS.md`
- [ ] Re-run the focused checks for the existing fix:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/napcat/new-device-flow.spec.ts test/modules/qqbot/plugins/plugin-controller-http-smoke.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next exec vitest run apps/web-antdv-next/src/api/qqbot/index.spec.ts
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typecheck
```
- [ ] Commit the verified baseline in API with:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add package.json pnpm-lock.yaml src/modules/qqbot/napcat/integration/napcat-login-api.client.ts src/modules/qqbot/plugin-platform/qqbot-plugin.controller.ts src/modules/qqbot/plugins/bangdream/src/application/main-data-store.ts test/modules/qqbot/napcat/new-device-flow.spec.ts test/modules/qqbot/plugins/plugin-controller-http-smoke.spec.ts
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "fix: 补齐QQBot插件分页与新设备二维码"
```
- [ ] Commit the verified baseline in Admin with:
```powershell
git -C D:\MyFiles\KT\Vue\kt-template-admin add apps/web-antdv-next/src/api/qqbot/index.ts apps/web-antdv-next/src/api/qqbot/index.spec.ts apps/web-antdv-next/src/views/qqbot/plugin/list.tsx
git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "fix: 对齐QQBot插件分页页面"
```
- [ ] Commit root governance if the diff is only the multi-agent and task-context record:
```powershell
git -C D:\MyFiles\KT add AGENTS.md TASKS.md
git -C D:\MyFiles\KT commit -m "docs: 固化KT多代理协作规则"
```
### Task 0.3: Freeze behavior and file inventory
**Files:**
- Create or update: `D:\MyFiles\KT\Node\kt-template-online-api\docs\refactor-v3\qqbot-architecture-convergence-inventory.md`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\architecture\qqbot-current-operation-matrix.spec.ts`
- [ ] Record current operation keys, aliases, command seed linkage, route paths, DTO response shapes, SSE event keys, table ownership, and known online smoke entry commands.
- [ ] Record obsolete file groups to delete after replacements pass:
- old feature buckets in `core/account|command|rule|send|...`
- old `plugin-platform/manifest|persistence|registry|runtime|sdk` buckets when moved into approved layers
- old `plugins/bangDream`, `plugins/ff14Market`, `plugins/fflogs`, `plugins/repeater`
- old NapCat `device|container|login|integration|persistence.ts` buckets
- [ ] Add an operation matrix test that asserts all 15 BangDream operation keys plus FF14, FFLogs, and Repeater operations are discoverable before refactor.
- [ ] Verify:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-current-operation-matrix.spec.ts
```
### Task 0.4: Commit Batch 0
- [ ] Run:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
git -C D:\MyFiles\KT\Vue\kt-template-admin diff --check
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --content-scan-mode changed
```
- [ ] Commit API inventory and matrix test:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add docs/refactor-v3/qqbot-architecture-convergence-inventory.md test/modules/qqbot/architecture/qqbot-current-operation-matrix.spec.ts
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "test: 冻结QQBot重构能力矩阵"
```
## Batch 1: RED Architecture Gates
### Task 1.1: Add core and module boundary gates
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\architecture\qqbot-module-boundary.spec.ts`
- [ ] Add a scan that fails while any file under `src/modules/qqbot/core/**` imports `src/modules/qqbot/plugins/**`.
- [ ] Add a scan that fails while `QqbotCoreModule` registers concrete plugin services, `QqbotPluginController`, plugin registry concrete classes, or plugin SDK HTTP clients.
- [ ] Add a scan that fails while `src/modules/qqbot/core/domain/**` imports Nest, TypeORM, Docker/process/HTTP, Admin, or Plugin Platform concrete implementation.
- [ ] Add a scan that fails while `src/modules/qqbot/napcat/application/**` builds Docker/SSH shell strings.
- [ ] Verify RED:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts
```
Expected: fails on current structure with explicit path findings.
### Task 1.2: Add plugin package hard gates
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\architecture\qqbot-plugin-package-boundary.spec.ts`
- [ ] Assert built-in plugin directories are exactly:
```text
bangdream
ff14-market
fflogs
repeater
```
- [ ] Assert each plugin has:
```text
plugin.json
src/index.ts
src/operations
src/events
src/domain
src/application
src/infrastructure/integration
src/infrastructure/storage
src/config
src/assets
src/migrations
src/tests
```
- [ ] Ban from plugin runtime source:
- `@nestjs/`
- `@/modules/admin`
- `@/modules/qqbot/core`
- `ConfigService`
- `DictService`
- `process.env`
- direct `axios`
- direct `fetch`
- direct `fs` or `node:fs`
- import-time timers or immediate remote loads
- [ ] Assert operation metadata source of truth is `plugin.json`, with no parallel TS operation registry exporting duplicate key/alias descriptions.
- [ ] Verify RED:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts
```
Expected: fails on current `bangDream`, `ff14Market`, Nest services, and old registries.
### Task 1.3: Add runtime behavior gates
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugin-platform\plugin-lifecycle-runtime.spec.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\core\qqbot-core-plugin-ports.spec.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-persistent-login-state.spec.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangdream-rewrite\bangdream-operation-parity.spec.ts`
- [ ] Lifecycle test covers install, enable, active registry refresh, operation execution, event dispatch, disable, uninstall, timeout, crash isolation, and runtime event persistence.
- [ ] Core ports test covers command prefix and alias matching in core, raw args handed to Plugin Platform, plugin output normalized back to command log/send queue, and plugin event dispatch after core does not consume a message.
- [ ] NapCat persistence test covers device identity reuse, session recovery, captcha challenge recovery, new-device challenge recovery, cleanup failure blocking success, and manual QR fallback state.
- [ ] BangDream parity test covers the 15 operation keys and asserts `bangdream.event.stage` keeps `imageCount=5`.
- [ ] Verify RED:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts
```
Expected: fails because current runtime and plugin package boundaries are not the approved model.
### Task 1.4: Commit Batch 1 RED gates
- [ ] Commit only tests and docs:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add test/modules/qqbot docs/refactor-v3/qqbot-architecture-convergence-inventory.md
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "test: 增加QQBot强收敛门禁"
```
## Batch 2: Core, Plugin Platform, And NapCat Structural Shells
### Task 2.1: Create approved module skeletons
**Files:**
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\core\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\**`
- [ ] Move core controllers/DTOs/constants into `core/contract`.
- [ ] Move core use-case services into `core/application`.
- [ ] Move pure policies, parser-independent command matching, rule state, permission state, send queue policy, and event normalization into `core/domain`.
- [ ] Move core TypeORM entities/repositories into `core/infrastructure/persistence`.
- [ ] Move OneBot WS, MQTT/bus, and runtime external adapters into `core/infrastructure/integration`.
- [ ] Add `core/schema/README.md` with owned tables, seed linkage, and verification SQL.
- [ ] Repeat the same structural split for `plugin-platform` and `napcat`.
- [ ] Update module barrels and imports without changing public route behavior.
### Task 2.2: Replace core plugin dependencies with ports
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\core\domain\plugin-execution.port.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\core\application\plugin-execution.adapter.ts`
- Modify: core command/rule/event services
- Modify: plugin-platform providers
- [ ] Define ports for:
- `executeOperation(input)`
- `dispatchEvent(input)`
- `listActiveOperations(accountContext)`
- `getOperationByCommand(commandId|operationKey)`
- [ ] Core command parser keeps only prefix, alias, command ID, and raw args extraction.
- [ ] FF14 positional parsing, FFLogs positional parsing, BangDream parsing, and Repeater triggering move out of core.
- [ ] Rule engine no longer directly calls Repeater; it dispatches an unconsumed message event to Plugin Platform.
- [ ] Core module imports Plugin Platform through exported abstract provider tokens only.
### Task 2.3: Verify Batch 2
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/core/qqbot-core-command-contract.spec.ts test/modules/qqbot/core/qqbot-core-send-contract.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
- [ ] Run changed-file ESLint on touched API files.
- [ ] Confirm no deleted public route breaks by running the local command smoke after starting or reusing the local API service.
### Task 2.4: Commit Batch 2
- [ ] Run `git diff --check` and KT global review.
- [ ] Commit:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/core src/modules/qqbot/plugin-platform src/modules/qqbot/napcat test/modules/qqbot docs/refactor-v3 API.md README.md
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 收敛QQBot核心模块边界"
```
## Batch 3: Plugin Platform Runtime And CLI
### Task 3.1: Implement manifest and package ownership
**Files:**
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\domain\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\application\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\infrastructure\persistence\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\infrastructure\integration\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\qqbot-plugin\**`
- [ ] Manifest parser validates key, version, runtime, operations, events, permissions, config schema, assets, migrations, and legacy aliases.
- [ ] Package installer enforces hash, size, path whitelist, no path traversal, no hidden host import shortcuts, and controlled install root.
- [ ] `pnpm qqbot-plugin create <key>` scaffolds the exact package structure and no legacy directories.
- [ ] `pnpm qqbot-plugin validate <path>` runs manifest validation plus source boundary scan.
- [ ] `pnpm qqbot-plugin pack <path>` creates a package with deterministic file list and hash.
- [ ] `pnpm qqbot-plugin install-local <path>` validates, persists version, installs package, and returns installation/version IDs.
### Task 3.2: Implement lifecycle, worker runtime, registry, executor, dispatcher
**Files:**
- Modify: `src/modules/qqbot/plugin-platform/application/**`
- Modify: `src/modules/qqbot/plugin-platform/domain/**`
- Modify: `src/modules/qqbot/plugin-platform/infrastructure/integration/**`
- Modify: `src/modules/qqbot/plugin-platform/contract/**`
- [ ] Enable starts worker, loads manifest, activates plugin, persists runtime event, refreshes active operation/event registry.
- [ ] Disable deactivates and disposes worker, removes active operations/events, persists runtime event, and leaves command seed rows inactive.
- [ ] Upgrade enters `upgrading`, starts new worker, health-checks it, swaps active registry after success, and preserves previous active version on failure.
- [ ] Uninstall refuses active plugin, removes package and install records after disable, and records runtime event.
- [ ] Operation executor handles timeout, crash, structured plugin error, command log mapping, image output, and reply text without leaking secret summaries.
- [ ] Event dispatcher handles account binding, plugin config, event context, and plugin send queue SDK.
- [ ] Runtime event persistence is append-only and queryable by plugin, installation, severity, event type, and time.
- [ ] `/qqbot/plugin/*` compatibility routes move under `plugin-platform/contract`.
- [ ] `/qqbot/plugin-platform/*` management routes expose validate, upload/install, install-local, enable, disable, upgrade, uninstall, config, runtime events, account bindings, capabilities, operations page/list, and event handlers.
### Task 3.3: Verify Batch 3
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugin-platform/manifest.spec.ts test/modules/qqbot/plugin-platform/cli.spec.ts test/modules/qqbot/plugin-platform/persistence-contract.spec.ts test/modules/qqbot/plugin-platform/plugin-platform-api-contract.spec.ts test/modules/qqbot/plugin-platform/worker-runtime.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
- [ ] Run CLI smoke:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api qqbot-plugin create smoke-plugin --out .kt-workspace\test-artifacts\qqbot-plugin-cli\smoke-plugin
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api qqbot-plugin validate .kt-workspace\test-artifacts\qqbot-plugin-cli\smoke-plugin
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api qqbot-plugin pack .kt-workspace\test-artifacts\qqbot-plugin-cli\smoke-plugin --out .kt-workspace\test-artifacts\qqbot-plugin-cli
```
- [ ] Run local authenticated smoke for operation page/list and install/enable/disable if API service is available.
### Task 3.4: Commit Batch 3
- [ ] Run `git diff --check`, cleanup CLI artifacts under `.kt-workspace`, and KT global review.
- [ ] Commit:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/plugin-platform scripts/qqbot-plugin test/modules/qqbot/plugin-platform test/modules/qqbot/architecture docs/refactor-v3 API.md README.md package.json pnpm-lock.yaml
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 重建QQBot插件平台运行时"
```
## Batch 4: BangDream Full Rewrite
### Task 4.1: Build new `bangdream` package shell
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangdream\plugin.json`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangdream\src\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangdream\src\assets\**`
- Modify: command seed SQL and plugin platform seed SQL
- [ ] Use key `bangdream`; preserve legacy alias `bangDream` only in manifest compatibility and seed mapping.
- [ ] `plugin.json` is the only operation metadata source for key, aliases, trigger mode, descriptions, handler names, cooldowns, and permissions.
- [ ] `src/index.ts` exports only `createPlugin()`.
- [ ] Copy or move static assets into `src/assets` with license files preserved.
- [ ] Add package-local config schema and default dictionary/static config loaders through host SDK storage/assets.
### Task 4.2: Rewrite BangDream operations
**Files:**
- New package operation dirs:
- `src/operations/song-search`
- `src/operations/song-chart`
- `src/operations/song-random`
- `src/operations/song-meta`
- `src/operations/card-search`
- `src/operations/card-illustration`
- `src/operations/character-search`
- `src/operations/event-search`
- `src/operations/event-stage`
- `src/operations/player-search`
- `src/operations/gacha-search`
- `src/operations/gacha-simulate`
- `src/operations/cutoff-detail`
- `src/operations/cutoff-all`
- `src/operations/cutoff-recent`
- [ ] For every operation, define parser, use case, output contract, renderer call, fixture inputs, and parity expectations.
- [ ] Keep domain code in:
- `src/domain/song`
- `src/domain/card`
- `src/domain/character`
- `src/domain/event`
- `src/domain/gacha`
- `src/domain/player`
- `src/domain/cutoff`
- `src/domain/catalog`
- [ ] Keep orchestration in `src/application`.
- [ ] Keep Bestdori, HHWX, asset loading, cache, and static patch adapters under `src/infrastructure/integration` or `src/infrastructure/storage`.
- [ ] Keep visual tokens and reusable canvas helpers under operation-local layout files or a small theme module inside the package.
- [ ] Do not import old BangDream implementation from new runtime code.
- [ ] Preserve visible Chinese wording and error semantics for known commands.
- [ ] Preserve image output counts, especially `bangdream.event.stage` `imageCount=5`.
### Task 4.3: Remove old BangDream implementation
**Files:**
- Delete: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangDream\**`
- Modify import paths, seed references, tests, docs, and runtime mapping.
- [ ] Delete old facade, old application services, old registry, old shared buckets, old provider buckets, old theme buckets, old static config buckets, and import-time side effects.
- [ ] Keep license files by moving them into the new package.
- [ ] Preserve only explicit oracle fixtures under tests if they are required for parity checks.
### Task 4.4: Verify Batch 4
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugins/plugin-platform-migration.spec.ts test/modules/qqbot/plugins/plugin-registry-compat.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
- [ ] Run BangDream render smoke:
```powershell
powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-api\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text 310
powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-api\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.song.search -Text "fire bird"
powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-api\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.card.search -Text "香澄"
powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-api\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.cutoff.detail -Text "ycx"
powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-api\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.gacha.simulate -Text "10"
```
- [ ] Verify event stage smoke reports `imageCount=5`.
- [ ] Run a local `/qqbot/command/test` smoke for at least one BangDream text command with command ID and full command text.
### Task 4.5: Commit Batch 4
- [ ] Run `git diff --check` and KT global review.
- [ ] Commit:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/plugins/bangdream test/modules/qqbot/plugins/bangdream-rewrite test/modules/qqbot/architecture sql/refactor-v3 docs/refactor-v3 API.md
git -C D:\MyFiles\KT\Node\kt-template-online-api add -u src/modules/qqbot/plugins/bangdream/src
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 重写BangDream插件"
```
## Batch 5: FF14 Market, FFLogs, And Repeater Rewrite
### Task 5.1: Rewrite FF14 Market package
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\ff14-market\plugin.json`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\ff14-market\src\**`
- Delete: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\ff14Market\**`
- [ ] Split `resolve-item` and `market-price` operations.
- [ ] Move world, region, data center, item alias, and command parsing into plugin domain/application.
- [ ] Move XIVAPI and Universalis access into `src/infrastructure/integration` through host SDK HTTP.
- [ ] Move default world/dictionary into plugin config and SDK-backed storage.
### Task 5.2: Rewrite FFLogs package
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\fflogs\plugin.json`
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\fflogs\src\**`
- [ ] Split OAuth token, GraphQL client, encounter catalog, localization, reply formatter, and config defaults.
- [ ] Store credential references in plugin config schema only.
- [ ] Prove no real secret appears in manifest, tests, docs, or committed config.
### Task 5.3: Rewrite Repeater package
**Files:**
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\repeater\plugin.json`
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\repeater\src\**`
- [ ] Implement message event handler under `src/events/message`.
- [ ] Move repeat state, threshold, cooldown, and text filtering into plugin domain.
- [ ] Use event context for account binding and SDK send queue for replies.
- [ ] Remove direct `QqbotSendService` usage.
### Task 5.4: Verify Batch 5
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugins/ff14-market test/modules/qqbot/plugins/fflogs test/modules/qqbot/plugins/repeater test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
- [ ] Run local `/qqbot/command/test` smoke for one FF14 command and one FFLogs command.
- [ ] Run local event dispatcher smoke for Repeater.
### Task 5.5: Commit Batch 5
- [ ] Run `git diff --check` and KT global review.
- [ ] Commit:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/plugins/ff14-market src/modules/qqbot/plugins/fflogs src/modules/qqbot/plugins/repeater test/modules/qqbot/plugins test/modules/qqbot/architecture sql/refactor-v3 docs/refactor-v3 API.md
git -C D:\MyFiles\KT\Node\kt-template-online-api add -u src/modules/qqbot/plugins/ff14-market
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 规范化QQBot内置插件包"
```
## Batch 6: NapCat Runtime Persistence And Login Flow
### Task 6.1: Rebuild NapCat persistence model
**Files:**
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\infrastructure\persistence\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\00-full-schema.sql`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\01-seed-core.sql`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\99-verify.sql`
- [ ] Use target tables:
- `napcat_container`
- `napcat_device_identity`
- `napcat_account_binding`
- `napcat_login_session`
- `napcat_login_challenge`
- `napcat_runtime_cleanup`
- [ ] Keep legacy `qqbot_account_napcat` compatibility only through migration/adapter code until schema and online data are rebuilt.
- [ ] Persist container name, data dir, hostname, machine-id path, MAC, account binding, verification state, last login evidence, session state, challenge state, cleanup state, and error notice.
- [ ] Treat in-memory session as SSE listener cache only.
### Task 6.2: Split NapCat application and integration
**Files:**
- Modify: `src/modules/qqbot/napcat/contract/**`
- Modify: `src/modules/qqbot/napcat/application/**`
- Modify: `src/modules/qqbot/napcat/domain/**`
- Modify: `src/modules/qqbot/napcat/infrastructure/integration/**`
- [ ] Contract owns scan/create, scan/refresh, scan/status, captcha submit, SSE events, account runtime status, and route compatibility.
- [ ] Application owns login session use cases, container use cases, account binding use cases, and cleanup use cases.
- [ ] Domain owns quick/password/captcha/new-device/manual QR state machine and cleanup blocking rules.
- [ ] Integration owns NapCat WebUI client, Docker/SSH adapter, container log reader, QR/captcha/new-device API adapter.
- [ ] Docker/SSH shell construction appears only in `infrastructure/integration`.
### Task 6.3: Implement new-device API chain
**Files:**
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\infrastructure\integration\napcat-login-api.client.ts`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\application\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\contract\**`
- [ ] Implement methods:
```ts
getNewDeviceQRCode(sessionId: string): Promise<NewDeviceQrCode>
pollNewDeviceQR(sessionId: string): Promise<NewDeviceQrStatus>
newDeviceLogin(sessionId: string): Promise<NewDeviceLoginResult>
```
- [ ] Map states:
```text
qr-pending -> scanned -> confirming -> verified
```
- [ ] If upstream only returns `jumpUrl`, convert it into a QR code payload for Admin while preserving the original URL as audit evidence.
- [ ] Keep captcha and new-device challenges separate.
- [ ] Keep cleanup failure as a blocker for success and QR fallback.
### Task 6.4: Verify Batch 6
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/napcat/device-identity.spec.ts test/modules/qqbot/napcat/login-state-machine.spec.ts test/modules/qqbot/napcat/new-device-flow.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
```
- [ ] Run local simulated NapCat smoke proving:
- same account rebuild reuses data directory, hostname, machine-id path, and MAC
- captcha challenge remains pending across polling
- new-device QR uses `GetNewDeviceQRCode`
- scan status transitions to scanned and confirming
- `NewDeviceLogin` completes
- cleanup failure blocks success
### Task 6.5: Commit Batch 6
- [ ] Run `git diff --check` and KT global review.
- [ ] Commit:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/napcat test/modules/qqbot/napcat test/modules/qqbot/architecture sql/refactor-v3 docs/refactor-v3 API.md README.md
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 重建NapCat设备与登录运行时"
```
## Batch 7: Admin QQBot Boundary Sync
### Task 7.1: Split Admin API callers
**Files:**
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\index.ts`
- Create: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\plugin.ts`
- Create: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\napcat.ts`
- [ ] `index.ts` keeps core account, command, rule, message, send log, and dashboard callers.
- [ ] `plugin.ts` owns capability page/list, installation lifecycle, manifest/package actions, runtime events, account bindings, config, and plugin operations.
- [ ] `napcat.ts` owns login/session/device/SSE/captcha/new-device callers.
- [ ] Add focused Vitest coverage for response wrappers, pagination, SSE event normalization, and QR/captcha/new-device state mapping.
### Task 7.2: Split Admin pages by state domain
**Files:**
- Modify/create under `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\**`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\router\routes\modules\qqbot.ts`
- [ ] Core pages own account, command, rule, message, send queue, and dashboard state.
- [ ] Plugin Platform pages own capability list, installation lifecycle, runtime events, account bindings, manifest/package actions, and config drawers.
- [ ] NapCat pages own account login, scan status, captcha, new-device QR, and runtime state.
- [ ] Keep route pages with a single stable root element.
- [ ] Use Chinese progress labels for:
```text
正在快速登录
快速登录失败,进入密码登录
正在密码登录
需要验证码
验证码已提交,等待确认
需要新设备验证二维码
新设备二维码待扫码
新设备二维码已扫码
新设备确认中
新设备验证成功,继续登录
正在生成手动二维码
登录成功
登录失败
运行态清理失败
```
- [ ] Render captcha and new-device QR as separate states.
- [ ] Do not clear a captcha URL only because a later status response omits the URL.
### Task 7.3: Verify Batch 7
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next exec vitest run apps/web-antdv-next/src/api/qqbot
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typecheck
```
- [ ] Start or reuse Admin dev server and API local service.
- [ ] Use in-app browser smoke for:
- `/qqbot/plugin`
- `/qqbot/command`
- account login/NapCat login view
- [ ] Confirm browser console has no page render errors, route-switch blank page warnings, or API caller shape errors.
### Task 7.4: Commit Batch 7
- [ ] Run `git diff --check` and KT global review for Admin.
- [ ] Commit:
```powershell
git -C D:\MyFiles\KT\Vue\kt-template-admin add apps/web-antdv-next/src/api/qqbot apps/web-antdv-next/src/views/qqbot apps/web-antdv-next/src/router/routes/modules/qqbot.ts
git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "refactor: 收敛QQBot管理页面边界"
```
## Batch 8: Delete Obsolete Code And Governance Sync
### Task 8.1: Remove obsolete API code
**Files:**
- API QQBot files under `src/modules/qqbot/**`
- Tests under `test/modules/qqbot/**`
- Docs under `docs/refactor-v3/**`, `API.md`, `README.md`
- [ ] Remove any empty or obsolete buckets left by the refactor.
- [ ] Remove old plugin compatibility code after tests prove manifest legacy alias support works.
- [ ] Remove old tests that only protect old folder shapes.
- [ ] Keep compatibility routes only at contract/controller boundary.
- [ ] Run:
```powershell
rg -n "bangDream|ff14Market|QqbotBangDream|QqbotFf14|QqbotFflogs|QqbotRepeater|src/modules/qqbot/plugins/.*/qqbot-.*\\.plugin|src/modules/qqbot/plugins/.*/qqbot-.*\\.service" D:\MyFiles\KT\Node\kt-template-online-api\src D:\MyFiles\KT\Node\kt-template-online-api\test
```
Expected: no old runtime implementation references; only explicit legacy alias fixtures or compatibility tests remain.
### Task 8.2: Sync durable rules and task context
**Files:**
- Modify: `D:\MyFiles\KT\AGENTS.md`
- Modify: `D:\MyFiles\KT\TASKS.md`
- [ ] Replace stale BangDream stable-runtime guidance that allowed old `bangDream` business bucket structure.
- [ ] Add the new durable rule: built-in QQBot plugins use platform package structure and key names `bangdream`, `ff14-market`, `fflogs`, `repeater`.
- [ ] Record the completed architecture convergence batches and validation evidence in `TASKS.md` using short scope/keywords/verification text.
### Task 8.3: Verify Batch 8
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typecheck
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --content-scan-mode changed
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project admin --content-scan-mode changed
```
### Task 8.4: Commit Batch 8
- [ ] Commit API cleanup:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot test/modules/qqbot docs/refactor-v3 API.md README.md
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 清理QQBot旧架构代码"
```
- [ ] Commit root governance:
```powershell
git -C D:\MyFiles\KT add AGENTS.md TASKS.md
git -C D:\MyFiles\KT commit -m "docs: 更新QQBot插件架构规则"
```
## Batch 9: Local Full Closure
### Task 9.1: API local verification
- [ ] Run:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
```
### Task 9.2: API local real interface smoke
- [ ] Start or reuse a bounded local API service.
- [ ] Smoke:
- `GET /health/runtime`
- `GET /qqbot/plugin/operation/page`
- `GET /qqbot/plugin/operation/list`
- plugin install/enable/disable with local smoke package
- `/qqbot/command/test` for BangDream
- `/qqbot/command/test` for FF14 or FFLogs
- simulated Repeater event dispatch
- NapCat scan status and new-device simulated flow
- [ ] Save generated evidence under `.kt-workspace/test-artifacts`.
- [ ] Stop only Node/API processes started for this validation.
### Task 9.3: Admin local verification and browser smoke
- [ ] Run:
```powershell
git -C D:\MyFiles\KT\Vue\kt-template-admin status --short --branch
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next exec vitest run apps/web-antdv-next/src/api/qqbot
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typecheck
git -C D:\MyFiles\KT\Vue\kt-template-admin diff --check
```
- [ ] Use browser smoke for QQBot plugin platform page, command page, and NapCat login page.
- [ ] Stop only Vite/Admin processes started for this validation.
### Task 9.4: Documentation, cleanup, review, and commit status
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run cleanup-history -- --dry-run
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --content-scan-mode changed
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project admin --content-scan-mode changed
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project root --content-scan-mode changed
```
- [ ] Use Superpowers `requesting-code-review` before claiming local closure.
- [ ] Resolve real Important findings and re-run focused review.
## Batch 10: Push, Deploy Observation, And Online Closure
### Task 10.1: Push implementation branches
- [ ] Confirm current branch and commits:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch
git -C D:\MyFiles\KT\Vue\kt-template-admin status --short --branch
git -C D:\MyFiles\KT status --short --branch
```
- [ ] Push API and Admin branches:
```powershell
git -C D:\MyFiles\KT\Node\kt-template-online-api push -u origin dev-api-architecture-convergence-v3
git -C D:\MyFiles\KT\Vue\kt-template-admin push -u origin dev-admin-architecture-convergence-v3
```
### Task 10.2: Observe Jenkins and K8s
- [ ] Observe API deployment with the stabilized workflow:
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run deploy-observation -- --project api --job KT-Template/KT-Template-API/main --execute
```
- [ ] Collect:
- Jenkins build number
- source commit hash
- image tag
- Deployment generation and observedGeneration
- desired, updated, ready counts
- selected Pod image
- restart count
- logs/events when failing
- [ ] Observe Admin deployment with the corresponding Jenkins/K8s evidence path used in the repo.
### Task 10.3: Online DB and seed alignment
- [ ] Before any write, state:
- source DB
- target DB
- backup path under `.kt-workspace/db-sync`
- rollback command
- SQL scripts to apply
- verification query set
- [ ] Backup online database.
- [ ] Apply schema/seed only after backup succeeds.
- [ ] Run verification SQL:
```powershell
mysql ... < D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\99-verify.sql
```
### Task 10.4: Online functional smoke
- [ ] Smoke online:
- `/health/runtime`
- Admin login
- Admin menu
- QQBot plugin operation page/list
- plugin install/enable/health on a controlled test package
- `/qqbot/command/test` using operationKey-derived command ID and full command text
- one BangDream operation that returns image output
- one non-BangDream plugin operation
- NapCat real account login flow
- [ ] If new-device appears, evidence must include:
```text
GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin
```
- [ ] Strip `replyText` from online QQBot smoke summaries.
- [ ] Pull back image smoke output under `.kt-workspace/test-artifacts` when image output is tested.
### Task 10.5: Final closeout
- [ ] Run:
```powershell
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run cleanup-history -- --dry-run
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run self-test
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --content-scan-mode changed
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project admin --content-scan-mode changed
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project root --content-scan-mode changed
```
- [ ] Run Superpowers `verification-before-completion`.
- [ ] Run Superpowers `finishing-a-development-branch`.
- [ ] Report final evidence with:
- API/Admin/root commit hashes
- pushed branch names
- Jenkins/K8s deployment evidence
- online smoke evidence
- NapCat real account outcome
- remaining risk if any check is blocked by external state
## Cross-Batch Review Rules
- Every batch that changes code runs `git diff --check`.
- Every batch that changes API/Admin contracts updates `API.md`, `docs/refactor-v3/**`, SQL seed/schema, Admin caller, and tests in the same or immediately following batch.
- Every batch commit is preceded by KT global review for the changed repo.
- After implementation batches, use Superpowers `requesting-code-review` before claiming the batch done.
- If review reports a real Important issue, fix and re-run the relevant review before commit.
- If a reviewer reports a false positive caused by workflow rules, solidify the false-positive handling in `mcp/ktWorkflow` instead of leaving it as chat context.
## Plan Self-Review
| Approved Requirement | Plan Coverage |
| --- | --- |
| core/plugin-platform/napcat third-phase structure | Batches 1, 2, 6, 8 |
| Core cannot depend on concrete plugins | Batches 1, 2 |
| Plugin Platform owns runtime/lifecycle/registry/SDK | Batch 3 |
| Unified plugin package structure | Batches 1, 3, 4, 5, 8 |
| BangDream full rewrite and 15 operation parity | Batch 4 |
| FF14 Market, FFLogs, Repeater rewrite | Batch 5 |
| NapCat persistent device/session/challenge/cleanup | Batch 6 |
| New-device flow uses `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` | Batch 6 and Batch 10 |
| Admin caller/page boundary split and Chinese progress | Batch 7 |
| Schema/table redesign and verification | Batches 3, 4, 5, 6, 10 |
| Local tests, local interface smoke, browser smoke | Batch 9 |
| Push, Jenkins/K8s, online command and NapCat account closure | Batch 10 |
| Existing dirty baseline preserved before rewrite | Batch 0 |
## Implementation Entry
Start execution with Superpowers `subagent-driven-development`. Use subagents only for independent read-only audit or code-review work with a Context Packet. Main-thread implementation should begin at Batch 0 and must not skip RED gates in Batch 1.

View File

@ -0,0 +1,365 @@
# API/Admin 第三期架构收敛批次设计
## 背景
第三期全量重构已经完成了基础能力、schema、插件平台、NapCat 登录链路和线上闭环但当前源码结构仍没有完全兑现第三期目标架构API 业务实现仍散落在 `src/admin`、`src/blog`、`src/minio`、`src/wordpress`、`src/qqbot``src/modules/**` 仍大量反向导入旧根。用户已确认本批次选择“方案 A强门禁全收敛”。
本批次目标不是“继续能跑”,而是让第三期目标架构在当前代码里变成事实:旧根删除,业务实现归入 `src/modules/**`结构测试阻止回退API/Admin 全模块按功能域整理,并在迁移过程中全面清理无用代码、废话代码和重复胶水,同时保持现有功能一致。瘦身不是 QQBot 专项,而是覆盖 Admin/Auth、Platform Config、Blog、WordPress、Asset、QQBot Core、NapCat、Plugin Platform、现有插件、Admin 所有相关 caller 和页面状态的通用门禁。
## 已确认原则
- 先清干净工作区,再开始架构收敛。
- 清理前必须分类:旧产物可删除,当前目标产物才提交,不确定的改动先停下来确认。
- 不做无意义备份、临时日志提交、根目录临时文件或为了“保险”留下的补丁文件。
- API 与 Admin 使用开发分支,不使用 `.worktree`
- 本批次必须覆盖父级模块、子模块、插件目录和 Admin 页面/caller/state不只移动父级目录。
- 全面瘦身是每个模块迁移的完成条件:每个模块都要扫描旧产物、重复类型、重复状态文案、无引用文件、纯转发胶水和可复用边界,不允许只在 QQBot 上做。
- 功能行为保持一致,内部结构可以破坏式收敛。
- 完成条件必须由结构测试、typecheck、Jest、真实接口 smoke、Admin typecheck/page smoke 和 review 共同证明。
## 当前状态证据
清理后工作区状态:
- 根仓库 `D:\MyFiles\KT``main`,干净。
- API 仓库 `D:\MyFiles\KT\Node\kt-template-online-api``dev-api-architecture-convergence-v3`,干净。
- Admin 仓库 `D:\MyFiles\KT\Vue\kt-template-admin``dev-admin-architecture-convergence-v3`,干净。
- 其他 KT 子仓库:干净。
API 当前旧结构计数:
| 路径 | 文件数 | 目录数 | 本批次结论 |
| --- | ---: | ---: | --- |
| `src/admin` | 42 | 11 | 旧根,必须迁入 `src/modules/admin/**` 后删除。 |
| `src/blog` | 13 | 0 | 旧根,必须迁入 `src/modules/blog/**` 后删除。 |
| `src/minio` | 5 | 0 | 旧根,必须迁入 `src/modules/asset/**` 后删除。 |
| `src/wordpress` | 9 | 0 | 旧根,必须迁入 `src/modules/wordpress/**` 后删除。 |
| `src/qqbot` | 55 | 14 | 旧根,必须迁入 `src/modules/qqbot/**` 后删除。 |
| `src/modules` | 293 | 52 | 目标业务根,但当前仍依赖旧根。 |
| `src/common` | 26 | 11 | 允许保留,仅限跨模块基础能力。 |
| `src/runtime` | 13 | 5 | 允许保留,仅限运行时基础能力。 |
`src/modules/**` 当前反向导入旧根计数:
| 禁止导入 | 当前命中数 | 本批次结论 |
| --- | ---: | --- |
| `@/admin/` | 37 | 必须归零。 |
| `@/blog/` | 7 | 必须归零。 |
| `@/minio/` | 3 | 必须归零。 |
| `@/wordpress/` | 7 | 必须归零。 |
| `@/qqbot/` | 50 | 必须归零。 |
Admin 当前结构证据:
- `apps/web-antdv-next/src/api/qqbot` 目前有 `index.ts`、`napcat.ts`、`plugin.ts` 和测试文件caller 初步拆分但仍需按 Core、Plugin Platform、NapCat 的契约边界收敛类型和复用请求模型。
- `apps/web-antdv-next/src/views/qqbot` 已按 account、command、conversation、dashboard、message、permission、plugin、rule、sendLog 等页面散开,但页面状态、状态标签、登录进度、插件操作和 QQBot 基础管理仍需要统一复用边界。
## 范围
### API
必须完成:
- 删除旧根:`src/admin`、`src/blog`、`src/minio`、`src/wordpress`、`src/qqbot`。
- 所有业务实现迁入 `src/modules/**`
- `src/modules/admin/**`
- `src/modules/blog/**`
- `src/modules/wordpress/**`
- `src/modules/asset/**`
- `src/modules/qqbot/core/**`
- `src/modules/qqbot/napcat/**`
- `src/modules/qqbot/plugin-platform/**`
- `src/modules/qqbot/plugins/**`
- `src/modules/**` 不再导入 `@/admin/*`、`@/blog/*`、`@/minio/*`、`@/wordpress/*`、`@/qqbot/*`。
- `src/app.module.ts` 只注册目标模块,不注册旧根 shim。
- Entity、DTO、Controller、Service、domain policy、integration client、plugin implementation、tests 按实际模块归属移动。
- `src/common` 只保留真正跨模块基础能力例如响应、错误、时间、Snowflake、日志、装饰器、通用工具。
- `src/runtime` 只保留运行时基础能力,例如 HTTP/process/Docker adapter、runtime evidence、health。
- 删除或合并迁移后无引用的重复类型、重复常量、临时兼容胶水、空目录和纯转发文件。
- 对每个 API 模块执行瘦身扫描输出删除、合并、保留和不确定项的证据Admin/Auth、Blog、WordPress、Asset、Runtime/Common、QQBot、NapCat、Plugin Platform、插件目录全部适用。
### Admin
必须完成:
- 按功能域同步 API caller 和类型:
- Admin/Auth/Platform Config。
- Blog/WordPress/Asset。
- QQBot Core。
- QQBot Plugin Platform。
- NapCat 登录与设备。
- QQBot 页面按 Core、Plugin Platform、NapCat 三条边界整理状态和组件,避免账号页、插件页、登录页各自复制状态标签、进度文案、错误归一化和请求处理。
- System、Blog、WordPress、Asset 页面也要做同样瘦身:清理无引用 caller、重复请求包装、重复表格列定义、过时类型、无路由页面、无菜单入口页面和没有复用价值的中间组件。
- 插件管理、NapCat 登录进度、QQBot 账号/命令/规则/消息/发送队列现有功能保持一致。
- 清理无引用组件、重复枚举、过时类型、临时兼容 caller 和只包一层的无意义函数。
### 测试与文档
必须补齐:
- API 结构测试:旧根不存在、`src/modules/**` 禁止导入旧根、目标模块边界存在。
- API 行为验证typecheck、聚焦 Jest、核心接口真实本地 smoke。
- Admin 行为验证typecheck、关键 QQBot/插件/NapCat 页面 smoke。
- 文档同步:架构收敛说明、必要 API/Admin 契约说明、`TASKS.md` 简短记录。
- KT global review 与 Superpowers code review。
## 非目标
- 不新增业务功能。
- 不重新发明第三期 schema除非迁移 Entity 暴露出当前 schema 与代码不一致;若发现,必须单独列为修复项并有验证。
- 不为了目录好看创建空的 `domain/application/infrastructure` 文件夹。
- 不保留旧根作为“兼容层”。
- 不把功能保持一致理解为保留内部旧路径。
- 不清理与本批次无关的其他 KT 子仓库。
- 不推送、不部署、不做线上数据库操作,除非后续用户明确要求进入上线闭环。
## 目标架构
API 目标顶层:
```text
src/
app.module.ts
common/
runtime/
modules/
admin/
asset/
blog/
wordpress/
qqbot/
```
允许的业务模块内部结构按实际复杂度使用:
```text
module/
contract/
application/
domain/
infrastructure/
tests/
```
规则:
- `contract` 放 Controller、DTO、SSE event、外部 API 适配。
- `application` 放用例编排、事务、跨服务状态流转。
- `domain` 放不依赖 Nest/TypeORM/Docker/HTTP 的规则和值对象。
- `infrastructure` 放 Entity、Repository、外部集成 client、Docker/NapCat/WordPress/MinIO/worker adapter。
- 小模块可以少层,但不能通过一个大 `*.module.ts` 继续导入旧根。
- 插件代码仍在 `src/modules/qqbot/plugins/**`,但插件内部也要按业务能力瘦身,不能把旧顶层 bucket 迁回插件内部。
Admin 目标:
```text
apps/web-antdv-next/src/
api/
system/
blog/
qqbot/
core or index
plugin
napcat
views/
system/
blog/
qqbot/
account/
command/
message/
permission/
plugin/
napcat/
shared/
```
Admin 允许沿用现有 Vben/Antdv/Vue TSX 组织方式,但状态文案、状态 tag、SSE 进度映射、插件操作按钮和 API 错误解析必须抽到可复用边界。
## 全模块瘦身门禁
瘦身门禁和目录收敛同级,不能作为后续优化延期。每个模块迁移任务都必须同时回答三件事:删掉了什么,合并了什么,为什么保留。
### API 瘦身范围
每个 API 域都要执行:
- Admin/Auth/Platform Config清理旧 guard/module 转发、重复 DTO、无菜单入口 controller、过时 example、重复 dict/component/notice 类型。
- Blog清理旧 article/term/theme 类型重复、只为旧路径存在的 service wrapper、无引用 Markdown helper。
- WordPress清理重复远端 DTO、旧 REST fallback 胶水中无测试保护的分支、无引用 sync helper。
- Asset/MinIO清理旧 `minio` 命名遗留中只表达路径而不表达资产域的 wrapper保留对外路由兼容但内部命名收敛到 asset。
- Runtime/Common清理模块私有工具误放 common 的代码common 只保留跨两个以上业务域复用且有当前引用的能力。
- QQBot Core清理旧 registry、旧状态别名、重复权限/发送/消息类型和只转发旧服务的 shell。
- NapCat清理重复登录状态映射、重复 Docker 环境拼装、过期 QR/captcha/new-device 兼容分支;保留已验证的设备持久化和安全校验语义。
- Plugin Platform清理旧 `@/qqbot/plugin` 平台依赖、重复 manifest/operation 类型、绕过 SDK 的直接调用。
- 现有插件BangDream、FF14 Market、FFLogs、Repeater 都要扫内部无用 bucket、重复 provider、重复 layout/theme/helper、过期 legacy key 胶水和直接 HTTP 绕过。
API 瘦身完成证据:
- 每个旧根删除前后都有 `rg` 引用扫描。
- 删除文件必须有无引用证据反射、Nest DI、SQL seed、manifest 或外部路由入口必须用测试或 smoke 证明。
- `src/common` 新增或保留的能力必须至少被两个业务域引用,单模块私有能力迁回模块内。
- 没有仅做旧路径转发的 module/controller/service。
### Admin 瘦身范围
Admin 瘦身覆盖所有相关功能域:
- System登录、菜单、权限、用户、角色、部门、字典、通知、设置页面清理重复类型、重复表格配置、旧路由残留和无入口页面。
- Blog/WordPress/Asset清理重复 caller、重复列表状态、重复上传/下载/导入状态、无引用组件和旧 API 类型。
- QQBot Core账号、命令、规则、消息、权限、发送队列共享状态标签、错误解析和表格动作。
- Plugin Platform插件安装、启用、禁用、配置、健康、事件、账号绑定共享操作状态和表单模型。
- NapCat设备、登录 session、验证码、新设备二维码和 SSE 进度共享中文状态映射。
Admin 瘦身完成证据:
- `rg` 证明删除的 caller、组件、类型没有引用。
- 菜单、路由和页面文件一致;不存在有路由无页面、有页面无入口且无测试覆盖的旧页面。
- 重复状态文案和 tag 映射收敛到共享模块System、Blog/Asset、QQBot/Plugin/NapCat 各自不复制同一套逻辑。
- Admin typecheck 和关键页面 smoke 证明功能保持一致。
## 清理分类标准
每个待处理文件必须归类后再改:
| 分类 | 判断标准 | 动作 |
| --- | --- | --- |
| 旧产物 | 位于旧根、只为旧路径存在、迁移后无调用、过渡 shim、重复兼容类型、临时胶水。 | 迁移必要逻辑后删除,不提交保留副本。 |
| 应提交 | 新目标结构、结构测试、必要行为修复、文档同步、验证脚本或真实复用抽象。 | 分批提交。 |
| 需保留 | `src/common`/`src/runtime` 中真实跨模块能力、Admin/Vben 框架基础能力、功能保持所需兼容输入输出。 | 保留并把 import 改到目标边界。 |
| 不确定 | 看起来无引用但可能由反射、Nest DI、路由、SQL seed、插件 manifest 或外部入口调用。 | 先加搜索证据;只有测试或真实 smoke 证明无活入口后才能删除。 |
无意义内容包括:
- 单纯转发旧根的 shell module。
- 迁移后没人引用的 DTO/type/entity。
- 与当前功能无关的 demo 页面或临时测试入口。
- 只重复包一层 request、没有统一错误/状态价值的 Admin helper。
- 空目录、重复常量、重复中文状态文案。
- 生成日志、截图、patch 备份、临时 JSON。
## 收敛顺序
### 1. RED 结构门禁
先新增失败的结构测试,明确最终架构:
- `src/admin`、`src/blog`、`src/minio`、`src/wordpress`、`src/qqbot` 必须不存在。
- `src/modules/**` 不得导入旧根。
- `src/modules/{admin,asset,blog,wordpress,qqbot}` 必须存在。
- `src/modules/qqbot/{core,napcat,plugin-platform,plugins}` 必须存在。
- `src/app.module.ts` 不得导入旧根。
这些测试在迁移前应失败,迁移结束必须通过。
### 2. Admin/Auth/Platform Config
`src/admin/**` 迁入 `src/modules/admin/**`
- identityauth、user、role、menu、dept、guard、JWT。
- platform-configdict、component、notice、timezone、system-log`example` 只有在菜单、路由或测试仍证明它是现有功能入口时才迁移,否则删除。
- 清理重复 DTO、过渡 module、旧 import。
- Admin system caller/page 只做必要同步,保持登录、菜单、字典、通知等现有行为。
### 3. Blog/WordPress/Asset
`src/blog/**`、`src/wordpress/**`、`src/minio/**` 迁入目标模块:
- Blog 内容、术语、主题配置进入 `src/modules/blog/**`
- WordPress 授权、远端文章/分类/标签/主题进入 `src/modules/wordpress/**`
- MinIO controller/service 进入 `src/modules/asset/**`,对外路由保持兼容。
- 清理只为旧路径存在的 module 和 import。
- Admin blog/asset caller 保持现有页面行为。
### 4. QQBot Core 与 NapCat
`src/qqbot/account`、`command`、`connection`、`dashboard`、`dedupe`、`message`、`mqtt`、`permission`、`rule`、`send`、`config` 等核心能力迁入 `src/modules/qqbot/core/**`
`src/qqbot/napcat/**` 和登录相关服务迁入 `src/modules/qqbot/napcat/**`,保持:
- Docker 设备身份持久化。
- quick -> password -> captcha -> new-device -> manual QR 登录顺序。
- `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`
- SSE/Admin 中文进度。
- QQ 登录态、OneBot、WebUI、容器状态继续拆开。
### 5. Plugin Platform 与现有插件
收敛 `src/modules/qqbot/plugin-platform/**``src/modules/qqbot/plugins/**`
- 插件平台不得继续依赖旧 `@/qqbot/plugin` registry。
- 插件接口类型从目标平台 contract 或 SDK 引入。
- BangDream、FF14 Market、FFLogs、Repeater 插件内部扫描无用 bucket、重复 provider、旧 key 胶水和直接 HTTP 绕过。
- `legacyKeys` 只作为外部数据兼容语义保留;不能成为保留旧源码根的理由。
### 6. Admin 全域瘦身
整理 Admin 管理面,不只整理 QQBot
- System 管理:登录、菜单、权限、用户、角色、部门、字典、通知、设置。
- Blog/WordPress/Asset 管理:文章、术语、主题配置、远端同步、上传下载和对象引用。
- Core 管理:账号、命令、规则、消息、权限、发送队列。
- Plugin Platform插件安装、启用、禁用、配置、健康、事件、账号绑定。
- NapCat设备身份、登录 session、验证码、新设备二维码、扫码/确认/成功/失败进度。
- 抽取共享状态映射、tag 渲染、SSE 进度解析、API 错误归一化、表格动作模型和上传/导入状态。
- 删除全域无引用页面、组件、重复类型和临时兼容函数。
### 7. 旧根删除与最终验证
删除旧根后运行完整门禁:
- `rg --files src/admin src/blog src/minio src/wordpress src/qqbot` 必须找不到路径。
- `rg '@/admin/|@/blog/|@/minio/|@/wordpress/|@/qqbot/' src/modules` 必须无命中。
- API `pnpm run typecheck` 通过。
- API 聚焦 Jest 通过;删除旧根后的最终批次运行 API 全量 Jest。
- API 本地真实 smoke 覆盖 `/health/runtime`、Admin 登录/菜单、Blog public、Asset 上传/下载、QQBot command test、插件平台、NapCat 模拟登录状态机。
- Admin typecheck 通过。
- Admin 关键页面 smoke 通过。
- KT global review 与 Superpowers review 无阻断。
## 提交策略
本批次提交必须表达真实进展:
1. `test/refactor: 增加架构收敛门禁`
2. `refactor: 收敛Admin与平台配置模块`
3. `refactor: 收敛Blog WordPress Asset模块`
4. `refactor: 收敛QQBot核心与NapCat模块`
5. `refactor: 收敛QQBot插件平台与现有插件`
6. `refactor: 收敛Admin全域管理边界`
7. `test: 完成架构收敛验证闭环`
实际执行时可按风险拆得更细,但每个提交都必须满足:
- 没有混入生成产物或备份文件。
- 没有不相关仓库改动。
- 提交前 `git status` 已确认。
- 若删除文件,已经用搜索、类型检查或测试证明不是活入口。
## 验收标准
本批次完成必须同时满足:
- API 旧根 `src/admin`、`src/blog`、`src/minio`、`src/wordpress`、`src/qqbot` 全部不存在。
- `src/modules/**` 对旧根导入为 0。
- API 全模块完成瘦身门禁Admin/Auth、Blog、WordPress、Asset、Runtime/Common、QQBot、NapCat、Plugin Platform 和现有插件没有只为旧结构存在的无引用旧代码。
- Admin 全域页面和 caller 完成边界瘦身System、Blog/WordPress/Asset、QQBot/插件/NapCat 没有重复状态文案、重复请求胶水和明显无引用旧代码。
- 现有 API 路由和 Admin 功能保持一致。
- 插件平台和现有插件仍可安装、启用、执行、查看健康和运行事件。
- NapCat 登录链路仍覆盖设备持久化、验证码、新设备验证、手动 QR fallback 和中文进度。
- 所有计划中的验证命令有当前证据。
- 代码 review 门禁通过。
- 工作区最终干净。
## 交接到 writing-plans
用户审阅并确认本 spec 后,下一步进入 Superpowers `writing-plans`。计划必须把本 spec 拆成可执行任务,尤其要写清:
- 结构测试的 RED/GREEN 步骤和精确文件路径。
- 每个旧根迁移到哪个目标模块。
- 每批删除前的引用扫描命令。
- 每个 API/Admin 模块的全模块瘦身检查项。
- 每批需要保留的行为 smoke。
- Admin 共享状态/进度/错误处理抽取位置。
- 插件和 NapCat 的功能保持验证。
- 每批提交前的干净度检查和 review 门禁。

View File

@ -0,0 +1,394 @@
# QQBot 架构强收敛与 BangDream 重写设计
## 背景
第三期重构目标不是把文件移动到 `src/modules`而是让模块边界、插件平台、插件包、NapCat 运行时和 Admin 管理页都真实符合已批准的第三期架构。当前 QQBot 仍存在结构性偏差:
- `src/modules/qqbot/core/qqbot-core.module.ts` 仍直接注册 BangDream、FF14 Market、FFLogs、Repeater 具体插件 service。
- `src/modules/qqbot/plugin-platform/**` 目前主要是 manifest、实体和管理接口未真正接管 worker runtime、operation/event 路由和热插拔生命周期。
- `src/modules/qqbot/plugins/**` 仍是内置 Nest service 形态,插件代码仍能直接接触 `ConfigService`、`DictService`、`process.env`、`fs`、`axios`、QQBot core service。
- BangDream 旧实现目录和职责混乱存在大文件、大桶目录、operation 元数据双写和运行时副作用。它不能作为继续修补的基础。
- NapCat 已迁入 `src/modules/qqbot/napcat`,但 login/container 仍是大服务,内存 session、challenge、cleanup 语义没有完全进入第三期持久化模型。
- Admin 插件页把插件能力、安装记录、运行事件、账号绑定和 manifest 操作混在一个页面状态域里。
本设计是对第三期 QQBot 部分的强制返工收敛:实现必须以第三期规划文件结构为准,并把 BangDream 作为业务重写专项处理。
## 目标
1. QQBot 模块严格收敛到第三期业务模块统一结构。
2. Plugin Platform 真正成为插件安装、启停、worker runtime、operation/event registry 和热插拔的拥有者。
3. Core 不再依赖任何具体插件实现文件。
4. 每个插件使用同一套插件包目录规范,禁止按历史习惯自由生长。
5. BangDream 业务彻底重写,旧代码只作为行为参照和资产来源,能力 100% 还原。
6. NapCat Runtime 拆清 device、container、login session、challenge、cleanup 和 integration 责任。
7. Admin 同步拆清 QQBot core、plugin-platform、NapCat 的 caller 和页面状态。
## 非目标
- 不改变第三期已批准的全模块分层标准。
- 不引入另一套新的目录哲学。
- 不用“legacy adapter 长期保留”掩盖未收敛结构。
- 不为了快速通过测试保留 BangDream 旧 facade、旧 registry、大桶 shared 或旧 Nest plugin service。
- 不在本设计阶段改实现代码。
## 统一模块结构
QQBot 下的 core、plugin-platform、napcat 必须按第三期统一业务模块结构组织:
```text
src/modules/qqbot/core/
contract/
application/
domain/
infrastructure/
persistence/
integration/
schema/
src/modules/qqbot/plugin-platform/
contract/
application/
domain/
infrastructure/
persistence/
integration/
schema/
src/modules/qqbot/napcat/
contract/
application/
domain/
infrastructure/
persistence/
integration/
schema/
```
职责固定:
- `contract`Controller、DTO、Swagger 元信息、路由兼容适配。
- `application`:用例编排、事务边界、权限检查、状态流转。
- `domain`:纯业务规则、状态机、策略、值对象。不得依赖 Nest、TypeORM、Docker、HTTP、外部凭据。
- `infrastructure/persistence`TypeORM Entity、Repository、查询模型、schema mapper。
- `infrastructure/integration`OneBot WebSocket、NapCat WebUI、Docker/SSH/process、plugin worker RPC、外部 HTTP adapter。
- `schema`模块表设计说明、SQL 归属、验证 SQL、重建说明。
验收门禁:
- `core/**` 禁止 import `src/modules/qqbot/plugins/**`
- `core/**` 禁止注册 `QqbotPluginController`、具体 plugin service、plugin SDK HTTP client。
- `core/domain/**` 禁止依赖 Nest、TypeORM、HTTP、Docker、Admin、Plugin Platform 具体实现。
- `plugin-platform/**` 禁止把具体插件 service 硬编码进 registry。
- `napcat/application/**` 禁止直接拼 Docker/SSH shell这些必须在 `napcat/infrastructure/integration/**`
## Core 边界
Core 只保留 QQBot 基础能力:
- account identity、enabled/binding 状态。
- connection/session、OneBot reverse WebSocket 输入输出。
- message/conversation。
- command binding、command matching 基础逻辑。
- permission、rule、send queue、send log、dedupe、dashboard summary。
Core 调用插件只能通过抽象执行口:
- operation executor输入 command、raw args、message context返回 normalized input、output、replyText 或 error。
- event dispatcher输入 normalized event返回 handled 状态和可审计 runtime event。
插件专属解析从 core 移走。FF14 服务器路径解析、FFLogs positional 解析、BangDream 参数解析必须进入对应插件 operation。Core 只负责命令前缀和 alias 命中,把 raw args 交给插件执行口。
Repeater 不再由 rule engine 直接调用。权限、命令、规则都未消费消息后Core 将事件交给 Plugin Platform event dispatcher是否触发 repeater 由平台 active event handler 和插件 worker 决定。
## Plugin Platform 边界
Plugin Platform 拥有以下能力:
- manifest 解析和强校验。
- 插件包上传、hash、大小、路径白名单、受控安装目录。
- 安装、启用、禁用、升级、卸载状态机。
- worker runtime manager。
- child process 或 node worker driver。
- operation/event active registry。
- operation executor。
- event dispatcher。
- SDK host adaptersend queue、config、storage、HTTP、asset、runtime event、operation/event context。
- runtime event 持久化。
- account binding、plugin config、plugin asset、plugin-owned migration 管理。
启用插件时,平台必须启动 worker、load manifest、activate、刷新 active operation/event registry。禁用插件时平台必须 deactivate/dispose worker并从 active registry 移除对应 operation/event。升级必须先进入 `upgrading`,新版本 worker 健康后再替换 active registry失败时保留旧版本或标记 failed不允许留下半激活状态。
`/qqbot/plugin/*` 能力查询接口可以保留兼容路径,但实现归属必须在 `plugin-platform/contract`。`/qqbot/plugin-platform/*` 管理接口也归属同一 contract 层。
## 插件包结构规范
所有内置插件和线上安装插件必须使用同一套目录,不允许每个插件各自发散。内置插件源码位于:
```text
src/modules/qqbot/plugins/<pluginKey>/
plugin.json
src/
index.ts
operations/
events/
domain/
application/
infrastructure/
integration/
storage/
config/
assets/
migrations/
tests/
```
命名规则:
- `<pluginKey>` 使用平台 key统一 kebab-case 或纯小写。
- 现有目录迁移为 `bangdream`、`ff14-market`、`fflogs`、`repeater`。
- legacy key 只存在于 manifest alias 或 seed 数据,不作为目录名和主 key。
目录职责:
- `plugin.json`:唯一元数据源,声明 key、版本、权限、runtime、operations、events、assets、migrations、config schema。
- `src/index.ts`:唯一插件入口,只导出 `createPlugin()`
- `src/operations/`:命令/查询 operation一个 operation 一个目录或文件。
- `src/events/`:事件 handler一个 event handler 一个目录或文件。
- `src/domain/`:纯业务模型、规则、策略、状态机。
- `src/application/`:插件内部用例编排。
- `src/infrastructure/integration/`:外部 API adapter只能通过 host SDK HTTP 能力。
- `src/infrastructure/storage/`:插件私有缓存/存储 adapter只能通过 host SDK storage/asset。
- `src/config/`:默认配置、配置 schema、配置解析。不得读 `process.env`
- `src/assets/`:插件静态资源、字体、图片、静态数据文件。
- `src/migrations/`:插件自有数据表 migration。
- `src/tests/`:插件包内 contract/unit 测试模板。
插件包硬门禁:
- 禁止 `@nestjs/*`
- 禁止 `@/modules/admin/**`
- 禁止 `@/modules/qqbot/core/**`
- 禁止 `ConfigService`、`DictService`。
- 禁止 `process.env`
- 禁止直接 `axios`、`fetch`、`fs` 访问宿主能力。
- 禁止 operation metadata 在 `plugin.json` 和 TS registry 双写。
- 禁止模块 import 即启动 timer、加载远端数据或写文件。
## BangDream 重写专项
BangDream 不是迁移瘦身,而是业务彻底重写。旧实现只允许作为:
- 能力清单来源。
- operation key、alias、handlerName 参照。
- 输入解析和输出结构 oracle。
- 视觉资产、字体、静态配置来源。
- smoke 对照对象。
旧实现不得作为新实现依赖。新实现完成后必须删除旧 BangDream 业务结构,不保留 facade、大桶 shared、旧 operation registry、旧 main-data-store 或旧 file-cache。
BangDream 新目录:
```text
src/modules/qqbot/plugins/bangdream/
plugin.json
src/
index.ts
operations/
song-search/
song-chart/
song-random/
song-meta/
card-search/
card-illustration/
character-search/
event-search/
event-stage/
player-search/
gacha-search/
gacha-simulate/
cutoff-detail/
cutoff-all/
cutoff-recent/
events/
domain/
song/
card/
character/
event/
gacha/
player/
cutoff/
catalog/
application/
infrastructure/
integration/
storage/
config/
assets/
migrations/
tests/
```
必须 100% 还原的能力:
| Operation Key | 能力 |
| --- | --- |
| `bangdream.song.search` | 查曲 |
| `bangdream.song.chart` | 查谱面 |
| `bangdream.song.random` | 随机曲 |
| `bangdream.song.meta` | 查询分数表 |
| `bangdream.card.search` | 查卡 |
| `bangdream.card.illustration` | 查卡面 |
| `bangdream.character.search` | 查角色 |
| `bangdream.event.search` | 查活动 |
| `bangdream.event.stage` | 查试炼 |
| `bangdream.player.search` | 查玩家 |
| `bangdream.gacha.search` | 查卡池 |
| `bangdream.gacha.simulate` | 抽卡模拟 |
| `bangdream.cutoff.detail` | ycx |
| `bangdream.cutoff.all` | ycxall |
| `bangdream.cutoff.recent` | lsycx |
还原标准:
- operation key 不变。
- aliases 不变。
- online command seed/linkage 不变。
- 输入解析语义不变。
- 输出结构不变。
- 图片输出数量不变。
- `bangdream.event.stage` 保持当前拆图行为,`imageCount=5` 是硬门禁。
- 对外依赖 Bestdori、HHWX、静态修正数据和缓存语义保持可用。
- 可见中文文案和错误语义不降低。
重写策略:
1. 先从现有 `plugin.json` 和 command SQL 冻结能力矩阵。
2. 为每个 operation 建立 contract test使用旧实现或固定 fixture 作为 oracle。
3. 建立新的 `createPlugin()` 入口和 SDK-bound application 层。
4. 按 operation 逐个重写,旧实现只在测试中对照。
5. 全部 operation 通过后删除旧 BangDream 目录结构。
6. 最终静态扫描确认新 BangDream 不含 Nest/Admin/Core/env/fs/axios 依赖。
## 其他插件重写
FF14 Market
- 目录改为 `ff14-market`
- `resolve-item``market-price` 分成独立 operations。
- world catalog、region/data center/world 解析进入插件 domain。
- XIVAPI、Universalis 访问进入 `infrastructure/integration`,只通过 SDK HTTP。
- 字典或默认世界配置由 plugin config/SDK 注入,不直接依赖 Admin DictService。
FFLogs
- `qqbot-fflogs-client.service.ts` 必须拆除。
- OAuth token、GraphQL client、encounter catalog、localization、reply formatter、config defaults 分离。
- client id/secret 只通过 host secret/config reference 解析,不读宿主 env。
Repeater
- message event handler 位于 `src/events/message/`
- repeat state、阈值、冷却、文本过滤进入 domain。
- 账号绑定通过 event context/SDK 获得。
- 发送只走 SDK send queue不直接注入 `QqbotSendService`
## NapCat Runtime 收敛
NapCat 仍不是插件。它收敛到 `src/modules/qqbot/napcat` 的第三期模块结构:
- `contract`scan/create、scan/refresh、scan/status、captcha submit、SSE events、account runtime status 路由兼容。
- `application`:登录 session 用例、容器用例、账号绑定用例、cleanup 用例。
- `domain`quick/password/captcha/new-device/manual QR 状态机、cleanup 阻断规则、login challenge 状态。
- `infrastructure/persistence``napcat_container`、`napcat_device_identity`、`napcat_account_binding`、`napcat_login_session`、`napcat_login_challenge`、`napcat_runtime_cleanup`。
- `infrastructure/integration`NapCat WebUI client、Docker/SSH adapter、container log reader、QR/captcha/new-device API adapter。
内存 session 只允许作为 SSE listener cache不作为登录真相来源。验证码、新设备验证、清理失败必须可从持久化状态恢复。
## Admin 收敛
Admin 继续位于 `apps/web-antdv-next/src/views/qqbot/**`,但页面状态必须按功能域拆开:
- QQBot coreaccount、command、rule、message、send log、dashboard。
- Plugin Platformcapability list、installation lifecycle、runtime events、account bindings、manifest/package actions。
- NapCat账号登录、扫码、新设备、验证码、运行态状态。
API caller 保持第三期规划:
- `apps/web-antdv-next/src/api/qqbot/index.ts`QQBot core 兼容接口。
- `apps/web-antdv-next/src/api/qqbot/plugin.ts`Plugin Platform 管理接口。
- `apps/web-antdv-next/src/api/qqbot/napcat.ts`NapCat login/device/SSE 接口。
插件页不能继续由单个 TSX 文件承担能力列表、安装记录、运行事件、账号绑定、manifest 编辑的全部状态。实现时应拆成同一路由下的模块化组件或子视图,但不引入与第三期规划冲突的新顶层路由结构。
## 数据和 Schema
第三期 schema 是目标真相:
- Core 表只保存 QQBot 基础账号、连接、权限、命令、规则、会话、消息、发送和 dedupe。
- Plugin Platform 表保存 plugin、version、installation、operation、event handler、account binding、config、asset、runtime event。
- Plugin-owned data 使用插件 namespace 表。
- NapCat 表使用 `napcat_*` 目标命名,不再新增 `qqbot_napcat_*` legacy 表。
`plugin.json` 是插件 metadata 真相SQL seed 只保存平台安装和默认命令绑定,不再复制出另一套无法校验的 operation 元数据。
## 错误处理和运行态语义
- 插件 worker crash 只影响对应 installation记录 runtime event不拖垮 API。
- operation timeout 由平台统一控制,错误进入 command log 和 runtime event。
- 插件禁用后operation/event 不再出现在 active registry。
- 插件升级失败不得让旧版本和新版本同时 active。
- BangDream 单 operation 失败不得污染其他 operation 的缓存或状态。
- NapCat cleanup failure 继续阻断登录成功或 QR fallback并写入专用错误。
- NapCat QQ 登录态、OneBot 连接态、容器态、WebUI 态继续分离。
## 验证策略
必须先写 RED 门禁:
1. Core import/provider boundary scan。
2. Plugin package dependency boundary scan。
3. Plugin Platform lifecycle state machine tests。
4. Worker runtime real driver tests。
5. Operation executor 和 event dispatcher contract tests。
6. BangDream operation parity tests。
7. FF14/FFLogs/Reapter behavior tests。
8. NapCat persistence/state-machine recovery tests。
9. Admin caller/page state tests。
本地验证范围:
- API focused Jestcore、plugin-platform、plugins、napcat。
- API `pnpm run typecheck`
- API changed-file ESLint。
- Admin focused Vitest。
- Admin typecheck。
- 本地真实接口 smoke`/qqbot/plugin/operation/page`、`/qqbot/command/test`、plugin install/enable/disable、NapCat scan status。
- BangDream render smoke至少覆盖 event stage、song search、card search、cutoff、gacha simulate。
- Browser smokeQQBot 插件平台页和账号登录页。
线上闭环仍按第三期最终流程执行提交、推送、Jenkins/K8s 观察、线上 API/Admin smoke、QQBot command smoke、NapCat 真实账号登录闭环。
## 实施顺序
1. 冻结当前能力矩阵和旧实现 oracle。
2. 增加结构 RED 门禁,先让当前代码明确失败。
3. 重建 Plugin Platform runtime、lifecycle、registry、executor、dispatcher。
4. Core 改为只依赖平台执行口和事件分发口。
5. 彻底重写 BangDream 插件,逐 operation 迁移并跑 parity。
6. 重写 FF14 Market、FFLogs、Repeater。
7. 拆分 NapCat Runtime 到第三期模块结构。
8. Admin 同步拆 caller 和页面状态。
9. 删除旧目录、旧 facade、旧 registry、旧大桶 shared、旧 Nest plugin services。
10. 运行本地验证、全局 review、Superpowers review再进入上线闭环。
## 完成标准
- `src/modules/qqbot/core/**` 无插件实现依赖。
- `src/modules/qqbot/plugin-platform/**` 真正接管插件运行时。
- `src/modules/qqbot/plugins/**` 每个插件目录结构一致。
- BangDream 旧业务结构已删除15 个 operation 能力 100% 还原。
- 插件包静态扫描无 Nest/Admin/Core/env/fs/axios 违规依赖。
- NapCat login/container 大服务已拆到第三期结构session/challenge/cleanup 可持久化恢复。
- Admin 插件页状态域拆清。
- 本地和线上 smoke 通过,未验证项必须明确列出。

View File

@ -5,11 +5,19 @@
"compilerOptions": {
"assets": [
{
"include": "modules/qqbot/plugins/bangDream/assets/**/*",
"include": "modules/qqbot/plugins/*/plugin.json",
"outDir": "dist"
},
{
"include": "modules/qqbot/plugins/bangDream/static-config/**/*",
"include": "modules/qqbot/plugins/bangdream/TSUGU-LICENSE",
"outDir": "dist"
},
{
"include": "modules/qqbot/plugins/bangdream/src/assets/**/*",
"outDir": "dist"
},
{
"include": "modules/qqbot/plugins/bangdream/src/config/static/**/*",
"outDir": "dist"
}
],

View File

@ -54,6 +54,7 @@
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"pino-loki": "^3.0.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rehype-parse": "^9.0.1",
"rehype-raw": "^7.0.0",
@ -81,6 +82,7 @@
"@types/jest": "29.2.4",
"@types/lodash": "^4.17.24",
"@types/node": "22.19.19",
"@types/qrcode": "^1.5.6",
"@types/supertest": "^2.0.16",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.60.1",

View File

@ -71,6 +71,9 @@ importers:
pino-loki:
specifier: ^3.0.0
version: 3.0.0
qrcode:
specifier: ^1.5.4
version: 1.5.4
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
@ -147,6 +150,9 @@ importers:
'@types/node':
specifier: 22.19.19
version: 22.19.19
'@types/qrcode':
specifier: ^1.5.6
version: 1.5.6
'@types/supertest':
specifier: ^2.0.16
version: 2.0.16
@ -1206,6 +1212,9 @@ packages:
'@types/node@22.19.19':
resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==}
'@types/qrcode@1.5.6':
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
'@types/qs@6.15.1':
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
@ -1757,6 +1766,9 @@ packages:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@ -1904,6 +1916,10 @@ packages:
supports-color:
optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
@ -1978,6 +1994,9 @@ packages:
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
engines: {node: '>=0.3.1'}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
@ -3493,6 +3512,10 @@ packages:
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
engines: {node: '>=4.0.0'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@ -3549,6 +3572,11 @@ packages:
pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
qs@6.15.2:
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
engines: {node: '>=0.6'}
@ -3644,6 +3672,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
requireindex@1.2.0:
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
engines: {node: '>=0.10.5'}
@ -4392,6 +4423,9 @@ packages:
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which-typed-array@1.1.21:
resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==}
engines: {node: '>= 0.4'}
@ -4488,6 +4522,9 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -4498,10 +4535,18 @@ packages:
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
@ -5758,6 +5803,10 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/qrcode@1.5.6':
dependencies:
'@types/node': 22.19.19
'@types/qs@6.15.1': {}
'@types/range-parser@1.2.7': {}
@ -6377,6 +6426,12 @@ snapshots:
cli-width@4.1.0: {}
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@ -6498,6 +6553,8 @@ snapshots:
dependencies:
ms: 2.1.3
decamelize@1.2.0: {}
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
@ -6551,6 +6608,8 @@ snapshots:
diff@4.0.4: {}
dijkstrajs@1.0.3: {}
doctrine@3.0.0:
dependencies:
esutils: 2.0.3
@ -8568,6 +8627,8 @@ snapshots:
pngjs@3.4.0: {}
pngjs@5.0.0: {}
possible-typed-array-names@1.1.0: {}
prelude-ls@1.2.1: {}
@ -8613,6 +8674,12 @@ snapshots:
pure-rand@6.1.0: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
qs@6.15.2:
dependencies:
side-channel: 1.1.0
@ -8743,6 +8810,8 @@ snapshots:
require-from-string@2.0.2: {}
require-main-filename@2.0.0: {}
requireindex@1.2.0: {}
resolve-cwd@3.0.0:
@ -9496,6 +9565,8 @@ snapshots:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-module@2.0.1: {}
which-typed-array@1.1.21:
dependencies:
available-typed-arrays: 1.0.7
@ -9606,14 +9677,35 @@ snapshots:
xtend@4.0.2: {}
y18n@4.0.3: {}
y18n@5.0.8: {}
yallist@3.1.1: {}
yallist@4.0.0: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@21.1.1: {}
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yargs@17.7.2:
dependencies:
cliui: 8.0.1

View File

@ -4,6 +4,7 @@ param(
[string]$DisplayedServerList = "cn jp",
[string]$OutFile = ".kt-workspace/bangdream-smoke/bangdream-smoke.jpg",
[int]$TimeoutSeconds = 45,
[int]$ExpectedImageCount = 0,
[switch]$UseEasyBg
)
@ -38,6 +39,7 @@ $Payload = @{
}
operationKey = $OperationKey
outFile = $ResolvedOutFile
expectedImageCount = $ExpectedImageCount
}
$PayloadJson = $Payload | ConvertTo-Json -Compress -Depth 5
$PayloadBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($PayloadJson))
@ -45,17 +47,96 @@ $PayloadBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Paylo
$NodeCode = @"
const fs = require("fs");
const path = require("path");
const { ConfigService } = require("@nestjs/config");
const XLSX = require("xlsx");
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
const { ToolsService } = require("./src/common/services/tool.service");
const { QqbotBangDreamRendererService } = require("./src/modules/qqbot/plugins/bangDream/application/bangdream-renderer.facade");
const { TsuguApplicationService } = require("./src/modules/qqbot/plugins/bangDream/application/bangdream-application.service");
const { createPlugin } = require("./src/modules/qqbot/plugins/bangdream/src");
function createHttpError(message, statusCode) {
const error = new Error(message);
error.statusCode = statusCode;
error.response = { status: statusCode };
return error;
}
async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeoutMs = options.timeoutMs || 30000;
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, {
headers: options.headers,
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function readExcelRows(filePath) {
const workbook = XLSX.readFile(filePath);
const sheetName = workbook.SheetNames[0];
return XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
}
async function requestBuffer(url, options) {
const response = await fetchWithTimeout(url, options);
const body = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw createHttpError("BangDream resource request failed: " + response.status, response.status);
}
return {
body,
statusCode: response.status,
};
}
async function requestJson(url, options) {
const response = await fetchWithTimeout(url, options);
const text = await response.text();
if (!response.ok) {
throw createHttpError("BangDream JSON request failed: " + response.status, response.status);
}
return {
body: JSON.parse(text),
statusCode: response.status,
};
}
(async () => {
const renderer = new QqbotBangDreamRendererService(new ConfigService({}), undefined);
const service = new TsuguApplicationService(renderer, new ToolsService());
await service.onApplicationBootstrap();
const result = await service.execute(payload.operationKey, payload.input);
const manifest = readJsonFile(path.join(process.cwd(), "src/modules/qqbot/plugins/bangdream/plugin.json"));
const operations = manifest.operations.map((operation) => ({
handlerName: operation.handlerName,
key: operation.key,
}));
const plugin = createPlugin({
io: {
getConfig: (key) => process.env[key],
readAssetFile: async (filePath) => fs.promises.readFile(filePath),
readExcelRows: async (filePath) => readExcelRows(filePath),
readJsonFile: async (filePath) => readJsonFile(filePath),
readJsonFileSync: (filePath) => readJsonFile(filePath),
requestArrayBuffer: requestBuffer,
requestJson,
sleep: async (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
writeJsonFile: async (filePath, data) => {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data));
},
},
normalizeError: (error) => String(error?.message || error || "BangDream command failed"),
operations,
});
await plugin.activate();
const result = await plugin.executeOperation(payload.operationKey, payload.input);
if (payload.expectedImageCount > 0 && result.imageCount !== payload.expectedImageCount) {
throw new Error(
"Expected imageCount=" + payload.expectedImageCount + ", got " + result.imageCount,
);
}
const matches = [...result.replyText.matchAll(/base64:\/\/([A-Za-z0-9+/=]+)/g)];
if (matches.length === 0) throw new Error("No image CQ payload");
fs.mkdirSync(path.dirname(payload.outFile), { recursive: true });

View File

@ -4,7 +4,7 @@ import * as path from 'path';
import {
parseQqbotPluginManifest,
type QqbotPluginManifest,
} from '../../src/modules/qqbot/plugin-platform/manifest';
} from '../../src/modules/qqbot/plugin-platform/domain/manifest';
export type QqbotPluginCliCommand =
| 'create'
@ -21,11 +21,13 @@ export type QqbotPluginCliOptions = {
export type QqbotPluginCliResult = {
command: QqbotPluginCliCommand;
exitCode: number;
installationId?: string;
packageHash?: string;
packagePath?: string;
pluginKey?: string;
pluginRoot?: string;
version?: string;
versionId?: string;
};
type PackedPluginFile = {
@ -41,6 +43,18 @@ type PackedPlugin = {
const templateRoot = path.join(__dirname, 'templates', 'basic');
const packageExtension = '.qqbot-plugin.json';
const maxPackageFileBytes = 5 * 1024 * 1024;
const pluginKeyPattern = /^[a-z][a-z0-9-]{2,63}$/;
const allowedTopLevelEntries = new Set([
'LICENSE',
'LICENSE.txt',
'README.md',
'assets',
'migrations',
'plugin.json',
'src',
'tests',
]);
const sha256 = (content: Buffer | string) =>
crypto.createHash('sha256').update(content).digest('hex');
@ -68,6 +82,73 @@ const formatPluginName = (pluginKey: string) => {
.join(' ');
};
const getOptionValue = (argv: string[], optionName: string) => {
const optionIndex = argv.indexOf(optionName);
if (optionIndex < 0) return undefined;
const value = argv[optionIndex + 1];
if (!value || value.startsWith('--')) {
throw new Error(`Usage: ${optionName} <path>`);
}
return value;
};
const isInsideDirectory = (parent: string, child: string) => {
const relativePath = path.relative(parent, child);
return (
relativePath === '' ||
(!!relativePath &&
!relativePath.startsWith('..') &&
!path.isAbsolute(relativePath))
);
};
const findWorkspaceRoot = (cwd: string) => {
let current = path.resolve(cwd);
while (true) {
if (fs.existsSync(path.join(current, 'AGENTS.md'))) return current;
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
};
const getControlledRoots = (options: Required<QqbotPluginCliOptions>) => {
const cwd = path.resolve(options.cwd);
const roots = [cwd];
if (fs.existsSync(path.join(cwd, 'package.json'))) {
const workspaceRoot = findWorkspaceRoot(cwd);
if (workspaceRoot) roots.push(path.join(workspaceRoot, '.kt-workspace'));
}
return roots;
};
const resolveControlledPath = (
pathArg: string,
options: Required<QqbotPluginCliOptions>,
label: string,
) => {
const resolvedPath = path.resolve(options.cwd, pathArg);
const allowed = getControlledRoots(options).some((root) =>
isInsideDirectory(root, resolvedPath),
);
if (!allowed) {
throw new Error(`Unsafe ${label} output path: ${resolvedPath}`);
}
return resolvedPath;
};
function assertPluginKey(
pluginKey: string | undefined,
): asserts pluginKey is string {
if (!pluginKey) throw new Error('Usage: qqbot-plugin create <pluginKey>');
if (!pluginKeyPattern.test(pluginKey)) {
throw new Error(`Invalid QQBot plugin key: ${pluginKey}`);
}
}
const readManifest = (pluginRoot: string) => {
const manifestPath = path.join(pluginRoot, 'plugin.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
@ -116,16 +197,30 @@ const listPackageFiles = (pluginRoot: string): PackedPluginFile[] => {
const visit = (current: string) => {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
if (current === pluginRoot && ignoredRoots.has(entry.name)) continue;
if (entry.name.startsWith('.')) {
throw new Error(`Hidden files are not allowed in plugin packages: ${entry.name}`);
}
if (entry.isSymbolicLink()) {
throw new Error(`Symbolic links are not allowed in plugin packages: ${entry.name}`);
}
const absolutePath = path.join(current, entry.name);
const relativePath = path
.relative(pluginRoot, absolutePath)
.replace(/\\/g, '/');
const topLevelEntry = relativePath.split('/')[0];
if (!allowedTopLevelEntries.has(topLevelEntry)) {
throw new Error(`Plugin package path is not whitelisted: ${relativePath}`);
}
if (entry.isDirectory()) {
visit(absolutePath);
continue;
}
const fileSize = fs.statSync(absolutePath).size;
if (fileSize > maxPackageFileBytes) {
throw new Error(`Plugin package file is too large: ${relativePath}`);
}
const relativePath = path
.relative(pluginRoot, absolutePath)
.replace(/\\/g, '/');
files.push({
path: relativePath,
sha256: sha256(fs.readFileSync(absolutePath)),
@ -137,8 +232,32 @@ const listPackageFiles = (pluginRoot: string): PackedPluginFile[] => {
return files.sort((a, b) => a.path.localeCompare(b.path));
};
const assertPluginSourceBoundary = (pluginRoot: string) => {
const forbiddenPatterns: Array<[RegExp, string]> = [
[/\bfrom\s+['"]@nestjs\//, '@nestjs imports'],
[/\bfrom\s+['"](?:node:)?fs['"]/, 'fs imports'],
[/\brequire\(['"](?:node:)?fs['"]\)/, 'fs require'],
[/\bfrom\s+['"]axios['"]/, 'axios imports'],
[/\brequire\(['"]axios['"]\)/, 'axios require'],
[/\bprocess\.env\b/, 'process.env access'],
];
for (const file of listPackageFiles(pluginRoot)) {
if (!/\.[cm]?[tj]sx?$/.test(file.path)) continue;
const filePath = path.join(pluginRoot, file.path);
const source = fs.readFileSync(filePath, 'utf8');
const violation = forbiddenPatterns.find(([pattern]) => pattern.test(source));
if (violation) {
throw new Error(
`Forbidden plugin source boundary violation in ${file.path}: ${violation[1]}`,
);
}
}
};
const buildPackedPlugin = (pluginRoot: string): PackedPlugin => {
const manifest = readManifest(pluginRoot);
assertPluginSourceBoundary(pluginRoot);
const files = listPackageFiles(pluginRoot);
const contentHash = sha256(
stableStringify({
@ -172,10 +291,13 @@ const assertPackageIntegrity = (packedPlugin: PackedPlugin) => {
const createPlugin = (
pluginKey: string | undefined,
options: Required<QqbotPluginCliOptions>,
outputPathArg?: string,
): QqbotPluginCliResult => {
if (!pluginKey) throw new Error('Usage: qqbot-plugin create <pluginKey>');
assertPluginKey(pluginKey);
const pluginRoot = path.join(options.cwd, 'plugins', pluginKey);
const pluginRoot = outputPathArg
? resolveControlledPath(outputPathArg, options, 'create')
: path.join(options.cwd, 'plugins', pluginKey);
if (fs.existsSync(pluginRoot)) {
throw new Error(`Plugin already exists: ${pluginRoot}`);
}
@ -186,6 +308,15 @@ const createPlugin = (
});
fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'migrations'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'src', 'application'), {
recursive: true,
});
fs.mkdirSync(path.join(pluginRoot, 'src', 'domain'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'src', 'events'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'src', 'infrastructure'), {
recursive: true,
});
fs.mkdirSync(path.join(pluginRoot, 'src', 'operations'), { recursive: true });
fs.mkdirSync(path.join(pluginRoot, 'tests'), { recursive: true });
readManifest(pluginRoot);
@ -207,6 +338,7 @@ const validatePlugin = (
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
const manifest = readManifest(pluginRoot);
assertPluginSourceBoundary(pluginRoot);
options.stdout(
`Validated QQBot plugin: ${manifest.pluginKey}@${manifest.version}`,
);
@ -223,15 +355,18 @@ const validatePlugin = (
const packPlugin = (
pluginRootArg: string | undefined,
options: Required<QqbotPluginCliOptions>,
outputPathArg?: string,
): QqbotPluginCliResult => {
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin pack <path>');
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
const packedPlugin = buildPackedPlugin(pluginRoot);
const shortHash = packedPlugin.contentHash.slice(0, 12);
const packageRoot = outputPathArg
? resolveControlledPath(outputPathArg, options, 'pack')
: path.join(pluginRoot, 'dist');
const packagePath = path.join(
pluginRoot,
'dist',
packageRoot,
`${packedPlugin.manifest.pluginKey}-${packedPlugin.manifest.version}-${shortHash}${packageExtension}`,
);
@ -263,6 +398,12 @@ const installLocalPlugin = (
fs.readFileSync(packagePath, 'utf8'),
) as PackedPlugin;
assertPackageIntegrity(packedPlugin);
const versionId = sha256(
`version:${packedPlugin.manifest.pluginKey}:${packedPlugin.manifest.version}:${packedPlugin.contentHash}`,
).slice(0, 16);
const installationId = sha256(
`installation:${packagePath}:${packedPlugin.contentHash}`,
).slice(0, 16);
options.stdout(
`Installed local QQBot plugin package: ${packedPlugin.manifest.pluginKey}@${packedPlugin.manifest.version}`,
);
@ -270,10 +411,12 @@ const installLocalPlugin = (
return {
command: 'install-local',
exitCode: 0,
installationId,
packageHash: packedPlugin.contentHash,
packagePath,
pluginKey: packedPlugin.manifest.pluginKey,
version: packedPlugin.manifest.version,
versionId,
};
};
@ -284,17 +427,17 @@ export const runQqbotPluginCli = async (
const command = argv[0] as QqbotPluginCliCommand | undefined;
const resolvedOptions: Required<QqbotPluginCliOptions> = {
cwd: options.cwd || process.cwd(),
stderr: options.stderr || ((message) => console.error(message)),
stdout: options.stdout || ((message) => console.log(message)),
stderr: options.stderr || ((message) => process.stderr.write(`${message}\n`)),
stdout: options.stdout || ((message) => process.stdout.write(`${message}\n`)),
};
switch (command) {
case 'create':
return createPlugin(argv[1], resolvedOptions);
return createPlugin(argv[1], resolvedOptions, getOptionValue(argv, '--out'));
case 'install-local':
return installLocalPlugin(argv[1], resolvedOptions);
case 'pack':
return packPlugin(argv[1], resolvedOptions);
return packPlugin(argv[1], resolvedOptions, getOptionValue(argv, '--out'));
case 'validate':
return validatePlugin(argv[1], resolvedOptions);
default:

View File

@ -756,23 +756,23 @@ ON DUPLICATE KEY UPDATE
INSERT INTO `qqbot_command` (`id`, `code`, `name`, `aliases`, `prefixes`, `plugin_key`, `operation_key`, `parser_key`, `target_type`, `default_params`, `reply_template`, `error_template`, `enabled`, `priority`, `cooldown_ms`, `remark`)
VALUES
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!",""]', 'ff14Market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 1, 0, 1500, '默认示例命令;请在账号配置中绑定后启用'),
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!",""]', 'ff14-market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 1, 0, 1500, '默认示例命令;请在账号配置中绑定后启用'),
(2041700000000300502, 'fflogs_character', 'FFLogs 查询', '["fflogs","logs","查logs","查log"]', '["/","!",""]', 'fflogs', 'fflogs.character.summary', 'fflogsCharacter', 'all', '{"serverRegion":"CN"}', '', 'FFLogs 查询失败:{{error}}', 1, 0, 3000, '查询 FFLogs 角色公开排名带高难任务时返回最近10次记录格式/fflogs 角色名 服务器 [高难任务]'),
(2041700000000300503, 'bangdream_song', 'BangDream 查曲', '["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]', '["/","!",""]', 'bangDream', 'bangdream.song.search', 'plain', 'all', '{}', '', 'BangDream 查曲失败:{{error}}', 1, 0, 1500, '查询 BanG Dream 歌曲信息;格式:/查曲 歌曲名 或 /查曲 歌曲ID'),
(2041700000000300504, 'bangdream_song_chart', 'BangDream 查谱面', '["查谱面","谱面","bd谱面"]', '["/","!",""]', 'bangDream', 'bangdream.song.chart', 'plain', 'all', '{}', '', 'BangDream 查谱面失败:{{error}}', 1, 0, 1500, '查询歌曲谱面;格式:/查谱面 歌曲ID [难度]'),
(2041700000000300505, 'bangdream_song_random', 'BangDream 随机曲', '["随机曲","随机","bd随机"]', '["/","!",""]', 'bangDream', 'bangdream.song.random', 'plain', 'all', '{}', '', 'BangDream 随机曲失败:{{error}}', 1, 0, 1500, '按关键词随机歌曲;格式:/随机曲 [关键词]'),
(2041700000000300506, 'bangdream_song_meta', 'BangDream 分数表', '["查询分数表","查分数表","查询分数榜","查分数榜","bd分数表"]', '["/","!",""]', 'bangDream', 'bangdream.song.meta', 'plain', 'all', '{}', '', 'BangDream 分数表失败:{{error}}', 1, 0, 1500, '查询歌曲分数榜;格式:/查询分数表 [服务器]'),
(2041700000000300507, 'bangdream_card', 'BangDream 查卡', '["查卡","查卡牌","bd查卡"]', '["/","!",""]', 'bangDream', 'bangdream.card.search', 'plain', 'all', '{}', '', 'BangDream 查卡失败:{{error}}', 1, 0, 1500, '查询卡牌信息;格式:/查卡 卡牌关键词 或 /查卡 卡牌ID'),
(2041700000000300508, 'bangdream_card_illustration', 'BangDream 查卡面', '["查卡面","查卡插画","查插画","bd卡面"]', '["/","!",""]', 'bangDream', 'bangdream.card.illustration', 'plain', 'all', '{}', '', 'BangDream 查卡面失败:{{error}}', 1, 0, 1500, '查询卡牌插画;格式:/查卡面 卡牌ID'),
(2041700000000300509, 'bangdream_character', 'BangDream 查角色', '["查角色","bd角色"]', '["/","!",""]', 'bangDream', 'bangdream.character.search', 'plain', 'all', '{}', '', 'BangDream 查角色失败:{{error}}', 1, 0, 1500, '查询角色信息;格式:/查角色 角色关键词 或 /查角色 角色ID'),
(2041700000000300510, 'bangdream_event', 'BangDream 查活动', '["查活动","bd活动"]', '["/","!",""]', 'bangDream', 'bangdream.event.search', 'plain', 'all', '{}', '', 'BangDream 查活动失败:{{error}}', 1, 0, 1500, '查询活动信息;格式:/查活动 活动关键词 或 /查活动 活动ID'),
(2041700000000300511, 'bangdream_event_stage', 'BangDream 查试炼', '["查试炼","查stage","查舞台","查festival","查5v5"]', '["/","!",""]', 'bangDream', 'bangdream.event.stage', 'plain', 'all', '{}', '', 'BangDream 查试炼失败:{{error}}', 1, 0, 1500, '查询活动试炼;格式:/查试炼 [活动ID] [-m]'),
(2041700000000300512, 'bangdream_player', 'BangDream 查玩家', '["查玩家","查询玩家","bd玩家"]', '["/","!",""]', 'bangDream', 'bangdream.player.search', 'plain', 'all', '{}', '', 'BangDream 查玩家失败:{{error}}', 1, 0, 1500, '查询玩家信息;格式:/查玩家 玩家ID [服务器]'),
(2041700000000300513, 'bangdream_gacha', 'BangDream 查卡池', '["查卡池","bd卡池"]', '["/","!",""]', 'bangDream', 'bangdream.gacha.search', 'plain', 'all', '{}', '', 'BangDream 查卡池失败:{{error}}', 1, 0, 1500, '查询卡池信息;格式:/查卡池 卡池ID'),
(2041700000000300514, 'bangdream_gacha_simulate', 'BangDream 抽卡模拟', '["抽卡模拟","bd抽卡"]', '["/","!",""]', 'bangDream', 'bangdream.gacha.simulate', 'plain', 'all', '{}', '', 'BangDream 抽卡模拟失败:{{error}}', 1, 0, 3000, '模拟抽卡;格式:/抽卡模拟 [次数] [卡池ID]'),
(2041700000000300515, 'bangdream_cutoff_detail', 'BangDream ycx', '["ycx","预测线","查档线","bd档线"]', '["/","!",""]', 'bangDream', 'bangdream.cutoff.detail', 'plain', 'all', '{}', '', 'BangDream ycx 失败:{{error}}', 1, 0, 3000, '查询指定档位预测线;格式:/ycx 档位 [活动ID] [服务器]'),
(2041700000000300516, 'bangdream_cutoff_all', 'BangDream ycxall', '["ycxall","myycx","全部档线"]', '["/","!",""]', 'bangDream', 'bangdream.cutoff.all', 'plain', 'all', '{}', '', 'BangDream ycxall 失败:{{error}}', 1, 0, 3000, '查询所有档位预测线;格式:/ycxall [活动ID] [服务器]'),
(2041700000000300517, 'bangdream_cutoff_recent', 'BangDream lsycx', '["lsycx","历史档线","近期档线"]', '["/","!",""]', 'bangDream', 'bangdream.cutoff.recent', 'plain', 'all', '{}', '', 'BangDream lsycx 失败:{{error}}', 1, 0, 3000, '查询同类型活动档线;格式:/lsycx 档位 [活动ID] [服务器]')
(2041700000000300503, 'bangdream_song', 'BangDream 查曲', '["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]', '["/","!",""]', 'bangdream', 'bangdream.song.search', 'plain', 'all', '{}', '', 'BangDream 查曲失败:{{error}}', 1, 0, 1500, '查询 BanG Dream 歌曲信息;格式:/查曲 歌曲名 或 /查曲 歌曲ID'),
(2041700000000300504, 'bangdream_song_chart', 'BangDream 查谱面', '["查谱面","谱面","bd谱面"]', '["/","!",""]', 'bangdream', 'bangdream.song.chart', 'plain', 'all', '{}', '', 'BangDream 查谱面失败:{{error}}', 1, 0, 1500, '查询歌曲谱面;格式:/查谱面 歌曲ID [难度]'),
(2041700000000300505, 'bangdream_song_random', 'BangDream 随机曲', '["随机曲","随机","bd随机"]', '["/","!",""]', 'bangdream', 'bangdream.song.random', 'plain', 'all', '{}', '', 'BangDream 随机曲失败:{{error}}', 1, 0, 1500, '按关键词随机歌曲;格式:/随机曲 [关键词]'),
(2041700000000300506, 'bangdream_song_meta', 'BangDream 分数表', '["查询分数表","查分数表","查询分数榜","查分数榜","bd分数表"]', '["/","!",""]', 'bangdream', 'bangdream.song.meta', 'plain', 'all', '{}', '', 'BangDream 分数表失败:{{error}}', 1, 0, 1500, '查询歌曲分数榜;格式:/查询分数表 [服务器]'),
(2041700000000300507, 'bangdream_card', 'BangDream 查卡', '["查卡","查卡牌","bd查卡"]', '["/","!",""]', 'bangdream', 'bangdream.card.search', 'plain', 'all', '{}', '', 'BangDream 查卡失败:{{error}}', 1, 0, 1500, '查询卡牌信息;格式:/查卡 卡牌关键词 或 /查卡 卡牌ID'),
(2041700000000300508, 'bangdream_card_illustration', 'BangDream 查卡面', '["查卡面","查卡插画","查插画","bd卡面"]', '["/","!",""]', 'bangdream', 'bangdream.card.illustration', 'plain', 'all', '{}', '', 'BangDream 查卡面失败:{{error}}', 1, 0, 1500, '查询卡牌插画;格式:/查卡面 卡牌ID'),
(2041700000000300509, 'bangdream_character', 'BangDream 查角色', '["查角色","bd角色"]', '["/","!",""]', 'bangdream', 'bangdream.character.search', 'plain', 'all', '{}', '', 'BangDream 查角色失败:{{error}}', 1, 0, 1500, '查询角色信息;格式:/查角色 角色关键词 或 /查角色 角色ID'),
(2041700000000300510, 'bangdream_event', 'BangDream 查活动', '["查活动","bd活动"]', '["/","!",""]', 'bangdream', 'bangdream.event.search', 'plain', 'all', '{}', '', 'BangDream 查活动失败:{{error}}', 1, 0, 1500, '查询活动信息;格式:/查活动 活动关键词 或 /查活动 活动ID'),
(2041700000000300511, 'bangdream_event_stage', 'BangDream 查试炼', '["查试炼","查stage","查舞台","查festival","查5v5"]', '["/","!",""]', 'bangdream', 'bangdream.event.stage', 'plain', 'all', '{}', '', 'BangDream 查试炼失败:{{error}}', 1, 0, 1500, '查询活动试炼;格式:/查试炼 [活动ID] [-m]'),
(2041700000000300512, 'bangdream_player', 'BangDream 查玩家', '["查玩家","查询玩家","bd玩家"]', '["/","!",""]', 'bangdream', 'bangdream.player.search', 'plain', 'all', '{}', '', 'BangDream 查玩家失败:{{error}}', 1, 0, 1500, '查询玩家信息;格式:/查玩家 玩家ID [服务器]'),
(2041700000000300513, 'bangdream_gacha', 'BangDream 查卡池', '["查卡池","bd卡池"]', '["/","!",""]', 'bangdream', 'bangdream.gacha.search', 'plain', 'all', '{}', '', 'BangDream 查卡池失败:{{error}}', 1, 0, 1500, '查询卡池信息;格式:/查卡池 卡池ID'),
(2041700000000300514, 'bangdream_gacha_simulate', 'BangDream 抽卡模拟', '["抽卡模拟","bd抽卡"]', '["/","!",""]', 'bangdream', 'bangdream.gacha.simulate', 'plain', 'all', '{}', '', 'BangDream 抽卡模拟失败:{{error}}', 1, 0, 3000, '模拟抽卡;格式:/抽卡模拟 [次数] [卡池ID]'),
(2041700000000300515, 'bangdream_cutoff_detail', 'BangDream ycx', '["ycx","预测线","查档线","bd档线"]', '["/","!",""]', 'bangdream', 'bangdream.cutoff.detail', 'plain', 'all', '{}', '', 'BangDream ycx 失败:{{error}}', 1, 0, 3000, '查询指定档位预测线;格式:/ycx 档位 [活动ID] [服务器]'),
(2041700000000300516, 'bangdream_cutoff_all', 'BangDream ycxall', '["ycxall","myycx","全部档线"]', '["/","!",""]', 'bangdream', 'bangdream.cutoff.all', 'plain', 'all', '{}', '', 'BangDream ycxall 失败:{{error}}', 1, 0, 3000, '查询所有档位预测线;格式:/ycxall [活动ID] [服务器]'),
(2041700000000300517, 'bangdream_cutoff_recent', 'BangDream lsycx', '["lsycx","历史档线","近期档线"]', '["/","!",""]', 'bangdream', 'bangdream.cutoff.recent', 'plain', 'all', '{}', '', 'BangDream lsycx 失败:{{error}}', 1, 0, 3000, '查询同类型活动档线;格式:/lsycx 档位 [活动ID] [服务器]')
ON DUPLICATE KEY UPDATE
`code` = VALUES(`code`),
`name` = VALUES(`name`),

View File

@ -951,11 +951,14 @@ CREATE TABLE IF NOT EXISTS napcat_account_binding (
CREATE TABLE IF NOT EXISTS napcat_login_session (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
account_id BIGINT NULL,
session_key VARCHAR(128) NOT NULL,
login_stage VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
progress_message VARCHAR(255) NOT NULL,
session_payload JSON NULL,
expires_at DATETIME NULL,
completed_at DATETIME NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_napcat_login_session_key (session_key),

View File

@ -274,7 +274,8 @@ INSERT INTO qqbot_command (
plugin_key,
enabled,
cooldown_seconds
) VALUES (
) VALUES
(
1000000000000000201,
'bangdream.song.search',
'bangdream_song',
@ -284,11 +285,167 @@ INSERT INTO qqbot_command (
'bangdream',
1,
2
) ON DUPLICATE KEY UPDATE
),
(
1000000000000000202,
'bangdream.song.chart',
'bangdream_song_chart',
'bangdream_song_chart',
'BangDream 查谱面',
'["查谱面","谱面","bd谱面"]',
'bangdream',
1,
2
),
(
1000000000000000203,
'bangdream.song.random',
'bangdream_song_random',
'bangdream_song_random',
'BangDream 随机曲',
'["随机曲","随机","bd随机"]',
'bangdream',
1,
2
),
(
1000000000000000204,
'bangdream.song.meta',
'bangdream_song_meta',
'bangdream_song_meta',
'BangDream 分数表',
'["查询分数表","查分数表","查询分数榜","查分数榜","bd分数表"]',
'bangdream',
1,
2
),
(
1000000000000000205,
'bangdream.card.search',
'bangdream_card',
'bangdream_card',
'BangDream 查卡',
'["查卡","查卡牌","bd查卡"]',
'bangdream',
1,
2
),
(
1000000000000000206,
'bangdream.card.illustration',
'bangdream_card_illustration',
'bangdream_card_illustration',
'BangDream 查卡面',
'["查卡面","查卡插画","查插画","bd卡面"]',
'bangdream',
1,
2
),
(
1000000000000000207,
'bangdream.character.search',
'bangdream_character',
'bangdream_character',
'BangDream 查角色',
'["查角色","bd角色"]',
'bangdream',
1,
2
),
(
1000000000000000208,
'bangdream.event.search',
'bangdream_event',
'bangdream_event',
'BangDream 查活动',
'["查活动","bd活动"]',
'bangdream',
1,
2
),
(
1000000000000000209,
'bangdream.event.stage',
'bangdream_event_stage',
'bangdream_event_stage',
'BangDream 查试炼',
'["查试炼","查stage","查舞台","查festival","查5v5"]',
'bangdream',
1,
2
),
(
1000000000000000210,
'bangdream.player.search',
'bangdream_player',
'bangdream_player',
'BangDream 查玩家',
'["查玩家","查询玩家","bd玩家"]',
'bangdream',
1,
2
),
(
1000000000000000211,
'bangdream.gacha.search',
'bangdream_gacha',
'bangdream_gacha',
'BangDream 查卡池',
'["查卡池","bd卡池"]',
'bangdream',
1,
2
),
(
1000000000000000212,
'bangdream.gacha.simulate',
'bangdream_gacha_simulate',
'bangdream_gacha_simulate',
'BangDream 抽卡模拟',
'["抽卡模拟","bd抽卡"]',
'bangdream',
1,
3
),
(
1000000000000000213,
'bangdream.cutoff.detail',
'bangdream_cutoff_detail',
'bangdream_cutoff_detail',
'BangDream ycx',
'["ycx","预测线","查档线","bd档线"]',
'bangdream',
1,
3
),
(
1000000000000000214,
'bangdream.cutoff.all',
'bangdream_cutoff_all',
'bangdream_cutoff_all',
'BangDream ycxall',
'["ycxall","myycx","全部档线"]',
'bangdream',
1,
3
),
(
1000000000000000215,
'bangdream.cutoff.recent',
'bangdream_cutoff_recent',
'bangdream_cutoff_recent',
'BangDream lsycx',
'["lsycx","历史档线","近期档线"]',
'bangdream',
1,
3
)
ON DUPLICATE KEY UPDATE
operation_key = VALUES(operation_key),
code = VALUES(code),
name = VALUES(name),
aliases = VALUES(aliases),
plugin_key = VALUES(plugin_key),
enabled = VALUES(enabled),
cooldown_seconds = VALUES(cooldown_seconds);
cooldown_seconds = VALUES(cooldown_seconds),
is_deleted = 0;

View File

@ -5,7 +5,12 @@ SELECT 'platform_setting' AS table_name, COUNT(*) AS row_count FROM platform_set
SELECT 'admin_dict' AS table_name, COUNT(*) AS row_count FROM admin_dict;
SELECT 'qqbot_command' AS table_name, COUNT(*) AS row_count FROM qqbot_command;
SELECT 'qqbot_plugin' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin;
SELECT 'napcat_container' AS table_name, COUNT(*) AS row_count FROM napcat_container;
SELECT 'napcat_device_identity' AS table_name, COUNT(*) AS row_count FROM napcat_device_identity;
SELECT 'napcat_account_binding' AS table_name, COUNT(*) AS row_count FROM napcat_account_binding;
SELECT 'napcat_login_session' AS table_name, COUNT(*) AS row_count FROM napcat_login_session;
SELECT 'napcat_login_challenge' AS table_name, COUNT(*) AS row_count FROM napcat_login_challenge;
SELECT 'napcat_runtime_cleanup' AS table_name, COUNT(*) AS row_count FROM napcat_runtime_cleanup;
SELECT 'seed_admin_user' AS check_name, COUNT(*) AS matched_rows
FROM admin_user
@ -31,6 +36,13 @@ WHERE command_key = 'bangdream_song'
AND plugin_key = 'bangdream'
AND enabled = 1;
SELECT 'seed_qqbot_command_bangdream_all' AS check_name, COUNT(*) AS matched_rows
FROM qqbot_command
WHERE plugin_key = 'bangdream'
AND operation_key LIKE 'bangdream.%'
AND enabled = 1
AND is_deleted = 0;
SELECT 'index_admin_user_username' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
@ -60,3 +72,27 @@ FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_device_identity'
AND index_name = 'uk_napcat_device_identity_account';
SELECT 'index_napcat_account_binding_account' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_account_binding'
AND index_name = 'uk_napcat_account_binding_account';
SELECT 'index_napcat_login_session_key' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_login_session'
AND index_name = 'uk_napcat_login_session_key';
SELECT 'index_napcat_login_challenge_session' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_login_challenge'
AND index_name = 'idx_napcat_login_challenge_session';
SELECT 'index_napcat_runtime_cleanup_session' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_runtime_cleanup'
AND index_name = 'idx_napcat_runtime_cleanup_session';

View File

@ -1,68 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from './auth/admin-auth-guard.module';
import { AdminAuthController } from './auth/admin-auth.controller';
import { ComponentController } from './component/component.controller';
import { Component } from './component/component.entity';
import { ComponentService } from './component/component.service';
import { AdminDeptController } from './dept/admin-dept.controller';
import { AdminDept } from './dept/admin-dept.entity';
import { AdminDeptService } from './dept/admin-dept.service';
import { DictModule } from './dict/dict.module';
import { AdminExampleController } from './example/admin-example.controller';
import { AdminMenuController } from './menu/admin-menu.controller';
import { AdminMenu } from './menu/admin-menu.entity';
import { AdminMenuService } from './menu/admin-menu.service';
import { AdminRoleController } from './role/admin-role.controller';
import { AdminRole } from './role/admin-role.entity';
import { AdminRoleService } from './role/admin-role.service';
import { NoticeModule } from './notice/notice.module';
import { SystemLogController } from './system-log/system-log.controller';
import { SystemLogService } from './system-log/system-log.service';
import { AdminTimezoneController } from './timezone/admin-timezone.controller';
import { AdminTimezoneService } from './timezone/admin-timezone.service';
import { AdminUserManageController } from './user/admin-user-manage.controller';
import { AdminUserController } from './user/admin-user.controller';
import { AdminUser } from './user/admin-user.entity';
import { AdminUserService } from './user/admin-user.service';
import { MinioClientModule } from '@/minio/minio.module';
import { WordpressModule } from '@/wordpress/wordpress.module';
@Module({
imports: [
TypeOrmModule.forFeature([
AdminUser,
AdminRole,
AdminMenu,
AdminDept,
Component,
]),
AdminAuthGuardModule,
DictModule,
NoticeModule,
MinioClientModule,
WordpressModule,
],
controllers: [
AdminAuthController,
AdminUserController,
AdminUserManageController,
AdminMenuController,
AdminRoleController,
AdminDeptController,
ComponentController,
SystemLogController,
AdminTimezoneController,
AdminExampleController,
],
providers: [
ComponentService,
AdminDeptService,
AdminMenuService,
AdminRoleService,
SystemLogService,
AdminTimezoneService,
AdminUserService,
],
})
export class AdminModule {}

View File

@ -1,165 +0,0 @@
import {
Controller,
Get,
Post,
Query,
Res,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Response } from 'express';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { formatKtDateTime, Public, vbenPage, vbenSuccess } from '@/common';
import { MinioClientService } from '@/minio/minio.service';
import type { MinioUploadFile } from '@/minio/minio.types';
import type { AdminDemoTableRow } from '../admin.types';
const DEMO_ROWS: AdminDemoTableRow[] = Array.from(
{ length: 100 },
(_, index) => {
const sequence = index + 1;
const categories = ['Dashboard', 'Form', 'Table', 'Chart', 'Workflow'];
const colors = ['Blue', 'Green', 'Purple', 'Orange', 'Slate'];
const statuses = ['success', 'warning', 'error'];
return {
available: sequence % 3 !== 0,
category: categories[index % categories.length],
color: colors[index % colors.length],
currency: 'CNY',
description: `真实 API 示例数据 ${sequence}`,
id: `demo-${String(sequence).padStart(3, '0')}`,
imageUrl:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
imageUrl2:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
inProduction: sequence % 2 === 0,
open: sequence % 4 === 0,
price: `${(sequence * 3.6 + 19).toFixed(2)}`,
productName: `KT Admin 模板能力 ${sequence}`,
quantity: 10 + sequence,
rating: Number((3 + (sequence % 20) / 10).toFixed(1)),
releaseDate: formatKtDateTime(
new Date(2026, index % 12, (index % 28) + 1),
),
status: statuses[index % statuses.length],
tags: [
'kt',
'admin',
categories[index % categories.length].toLowerCase(),
],
weight: Number((1 + sequence / 10).toFixed(2)),
};
},
);
@ApiTags('Admin - 示例')
@Controller()
@UseGuards(JwtAuthGuard)
export class AdminExampleController {
constructor(private readonly minioClientService: MinioClientService) {}
@Post('upload')
@ApiOperation({ summary: '上传示例文件' })
@UseInterceptors(FileInterceptor('file'))
async upload(@UploadedFile() file: MinioUploadFile) {
const result = await this.minioClientService.uploadObject({ file });
return vbenSuccess({
url: result.url,
});
}
@Get('table/list')
@ApiOperation({ summary: '获取示例表格分页列表' })
async tableList(@Query() query: Record<string, any>) {
const page = Math.max(Number(query.page || 1), 1);
const pageSize = Math.max(Number(query.pageSize || 10), 1);
const sorted = this.sortRows([...DEMO_ROWS], query.sortBy, query.sortOrder);
const items = sorted.slice((page - 1) * pageSize, page * pageSize);
return vbenPage(items, sorted.length);
}
@Get('status')
@ApiOperation({ summary: '返回指定状态码示例' })
@Public()
status(@Query('status') status: string, @Res() res: Response) {
const code = Number(status) || 200;
if (code === 200) {
res.status(code).send(vbenSuccess(`${code}`));
return;
}
res.status(code).send({
code,
msg: `${code}`,
err: `${code}`,
});
}
@Get('demo/bigint')
@ApiOperation({ summary: '获取 BigInt 序列化示例' })
async bigint(@Res() res: Response) {
res.setHeader('Content-Type', 'application/json');
res.send(`{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 123456789012345678901234567890123456789012345678901234567890,
"name": "John Doe",
"age": 30,
"email": "john-doe@demo.com"
},
{
"id": 987654321098765432109876543210987654321098765432109876543210,
"name": "Jane Smith",
"age": 25,
"email": "jane@demo.com"
}
]
}`);
}
@Get('test')
@ApiOperation({ summary: 'GET 测试接口' })
@Public()
testGet() {
return vbenSuccess('Test get handler');
}
@Post('test')
@ApiOperation({ summary: 'POST 测试接口' })
@Public()
testPost() {
return vbenSuccess('Test post handler');
}
private sortRows(
rows: AdminDemoTableRow[],
sortBy?: string,
sortOrder?: string,
) {
if (!sortBy || !Object.hasOwn(rows[0], sortBy)) return rows;
return rows.sort((prev, next) => {
const prevValue = prev[sortBy];
const nextValue = next[sortBy];
const result = String(prevValue).localeCompare(
String(nextValue),
'zh-CN',
{
numeric: true,
sensitivity: 'base',
},
);
return sortOrder === 'desc' ? -result : result;
});
}
}

View File

@ -1,31 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { CommonModule } from '@/common';
import { WordpressModule } from '@/wordpress/wordpress.module';
import { BlogArticleController } from './blog-article.controller';
import { BlogArticle } from './blog-article.entity';
import { BlogArticleService } from './blog-article.service';
import { BlogThemeConfigController } from './blog-theme-config.controller';
import { BlogThemeConfig } from './blog-theme-config.entity';
import { BlogThemeConfigService } from './blog-theme-config.service';
import { BlogTermController } from './blog-term.controller';
import { BlogTerm } from './blog-term.entity';
import { BlogTermService } from './blog-term.service';
@Module({
imports: [
AdminAuthGuardModule,
CommonModule,
WordpressModule,
TypeOrmModule.forFeature([BlogArticle, BlogTerm, BlogThemeConfig]),
],
controllers: [
BlogArticleController,
BlogTermController,
BlogThemeConfigController,
],
providers: [BlogArticleService, BlogTermService, BlogThemeConfigService],
exports: [BlogArticleService, BlogTermService, BlogThemeConfigService],
})
export class BlogModule {}

View File

@ -1,13 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { MinioClientController } from './minio.controller';
import { MinioClientService } from './minio.service';
@Module({
imports: [AdminAuthGuardModule, ConfigModule],
controllers: [MinioClientController],
providers: [MinioClientService],
exports: [MinioClientService],
})
export class MinioClientModule {}

View File

@ -1,28 +1,7 @@
import type { Request } from 'express';
import type { AdminMenu } from './menu/admin-menu.entity';
import type { AdminRole } from './role/admin-role.entity';
import type { AdminUser } from './user/admin-user.entity';
export type AdminDemoTableRow = {
available: boolean;
category: string;
color: string;
currency: string;
description: string;
id: string;
imageUrl: string;
imageUrl2: string;
inProduction: boolean;
open: boolean;
price: string;
productName: string;
quantity: number;
rating: number;
releaseDate: string;
status: string;
tags: string[];
weight: number;
};
import type { AdminMenu } from '../identity/menu/admin-menu.entity';
import type { AdminRole } from '../identity/role/admin-role.entity';
import type { AdminUser } from '../identity/user/admin-user.entity';
export type AdminDictItem = {
childrenCode?: string | null;

View File

@ -1,20 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { AdminAuthController } from '@/admin/auth/admin-auth.controller';
import { AdminDeptController } from '@/admin/dept/admin-dept.controller';
import { AdminDept } from '@/admin/dept/admin-dept.entity';
import { AdminDeptService } from '@/admin/dept/admin-dept.service';
import { AdminMenuController } from '@/admin/menu/admin-menu.controller';
import { AdminMenu } from '@/admin/menu/admin-menu.entity';
import { AdminMenuService } from '@/admin/menu/admin-menu.service';
import { AdminRoleController } from '@/admin/role/admin-role.controller';
import { AdminRole } from '@/admin/role/admin-role.entity';
import { AdminRoleService } from '@/admin/role/admin-role.service';
import { AdminUserManageController } from '@/admin/user/admin-user-manage.controller';
import { AdminUserController } from '@/admin/user/admin-user.controller';
import { AdminUser } from '@/admin/user/admin-user.entity';
import { AdminUserService } from '@/admin/user/admin-user.service';
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
import { AdminAuthController } from '@/modules/admin/identity/auth/admin-auth.controller';
import { AdminDeptController } from '@/modules/admin/identity/dept/admin-dept.controller';
import { AdminDept } from '@/modules/admin/identity/dept/admin-dept.entity';
import { AdminDeptService } from '@/modules/admin/identity/dept/admin-dept.service';
import { AdminMenuController } from '@/modules/admin/identity/menu/admin-menu.controller';
import { AdminMenu } from '@/modules/admin/identity/menu/admin-menu.entity';
import { AdminMenuService } from '@/modules/admin/identity/menu/admin-menu.service';
import { AdminRoleController } from '@/modules/admin/identity/role/admin-role.controller';
import { AdminRole } from '@/modules/admin/identity/role/admin-role.entity';
import { AdminRoleService } from '@/modules/admin/identity/role/admin-role.service';
import { AdminUserManageController } from '@/modules/admin/identity/user/admin-user-manage.controller';
import { AdminUserController } from '@/modules/admin/identity/user/admin-user.controller';
import { AdminUser } from '@/modules/admin/identity/user/admin-user.entity';
import { AdminUserService } from '@/modules/admin/identity/user/admin-user.service';
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
export const ADMIN_IDENTITY_CONTROLLERS = [

View File

@ -17,7 +17,7 @@ import { AdminAuthService } from './admin-auth.service';
import { AdminLoginDto } from './admin-auth.dto';
import { AdminPasswordCryptoService } from './admin-password-crypto.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { WordpressService } from '@/wordpress/wordpress.service';
import { WordpressService } from '@/modules/wordpress/application/wordpress.service';
@ApiTags('Admin - 认证')
@Controller()

View File

@ -1,6 +1,6 @@
import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common';
import { throwVbenError } from '@/common';
import type { AdminRequest } from '../admin.types';
import type { AdminRequest } from '../../contract/admin.types';
@Injectable()
export class AdminSuperGuard implements CanActivate {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createHmac, timingSafeEqual } from 'crypto';
import type { AdminTokenPayload } from '../admin.types';
import type { AdminTokenPayload } from '../../contract/admin.types';
@Injectable()
export class AdminTokenService {

View File

@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AdminAuthService } from './admin-auth.service';
import { IS_PUBLIC_KEY } from '@/common';
import type { AdminRequest } from '../admin.types';
import type { AdminRequest } from '../../contract/admin.types';
@Injectable()
export class JwtAuthGuard implements CanActivate {

View File

@ -12,7 +12,7 @@ import {
KtUpdateDateColumn,
} from '@/common';
import { AdminRole } from '../role/admin-role.entity';
import type { AdminMenuMeta, AdminMenuType } from '../admin.types';
import type { AdminMenuMeta, AdminMenuType } from '../../contract/admin.types';
@Entity('admin_menu')
export class AdminMenu {

View File

@ -4,7 +4,7 @@ import { In, Repository } from 'typeorm';
import { toTree } from '@/common';
import { AdminUser } from '../user/admin-user.entity';
import { AdminMenu } from './admin-menu.entity';
import type { AdminMenuInput, AdminMenuMeta } from '../admin.types';
import type { AdminMenuInput, AdminMenuMeta } from '../../contract/admin.types';
@Injectable()
export class AdminMenuService {

View File

@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
import { throwVbenError } from '@/common';
import { AdminMenu } from '../menu/admin-menu.entity';
import { AdminRole } from './admin-role.entity';
import type { AdminRoleInput, AdminRoleListQuery } from '../admin.types';
import type { AdminRoleInput, AdminRoleListQuery } from '../../contract/admin.types';
@Injectable()
export class AdminRoleService {

View File

@ -5,7 +5,7 @@ import { throwVbenError } from '@/common';
import { AdminDept } from '../dept/admin-dept.entity';
import { AdminRole } from '../role/admin-role.entity';
import { AdminUser } from './admin-user.entity';
import type { AdminUserInput, AdminUserListQuery } from '../admin.types';
import type { AdminUserInput, AdminUserListQuery } from '../../contract/admin.types';
@Injectable()
export class AdminUserService {

View File

@ -1,26 +1,24 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { ComponentController } from '@/admin/component/component.controller';
import { Component } from '@/admin/component/component.entity';
import { ComponentService } from '@/admin/component/component.service';
import { DictController } from '@/admin/dict/dict.controller';
import { DictModule } from '@/admin/dict/dict.module';
import { AdminNoticeController } from '@/admin/notice/admin-notice.controller';
import { NoticeModule } from '@/admin/notice/notice.module';
import { SystemLogController } from '@/admin/system-log/system-log.controller';
import { SystemLogService } from '@/admin/system-log/system-log.service';
import { AdminTimezoneController } from '@/admin/timezone/admin-timezone.controller';
import { AdminTimezoneService } from '@/admin/timezone/admin-timezone.service';
import { AdminUser } from '@/admin/user/admin-user.entity';
import { AdminExampleController } from '@/admin/example/admin-example.controller';
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
import { ComponentController } from '@/modules/admin/platform-config/component/component.controller';
import { Component } from '@/modules/admin/platform-config/component/component.entity';
import { ComponentService } from '@/modules/admin/platform-config/component/component.service';
import { DictController } from '@/modules/admin/platform-config/dict/dict.controller';
import { DictModule } from '@/modules/admin/platform-config/dict/dict.module';
import { AdminNoticeController } from '@/modules/admin/platform-config/notice/admin-notice.controller';
import { NoticeModule } from '@/modules/admin/platform-config/notice/notice.module';
import { SystemLogController } from '@/modules/admin/platform-config/system-log/system-log.controller';
import { SystemLogService } from '@/modules/admin/platform-config/system-log/system-log.service';
import { AdminTimezoneController } from '@/modules/admin/platform-config/timezone/admin-timezone.controller';
import { AdminTimezoneService } from '@/modules/admin/platform-config/timezone/admin-timezone.service';
import { AdminUser } from '@/modules/admin/identity/user/admin-user.entity';
import { AssetModule } from '@/modules/asset/asset.module';
export const ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS = [
ComponentController,
SystemLogController,
AdminTimezoneController,
AdminExampleController,
];
export const ADMIN_PLATFORM_CONFIG_IMPORTED_CONTROLLERS = [

View File

@ -26,7 +26,7 @@ import {
type KtPageParams,
ToolsService,
} from '@/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
import { ComponentService } from './component.service';
import { Component } from './component.entity';

View File

@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
import { Component } from './component.entity';
import { ToolsService, type KtPage, type KtPageParams } from '@/common';
import { isNumber, omit, pick } from 'lodash';
import { DictService } from '@/admin/dict/dict.service';
import { DictService } from '@/modules/admin/platform-config/dict/dict.service';
@Injectable()
export class ComponentService {

View File

@ -28,7 +28,7 @@ import {
AdminDictUpdateDto,
DictDto,
} from './dict.dto';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
const componentTypeDictExample = [
{

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '../auth/admin-auth-guard.module';
import { AdminAuthGuardModule } from '../../identity/auth/admin-auth-guard.module';
import { DictController } from './dict.controller';
import { DictService } from './dict.service';
import { AdminDict } from './admin-dict.entity';

View File

@ -18,7 +18,7 @@ import type {
AdminDictItem,
AdminDictSerialized,
AdminDictTreeItem,
} from '../admin.types';
} from '../../contract/admin.types';
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';

View File

@ -1,8 +1,8 @@
import { Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { vbenPage, vbenSuccess } from '@/common';
import { AdminSuperGuard } from '../auth/admin-super.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AdminSuperGuard } from '../../identity/auth/admin-super.guard';
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
import { AdminNoticeQueryDto } from './admin-notice.dto';
import { AdminNoticeService } from './admin-notice.service';

View File

@ -4,7 +4,7 @@ import { SYSTEM_NOTICE_PUBLISHER } from '@/common';
import { AdminNoticeController } from './admin-notice.controller';
import { AdminNotice } from './admin-notice.entity';
import { AdminNoticeService } from './admin-notice.service';
import { AdminAuthGuardModule } from '../auth/admin-auth-guard.module';
import { AdminAuthGuardModule } from '../../identity/auth/admin-auth-guard.module';
@Global()
@Module({

View File

@ -7,7 +7,7 @@ import {
vbenPage,
vbenSuccess,
} from '@/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
import {
SystemLogDto,
SystemLogQueryDto,

View File

@ -1,8 +1,8 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CurrentAdminUser, vbenSuccess } from '@/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AdminUser } from '../user/admin-user.entity';
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
import { AdminUser } from '../../identity/user/admin-user.entity';
import { AdminTimezoneService } from './admin-timezone.service';
const TIMEZONE_OPTIONS = [

View File

@ -2,7 +2,7 @@ import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { throwVbenError } from '@/common';
import { AdminUser } from '../user/admin-user.entity';
import { AdminUser } from '../../identity/user/admin-user.entity';
@Injectable()
export class AdminTimezoneService {

View File

@ -5,7 +5,7 @@ import type {
MinioListObjectOptions,
MinioObjectResult,
MinioUploadObjectOptions,
} from './minio.types';
} from '../domain/asset-minio.types';
@Injectable()
export class MinioClientService {

View File

@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { MinioClientController } from '@/minio/minio.controller';
import { MinioClientModule } from '@/minio/minio.module';
import { MinioClientService } from '@/minio/minio.service';
import { ConfigModule } from '@nestjs/config';
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
import { MinioClientService } from './application/asset-minio.service';
import { MinioClientController } from './contract/asset-minio.controller';
export const ASSET_CONTROLLERS = [MinioClientController];
@ -41,7 +42,9 @@ export const ASSET_DOMAIN_CONTRACT = {
} as const;
@Module({
imports: [MinioClientModule],
exports: [MinioClientModule],
imports: [AdminAuthGuardModule, ConfigModule],
controllers: ASSET_CONTROLLERS,
providers: ASSET_PROVIDERS,
exports: ASSET_PROVIDERS,
})
export class AssetModule {}

View File

@ -21,8 +21,8 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Response } from 'express';
import { MinioClientService } from './minio.service';
import type { MinioUploadFile } from './minio.types';
import { MinioClientService } from '../application/asset-minio.service';
import type { MinioUploadFile } from '../domain/asset-minio.types';
import {
ApiFileDownloadResponse,
ApiArrayResponse,
@ -35,8 +35,8 @@ import {
MinioBucketStatusDto,
MinioObjectDto,
MinioUploadResultDto,
} from './minio.dto';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
} from './asset-minio.dto';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
const PROXY_RESOURCE_TIMEOUT = 1000 * 15;
const PROXY_RESOURCE_CONTENT_TYPES = [

View File

@ -2,19 +2,19 @@ import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Brackets, Repository } from 'typeorm';
import { MarkdownService, throwVbenError, ToolsService } from '@/common';
import { WordpressService } from '@/modules/wordpress/application/wordpress.service';
import {
BlogArticle,
type BlogArticleStatus,
type BlogArticleTerm,
} from './blog-article.entity';
} from '../infrastructure/persistence/blog-article.entity';
import type {
BlogArticleBodyDto,
BlogArticleImportWordpressDto,
BlogArticleListQueryDto,
BlogArticleTermOptionsQueryDto,
BlogArticleUpdateBodyDto,
} from './blog-article.dto';
import { WordpressService } from '@/wordpress/wordpress.service';
} from '../contract/blog-article.dto';
import { BlogTermService } from './blog-term.service';
@Injectable()

View File

@ -5,13 +5,16 @@ import { throwVbenError, ToolsService } from '@/common';
import {
BlogArticle,
type BlogArticleTerm,
} from './blog-article.entity';
import { BlogTerm, type BlogTermKind } from './blog-term.entity';
} from '../infrastructure/persistence/blog-article.entity';
import {
BlogTerm,
type BlogTermKind,
} from '../infrastructure/persistence/blog-term.entity';
import type {
BlogTermBodyDto,
BlogTermListQueryDto,
BlogTermUpdateBodyDto,
} from './blog-term.dto';
} from '../contract/blog-term.dto';
type CountedBlogTerm = BlogArticleTerm & {
count: number;

View File

@ -2,10 +2,10 @@ import { HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { throwVbenError, ToolsService } from '@/common';
import { WordpressService } from '@/wordpress/wordpress.service';
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
import { BlogThemeConfigBodyDto } from './blog-theme-config.dto';
import { BlogThemeConfig } from './blog-theme-config.entity';
import { WordpressService } from '@/modules/wordpress/application/wordpress.service';
import type { WordpressArgonThemeConfig } from '@/modules/wordpress/domain/wordpress.types';
import { BlogThemeConfigBodyDto } from '../contract/blog-theme-config.dto';
import { BlogThemeConfig } from '../infrastructure/persistence/blog-theme-config.entity';
const DEFAULT_THEME_ID = 'argon';

View File

@ -1,11 +1,17 @@
import { Module } from '@nestjs/common';
import { BlogArticleController } from '@/blog/blog-article.controller';
import { BlogArticleService } from '@/blog/blog-article.service';
import { BlogModule } from '@/blog/blog.module';
import { BlogTermController } from '@/blog/blog-term.controller';
import { BlogTermService } from '@/blog/blog-term.service';
import { BlogThemeConfigController } from '@/blog/blog-theme-config.controller';
import { BlogThemeConfigService } from '@/blog/blog-theme-config.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonModule } from '@/common';
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
import { BlogArticleService } from './application/blog-article.service';
import { BlogTermService } from './application/blog-term.service';
import { BlogThemeConfigService } from './application/blog-theme-config.service';
import { BlogArticleController } from './contract/blog-article.controller';
import { BlogTermController } from './contract/blog-term.controller';
import { BlogThemeConfigController } from './contract/blog-theme-config.controller';
import { BlogArticle } from './infrastructure/persistence/blog-article.entity';
import { BlogTerm } from './infrastructure/persistence/blog-term.entity';
import { BlogThemeConfig } from './infrastructure/persistence/blog-theme-config.entity';
export const BLOG_CONTENT_CONTROLLERS = [
BlogArticleController,
@ -60,7 +66,14 @@ export const BLOG_CONTENT_DOMAIN_CONTRACT = {
} as const;
@Module({
imports: [BlogModule],
exports: [BlogModule],
imports: [
AdminAuthGuardModule,
CommonModule,
WordpressMirrorModule,
TypeOrmModule.forFeature([BlogArticle, BlogTerm, BlogThemeConfig]),
],
controllers: BLOG_CONTENT_CONTROLLERS,
providers: BLOG_CONTENT_PROVIDERS,
exports: BLOG_CONTENT_PROVIDERS,
})
export class BlogContentModule {}

View File

@ -10,7 +10,7 @@ import {
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
import { Public, vbenSuccess } from '@/common';
import {
BlogArticleBodyDto,
@ -19,7 +19,7 @@ import {
BlogArticleTermOptionsQueryDto,
BlogArticleUpdateBodyDto,
} from './blog-article.dto';
import { BlogArticleService } from './blog-article.service';
import { BlogArticleService } from '../application/blog-article.service';
@ApiTags('Blog - 文章')
@Controller('blog/article')

View File

@ -1,5 +1,8 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import type { BlogArticleStatus, BlogArticleTerm } from './blog-article.entity';
import type {
BlogArticleStatus,
BlogArticleTerm,
} from '../infrastructure/persistence/blog-article.entity';
export class BlogArticleListQueryDto {
@ApiPropertyOptional({

View File

@ -10,15 +10,15 @@ import {
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
import { vbenSuccess } from '@/common';
import type { BlogTermKind } from './blog-term.entity';
import type { BlogTermKind } from '../infrastructure/persistence/blog-term.entity';
import {
BlogTermBodyDto,
BlogTermListQueryDto,
BlogTermUpdateBodyDto,
} from './blog-term.dto';
import { BlogTermService } from './blog-term.service';
import { BlogTermService } from '../application/blog-term.service';
@ApiTags('Blog - 分类标签')
@Controller('blog')

View File

@ -9,10 +9,10 @@ import {
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
import { Public, vbenSuccess } from '@/common';
import { BlogThemeConfigBodyDto } from './blog-theme-config.dto';
import { BlogThemeConfigService } from './blog-theme-config.service';
import { BlogThemeConfigService } from '../application/blog-theme-config.service';
@ApiTags('Blog - 主题')
@Controller('blog/theme')

View File

@ -1,5 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
import type { WordpressArgonThemeConfig } from '@/modules/wordpress/domain/wordpress.types';
export class BlogThemeConfigBodyDto {
@ApiPropertyOptional({

View File

@ -1,7 +1,7 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
import type { WordpressArgonThemeConfig } from '@/modules/wordpress/domain/wordpress.types';
@Entity('blog_theme_config')
export class BlogThemeConfig {

View File

@ -8,27 +8,27 @@ import {
throwVbenError,
ToolsService,
} from '@/common';
import { AdminPasswordCryptoService } from '@/admin/auth/admin-password-crypto.service';
import { QqbotAccountAbility } from './qqbot-account-ability.entity';
import { QqbotAccount } from './qqbot-account.entity';
import { AdminPasswordCryptoService } from '@/modules/admin/identity/auth/admin-password-crypto.service';
import { QqbotAccountAbility } from '../../infrastructure/persistence/account/qqbot-account-ability.entity';
import { QqbotAccount } from '../../infrastructure/persistence/account/qqbot-account.entity';
import type {
QqbotAccountBodyDto,
QqbotAccountQueryDto,
QqbotAccountUpdateDto,
} from './qqbot-account.dto';
import { QqbotAccountNapcat } from '../napcat/qqbot-account-napcat.entity';
import { QqbotNapcatContainer } from '../napcat/qqbot-napcat-container.entity';
import { QqbotNapcatContainerService } from '../napcat/qqbot-napcat-container.service';
} from '../../contract/account/qqbot-account.dto';
import { QqbotAccountNapcat } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-account-napcat.entity';
import { QqbotNapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-napcat-container.entity';
import { QqbotNapcatContainerService } from '@/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
import {
QQBOT_DEFAULT_PAGE_NO,
QQBOT_DEFAULT_PAGE_SIZE,
} from '../qqbot.constants';
} from '../../contract/qqbot.constants';
import type {
QqbotAccountAbilityType,
QqbotAccountListItem,
QqbotConnectionRole,
QqbotNapcatRuntimeStatusSnapshot,
} from '../qqbot.types';
} from '../../contract/qqbot.types';
const NAPCAT_RUNTIME_CHECK_TTL_MS = 30_000;
const NAPCAT_AUTO_LOGIN_CLEANUP_FAILED_MESSAGE =

View File

@ -1,10 +1,13 @@
import { Injectable, Logger } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ToolsService } from '@/common';
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
import type { QqbotNormalizedMessage } from '../qqbot.types';
import {
QQBOT_PLUGIN_EXECUTION_PORT,
type QqbotPluginExecutionPort,
} from '../../domain/plugin-execution.port';
import type { QqbotNormalizedMessage } from '../../contract/qqbot.types';
import { QqbotSendService } from '../send/qqbot-send.service';
import type { QqbotCommandTestDto } from './qqbot-command.dto';
import type { QqbotCommand } from './qqbot-command.entity';
import type { QqbotCommandTestDto } from '../../contract/command/qqbot-command.dto';
import type { QqbotCommand } from '../../infrastructure/persistence/command/qqbot-command.entity';
import { QqbotCommandParserService } from './qqbot-command-parser.service';
import { QqbotCommandService } from './qqbot-command.service';
import { QqbotReplyTemplateService } from './qqbot-reply-template.service';
@ -16,7 +19,8 @@ export class QqbotCommandEngineService {
constructor(
private readonly commandParser: QqbotCommandParserService,
private readonly commandService: QqbotCommandService,
private readonly pluginRegistry: QqbotPluginRegistryService,
@Inject(QQBOT_PLUGIN_EXECUTION_PORT)
private readonly pluginExecution: QqbotPluginExecutionPort,
private readonly replyTemplate: QqbotReplyTemplateService,
private readonly sendService: QqbotSendService,
private readonly toolsService: ToolsService,
@ -32,16 +36,16 @@ export class QqbotCommandEngineService {
await this.commandService.markHit(command);
const input = this.mergeInput(command, matched.input);
try {
const output = await this.pluginRegistry.execute(
command.pluginKey,
command.operationKey,
input,
{
const output = await this.pluginExecution.executeOperation({
context: {
args: matched.input,
command,
message,
},
);
input,
operationKey: command.operationKey,
pluginKey: command.pluginKey,
});
const replyText = this.buildReplyText(command, input, output);
if (replyText) {
await this.sendService.sendText({
@ -97,16 +101,16 @@ export class QqbotCommandEngineService {
const input = this.mergeInput(command, matched.input);
try {
const output = await this.pluginRegistry.execute(
command.pluginKey,
command.operationKey,
input,
{
const output = await this.pluginExecution.executeOperation({
context: {
args: matched.input,
command,
message,
},
);
input,
operationKey: command.operationKey,
pluginKey: command.pluginKey,
});
const replyText = this.buildReplyText(command, input, output);
return {
command: this.commandService.toResponse(command),

View File

@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';
import type { QqbotCommandMatchResult } from '../../contract/qqbot.types';
import type { QqbotNormalizedMessage } from '../../contract/qqbot.types';
import type { QqbotCommand } from '../../infrastructure/persistence/command/qqbot-command.entity';
@Injectable()
export class QqbotCommandParserService {
async match(command: QqbotCommand, message: QqbotNormalizedMessage) {
const source = `${message.messageText || ''}`.trim();
if (!source) return null;
const aliases = this.getAliases(command);
const prefixes = this.getPrefixes(command);
for (const alias of aliases) {
for (const prefix of prefixes) {
const commandText = `${prefix}${alias}`.trim();
const rawArgs = this.pickArgs(source, commandText);
if (rawArgs === null) continue;
return {
alias,
input: this.parseRawInput(rawArgs),
matched: true,
rawArgs,
} satisfies QqbotCommandMatchResult;
}
}
return null;
}
getAliases(command: QqbotCommand) {
return this.normalizeList(command.aliases, [command.code, command.name]);
}
getPrefixes(command: QqbotCommand) {
return this.normalizeList(command.prefixes, ['/', '!', '']);
}
private pickArgs(source: string, commandText: string) {
if (!commandText) return null;
if (source === commandText) return '';
if (source.startsWith(`${commandText} `)) {
return source.slice(commandText.length).trim();
}
return null;
}
private parseRawInput(rawArgs: string) {
const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
return {
args,
raw: rawArgs,
text: rawArgs,
};
}
private normalizeList(value: string | undefined, fallback: string[]) {
const raw = `${value || ''}`.trim();
const parsed = this.tryParseJsonArray(raw);
const source = parsed.length > 0 ? parsed : raw.split(',');
const list = [...source, ...fallback]
.map((item) => `${item || ''}`.trim())
.filter(Boolean);
return [...new Set(list)];
}
private tryParseJsonArray(value: string) {
if (!value.startsWith('[')) return [];
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
}

View File

@ -1,27 +1,30 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Not, Repository } from 'typeorm';
import { throwVbenError, ToolsService } from '@/common';
import { QqbotAccountService } from '../account/qqbot-account.service';
import { QqbotPluginRegistryService } from '../plugin/qqbot-plugin-registry.service';
import {
QQBOT_PLUGIN_EXECUTION_PORT,
type QqbotPluginExecutionPort,
} from '../../domain/plugin-execution.port';
import {
QQBOT_DEFAULT_PAGE_NO,
QQBOT_DEFAULT_PAGE_SIZE,
} from '../qqbot.constants';
} from '../../contract/qqbot.constants';
import type {
QqbotCommandParserType,
QqbotNormalizedMessage,
QqbotRuleTargetType,
} from '../qqbot.types';
} from '../../contract/qqbot.types';
import type {
QqbotCommandBodyDto,
QqbotCommandQueryDto,
QqbotCommandUpdateDto,
} from './qqbot-command.dto';
import { isWithinCooldown } from '../qqbot-cooldown.policy';
import { QqbotCommandLog } from './qqbot-command-log.entity';
import { QqbotCommand } from './qqbot-command.entity';
} from '../../contract/command/qqbot-command.dto';
import { isWithinCooldown } from '../../domain/qqbot-cooldown.policy';
import { QqbotCommandLog } from '../../infrastructure/persistence/command/qqbot-command-log.entity';
import { QqbotCommand } from '../../infrastructure/persistence/command/qqbot-command.entity';
@Injectable()
export class QqbotCommandService {
@ -31,7 +34,8 @@ export class QqbotCommandService {
@InjectRepository(QqbotCommandLog)
private readonly commandLogRepository: Repository<QqbotCommandLog>,
private readonly accountService: QqbotAccountService,
private readonly pluginRegistry: QqbotPluginRegistryService,
@Inject(QQBOT_PLUGIN_EXECUTION_PORT)
private readonly pluginExecution: QqbotPluginExecutionPort,
private readonly toolsService: ToolsService,
private readonly configService: ConfigService,
) {}
@ -216,7 +220,7 @@ export class QqbotCommandService {
const operationKey = `${body.operationKey || ''}`.trim();
if (!code) throwVbenError('命令编码不能为空');
if (!body.name?.trim()) throwVbenError('命令名称不能为空');
this.pluginRegistry.assertOperation(pluginKey, operationKey);
await this.assertPluginOperation(pluginKey, operationKey);
return {
aliases: this.stringifyList(body.aliases),
@ -248,6 +252,22 @@ export class QqbotCommandService {
if (existed) throwVbenError(`命令编码已存在:${code}`);
}
private async assertPluginOperation(
pluginKey?: string,
operationKey?: string,
) {
if (!pluginKey || !operationKey) {
throwVbenError('请选择插件和插件能力');
}
const operation = await this.pluginExecution.getOperationByCommand({
operationKey,
pluginKey,
});
if (!operation) {
throwVbenError(`QQBot 插件能力不存在:${pluginKey}.${operationKey}`);
}
}
private stringifyList(value: string[] | string | undefined, fallback = []) {
const list = Array.isArray(value)
? value

View File

@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { QqbotConfig } from './qqbot-config.entity';
import type { QqbotPermissionConfig } from '../qqbot.types';
import { QqbotConfig } from '../../infrastructure/persistence/config/qqbot-config.entity';
import type { QqbotPermissionConfig } from '../../contract/qqbot.types';
const QQBOT_PERMISSION_CONFIG_KEYS = {
allowlistEnabled: 'permission.allowlistEnabled',

View File

@ -1,13 +1,13 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { QqbotAccount } from '../account/qqbot-account.entity';
import { QqbotConversation } from '../message/qqbot-conversation.entity';
import { QqbotMessage } from '../message/qqbot-message.entity';
import { QqbotBusService } from '../mqtt/qqbot-bus.service';
import { QqbotReverseWsService } from '../connection/qqbot-reverse-ws.service';
import { QqbotRule } from '../rule/qqbot-rule.entity';
import { QqbotSendLog } from '../send/qqbot-send-log.entity';
import { QqbotBusService } from '../../infrastructure/integration/bus/qqbot-bus.service';
import { QqbotReverseWsService } from '../../infrastructure/integration/connection/qqbot-reverse-ws.service';
import { QqbotAccount } from '../../infrastructure/persistence/account/qqbot-account.entity';
import { QqbotConversation } from '../../infrastructure/persistence/message/qqbot-conversation.entity';
import { QqbotMessage } from '../../infrastructure/persistence/message/qqbot-message.entity';
import { QqbotRule } from '../../infrastructure/persistence/rule/qqbot-rule.entity';
import { QqbotSendLog } from '../../infrastructure/persistence/send/qqbot-send-log.entity';
@Injectable()
export class QqbotDashboardService {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { QqbotDedupe } from './qqbot-dedupe.entity';
import { QqbotDedupe } from '../../infrastructure/persistence/dedupe/qqbot-dedupe.entity';
@Injectable()
export class QqbotDedupeService {

View File

@ -4,17 +4,17 @@ import {
SystemNoticePublisher,
ToolsService,
} from '@/common';
import { QQBOT_MQTT_TOPICS } from '../qqbot.constants';
import { QQBOT_MQTT_TOPICS } from '../../contract/qqbot.constants';
import { QqbotDedupeService } from '../dedupe/qqbot-dedupe.service';
import { QqbotMessageService } from '../message/qqbot-message.service';
import { QqbotBusService } from '../mqtt/qqbot-bus.service';
import type { QqbotOneBotEvent } from '../qqbot.types';
import { QqbotBusService } from '../../infrastructure/integration/bus/qqbot-bus.service';
import type { QqbotOneBotEvent } from '../../contract/qqbot.types';
import {
buildDedupeKey,
getOneBotOfflineReason,
isOneBotMessageEvent,
normalizeOneBotMessage,
} from './qqbot-event-normalizer';
} from '../../domain/event/qqbot-event-normalizer';
import { QqbotRuleEngineService } from '../rule/qqbot-rule-engine.service';
import { QqbotAccountService } from '../account/qqbot-account.service';

View File

@ -2,17 +2,20 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ToolsService } from '@/common';
import { QqbotConversation } from './qqbot-conversation.entity';
import { QqbotMessage } from './qqbot-message.entity';
import { QqbotConversation } from '../../infrastructure/persistence/message/qqbot-conversation.entity';
import { QqbotMessage } from '../../infrastructure/persistence/message/qqbot-message.entity';
import type {
QqbotConversationQueryDto,
QqbotMessageQueryDto,
} from './qqbot-message.dto';
import type { QqbotMessageType, QqbotNormalizedMessage } from '../qqbot.types';
} from '../../contract/message/qqbot-message.dto';
import type {
QqbotMessageType,
QqbotNormalizedMessage,
} from '../../contract/qqbot.types';
import {
QQBOT_DEFAULT_PAGE_NO,
QQBOT_DEFAULT_PAGE_SIZE,
} from '../qqbot.constants';
} from '../../contract/qqbot.constants';
@Injectable()
export class QqbotMessageService {

View File

@ -2,24 +2,24 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Brackets, Repository } from 'typeorm';
import { throwVbenError, ToolsService } from '@/common';
import { QqbotAllowlist } from './qqbot-allowlist.entity';
import { QqbotBlocklist } from './qqbot-blocklist.entity';
import { QqbotAllowlist } from '../../infrastructure/persistence/permission/qqbot-allowlist.entity';
import { QqbotBlocklist } from '../../infrastructure/persistence/permission/qqbot-blocklist.entity';
import type {
QqbotPermissionBodyDto,
QqbotPermissionConfigDto,
QqbotPermissionQueryDto,
QqbotPermissionUpdateDto,
} from './qqbot-permission.dto';
} from '../../contract/permission/qqbot-permission.dto';
import { QqbotConfigService } from '../config/qqbot-config.service';
import {
QQBOT_DEFAULT_PAGE_NO,
QQBOT_DEFAULT_PAGE_SIZE,
} from '../qqbot.constants';
} from '../../contract/qqbot.constants';
import type {
QqbotNormalizedMessage,
QqbotPermissionEntity,
QqbotPermissionKind,
} from '../qqbot.types';
} from '../../contract/qqbot.types';
@Injectable()
export class QqbotPermissionService {

View File

@ -1,9 +1,12 @@
import { Injectable, Logger } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ToolsService } from '@/common';
import type { QqbotNormalizedMessage } from '../qqbot.types';
import {
QQBOT_PLUGIN_EXECUTION_PORT,
type QqbotPluginExecutionPort,
} from '../../domain/plugin-execution.port';
import type { QqbotNormalizedMessage } from '../../contract/qqbot.types';
import { QqbotCommandEngineService } from '../command/qqbot-command-engine.service';
import { QqbotPermissionService } from '../permission/qqbot-permission.service';
import { QqbotRepeaterPluginService } from '@/modules/qqbot/plugins/repeater/qqbot-repeater.plugin';
import { QqbotSendService } from '../send/qqbot-send.service';
import { QqbotRuleService } from './qqbot-rule.service';
@ -14,7 +17,8 @@ export class QqbotRuleEngineService {
constructor(
private readonly commandEngineService: QqbotCommandEngineService,
private readonly permissionService: QqbotPermissionService,
private readonly repeaterPluginService: QqbotRepeaterPluginService,
@Inject(QQBOT_PLUGIN_EXECUTION_PORT)
private readonly pluginExecution: QqbotPluginExecutionPort,
private readonly ruleService: QqbotRuleService,
private readonly sendService: QqbotSendService,
private readonly toolsService: ToolsService,
@ -49,6 +53,9 @@ export class QqbotRuleEngineService {
return;
}
await this.repeaterPluginService.handleMessage(message);
await this.pluginExecution.dispatchEvent({
eventKey: 'message',
message,
});
}
}

View File

@ -4,22 +4,22 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { throwVbenError, ToolsService } from '@/common';
import { QqbotAccountService } from '../account/qqbot-account.service';
import { QqbotRule } from './qqbot-rule.entity';
import { QqbotRule } from '../../infrastructure/persistence/rule/qqbot-rule.entity';
import type {
QqbotRuleBodyDto,
QqbotRuleQueryDto,
QqbotRuleUpdateDto,
} from './qqbot-rule.dto';
} from '../../contract/rule/qqbot-rule.dto';
import type {
QqbotNormalizedMessage,
QqbotRuleMatchType,
QqbotRuleTargetType,
} from '../qqbot.types';
} from '../../contract/qqbot.types';
import {
QQBOT_DEFAULT_PAGE_NO,
QQBOT_DEFAULT_PAGE_SIZE,
} from '../qqbot.constants';
import { isWithinCooldown } from '../qqbot-cooldown.policy';
} from '../../contract/qqbot.constants';
import { isWithinCooldown } from '../../domain/qqbot-cooldown.policy';
@Injectable()
export class QqbotRuleService {

View File

@ -4,24 +4,24 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { throwVbenError, ToolsService } from '@/common';
import { QqbotAccountService } from '../account/qqbot-account.service';
import { QQBOT_MQTT_TOPICS } from '../qqbot.constants';
import { QqbotBusService } from '../mqtt/qqbot-bus.service';
import { QQBOT_MQTT_TOPICS } from '../../contract/qqbot.constants';
import { QqbotBusService } from '../../infrastructure/integration/bus/qqbot-bus.service';
import { QqbotMessageService } from '../message/qqbot-message.service';
import type {
QqbotMessageType,
QqbotReverseActionSender,
} from '../qqbot.types';
} from '../../contract/qqbot.types';
import {
QQBOT_DEFAULT_PAGE_NO,
QQBOT_DEFAULT_PAGE_SIZE,
} from '../qqbot.constants';
} from '../../contract/qqbot.constants';
import { QqbotRateLimitService } from './qqbot-rate-limit.service';
import { QqbotSendLog } from './qqbot-send-log.entity';
import { QqbotSendLog } from '../../infrastructure/persistence/send/qqbot-send-log.entity';
import type {
QqbotSendGroupDto,
QqbotSendLogQueryDto,
QqbotSendPrivateDto,
} from './qqbot-send.dto';
} from '../../contract/send/qqbot-send.dto';
@Injectable()
export class QqbotSendService {
@ -186,7 +186,7 @@ export class QqbotSendService {
private async getReverseWsService(): Promise<QqbotReverseActionSender> {
const { QqbotReverseWsService } =
await import('../connection/qqbot-reverse-ws.service');
await import('../../infrastructure/integration/connection/qqbot-reverse-ws.service');
return this.moduleRef.get<QqbotReverseActionSender>(QqbotReverseWsService, {
strict: false,
});

View File

@ -10,7 +10,7 @@ import {
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
import { vbenSuccess } from '@/common';
import {
QqbotAccountBodyDto,
@ -19,9 +19,9 @@ import {
QqbotAccountScanStatusDto,
QqbotAccountUpdateDto,
} from './qqbot-account.dto';
import { QqbotAccountService } from './qqbot-account.service';
import { QqbotNapcatLoginService } from './qqbot-napcat-login.service';
import { QqbotReverseWsService } from '../connection/qqbot-reverse-ws.service';
import { QqbotAccountService } from '../../application/account/qqbot-account.service';
import { QqbotNapcatLoginService } from '@/modules/qqbot/napcat/application/login/qqbot-napcat-login.service';
import { QqbotReverseWsService } from '../../infrastructure/integration/connection/qqbot-reverse-ws.service';
@ApiTags('QQBot - 账号连接')
@Controller('qqbot/account')

View File

@ -9,7 +9,7 @@ import {
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
import { ToolsService, vbenSuccess } from '@/common';
import {
QqbotCommandBodyDto,
@ -17,8 +17,8 @@ import {
QqbotCommandTestDto,
QqbotCommandUpdateDto,
} from './qqbot-command.dto';
import { QqbotCommandEngineService } from './qqbot-command-engine.service';
import { QqbotCommandService } from './qqbot-command.service';
import { QqbotCommandEngineService } from '../../application/command/qqbot-command-engine.service';
import { QqbotCommandService } from '../../application/command/qqbot-command.service';
@ApiTags('QQBot - 在线命令')
@Controller('qqbot/command')

Some files were not shown because too many files have changed in this diff Show More