Compare commits
No commits in common. "7edbb97e70412b4e5b56c787393279c00090f8db" and "01a7e79bd17a7550c6984be22b4b242abe10da01" have entirely different histories.
7edbb97e70
...
01a7e79bd1
10
API.md
10
API.md
@ -449,7 +449,7 @@ pnpm qqbot-plugin install-local <packageFile>
|
|||||||
|
|
||||||
### BangDream
|
### BangDream
|
||||||
|
|
||||||
插件 key:`bangdream`。旧 `bangDream` 作为兼容别名仍可解析;当前源码根目录为 `src/modules/qqbot/plugins/bangdream/src`,按第三期插件结构拆分为 `operations`、`domain/*`、`application`、`infrastructure/integration`、`infrastructure/storage`、`config`、`assets` 和 `theme`,不再使用旧 `tsugu` 子目录、宿主 builtins 包装层或纯转接目录。
|
插件 key:`bangdream`。旧 `bangDream` 作为兼容别名仍可解析;当前源码根目录为 `src/modules/qqbot/plugins/bangDream`,不再使用旧 `tsugu` 子目录。
|
||||||
|
|
||||||
| operation key | 命令 | 说明 |
|
| operation key | 命令 | 说明 |
|
||||||
| ----------------------------- | ------------- | ------------------------ |
|
| ----------------------------- | ------------- | ------------------------ |
|
||||||
@ -469,11 +469,11 @@ pnpm qqbot-plugin install-local <packageFile>
|
|||||||
| `bangdream.cutoff.all` | `/ycxall` | 全档位预测线 |
|
| `bangdream.cutoff.all` | `/ycxall` | 全档位预测线 |
|
||||||
| `bangdream.cutoff.recent` | `/lsycx` | 历史/近期档线 |
|
| `bangdream.cutoff.recent` | `/lsycx` | 历史/近期档线 |
|
||||||
|
|
||||||
`plugins/bangdream/plugin.json` 是 BangDream operation、handlerName、别名、权限、超时和说明的单一来源。新增或调整命令必须同步在线命令 SQL,并跑 manifest/command-SQL 测试。
|
`registry/operation-registry.ts` 是 BangDream operation、handlerName、别名、冷却和说明的单一来源。新增或调整命令必须同步在线命令 SQL,并跑 registry/command-SQL 测试。
|
||||||
|
|
||||||
### FF14 Market
|
### FF14 Market
|
||||||
|
|
||||||
插件 key:`ff14-market`。旧 `ff14Market` 作为兼容别名仍可解析;源码按第三期插件结构拆分为 `operations`、`application`、`domain`、`infrastructure/integration` 和 `config`。
|
插件 key:`ff14-market`。旧 `ff14Market` 作为兼容别名仍可解析。
|
||||||
|
|
||||||
| operation key | 说明 |
|
| operation key | 说明 |
|
||||||
| ------------------- | ------------------------------------------ |
|
| ------------------- | ------------------------------------------ |
|
||||||
@ -486,8 +486,6 @@ pnpm qqbot-plugin install-local <packageFile>
|
|||||||
|
|
||||||
插件 key:`fflogs`。
|
插件 key:`fflogs`。
|
||||||
|
|
||||||
源码按第三期插件结构拆分为 `operations`、`application`、`domain`、`infrastructure/integration`、`infrastructure/storage` 和 `config`。
|
|
||||||
|
|
||||||
| operation key | 说明 |
|
| operation key | 说明 |
|
||||||
| -------------------------- | --------------------------------------------------------------- |
|
| -------------------------- | --------------------------------------------------------------- |
|
||||||
| `fflogs.character.summary` | 查询 FFLogs 角色公开排名;传 `encounter` 时查询指定高难最近记录 |
|
| `fflogs.character.summary` | 查询 FFLogs 角色公开排名;传 `encounter` 时查询指定高难最近记录 |
|
||||||
@ -529,7 +527,7 @@ BangDream 图片 smoke:
|
|||||||
|
|
||||||
```powershell
|
```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.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
|
.\scripts\bangdream-render-smoke.ps1 -OperationKey bangdream.event.stage -Text "310" -OutFile ".kt-workspace/bangdream-smoke/stage.jpg"
|
||||||
```
|
```
|
||||||
|
|
||||||
Jenkins/K8s 发布后还需要观察 rollout、新 Pod 日志,并跑真实运行态 smoke;推送成功不等于发布完成。
|
Jenkins/K8s 发布后还需要观察 rollout、新 Pod 日志,并跑真实运行态 smoke;推送成功不等于发布完成。
|
||||||
|
|||||||
11
README.md
11
README.md
@ -23,8 +23,8 @@
|
|||||||
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
||||||
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志和插件平台 |
|
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志和插件平台 |
|
||||||
| `modules/qqbot/plugin-platform` | QQBot 插件 manifest 校验、版本安装、运行事件、受控 SDK 和 CLI 脚手架 |
|
| `modules/qqbot/plugin-platform` | QQBot 插件 manifest 校验、版本安装、运行事件、受控 SDK 和 CLI 脚手架 |
|
||||||
| `qqbot/plugins/bangdream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
|
| `qqbot/plugins/bangDream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
|
||||||
| `qqbot/plugins/ff14-market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
|
| `qqbot/plugins/ff14Market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
|
||||||
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
|
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
|
||||||
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
|
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
|
||||||
| `common` | 响应封装、异常过滤、请求日志、日期格式化、字典解码、Snowflake、工具服务 |
|
| `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 设备身份按账号持久化到 `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 新设备验证走同一 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` 站内信告警;看门狗不自动进入扫码阶段。
|
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000`,`QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,使掉线/被踢无需管理员打开列表页即可及时发现;检测到离线后先尝试 `ACCOUNT` 历史会话快速登录,再尝试账号保存的登录密码,仍失败时写入离线原因并复用 `super` 站内信告警;看门狗不自动进入扫码阶段。
|
||||||
- BangDream 当前源码根目录是 `src/modules/qqbot/plugins/bangdream/src`;按第三期插件结构放置真实职责代码:业务在 `domain/*`,编排在 `application`,操作在 `operations`,外部 API 在 `infrastructure/integration`,缓存/静态修正在 `infrastructure/storage`,字典和静态配置在 `config`,视觉渲染公共件在 `theme`;不要恢复旧 `tsugu` 层级、旧大桶目录、纯 re-export 转接文件或空 `.gitkeep` 目录壳。
|
- BangDream 当前源码根目录是 `src/modules/qqbot/plugins/bangDream`;不要恢复旧 `tsugu` 层级或旧大桶目录。
|
||||||
- BangDream 在线命令以 `plugins/bangdream/plugin.json` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 manifest/command-SQL 测试。
|
- BangDream 在线命令以 `registry/operation-registry.ts` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 registry/command-SQL 测试。
|
||||||
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
|
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
|
||||||
|
|
||||||
## 轻量验证
|
## 轻量验证
|
||||||
@ -184,7 +184,6 @@ BangDream 图片能力改动:
|
|||||||
|
|
||||||
```powershell
|
```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.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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
接口改动必须启动或复用本地服务,并真实调用一次对应接口。
|
接口改动必须启动或复用本地服务,并真实调用一次对应接口。
|
||||||
@ -197,4 +196,4 @@ BangDream 图片能力改动:
|
|||||||
|
|
||||||
| 一级来源 | 使用方式 | License |
|
| 一级来源 | 使用方式 | License |
|
||||||
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
||||||
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/modules/qqbot/plugins/bangdream/src`,保留本地 `TSUGU-LICENSE` | MIT |
|
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/modules/qqbot/plugins/bangDream`,保留本地 `TSUGU-LICENSE` | MIT |
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
# 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
@ -1191,13 +1191,13 @@ git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加QQBot插件管
|
|||||||
|
|
||||||
**Files:**
|
**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\plugin.json`
|
||||||
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangdream\**`
|
- 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\**`
|
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangDream\**`
|
||||||
|
|
||||||
- [x] **Step 1: Preserve business directories**
|
- [x] **Step 1: Preserve business directories**
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
- [x] **Step 2: Convert operation registry to manifest metadata**
|
- [x] **Step 2: Convert operation registry to manifest metadata**
|
||||||
|
|
||||||
@ -1208,22 +1208,22 @@ Keep operation keys and aliases stable. `handlerName` becomes worker-internal.
|
|||||||
Run:
|
Run:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
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 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 run typecheck
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PASS. Event stage smoke keeps `imageCount=5`.
|
Expected: PASS. Event stage smoke keeps `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`.
|
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`.
|
||||||
|
|
||||||
### Task 6.2: Rewrite FF14 Market, FFLogs, and Repeater
|
### Task 6.2: Rewrite FF14 Market, FFLogs, and Repeater
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|
||||||
- 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\ff14Market\**`
|
||||||
- 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\fflogs\**`
|
||||||
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\repeater\**`
|
- 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\ff14-market\**`
|
- 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\fflogs\**`
|
- 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\**`
|
- 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:
|
Run:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
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 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 run typecheck
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PASS.
|
Expected: PASS.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### Task 6.3: Commit Batch 6
|
### Task 6.3: Commit Batch 6
|
||||||
|
|
||||||
|
|||||||
@ -1,949 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -1,365 +0,0 @@
|
|||||||
# 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/**`:
|
|
||||||
|
|
||||||
- identity:auth、user、role、menu、dept、guard、JWT。
|
|
||||||
- platform-config:dict、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 门禁。
|
|
||||||
@ -1,394 +0,0 @@
|
|||||||
# 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 adapter:send 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 core:account、command、rule、message、send log、dashboard。
|
|
||||||
- Plugin Platform:capability 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 Jest:core、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 smoke:QQBot 插件平台页和账号登录页。
|
|
||||||
|
|
||||||
线上闭环仍按第三期最终流程执行:提交、推送、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 通过,未验证项必须明确列出。
|
|
||||||
@ -5,19 +5,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"include": "modules/qqbot/plugins/*/plugin.json",
|
"include": "modules/qqbot/plugins/bangDream/assets/**/*",
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "modules/qqbot/plugins/bangdream/TSUGU-LICENSE",
|
"include": "modules/qqbot/plugins/bangDream/static-config/**/*",
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "modules/qqbot/plugins/bangdream/src/assets/**/*",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"include": "modules/qqbot/plugins/bangdream/src/config/static/**/*",
|
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -54,7 +54,6 @@
|
|||||||
"pino": "^10.3.1",
|
"pino": "^10.3.1",
|
||||||
"pino-http": "^11.0.0",
|
"pino-http": "^11.0.0",
|
||||||
"pino-loki": "^3.0.0",
|
"pino-loki": "^3.0.0",
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rehype-parse": "^9.0.1",
|
"rehype-parse": "^9.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
@ -82,7 +81,6 @@
|
|||||||
"@types/jest": "29.2.4",
|
"@types/jest": "29.2.4",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/node": "22.19.19",
|
"@types/node": "22.19.19",
|
||||||
"@types/qrcode": "^1.5.6",
|
|
||||||
"@types/supertest": "^2.0.16",
|
"@types/supertest": "^2.0.16",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||||
|
|||||||
@ -71,9 +71,6 @@ importers:
|
|||||||
pino-loki:
|
pino-loki:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
qrcode:
|
|
||||||
specifier: ^1.5.4
|
|
||||||
version: 1.5.4
|
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@ -150,9 +147,6 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 22.19.19
|
specifier: 22.19.19
|
||||||
version: 22.19.19
|
version: 22.19.19
|
||||||
'@types/qrcode':
|
|
||||||
specifier: ^1.5.6
|
|
||||||
version: 1.5.6
|
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^2.0.16
|
specifier: ^2.0.16
|
||||||
version: 2.0.16
|
version: 2.0.16
|
||||||
@ -1212,9 +1206,6 @@ packages:
|
|||||||
'@types/node@22.19.19':
|
'@types/node@22.19.19':
|
||||||
resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==}
|
resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==}
|
||||||
|
|
||||||
'@types/qrcode@1.5.6':
|
|
||||||
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
|
|
||||||
|
|
||||||
'@types/qs@6.15.1':
|
'@types/qs@6.15.1':
|
||||||
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
|
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
|
||||||
|
|
||||||
@ -1766,9 +1757,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
cliui@6.0.0:
|
|
||||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -1916,10 +1904,6 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
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:
|
decode-named-character-reference@1.3.0:
|
||||||
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||||
|
|
||||||
@ -1994,9 +1978,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
|
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
dijkstrajs@1.0.3:
|
|
||||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
|
||||||
|
|
||||||
doctrine@3.0.0:
|
doctrine@3.0.0:
|
||||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@ -3512,10 +3493,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
||||||
engines: {node: '>=4.0.0'}
|
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:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -3572,11 +3549,6 @@ packages:
|
|||||||
pure-rand@6.1.0:
|
pure-rand@6.1.0:
|
||||||
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
||||||
|
|
||||||
qrcode@1.5.4:
|
|
||||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
|
||||||
engines: {node: '>=10.13.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
qs@6.15.2:
|
qs@6.15.2:
|
||||||
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
|
resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@ -3672,9 +3644,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
require-main-filename@2.0.0:
|
|
||||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
|
||||||
|
|
||||||
requireindex@1.2.0:
|
requireindex@1.2.0:
|
||||||
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
||||||
engines: {node: '>=0.10.5'}
|
engines: {node: '>=0.10.5'}
|
||||||
@ -4423,9 +4392,6 @@ packages:
|
|||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
which-module@2.0.1:
|
|
||||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
|
||||||
|
|
||||||
which-typed-array@1.1.21:
|
which-typed-array@1.1.21:
|
||||||
resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==}
|
resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -4522,9 +4488,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
y18n@4.0.3:
|
|
||||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -4535,18 +4498,10 @@ packages:
|
|||||||
yallist@4.0.0:
|
yallist@4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
|
||||||
yargs-parser@18.1.3:
|
|
||||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
yargs-parser@21.1.1:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yargs@15.4.1:
|
|
||||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -5803,10 +5758,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/qrcode@1.5.6':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 22.19.19
|
|
||||||
|
|
||||||
'@types/qs@6.15.1': {}
|
'@types/qs@6.15.1': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
@ -6426,12 +6377,6 @@ snapshots:
|
|||||||
|
|
||||||
cli-width@4.1.0: {}
|
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:
|
cliui@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@ -6553,8 +6498,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
decamelize@1.2.0: {}
|
|
||||||
|
|
||||||
decode-named-character-reference@1.3.0:
|
decode-named-character-reference@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
@ -6608,8 +6551,6 @@ snapshots:
|
|||||||
|
|
||||||
diff@4.0.4: {}
|
diff@4.0.4: {}
|
||||||
|
|
||||||
dijkstrajs@1.0.3: {}
|
|
||||||
|
|
||||||
doctrine@3.0.0:
|
doctrine@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
@ -8627,8 +8568,6 @@ snapshots:
|
|||||||
|
|
||||||
pngjs@3.4.0: {}
|
pngjs@3.4.0: {}
|
||||||
|
|
||||||
pngjs@5.0.0: {}
|
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
@ -8674,12 +8613,6 @@ snapshots:
|
|||||||
|
|
||||||
pure-rand@6.1.0: {}
|
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:
|
qs@6.15.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
@ -8810,8 +8743,6 @@ snapshots:
|
|||||||
|
|
||||||
require-from-string@2.0.2: {}
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
require-main-filename@2.0.0: {}
|
|
||||||
|
|
||||||
requireindex@1.2.0: {}
|
requireindex@1.2.0: {}
|
||||||
|
|
||||||
resolve-cwd@3.0.0:
|
resolve-cwd@3.0.0:
|
||||||
@ -9565,8 +9496,6 @@ snapshots:
|
|||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
webidl-conversions: 3.0.1
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
which-module@2.0.1: {}
|
|
||||||
|
|
||||||
which-typed-array@1.1.21:
|
which-typed-array@1.1.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
available-typed-arrays: 1.0.7
|
available-typed-arrays: 1.0.7
|
||||||
@ -9677,35 +9606,14 @@ snapshots:
|
|||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@4.0.3: {}
|
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yallist@4.0.0: {}
|
yallist@4.0.0: {}
|
||||||
|
|
||||||
yargs-parser@18.1.3:
|
|
||||||
dependencies:
|
|
||||||
camelcase: 5.3.1
|
|
||||||
decamelize: 1.2.0
|
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
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:
|
yargs@17.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 8.0.1
|
cliui: 8.0.1
|
||||||
|
|||||||
@ -4,7 +4,6 @@ param(
|
|||||||
[string]$DisplayedServerList = "cn jp",
|
[string]$DisplayedServerList = "cn jp",
|
||||||
[string]$OutFile = ".kt-workspace/bangdream-smoke/bangdream-smoke.jpg",
|
[string]$OutFile = ".kt-workspace/bangdream-smoke/bangdream-smoke.jpg",
|
||||||
[int]$TimeoutSeconds = 45,
|
[int]$TimeoutSeconds = 45,
|
||||||
[int]$ExpectedImageCount = 0,
|
|
||||||
[switch]$UseEasyBg
|
[switch]$UseEasyBg
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +38,6 @@ $Payload = @{
|
|||||||
}
|
}
|
||||||
operationKey = $OperationKey
|
operationKey = $OperationKey
|
||||||
outFile = $ResolvedOutFile
|
outFile = $ResolvedOutFile
|
||||||
expectedImageCount = $ExpectedImageCount
|
|
||||||
}
|
}
|
||||||
$PayloadJson = $Payload | ConvertTo-Json -Compress -Depth 5
|
$PayloadJson = $Payload | ConvertTo-Json -Compress -Depth 5
|
||||||
$PayloadBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($PayloadJson))
|
$PayloadBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($PayloadJson))
|
||||||
@ -47,96 +45,17 @@ $PayloadBase64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Paylo
|
|||||||
$NodeCode = @"
|
$NodeCode = @"
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const XLSX = require("xlsx");
|
const { ConfigService } = require("@nestjs/config");
|
||||||
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
|
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
|
||||||
const { createPlugin } = require("./src/modules/qqbot/plugins/bangdream/src");
|
const { ToolsService } = require("./src/common/services/tool.service");
|
||||||
|
const { QqbotBangDreamRendererService } = require("./src/modules/qqbot/plugins/bangDream/application/bangdream-renderer.facade");
|
||||||
function createHttpError(message, statusCode) {
|
const { TsuguApplicationService } = require("./src/modules/qqbot/plugins/bangDream/application/bangdream-application.service");
|
||||||
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 () => {
|
(async () => {
|
||||||
const manifest = readJsonFile(path.join(process.cwd(), "src/modules/qqbot/plugins/bangdream/plugin.json"));
|
const renderer = new QqbotBangDreamRendererService(new ConfigService({}), undefined);
|
||||||
const operations = manifest.operations.map((operation) => ({
|
const service = new TsuguApplicationService(renderer, new ToolsService());
|
||||||
handlerName: operation.handlerName,
|
await service.onApplicationBootstrap();
|
||||||
key: operation.key,
|
const result = await service.execute(payload.operationKey, payload.input);
|
||||||
}));
|
|
||||||
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)];
|
const matches = [...result.replyText.matchAll(/base64:\/\/([A-Za-z0-9+/=]+)/g)];
|
||||||
if (matches.length === 0) throw new Error("No image CQ payload");
|
if (matches.length === 0) throw new Error("No image CQ payload");
|
||||||
fs.mkdirSync(path.dirname(payload.outFile), { recursive: true });
|
fs.mkdirSync(path.dirname(payload.outFile), { recursive: true });
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import * as path from 'path';
|
|||||||
import {
|
import {
|
||||||
parseQqbotPluginManifest,
|
parseQqbotPluginManifest,
|
||||||
type QqbotPluginManifest,
|
type QqbotPluginManifest,
|
||||||
} from '../../src/modules/qqbot/plugin-platform/domain/manifest';
|
} from '../../src/modules/qqbot/plugin-platform/manifest';
|
||||||
|
|
||||||
export type QqbotPluginCliCommand =
|
export type QqbotPluginCliCommand =
|
||||||
| 'create'
|
| 'create'
|
||||||
@ -21,13 +21,11 @@ export type QqbotPluginCliOptions = {
|
|||||||
export type QqbotPluginCliResult = {
|
export type QqbotPluginCliResult = {
|
||||||
command: QqbotPluginCliCommand;
|
command: QqbotPluginCliCommand;
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
installationId?: string;
|
|
||||||
packageHash?: string;
|
packageHash?: string;
|
||||||
packagePath?: string;
|
packagePath?: string;
|
||||||
pluginKey?: string;
|
pluginKey?: string;
|
||||||
pluginRoot?: string;
|
pluginRoot?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
versionId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type PackedPluginFile = {
|
type PackedPluginFile = {
|
||||||
@ -43,18 +41,6 @@ type PackedPlugin = {
|
|||||||
|
|
||||||
const templateRoot = path.join(__dirname, 'templates', 'basic');
|
const templateRoot = path.join(__dirname, 'templates', 'basic');
|
||||||
const packageExtension = '.qqbot-plugin.json';
|
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) =>
|
const sha256 = (content: Buffer | string) =>
|
||||||
crypto.createHash('sha256').update(content).digest('hex');
|
crypto.createHash('sha256').update(content).digest('hex');
|
||||||
@ -82,73 +68,6 @@ const formatPluginName = (pluginKey: string) => {
|
|||||||
.join(' ');
|
.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 readManifest = (pluginRoot: string) => {
|
||||||
const manifestPath = path.join(pluginRoot, 'plugin.json');
|
const manifestPath = path.join(pluginRoot, 'plugin.json');
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
|
||||||
@ -197,30 +116,16 @@ const listPackageFiles = (pluginRoot: string): PackedPluginFile[] => {
|
|||||||
const visit = (current: string) => {
|
const visit = (current: string) => {
|
||||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||||
if (current === pluginRoot && ignoredRoots.has(entry.name)) continue;
|
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 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()) {
|
if (entry.isDirectory()) {
|
||||||
visit(absolutePath);
|
visit(absolutePath);
|
||||||
continue;
|
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({
|
files.push({
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
sha256: sha256(fs.readFileSync(absolutePath)),
|
sha256: sha256(fs.readFileSync(absolutePath)),
|
||||||
@ -232,32 +137,8 @@ const listPackageFiles = (pluginRoot: string): PackedPluginFile[] => {
|
|||||||
return files.sort((a, b) => a.path.localeCompare(b.path));
|
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 buildPackedPlugin = (pluginRoot: string): PackedPlugin => {
|
||||||
const manifest = readManifest(pluginRoot);
|
const manifest = readManifest(pluginRoot);
|
||||||
assertPluginSourceBoundary(pluginRoot);
|
|
||||||
const files = listPackageFiles(pluginRoot);
|
const files = listPackageFiles(pluginRoot);
|
||||||
const contentHash = sha256(
|
const contentHash = sha256(
|
||||||
stableStringify({
|
stableStringify({
|
||||||
@ -291,13 +172,10 @@ const assertPackageIntegrity = (packedPlugin: PackedPlugin) => {
|
|||||||
const createPlugin = (
|
const createPlugin = (
|
||||||
pluginKey: string | undefined,
|
pluginKey: string | undefined,
|
||||||
options: Required<QqbotPluginCliOptions>,
|
options: Required<QqbotPluginCliOptions>,
|
||||||
outputPathArg?: string,
|
|
||||||
): QqbotPluginCliResult => {
|
): QqbotPluginCliResult => {
|
||||||
assertPluginKey(pluginKey);
|
if (!pluginKey) throw new Error('Usage: qqbot-plugin create <pluginKey>');
|
||||||
|
|
||||||
const pluginRoot = outputPathArg
|
const pluginRoot = path.join(options.cwd, 'plugins', pluginKey);
|
||||||
? resolveControlledPath(outputPathArg, options, 'create')
|
|
||||||
: path.join(options.cwd, 'plugins', pluginKey);
|
|
||||||
if (fs.existsSync(pluginRoot)) {
|
if (fs.existsSync(pluginRoot)) {
|
||||||
throw new Error(`Plugin already exists: ${pluginRoot}`);
|
throw new Error(`Plugin already exists: ${pluginRoot}`);
|
||||||
}
|
}
|
||||||
@ -308,15 +186,6 @@ const createPlugin = (
|
|||||||
});
|
});
|
||||||
fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true });
|
fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true });
|
||||||
fs.mkdirSync(path.join(pluginRoot, 'migrations'), { 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 });
|
fs.mkdirSync(path.join(pluginRoot, 'tests'), { recursive: true });
|
||||||
readManifest(pluginRoot);
|
readManifest(pluginRoot);
|
||||||
|
|
||||||
@ -338,7 +207,6 @@ const validatePlugin = (
|
|||||||
|
|
||||||
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
||||||
const manifest = readManifest(pluginRoot);
|
const manifest = readManifest(pluginRoot);
|
||||||
assertPluginSourceBoundary(pluginRoot);
|
|
||||||
options.stdout(
|
options.stdout(
|
||||||
`Validated QQBot plugin: ${manifest.pluginKey}@${manifest.version}`,
|
`Validated QQBot plugin: ${manifest.pluginKey}@${manifest.version}`,
|
||||||
);
|
);
|
||||||
@ -355,18 +223,15 @@ const validatePlugin = (
|
|||||||
const packPlugin = (
|
const packPlugin = (
|
||||||
pluginRootArg: string | undefined,
|
pluginRootArg: string | undefined,
|
||||||
options: Required<QqbotPluginCliOptions>,
|
options: Required<QqbotPluginCliOptions>,
|
||||||
outputPathArg?: string,
|
|
||||||
): QqbotPluginCliResult => {
|
): QqbotPluginCliResult => {
|
||||||
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin pack <path>');
|
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin pack <path>');
|
||||||
|
|
||||||
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
||||||
const packedPlugin = buildPackedPlugin(pluginRoot);
|
const packedPlugin = buildPackedPlugin(pluginRoot);
|
||||||
const shortHash = packedPlugin.contentHash.slice(0, 12);
|
const shortHash = packedPlugin.contentHash.slice(0, 12);
|
||||||
const packageRoot = outputPathArg
|
|
||||||
? resolveControlledPath(outputPathArg, options, 'pack')
|
|
||||||
: path.join(pluginRoot, 'dist');
|
|
||||||
const packagePath = path.join(
|
const packagePath = path.join(
|
||||||
packageRoot,
|
pluginRoot,
|
||||||
|
'dist',
|
||||||
`${packedPlugin.manifest.pluginKey}-${packedPlugin.manifest.version}-${shortHash}${packageExtension}`,
|
`${packedPlugin.manifest.pluginKey}-${packedPlugin.manifest.version}-${shortHash}${packageExtension}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -398,12 +263,6 @@ const installLocalPlugin = (
|
|||||||
fs.readFileSync(packagePath, 'utf8'),
|
fs.readFileSync(packagePath, 'utf8'),
|
||||||
) as PackedPlugin;
|
) as PackedPlugin;
|
||||||
assertPackageIntegrity(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(
|
options.stdout(
|
||||||
`Installed local QQBot plugin package: ${packedPlugin.manifest.pluginKey}@${packedPlugin.manifest.version}`,
|
`Installed local QQBot plugin package: ${packedPlugin.manifest.pluginKey}@${packedPlugin.manifest.version}`,
|
||||||
);
|
);
|
||||||
@ -411,12 +270,10 @@ const installLocalPlugin = (
|
|||||||
return {
|
return {
|
||||||
command: 'install-local',
|
command: 'install-local',
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
installationId,
|
|
||||||
packageHash: packedPlugin.contentHash,
|
packageHash: packedPlugin.contentHash,
|
||||||
packagePath,
|
packagePath,
|
||||||
pluginKey: packedPlugin.manifest.pluginKey,
|
pluginKey: packedPlugin.manifest.pluginKey,
|
||||||
version: packedPlugin.manifest.version,
|
version: packedPlugin.manifest.version,
|
||||||
versionId,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -427,17 +284,17 @@ export const runQqbotPluginCli = async (
|
|||||||
const command = argv[0] as QqbotPluginCliCommand | undefined;
|
const command = argv[0] as QqbotPluginCliCommand | undefined;
|
||||||
const resolvedOptions: Required<QqbotPluginCliOptions> = {
|
const resolvedOptions: Required<QqbotPluginCliOptions> = {
|
||||||
cwd: options.cwd || process.cwd(),
|
cwd: options.cwd || process.cwd(),
|
||||||
stderr: options.stderr || ((message) => process.stderr.write(`${message}\n`)),
|
stderr: options.stderr || ((message) => console.error(message)),
|
||||||
stdout: options.stdout || ((message) => process.stdout.write(`${message}\n`)),
|
stdout: options.stdout || ((message) => console.log(message)),
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'create':
|
case 'create':
|
||||||
return createPlugin(argv[1], resolvedOptions, getOptionValue(argv, '--out'));
|
return createPlugin(argv[1], resolvedOptions);
|
||||||
case 'install-local':
|
case 'install-local':
|
||||||
return installLocalPlugin(argv[1], resolvedOptions);
|
return installLocalPlugin(argv[1], resolvedOptions);
|
||||||
case 'pack':
|
case 'pack':
|
||||||
return packPlugin(argv[1], resolvedOptions, getOptionValue(argv, '--out'));
|
return packPlugin(argv[1], resolvedOptions);
|
||||||
case 'validate':
|
case 'validate':
|
||||||
return validatePlugin(argv[1], resolvedOptions);
|
return validatePlugin(argv[1], resolvedOptions);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -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`)
|
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
|
VALUES
|
||||||
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!","!"]', 'ff14-market', 'ff14.market.price', 'ff14Price', 'all', '{"language":"chs","world":"中国"}', '', 'FF14 查价失败:{{error}}', 1, 0, 1500, '默认示例命令;请在账号配置中绑定后启用'),
|
(2041700000000300501, 'ff14_price', 'FF14 查价', '["查价","price","ff14price"]', '["/","!","!"]', 'ff14Market', '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 角色名 服务器 [高难任务]'),
|
(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'),
|
(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 [难度]'),
|
(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, '按关键词随机歌曲;格式:/随机曲 [关键词]'),
|
(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, '查询歌曲分数榜;格式:/查询分数表 [服务器]'),
|
(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'),
|
(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'),
|
(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'),
|
(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'),
|
(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]'),
|
(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 [服务器]'),
|
(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'),
|
(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]'),
|
(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] [服务器]'),
|
(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] [服务器]'),
|
(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] [服务器]')
|
(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
|
ON DUPLICATE KEY UPDATE
|
||||||
`code` = VALUES(`code`),
|
`code` = VALUES(`code`),
|
||||||
`name` = VALUES(`name`),
|
`name` = VALUES(`name`),
|
||||||
|
|||||||
@ -951,14 +951,11 @@ CREATE TABLE IF NOT EXISTS napcat_account_binding (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_login_session (
|
CREATE TABLE IF NOT EXISTS napcat_login_session (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
account_id BIGINT NULL,
|
account_id BIGINT NOT NULL,
|
||||||
session_key VARCHAR(128) NOT NULL,
|
session_key VARCHAR(128) NOT NULL,
|
||||||
login_stage VARCHAR(64) NOT NULL,
|
login_stage VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
progress_message VARCHAR(255) 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,
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_napcat_login_session_key (session_key),
|
UNIQUE KEY uk_napcat_login_session_key (session_key),
|
||||||
|
|||||||
@ -274,178 +274,21 @@ INSERT INTO qqbot_command (
|
|||||||
plugin_key,
|
plugin_key,
|
||||||
enabled,
|
enabled,
|
||||||
cooldown_seconds
|
cooldown_seconds
|
||||||
) VALUES
|
) VALUES (
|
||||||
(
|
1000000000000000201,
|
||||||
1000000000000000201,
|
'bangdream.song.search',
|
||||||
'bangdream.song.search',
|
'bangdream_song',
|
||||||
'bangdream_song',
|
'bd',
|
||||||
'bd',
|
'BangDream 查歌',
|
||||||
'BangDream 查歌',
|
'["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]',
|
||||||
'["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]',
|
'bangdream',
|
||||||
'bangdream',
|
1,
|
||||||
1,
|
2
|
||||||
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),
|
operation_key = VALUES(operation_key),
|
||||||
code = VALUES(code),
|
code = VALUES(code),
|
||||||
name = VALUES(name),
|
name = VALUES(name),
|
||||||
aliases = VALUES(aliases),
|
aliases = VALUES(aliases),
|
||||||
plugin_key = VALUES(plugin_key),
|
plugin_key = VALUES(plugin_key),
|
||||||
enabled = VALUES(enabled),
|
enabled = VALUES(enabled),
|
||||||
cooldown_seconds = VALUES(cooldown_seconds),
|
cooldown_seconds = VALUES(cooldown_seconds);
|
||||||
is_deleted = 0;
|
|
||||||
|
|||||||
@ -5,12 +5,7 @@ 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 '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_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 '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_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
|
SELECT 'seed_admin_user' AS check_name, COUNT(*) AS matched_rows
|
||||||
FROM admin_user
|
FROM admin_user
|
||||||
@ -36,13 +31,6 @@ WHERE command_key = 'bangdream_song'
|
|||||||
AND plugin_key = 'bangdream'
|
AND plugin_key = 'bangdream'
|
||||||
AND enabled = 1;
|
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
|
SELECT 'index_admin_user_username' AS check_name, COUNT(*) AS matched_rows
|
||||||
FROM information_schema.statistics
|
FROM information_schema.statistics
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
@ -72,27 +60,3 @@ FROM information_schema.statistics
|
|||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
AND table_name = 'napcat_device_identity'
|
AND table_name = 'napcat_device_identity'
|
||||||
AND index_name = 'uk_napcat_device_identity_account';
|
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';
|
|
||||||
|
|||||||
68
src/admin/admin.module.ts
Normal file
68
src/admin/admin.module.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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 {}
|
||||||
@ -1,7 +1,28 @@
|
|||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import type { AdminMenu } from '../identity/menu/admin-menu.entity';
|
import type { AdminMenu } from './menu/admin-menu.entity';
|
||||||
import type { AdminRole } from '../identity/role/admin-role.entity';
|
import type { AdminRole } from './role/admin-role.entity';
|
||||||
import type { AdminUser } from '../identity/user/admin-user.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;
|
||||||
|
};
|
||||||
|
|
||||||
export type AdminDictItem = {
|
export type AdminDictItem = {
|
||||||
childrenCode?: string | null;
|
childrenCode?: string | null;
|
||||||
@ -17,7 +17,7 @@ import { AdminAuthService } from './admin-auth.service';
|
|||||||
import { AdminLoginDto } from './admin-auth.dto';
|
import { AdminLoginDto } from './admin-auth.dto';
|
||||||
import { AdminPasswordCryptoService } from './admin-password-crypto.service';
|
import { AdminPasswordCryptoService } from './admin-password-crypto.service';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||||
import { WordpressService } from '@/modules/wordpress/application/wordpress.service';
|
import { WordpressService } from '@/wordpress/wordpress.service';
|
||||||
|
|
||||||
@ApiTags('Admin - 认证')
|
@ApiTags('Admin - 认证')
|
||||||
@Controller()
|
@Controller()
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { throwVbenError } from '@/common';
|
import { throwVbenError } from '@/common';
|
||||||
import type { AdminRequest } from '../../contract/admin.types';
|
import type { AdminRequest } from '../admin.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminSuperGuard implements CanActivate {
|
export class AdminSuperGuard implements CanActivate {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { createHmac, timingSafeEqual } from 'crypto';
|
import { createHmac, timingSafeEqual } from 'crypto';
|
||||||
import type { AdminTokenPayload } from '../../contract/admin.types';
|
import type { AdminTokenPayload } from '../admin.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminTokenService {
|
export class AdminTokenService {
|
||||||
@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
|||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { AdminAuthService } from './admin-auth.service';
|
import { AdminAuthService } from './admin-auth.service';
|
||||||
import { IS_PUBLIC_KEY } from '@/common';
|
import { IS_PUBLIC_KEY } from '@/common';
|
||||||
import type { AdminRequest } from '../../contract/admin.types';
|
import type { AdminRequest } from '../admin.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtAuthGuard implements CanActivate {
|
export class JwtAuthGuard implements CanActivate {
|
||||||
@ -26,7 +26,7 @@ import {
|
|||||||
type KtPageParams,
|
type KtPageParams,
|
||||||
ToolsService,
|
ToolsService,
|
||||||
} from '@/common';
|
} from '@/common';
|
||||||
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { ComponentService } from './component.service';
|
import { ComponentService } from './component.service';
|
||||||
import { Component } from './component.entity';
|
import { Component } from './component.entity';
|
||||||
|
|
||||||
@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
|
|||||||
import { Component } from './component.entity';
|
import { Component } from './component.entity';
|
||||||
import { ToolsService, type KtPage, type KtPageParams } from '@/common';
|
import { ToolsService, type KtPage, type KtPageParams } from '@/common';
|
||||||
import { isNumber, omit, pick } from 'lodash';
|
import { isNumber, omit, pick } from 'lodash';
|
||||||
import { DictService } from '@/modules/admin/platform-config/dict/dict.service';
|
import { DictService } from '@/admin/dict/dict.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ComponentService {
|
export class ComponentService {
|
||||||
@ -28,7 +28,7 @@ import {
|
|||||||
AdminDictUpdateDto,
|
AdminDictUpdateDto,
|
||||||
DictDto,
|
DictDto,
|
||||||
} from './dict.dto';
|
} from './dict.dto';
|
||||||
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
|
||||||
const componentTypeDictExample = [
|
const componentTypeDictExample = [
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminAuthGuardModule } from '../../identity/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '../auth/admin-auth-guard.module';
|
||||||
import { DictController } from './dict.controller';
|
import { DictController } from './dict.controller';
|
||||||
import { DictService } from './dict.service';
|
import { DictService } from './dict.service';
|
||||||
import { AdminDict } from './admin-dict.entity';
|
import { AdminDict } from './admin-dict.entity';
|
||||||
@ -18,7 +18,7 @@ import type {
|
|||||||
AdminDictItem,
|
AdminDictItem,
|
||||||
AdminDictSerialized,
|
AdminDictSerialized,
|
||||||
AdminDictTreeItem,
|
AdminDictTreeItem,
|
||||||
} from '../../contract/admin.types';
|
} from '../admin.types';
|
||||||
|
|
||||||
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';
|
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';
|
||||||
|
|
||||||
165
src/admin/example/admin-example.controller.ts
Normal file
165
src/admin/example/admin-example.controller.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
KtUpdateDateColumn,
|
KtUpdateDateColumn,
|
||||||
} from '@/common';
|
} from '@/common';
|
||||||
import { AdminRole } from '../role/admin-role.entity';
|
import { AdminRole } from '../role/admin-role.entity';
|
||||||
import type { AdminMenuMeta, AdminMenuType } from '../../contract/admin.types';
|
import type { AdminMenuMeta, AdminMenuType } from '../admin.types';
|
||||||
|
|
||||||
@Entity('admin_menu')
|
@Entity('admin_menu')
|
||||||
export class AdminMenu {
|
export class AdminMenu {
|
||||||
@ -4,7 +4,7 @@ import { In, Repository } from 'typeorm';
|
|||||||
import { toTree } from '@/common';
|
import { toTree } from '@/common';
|
||||||
import { AdminUser } from '../user/admin-user.entity';
|
import { AdminUser } from '../user/admin-user.entity';
|
||||||
import { AdminMenu } from './admin-menu.entity';
|
import { AdminMenu } from './admin-menu.entity';
|
||||||
import type { AdminMenuInput, AdminMenuMeta } from '../../contract/admin.types';
|
import type { AdminMenuInput, AdminMenuMeta } from '../admin.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminMenuService {
|
export class AdminMenuService {
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
import { Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { vbenPage, vbenSuccess } from '@/common';
|
import { vbenPage, vbenSuccess } from '@/common';
|
||||||
import { AdminSuperGuard } from '../../identity/auth/admin-super.guard';
|
import { AdminSuperGuard } from '../auth/admin-super.guard';
|
||||||
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { AdminNoticeQueryDto } from './admin-notice.dto';
|
import { AdminNoticeQueryDto } from './admin-notice.dto';
|
||||||
import { AdminNoticeService } from './admin-notice.service';
|
import { AdminNoticeService } from './admin-notice.service';
|
||||||
|
|
||||||
@ -4,7 +4,7 @@ import { SYSTEM_NOTICE_PUBLISHER } from '@/common';
|
|||||||
import { AdminNoticeController } from './admin-notice.controller';
|
import { AdminNoticeController } from './admin-notice.controller';
|
||||||
import { AdminNotice } from './admin-notice.entity';
|
import { AdminNotice } from './admin-notice.entity';
|
||||||
import { AdminNoticeService } from './admin-notice.service';
|
import { AdminNoticeService } from './admin-notice.service';
|
||||||
import { AdminAuthGuardModule } from '../../identity/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '../auth/admin-auth-guard.module';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
@ -4,7 +4,7 @@ import { Repository } from 'typeorm';
|
|||||||
import { throwVbenError } from '@/common';
|
import { throwVbenError } from '@/common';
|
||||||
import { AdminMenu } from '../menu/admin-menu.entity';
|
import { AdminMenu } from '../menu/admin-menu.entity';
|
||||||
import { AdminRole } from './admin-role.entity';
|
import { AdminRole } from './admin-role.entity';
|
||||||
import type { AdminRoleInput, AdminRoleListQuery } from '../../contract/admin.types';
|
import type { AdminRoleInput, AdminRoleListQuery } from '../admin.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminRoleService {
|
export class AdminRoleService {
|
||||||
@ -7,7 +7,7 @@ import {
|
|||||||
vbenPage,
|
vbenPage,
|
||||||
vbenSuccess,
|
vbenSuccess,
|
||||||
} from '@/common';
|
} from '@/common';
|
||||||
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import {
|
import {
|
||||||
SystemLogDto,
|
SystemLogDto,
|
||||||
SystemLogQueryDto,
|
SystemLogQueryDto,
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { CurrentAdminUser, vbenSuccess } from '@/common';
|
import { CurrentAdminUser, vbenSuccess } from '@/common';
|
||||||
import { JwtAuthGuard } from '../../identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { AdminUser } from '../../identity/user/admin-user.entity';
|
import { AdminUser } from '../user/admin-user.entity';
|
||||||
import { AdminTimezoneService } from './admin-timezone.service';
|
import { AdminTimezoneService } from './admin-timezone.service';
|
||||||
|
|
||||||
const TIMEZONE_OPTIONS = [
|
const TIMEZONE_OPTIONS = [
|
||||||
@ -2,7 +2,7 @@ import { HttpStatus, Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { throwVbenError } from '@/common';
|
import { throwVbenError } from '@/common';
|
||||||
import { AdminUser } from '../../identity/user/admin-user.entity';
|
import { AdminUser } from '../user/admin-user.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminTimezoneService {
|
export class AdminTimezoneService {
|
||||||
@ -5,7 +5,7 @@ import { throwVbenError } from '@/common';
|
|||||||
import { AdminDept } from '../dept/admin-dept.entity';
|
import { AdminDept } from '../dept/admin-dept.entity';
|
||||||
import { AdminRole } from '../role/admin-role.entity';
|
import { AdminRole } from '../role/admin-role.entity';
|
||||||
import { AdminUser } from './admin-user.entity';
|
import { AdminUser } from './admin-user.entity';
|
||||||
import type { AdminUserInput, AdminUserListQuery } from '../../contract/admin.types';
|
import type { AdminUserInput, AdminUserListQuery } from '../admin.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminUserService {
|
export class AdminUserService {
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { Public, vbenSuccess } from '@/common';
|
import { Public, vbenSuccess } from '@/common';
|
||||||
import {
|
import {
|
||||||
BlogArticleBodyDto,
|
BlogArticleBodyDto,
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
BlogArticleTermOptionsQueryDto,
|
BlogArticleTermOptionsQueryDto,
|
||||||
BlogArticleUpdateBodyDto,
|
BlogArticleUpdateBodyDto,
|
||||||
} from './blog-article.dto';
|
} from './blog-article.dto';
|
||||||
import { BlogArticleService } from '../application/blog-article.service';
|
import { BlogArticleService } from './blog-article.service';
|
||||||
|
|
||||||
@ApiTags('Blog - 文章')
|
@ApiTags('Blog - 文章')
|
||||||
@Controller('blog/article')
|
@Controller('blog/article')
|
||||||
@ -1,8 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import type {
|
import type { BlogArticleStatus, BlogArticleTerm } from './blog-article.entity';
|
||||||
BlogArticleStatus,
|
|
||||||
BlogArticleTerm,
|
|
||||||
} from '../infrastructure/persistence/blog-article.entity';
|
|
||||||
|
|
||||||
export class BlogArticleListQueryDto {
|
export class BlogArticleListQueryDto {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@ -2,19 +2,19 @@ import { HttpStatus, Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Brackets, Repository } from 'typeorm';
|
import { Brackets, Repository } from 'typeorm';
|
||||||
import { MarkdownService, throwVbenError, ToolsService } from '@/common';
|
import { MarkdownService, throwVbenError, ToolsService } from '@/common';
|
||||||
import { WordpressService } from '@/modules/wordpress/application/wordpress.service';
|
|
||||||
import {
|
import {
|
||||||
BlogArticle,
|
BlogArticle,
|
||||||
type BlogArticleStatus,
|
type BlogArticleStatus,
|
||||||
type BlogArticleTerm,
|
type BlogArticleTerm,
|
||||||
} from '../infrastructure/persistence/blog-article.entity';
|
} from './blog-article.entity';
|
||||||
import type {
|
import type {
|
||||||
BlogArticleBodyDto,
|
BlogArticleBodyDto,
|
||||||
BlogArticleImportWordpressDto,
|
BlogArticleImportWordpressDto,
|
||||||
BlogArticleListQueryDto,
|
BlogArticleListQueryDto,
|
||||||
BlogArticleTermOptionsQueryDto,
|
BlogArticleTermOptionsQueryDto,
|
||||||
BlogArticleUpdateBodyDto,
|
BlogArticleUpdateBodyDto,
|
||||||
} from '../contract/blog-article.dto';
|
} from './blog-article.dto';
|
||||||
|
import { WordpressService } from '@/wordpress/wordpress.service';
|
||||||
import { BlogTermService } from './blog-term.service';
|
import { BlogTermService } from './blog-term.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -10,15 +10,15 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { vbenSuccess } from '@/common';
|
import { vbenSuccess } from '@/common';
|
||||||
import type { BlogTermKind } from '../infrastructure/persistence/blog-term.entity';
|
import type { BlogTermKind } from './blog-term.entity';
|
||||||
import {
|
import {
|
||||||
BlogTermBodyDto,
|
BlogTermBodyDto,
|
||||||
BlogTermListQueryDto,
|
BlogTermListQueryDto,
|
||||||
BlogTermUpdateBodyDto,
|
BlogTermUpdateBodyDto,
|
||||||
} from './blog-term.dto';
|
} from './blog-term.dto';
|
||||||
import { BlogTermService } from '../application/blog-term.service';
|
import { BlogTermService } from './blog-term.service';
|
||||||
|
|
||||||
@ApiTags('Blog - 分类标签')
|
@ApiTags('Blog - 分类标签')
|
||||||
@Controller('blog')
|
@Controller('blog')
|
||||||
@ -5,16 +5,13 @@ import { throwVbenError, ToolsService } from '@/common';
|
|||||||
import {
|
import {
|
||||||
BlogArticle,
|
BlogArticle,
|
||||||
type BlogArticleTerm,
|
type BlogArticleTerm,
|
||||||
} from '../infrastructure/persistence/blog-article.entity';
|
} from './blog-article.entity';
|
||||||
import {
|
import { BlogTerm, type BlogTermKind } from './blog-term.entity';
|
||||||
BlogTerm,
|
|
||||||
type BlogTermKind,
|
|
||||||
} from '../infrastructure/persistence/blog-term.entity';
|
|
||||||
import type {
|
import type {
|
||||||
BlogTermBodyDto,
|
BlogTermBodyDto,
|
||||||
BlogTermListQueryDto,
|
BlogTermListQueryDto,
|
||||||
BlogTermUpdateBodyDto,
|
BlogTermUpdateBodyDto,
|
||||||
} from '../contract/blog-term.dto';
|
} from './blog-term.dto';
|
||||||
|
|
||||||
type CountedBlogTerm = BlogArticleTerm & {
|
type CountedBlogTerm = BlogArticleTerm & {
|
||||||
count: number;
|
count: number;
|
||||||
@ -9,10 +9,10 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { Public, vbenSuccess } from '@/common';
|
import { Public, vbenSuccess } from '@/common';
|
||||||
import { BlogThemeConfigBodyDto } from './blog-theme-config.dto';
|
import { BlogThemeConfigBodyDto } from './blog-theme-config.dto';
|
||||||
import { BlogThemeConfigService } from '../application/blog-theme-config.service';
|
import { BlogThemeConfigService } from './blog-theme-config.service';
|
||||||
|
|
||||||
@ApiTags('Blog - 主题')
|
@ApiTags('Blog - 主题')
|
||||||
@Controller('blog/theme')
|
@Controller('blog/theme')
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import type { WordpressArgonThemeConfig } from '@/modules/wordpress/domain/wordpress.types';
|
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
|
||||||
|
|
||||||
export class BlogThemeConfigBodyDto {
|
export class BlogThemeConfigBodyDto {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
|
import { KtCreateDateColumn, KtDateTime, KtUpdateDateColumn } from '@/common';
|
||||||
import type { WordpressArgonThemeConfig } from '@/modules/wordpress/domain/wordpress.types';
|
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
|
||||||
|
|
||||||
@Entity('blog_theme_config')
|
@Entity('blog_theme_config')
|
||||||
export class BlogThemeConfig {
|
export class BlogThemeConfig {
|
||||||
@ -2,10 +2,10 @@ import { HttpStatus, Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { throwVbenError, ToolsService } from '@/common';
|
import { throwVbenError, ToolsService } from '@/common';
|
||||||
import { WordpressService } from '@/modules/wordpress/application/wordpress.service';
|
import { WordpressService } from '@/wordpress/wordpress.service';
|
||||||
import type { WordpressArgonThemeConfig } from '@/modules/wordpress/domain/wordpress.types';
|
import type { WordpressArgonThemeConfig } from '@/wordpress/wordpress.types';
|
||||||
import { BlogThemeConfigBodyDto } from '../contract/blog-theme-config.dto';
|
import { BlogThemeConfigBodyDto } from './blog-theme-config.dto';
|
||||||
import { BlogThemeConfig } from '../infrastructure/persistence/blog-theme-config.entity';
|
import { BlogThemeConfig } from './blog-theme-config.entity';
|
||||||
|
|
||||||
const DEFAULT_THEME_ID = 'argon';
|
const DEFAULT_THEME_ID = 'argon';
|
||||||
|
|
||||||
31
src/blog/blog.module.ts
Normal file
31
src/blog/blog.module.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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 {}
|
||||||
@ -21,8 +21,8 @@ import {
|
|||||||
ApiTags,
|
ApiTags,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { MinioClientService } from '../application/asset-minio.service';
|
import { MinioClientService } from './minio.service';
|
||||||
import type { MinioUploadFile } from '../domain/asset-minio.types';
|
import type { MinioUploadFile } from './minio.types';
|
||||||
import {
|
import {
|
||||||
ApiFileDownloadResponse,
|
ApiFileDownloadResponse,
|
||||||
ApiArrayResponse,
|
ApiArrayResponse,
|
||||||
@ -35,8 +35,8 @@ import {
|
|||||||
MinioBucketStatusDto,
|
MinioBucketStatusDto,
|
||||||
MinioObjectDto,
|
MinioObjectDto,
|
||||||
MinioUploadResultDto,
|
MinioUploadResultDto,
|
||||||
} from './asset-minio.dto';
|
} from './minio.dto';
|
||||||
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
|
|
||||||
const PROXY_RESOURCE_TIMEOUT = 1000 * 15;
|
const PROXY_RESOURCE_TIMEOUT = 1000 * 15;
|
||||||
const PROXY_RESOURCE_CONTENT_TYPES = [
|
const PROXY_RESOURCE_CONTENT_TYPES = [
|
||||||
13
src/minio/minio.module.ts
Normal file
13
src/minio/minio.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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 {}
|
||||||
@ -5,7 +5,7 @@ import type {
|
|||||||
MinioListObjectOptions,
|
MinioListObjectOptions,
|
||||||
MinioObjectResult,
|
MinioObjectResult,
|
||||||
MinioUploadObjectOptions,
|
MinioUploadObjectOptions,
|
||||||
} from '../domain/asset-minio.types';
|
} from './minio.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MinioClientService {
|
export class MinioClientService {
|
||||||
@ -1,20 +1,20 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
||||||
import { AdminAuthController } from '@/modules/admin/identity/auth/admin-auth.controller';
|
import { AdminAuthController } from '@/admin/auth/admin-auth.controller';
|
||||||
import { AdminDeptController } from '@/modules/admin/identity/dept/admin-dept.controller';
|
import { AdminDeptController } from '@/admin/dept/admin-dept.controller';
|
||||||
import { AdminDept } from '@/modules/admin/identity/dept/admin-dept.entity';
|
import { AdminDept } from '@/admin/dept/admin-dept.entity';
|
||||||
import { AdminDeptService } from '@/modules/admin/identity/dept/admin-dept.service';
|
import { AdminDeptService } from '@/admin/dept/admin-dept.service';
|
||||||
import { AdminMenuController } from '@/modules/admin/identity/menu/admin-menu.controller';
|
import { AdminMenuController } from '@/admin/menu/admin-menu.controller';
|
||||||
import { AdminMenu } from '@/modules/admin/identity/menu/admin-menu.entity';
|
import { AdminMenu } from '@/admin/menu/admin-menu.entity';
|
||||||
import { AdminMenuService } from '@/modules/admin/identity/menu/admin-menu.service';
|
import { AdminMenuService } from '@/admin/menu/admin-menu.service';
|
||||||
import { AdminRoleController } from '@/modules/admin/identity/role/admin-role.controller';
|
import { AdminRoleController } from '@/admin/role/admin-role.controller';
|
||||||
import { AdminRole } from '@/modules/admin/identity/role/admin-role.entity';
|
import { AdminRole } from '@/admin/role/admin-role.entity';
|
||||||
import { AdminRoleService } from '@/modules/admin/identity/role/admin-role.service';
|
import { AdminRoleService } from '@/admin/role/admin-role.service';
|
||||||
import { AdminUserManageController } from '@/modules/admin/identity/user/admin-user-manage.controller';
|
import { AdminUserManageController } from '@/admin/user/admin-user-manage.controller';
|
||||||
import { AdminUserController } from '@/modules/admin/identity/user/admin-user.controller';
|
import { AdminUserController } from '@/admin/user/admin-user.controller';
|
||||||
import { AdminUser } from '@/modules/admin/identity/user/admin-user.entity';
|
import { AdminUser } from '@/admin/user/admin-user.entity';
|
||||||
import { AdminUserService } from '@/modules/admin/identity/user/admin-user.service';
|
import { AdminUserService } from '@/admin/user/admin-user.service';
|
||||||
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
|
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
|
||||||
|
|
||||||
export const ADMIN_IDENTITY_CONTROLLERS = [
|
export const ADMIN_IDENTITY_CONTROLLERS = [
|
||||||
|
|||||||
@ -1,24 +1,26 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
||||||
import { ComponentController } from '@/modules/admin/platform-config/component/component.controller';
|
import { ComponentController } from '@/admin/component/component.controller';
|
||||||
import { Component } from '@/modules/admin/platform-config/component/component.entity';
|
import { Component } from '@/admin/component/component.entity';
|
||||||
import { ComponentService } from '@/modules/admin/platform-config/component/component.service';
|
import { ComponentService } from '@/admin/component/component.service';
|
||||||
import { DictController } from '@/modules/admin/platform-config/dict/dict.controller';
|
import { DictController } from '@/admin/dict/dict.controller';
|
||||||
import { DictModule } from '@/modules/admin/platform-config/dict/dict.module';
|
import { DictModule } from '@/admin/dict/dict.module';
|
||||||
import { AdminNoticeController } from '@/modules/admin/platform-config/notice/admin-notice.controller';
|
import { AdminNoticeController } from '@/admin/notice/admin-notice.controller';
|
||||||
import { NoticeModule } from '@/modules/admin/platform-config/notice/notice.module';
|
import { NoticeModule } from '@/admin/notice/notice.module';
|
||||||
import { SystemLogController } from '@/modules/admin/platform-config/system-log/system-log.controller';
|
import { SystemLogController } from '@/admin/system-log/system-log.controller';
|
||||||
import { SystemLogService } from '@/modules/admin/platform-config/system-log/system-log.service';
|
import { SystemLogService } from '@/admin/system-log/system-log.service';
|
||||||
import { AdminTimezoneController } from '@/modules/admin/platform-config/timezone/admin-timezone.controller';
|
import { AdminTimezoneController } from '@/admin/timezone/admin-timezone.controller';
|
||||||
import { AdminTimezoneService } from '@/modules/admin/platform-config/timezone/admin-timezone.service';
|
import { AdminTimezoneService } from '@/admin/timezone/admin-timezone.service';
|
||||||
import { AdminUser } from '@/modules/admin/identity/user/admin-user.entity';
|
import { AdminUser } from '@/admin/user/admin-user.entity';
|
||||||
|
import { AdminExampleController } from '@/admin/example/admin-example.controller';
|
||||||
import { AssetModule } from '@/modules/asset/asset.module';
|
import { AssetModule } from '@/modules/asset/asset.module';
|
||||||
|
|
||||||
export const ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS = [
|
export const ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS = [
|
||||||
ComponentController,
|
ComponentController,
|
||||||
SystemLogController,
|
SystemLogController,
|
||||||
AdminTimezoneController,
|
AdminTimezoneController,
|
||||||
|
AdminExampleController,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ADMIN_PLATFORM_CONFIG_IMPORTED_CONTROLLERS = [
|
export const ADMIN_PLATFORM_CONFIG_IMPORTED_CONTROLLERS = [
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { MinioClientController } from '@/minio/minio.controller';
|
||||||
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
import { MinioClientModule } from '@/minio/minio.module';
|
||||||
import { MinioClientService } from './application/asset-minio.service';
|
import { MinioClientService } from '@/minio/minio.service';
|
||||||
import { MinioClientController } from './contract/asset-minio.controller';
|
|
||||||
|
|
||||||
export const ASSET_CONTROLLERS = [MinioClientController];
|
export const ASSET_CONTROLLERS = [MinioClientController];
|
||||||
|
|
||||||
@ -42,9 +41,7 @@ export const ASSET_DOMAIN_CONTRACT = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AdminAuthGuardModule, ConfigModule],
|
imports: [MinioClientModule],
|
||||||
controllers: ASSET_CONTROLLERS,
|
exports: [MinioClientModule],
|
||||||
providers: ASSET_PROVIDERS,
|
|
||||||
exports: ASSET_PROVIDERS,
|
|
||||||
})
|
})
|
||||||
export class AssetModule {}
|
export class AssetModule {}
|
||||||
|
|||||||
@ -1,17 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { BlogArticleController } from '@/blog/blog-article.controller';
|
||||||
import { CommonModule } from '@/common';
|
import { BlogArticleService } from '@/blog/blog-article.service';
|
||||||
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
import { BlogModule } from '@/blog/blog.module';
|
||||||
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
|
import { BlogTermController } from '@/blog/blog-term.controller';
|
||||||
import { BlogArticleService } from './application/blog-article.service';
|
import { BlogTermService } from '@/blog/blog-term.service';
|
||||||
import { BlogTermService } from './application/blog-term.service';
|
import { BlogThemeConfigController } from '@/blog/blog-theme-config.controller';
|
||||||
import { BlogThemeConfigService } from './application/blog-theme-config.service';
|
import { BlogThemeConfigService } from '@/blog/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 = [
|
export const BLOG_CONTENT_CONTROLLERS = [
|
||||||
BlogArticleController,
|
BlogArticleController,
|
||||||
@ -66,14 +60,7 @@ export const BLOG_CONTENT_DOMAIN_CONTRACT = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [BlogModule],
|
||||||
AdminAuthGuardModule,
|
exports: [BlogModule],
|
||||||
CommonModule,
|
|
||||||
WordpressMirrorModule,
|
|
||||||
TypeOrmModule.forFeature([BlogArticle, BlogTerm, BlogThemeConfig]),
|
|
||||||
],
|
|
||||||
controllers: BLOG_CONTENT_CONTROLLERS,
|
|
||||||
providers: BLOG_CONTENT_PROVIDERS,
|
|
||||||
exports: BLOG_CONTENT_PROVIDERS,
|
|
||||||
})
|
})
|
||||||
export class BlogContentModule {}
|
export class BlogContentModule {}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import type {
|
|
||||||
QqbotNormalizedMessage,
|
|
||||||
QqbotPluginOperationSummary,
|
|
||||||
} from '../contract/qqbot.types';
|
|
||||||
|
|
||||||
export const QQBOT_PLUGIN_EXECUTION_PORT = Symbol(
|
|
||||||
'QQBOT_PLUGIN_EXECUTION_PORT',
|
|
||||||
);
|
|
||||||
|
|
||||||
export type QqbotPluginOperationLookup = {
|
|
||||||
operationKey?: string;
|
|
||||||
pluginKey?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginExecutionInput = {
|
|
||||||
context?: Record<string, any>;
|
|
||||||
input: Record<string, any>;
|
|
||||||
operationKey: string;
|
|
||||||
pluginKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginEventDispatchInput = {
|
|
||||||
eventKey: 'message';
|
|
||||||
message: QqbotNormalizedMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginOperationListContext = {
|
|
||||||
selfId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface QqbotPluginExecutionPort {
|
|
||||||
dispatchEvent(input: QqbotPluginEventDispatchInput): Promise<boolean>;
|
|
||||||
executeOperation(input: QqbotPluginExecutionInput): Promise<any>;
|
|
||||||
getOperationByCommand(
|
|
||||||
command: QqbotPluginOperationLookup,
|
|
||||||
): Promise<null | QqbotPluginOperationSummary>;
|
|
||||||
listActiveOperations(
|
|
||||||
context?: QqbotPluginOperationListContext,
|
|
||||||
): Promise<QqbotPluginOperationSummary[]>;
|
|
||||||
}
|
|
||||||
@ -1,60 +1,68 @@
|
|||||||
import { forwardRef, Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AdminAuthGuardModule } from '@/modules/admin/identity/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
||||||
import { DictModule } from '@/modules/admin/platform-config/dict/dict.module';
|
import { DictModule } from '@/admin/dict/dict.module';
|
||||||
import { QqbotPluginPlatformModule } from '@/modules/qqbot/plugin-platform/plugin-platform.module';
|
import { QqbotAccountAbility } from '@/qqbot/account/qqbot-account-ability.entity';
|
||||||
import { QqbotAccountAbility } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account-ability.entity';
|
import { QqbotAccountController } from '@/qqbot/account/qqbot-account.controller';
|
||||||
import { QqbotAccountController } from '@/modules/qqbot/core/contract/account/qqbot-account.controller';
|
import { QqbotAccount } from '@/qqbot/account/qqbot-account.entity';
|
||||||
import { QqbotAccount } from '@/modules/qqbot/core/infrastructure/persistence/account/qqbot-account.entity';
|
import { QqbotAccountService } from '@/qqbot/account/qqbot-account.service';
|
||||||
import { QqbotAccountService } from '@/modules/qqbot/core/application/account/qqbot-account.service';
|
import { QqbotNapcatLoginService } from '@/qqbot/account/qqbot-napcat-login.service';
|
||||||
import { QqbotNapcatLoginService } from '@/modules/qqbot/napcat/application/login/qqbot-napcat-login.service';
|
import { QqbotNapcatWatchdogService } from '@/qqbot/account/qqbot-napcat-watchdog.service';
|
||||||
import { QqbotNapcatWatchdogService } from '@/modules/qqbot/napcat/application/login/qqbot-napcat-watchdog.service';
|
import { QqbotCommandController } from '@/qqbot/command/qqbot-command.controller';
|
||||||
import { QqbotCommandController } from '@/modules/qqbot/core/contract/command/qqbot-command.controller';
|
import { QqbotCommand } from '@/qqbot/command/qqbot-command.entity';
|
||||||
import { QqbotCommand } from '@/modules/qqbot/core/infrastructure/persistence/command/qqbot-command.entity';
|
import { QqbotCommandEngineService } from '@/qqbot/command/qqbot-command-engine.service';
|
||||||
import { QqbotCommandEngineService } from '@/modules/qqbot/core/application/command/qqbot-command-engine.service';
|
import { QqbotCommandLog } from '@/qqbot/command/qqbot-command-log.entity';
|
||||||
import { QqbotCommandLog } from '@/modules/qqbot/core/infrastructure/persistence/command/qqbot-command-log.entity';
|
import { QqbotCommandParserService } from '@/qqbot/command/qqbot-command-parser.service';
|
||||||
import { QqbotCommandParserService } from '@/modules/qqbot/core/application/command/qqbot-command-parser.service';
|
import { QqbotCommandService } from '@/qqbot/command/qqbot-command.service';
|
||||||
import { QqbotCommandService } from '@/modules/qqbot/core/application/command/qqbot-command.service';
|
import { QqbotReplyTemplateService } from '@/qqbot/command/qqbot-reply-template.service';
|
||||||
import { QqbotReplyTemplateService } from '@/modules/qqbot/core/application/command/qqbot-reply-template.service';
|
import { QqbotReverseWsService } from '@/qqbot/connection/qqbot-reverse-ws.service';
|
||||||
import { QqbotReverseWsService } from '@/modules/qqbot/core/infrastructure/integration/connection/qqbot-reverse-ws.service';
|
import { QqbotConfig } from '@/qqbot/config/qqbot-config.entity';
|
||||||
import { QqbotConfig } from '@/modules/qqbot/core/infrastructure/persistence/config/qqbot-config.entity';
|
import { QqbotConfigService } from '@/qqbot/config/qqbot-config.service';
|
||||||
import { QqbotConfigService } from '@/modules/qqbot/core/application/config/qqbot-config.service';
|
import { QqbotDashboardController } from '@/qqbot/dashboard/qqbot-dashboard.controller';
|
||||||
import { QqbotDashboardController } from '@/modules/qqbot/core/contract/dashboard/qqbot-dashboard.controller';
|
import { QqbotDashboardService } from '@/qqbot/dashboard/qqbot-dashboard.service';
|
||||||
import { QqbotDashboardService } from '@/modules/qqbot/core/application/dashboard/qqbot-dashboard.service';
|
import { QqbotDedupe } from '@/qqbot/dedupe/qqbot-dedupe.entity';
|
||||||
import { QqbotDedupe } from '@/modules/qqbot/core/infrastructure/persistence/dedupe/qqbot-dedupe.entity';
|
import { QqbotDedupeService } from '@/qqbot/dedupe/qqbot-dedupe.service';
|
||||||
import { QqbotDedupeService } from '@/modules/qqbot/core/application/dedupe/qqbot-dedupe.service';
|
import { QqbotEventService } from '@/qqbot/event/qqbot-event.service';
|
||||||
import { QqbotEventService } from '@/modules/qqbot/core/application/event/qqbot-event.service';
|
import { QqbotPluginHttpClientService } from '@/modules/qqbot/plugin-platform/sdk';
|
||||||
import { NapcatAccountBinding } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-account-binding.entity';
|
import {
|
||||||
import { NapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-container.entity';
|
NapcatDeviceIdentity,
|
||||||
import { NapcatDeviceIdentity } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-device-identity.entity';
|
NapcatDeviceIdentityService,
|
||||||
import { NapcatDeviceIdentityService } from '@/modules/qqbot/napcat/infrastructure/integration/device/napcat-device-identity.service';
|
} from '@/modules/qqbot/napcat';
|
||||||
import { NapcatLoginChallengeEntity } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-login-challenge.entity';
|
import { QqbotConversation } from '@/qqbot/message/qqbot-conversation.entity';
|
||||||
import { NapcatLoginSession } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-login-session.entity';
|
import { QqbotMessageController } from '@/qqbot/message/qqbot-message.controller';
|
||||||
import { NapcatLoginStateStoreService } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-login-state-store.service';
|
import { QqbotMessage } from '@/qqbot/message/qqbot-message.entity';
|
||||||
import { NapcatRuntimeCleanup } from '@/modules/qqbot/napcat/infrastructure/persistence/napcat-runtime-cleanup.entity';
|
import { QqbotMessageService } from '@/qqbot/message/qqbot-message.service';
|
||||||
import { QqbotConversation } from '@/modules/qqbot/core/infrastructure/persistence/message/qqbot-conversation.entity';
|
import { QqbotBusService } from '@/qqbot/mqtt/qqbot-bus.service';
|
||||||
import { QqbotMessageController } from '@/modules/qqbot/core/contract/message/qqbot-message.controller';
|
import { QqbotAccountNapcat } from '@/qqbot/napcat/qqbot-account-napcat.entity';
|
||||||
import { QqbotMessage } from '@/modules/qqbot/core/infrastructure/persistence/message/qqbot-message.entity';
|
import { QqbotNapcatContainer } from '@/qqbot/napcat/qqbot-napcat-container.entity';
|
||||||
import { QqbotMessageService } from '@/modules/qqbot/core/application/message/qqbot-message.service';
|
import { QqbotNapcatContainerService } from '@/qqbot/napcat/qqbot-napcat-container.service';
|
||||||
import { QqbotBusService } from '@/modules/qqbot/core/infrastructure/integration/bus/qqbot-bus.service';
|
import { QqbotAllowlist } from '@/qqbot/permission/qqbot-allowlist.entity';
|
||||||
import { QqbotAccountNapcat } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-account-napcat.entity';
|
import { QqbotBlocklist } from '@/qqbot/permission/qqbot-blocklist.entity';
|
||||||
import { QqbotNapcatContainer } from '@/modules/qqbot/napcat/infrastructure/persistence/qqbot-napcat-container.entity';
|
import { QqbotPermissionController } from '@/qqbot/permission/qqbot-permission.controller';
|
||||||
import { QqbotNapcatContainerService } from '@/modules/qqbot/napcat/infrastructure/integration/container/qqbot-napcat-container.service';
|
import { QqbotPermissionService } from '@/qqbot/permission/qqbot-permission.service';
|
||||||
import { QqbotAllowlist } from '@/modules/qqbot/core/infrastructure/persistence/permission/qqbot-allowlist.entity';
|
import { QqbotEventPluginRegistryService } from '@/qqbot/plugin/qqbot-event-plugin-registry.service';
|
||||||
import { QqbotBlocklist } from '@/modules/qqbot/core/infrastructure/persistence/permission/qqbot-blocklist.entity';
|
import { QqbotPluginController } from '@/qqbot/plugin/qqbot-plugin.controller';
|
||||||
import { QqbotPermissionController } from '@/modules/qqbot/core/contract/permission/qqbot-permission.controller';
|
import { QqbotPluginRegistryService } from '@/qqbot/plugin/qqbot-plugin-registry.service';
|
||||||
import { QqbotPermissionService } from '@/modules/qqbot/core/application/permission/qqbot-permission.service';
|
import { QqbotBangDreamClientService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-client.service';
|
||||||
import { QqbotRuleController } from '@/modules/qqbot/core/contract/rule/qqbot-rule.controller';
|
import { TsuguApplicationService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-application.service';
|
||||||
import { QqbotRule } from '@/modules/qqbot/core/infrastructure/persistence/rule/qqbot-rule.entity';
|
import { QqbotBangDreamRendererService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-renderer.facade';
|
||||||
import { QqbotRuleEngineService } from '@/modules/qqbot/core/application/rule/qqbot-rule-engine.service';
|
import { QqbotBangDreamPluginService } from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.plugin';
|
||||||
import { QqbotRuleService } from '@/modules/qqbot/core/application/rule/qqbot-rule.service';
|
import { QqbotFf14ClientService } from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-client.service';
|
||||||
import { QqbotRateLimitService } from '@/modules/qqbot/core/application/send/qqbot-rate-limit.service';
|
import { QqbotFf14MarketPluginService } from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||||
import { QqbotSendController } from '@/modules/qqbot/core/contract/send/qqbot-send.controller';
|
import { QqbotFflogsClientService } from '@/modules/qqbot/plugins/fflogs/qqbot-fflogs-client.service';
|
||||||
import { QqbotSendLog } from '@/modules/qqbot/core/infrastructure/persistence/send/qqbot-send-log.entity';
|
import { QqbotFflogsPluginService } from '@/modules/qqbot/plugins/fflogs/qqbot-fflogs.plugin';
|
||||||
import { QqbotSendService } from '@/modules/qqbot/core/application/send/qqbot-send.service';
|
import { QqbotRepeaterPluginService } from '@/modules/qqbot/plugins/repeater/qqbot-repeater.plugin';
|
||||||
|
import { QqbotRuleController } from '@/qqbot/rule/qqbot-rule.controller';
|
||||||
|
import { QqbotRule } from '@/qqbot/rule/qqbot-rule.entity';
|
||||||
|
import { QqbotRuleEngineService } from '@/qqbot/rule/qqbot-rule-engine.service';
|
||||||
|
import { QqbotRuleService } from '@/qqbot/rule/qqbot-rule.service';
|
||||||
|
import { QqbotRateLimitService } from '@/qqbot/send/qqbot-rate-limit.service';
|
||||||
|
import { QqbotSendController } from '@/qqbot/send/qqbot-send.controller';
|
||||||
|
import { QqbotSendLog } from '@/qqbot/send/qqbot-send-log.entity';
|
||||||
|
import { QqbotSendService } from '@/qqbot/send/qqbot-send.service';
|
||||||
|
|
||||||
export { QQBOT_CORE_DOMAIN_CONTRACT } from './contract/qqbot-core.contract';
|
export { QQBOT_CORE_DOMAIN_CONTRACT } from './qqbot-core.contract';
|
||||||
|
|
||||||
export const QQBOT_CORE_ENTITIES = [
|
export const QQBOT_CORE_ENTITIES = [
|
||||||
QqbotAccount,
|
QqbotAccount,
|
||||||
@ -67,12 +75,7 @@ export const QQBOT_CORE_ENTITIES = [
|
|||||||
QqbotConversation,
|
QqbotConversation,
|
||||||
QqbotDedupe,
|
QqbotDedupe,
|
||||||
QqbotMessage,
|
QqbotMessage,
|
||||||
NapcatAccountBinding,
|
|
||||||
NapcatContainer,
|
|
||||||
NapcatDeviceIdentity,
|
NapcatDeviceIdentity,
|
||||||
NapcatLoginChallengeEntity,
|
|
||||||
NapcatLoginSession,
|
|
||||||
NapcatRuntimeCleanup,
|
|
||||||
QqbotAccountNapcat,
|
QqbotAccountNapcat,
|
||||||
QqbotNapcatContainer,
|
QqbotNapcatContainer,
|
||||||
QqbotRule,
|
QqbotRule,
|
||||||
@ -85,6 +88,7 @@ export const QQBOT_CORE_CONTROLLERS = [
|
|||||||
QqbotDashboardController,
|
QqbotDashboardController,
|
||||||
QqbotMessageController,
|
QqbotMessageController,
|
||||||
QqbotPermissionController,
|
QqbotPermissionController,
|
||||||
|
QqbotPluginController,
|
||||||
QqbotRuleController,
|
QqbotRuleController,
|
||||||
QqbotSendController,
|
QqbotSendController,
|
||||||
];
|
];
|
||||||
@ -99,13 +103,24 @@ export const QQBOT_CORE_PROVIDERS = [
|
|||||||
QqbotDashboardService,
|
QqbotDashboardService,
|
||||||
QqbotDedupeService,
|
QqbotDedupeService,
|
||||||
QqbotEventService,
|
QqbotEventService,
|
||||||
|
QqbotPluginHttpClientService,
|
||||||
|
QqbotBangDreamClientService,
|
||||||
|
QqbotBangDreamPluginService,
|
||||||
|
QqbotBangDreamRendererService,
|
||||||
|
TsuguApplicationService,
|
||||||
|
QqbotFf14ClientService,
|
||||||
|
QqbotFf14MarketPluginService,
|
||||||
|
QqbotFflogsClientService,
|
||||||
|
QqbotFflogsPluginService,
|
||||||
QqbotMessageService,
|
QqbotMessageService,
|
||||||
NapcatDeviceIdentityService,
|
NapcatDeviceIdentityService,
|
||||||
NapcatLoginStateStoreService,
|
|
||||||
QqbotNapcatLoginService,
|
QqbotNapcatLoginService,
|
||||||
QqbotNapcatWatchdogService,
|
QqbotNapcatWatchdogService,
|
||||||
QqbotNapcatContainerService,
|
QqbotNapcatContainerService,
|
||||||
QqbotPermissionService,
|
QqbotPermissionService,
|
||||||
|
QqbotEventPluginRegistryService,
|
||||||
|
QqbotPluginRegistryService,
|
||||||
|
QqbotRepeaterPluginService,
|
||||||
QqbotRateLimitService,
|
QqbotRateLimitService,
|
||||||
QqbotReplyTemplateService,
|
QqbotReplyTemplateService,
|
||||||
QqbotReverseWsService,
|
QqbotReverseWsService,
|
||||||
@ -116,7 +131,6 @@ export const QQBOT_CORE_PROVIDERS = [
|
|||||||
|
|
||||||
export const QQBOT_CORE_EXPORTS = [
|
export const QQBOT_CORE_EXPORTS = [
|
||||||
QqbotAccountService,
|
QqbotAccountService,
|
||||||
QqbotSendService,
|
|
||||||
NapcatDeviceIdentityService,
|
NapcatDeviceIdentityService,
|
||||||
QqbotNapcatLoginService,
|
QqbotNapcatLoginService,
|
||||||
QqbotNapcatContainerService,
|
QqbotNapcatContainerService,
|
||||||
@ -128,7 +142,6 @@ export const QQBOT_CORE_EXPORTS = [
|
|||||||
ConfigModule,
|
ConfigModule,
|
||||||
AdminAuthGuardModule,
|
AdminAuthGuardModule,
|
||||||
DictModule,
|
DictModule,
|
||||||
forwardRef(() => QqbotPluginPlatformModule),
|
|
||||||
TypeOrmModule.forFeature(QQBOT_CORE_ENTITIES),
|
TypeOrmModule.forFeature(QQBOT_CORE_ENTITIES),
|
||||||
],
|
],
|
||||||
controllers: QQBOT_CORE_CONTROLLERS,
|
controllers: QQBOT_CORE_CONTROLLERS,
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
# QQBot Core Schema
|
|
||||||
|
|
||||||
Core owns account, command, rule, permission, message, conversation, config,
|
|
||||||
dedupe, and send-log persistence.
|
|
||||||
|
|
||||||
Primary tables:
|
|
||||||
|
|
||||||
- `qqbot_account`
|
|
||||||
- `qqbot_account_ability`
|
|
||||||
- `qqbot_command`
|
|
||||||
- `qqbot_command_log`
|
|
||||||
- `qqbot_config`
|
|
||||||
- `qqbot_conversation`
|
|
||||||
- `qqbot_dedupe`
|
|
||||||
- `qqbot_message`
|
|
||||||
- `qqbot_allowlist`
|
|
||||||
- `qqbot_blocklist`
|
|
||||||
- `qqbot_rule`
|
|
||||||
- `qqbot_send_log`
|
|
||||||
|
|
||||||
Seed linkage:
|
|
||||||
|
|
||||||
- Online command rows bind to accounts through `qqbot_account_ability`.
|
|
||||||
- Core command rows refer to plugin capabilities by `plugin_key` and
|
|
||||||
`operation_key`; execution is delegated through `QQBOT_PLUGIN_EXECUTION_PORT`.
|
|
||||||
|
|
||||||
Verification SQL:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT COUNT(*) FROM qqbot_account WHERE is_deleted = 0;
|
|
||||||
SELECT COUNT(*) FROM qqbot_command WHERE is_deleted = 0;
|
|
||||||
SELECT COUNT(*) FROM qqbot_account_ability WHERE is_deleted = 0;
|
|
||||||
```
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { NapcatDeviceIdentity } from '../../persistence/napcat-device-identity.entity';
|
import type { NapcatDeviceIdentity } from '../device/napcat-device-identity.entity';
|
||||||
|
|
||||||
export type NapcatDockerDeviceOptions = {
|
export type NapcatDockerDeviceOptions = {
|
||||||
dataDir: string;
|
dataDir: string;
|
||||||
@ -4,7 +4,7 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { ensureSnowflakeId } from '@/common';
|
import { ensureSnowflakeId } from '@/common';
|
||||||
import { NapcatDeviceIdentity } from '../../persistence/napcat-device-identity.entity';
|
import { NapcatDeviceIdentity } from './napcat-device-identity.entity';
|
||||||
|
|
||||||
type ResolveNapcatDeviceIdentityInput = {
|
type ResolveNapcatDeviceIdentityInput = {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@ -1,16 +1,6 @@
|
|||||||
export * from './infrastructure/persistence/napcat-account-binding.entity';
|
export * from './container/napcat-docker-device-options';
|
||||||
export * from './infrastructure/persistence/napcat-container.entity';
|
export * from './device/napcat-device-identity.entity';
|
||||||
export * from './infrastructure/persistence/napcat-device-identity.entity';
|
export * from './device/napcat-device-identity.service';
|
||||||
export * from './infrastructure/persistence/napcat-login-challenge.entity';
|
export * from './integration/napcat-login-api.client';
|
||||||
export * from './infrastructure/persistence/napcat-login-session.entity';
|
export * from './login/napcat-login-state-machine';
|
||||||
export * from './infrastructure/persistence/napcat-login-state-store.service';
|
export * from './persistence';
|
||||||
export * from './infrastructure/persistence/napcat-runtime-cleanup.entity';
|
|
||||||
export * from './infrastructure/integration/device/napcat-device-identity.service';
|
|
||||||
export * from './infrastructure/persistence/qqbot-account-napcat.entity';
|
|
||||||
export * from './infrastructure/persistence/qqbot-napcat-container.entity';
|
|
||||||
export * from './infrastructure/integration/napcat-login-api.client';
|
|
||||||
export * from './domain/login/napcat-login-state-machine';
|
|
||||||
export * from './application/login/qqbot-napcat-login.service';
|
|
||||||
export * from './application/login/qqbot-napcat-watchdog.service';
|
|
||||||
export * from './infrastructure/persistence';
|
|
||||||
export * from './infrastructure/integration/container/qqbot-napcat-container.service';
|
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
import { NapcatAccountBinding } from './napcat-account-binding.entity';
|
|
||||||
import { NapcatContainer } from './napcat-container.entity';
|
|
||||||
import { NapcatDeviceIdentity } from './napcat-device-identity.entity';
|
|
||||||
import { NapcatLoginChallengeEntity } from './napcat-login-challenge.entity';
|
|
||||||
import { NapcatLoginSession } from './napcat-login-session.entity';
|
|
||||||
import { NapcatRuntimeCleanup } from './napcat-runtime-cleanup.entity';
|
|
||||||
import { QqbotAccountNapcat } from './qqbot-account-napcat.entity';
|
|
||||||
import { QqbotNapcatContainer } from './qqbot-napcat-container.entity';
|
|
||||||
|
|
||||||
export const NAPCAT_RUNTIME_DOMAIN_CONTRACT = {
|
|
||||||
tables: [
|
|
||||||
'napcat_container',
|
|
||||||
'napcat_device_identity',
|
|
||||||
'napcat_account_binding',
|
|
||||||
'napcat_login_session',
|
|
||||||
'napcat_login_challenge',
|
|
||||||
'napcat_runtime_cleanup',
|
|
||||||
],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const NAPCAT_RUNTIME_ENTITIES = [
|
|
||||||
NapcatAccountBinding,
|
|
||||||
NapcatContainer,
|
|
||||||
NapcatDeviceIdentity,
|
|
||||||
NapcatLoginChallengeEntity,
|
|
||||||
NapcatLoginSession,
|
|
||||||
NapcatRuntimeCleanup,
|
|
||||||
QqbotAccountNapcat,
|
|
||||||
QqbotNapcatContainer,
|
|
||||||
];
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
import type { QqbotAccountNapcatBindStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
|
||||||
|
|
||||||
@Entity('napcat_account_binding')
|
|
||||||
@Index('uk_napcat_account_binding_account', ['accountId'], { unique: true })
|
|
||||||
export class NapcatAccountBinding {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', type: 'bigint' })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'container_id', type: 'bigint' })
|
|
||||||
containerId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'device_identity_id', type: 'bigint' })
|
|
||||||
deviceIdentityId: string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: QqbotAccountNapcatBindStatus;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
import type { QqbotNapcatContainerStatus } from '@/modules/qqbot/core/contract/qqbot.types';
|
|
||||||
|
|
||||||
@Entity('napcat_container')
|
|
||||||
@Index('uk_napcat_container_name', ['containerName'], { unique: true })
|
|
||||||
export class NapcatContainer {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', type: 'bigint' })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'container_name' })
|
|
||||||
containerName: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, name: 'image_name' })
|
|
||||||
imageName: string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: QqbotNapcatContainerStatus;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtDateTimeColumn,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
|
|
||||||
export type NapcatLoginChallengeType = 'captcha' | 'new-device';
|
|
||||||
|
|
||||||
@Entity('napcat_login_challenge')
|
|
||||||
@Index('idx_napcat_login_challenge_session', ['sessionId'])
|
|
||||||
export class NapcatLoginChallengeEntity {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'session_id', type: 'bigint' })
|
|
||||||
sessionId: string;
|
|
||||||
|
|
||||||
@Column({ length: 64, name: 'challenge_type' })
|
|
||||||
challengeType: NapcatLoginChallengeType;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: string;
|
|
||||||
|
|
||||||
@Column({ default: null, name: 'challenge_url', nullable: true, type: 'text' })
|
|
||||||
challengeUrl: null | string;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
default: null,
|
|
||||||
name: 'challenge_payload',
|
|
||||||
nullable: true,
|
|
||||||
type: 'json',
|
|
||||||
})
|
|
||||||
challengePayload: null | Record<string, unknown>;
|
|
||||||
|
|
||||||
@KtDateTimeColumn({
|
|
||||||
default: null,
|
|
||||||
name: 'resolved_at',
|
|
||||||
nullable: true,
|
|
||||||
type: 'datetime',
|
|
||||||
})
|
|
||||||
resolvedAt: KtDateTime | null;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtDateTimeColumn,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
import type {
|
|
||||||
QqbotLoginScanSession,
|
|
||||||
QqbotLoginScanStatus,
|
|
||||||
} from '@/modules/qqbot/core/contract/qqbot.types';
|
|
||||||
|
|
||||||
@Entity('napcat_login_session')
|
|
||||||
@Index('uk_napcat_login_session_key', ['sessionKey'], { unique: true })
|
|
||||||
@Index('idx_napcat_login_session_account', ['accountId'])
|
|
||||||
export class NapcatLoginSession {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ default: null, name: 'account_id', nullable: true, type: 'bigint' })
|
|
||||||
accountId: null | string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'session_key' })
|
|
||||||
sessionKey: string;
|
|
||||||
|
|
||||||
@Column({ length: 64, name: 'login_stage' })
|
|
||||||
loginStage: string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: QqbotLoginScanStatus;
|
|
||||||
|
|
||||||
@Column({ length: 255, name: 'progress_message' })
|
|
||||||
progressMessage: string;
|
|
||||||
|
|
||||||
@Column({ default: null, name: 'session_payload', nullable: true, type: 'json' })
|
|
||||||
sessionPayload: null | QqbotLoginScanSession;
|
|
||||||
|
|
||||||
@KtDateTimeColumn({
|
|
||||||
default: null,
|
|
||||||
name: 'expires_at',
|
|
||||||
nullable: true,
|
|
||||||
type: 'datetime',
|
|
||||||
})
|
|
||||||
expiresAt: KtDateTime | null;
|
|
||||||
|
|
||||||
@KtDateTimeColumn({
|
|
||||||
default: null,
|
|
||||||
name: 'completed_at',
|
|
||||||
nullable: true,
|
|
||||||
type: 'datetime',
|
|
||||||
})
|
|
||||||
completedAt: KtDateTime | null;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
import { Injectable, Optional } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import type { QqbotLoginScanSession } from '@/modules/qqbot/core/contract/qqbot.types';
|
|
||||||
import {
|
|
||||||
NapcatLoginChallengeEntity,
|
|
||||||
type NapcatLoginChallengeType,
|
|
||||||
} from './napcat-login-challenge.entity';
|
|
||||||
import { NapcatLoginSession } from './napcat-login-session.entity';
|
|
||||||
import {
|
|
||||||
NapcatRuntimeCleanup,
|
|
||||||
type NapcatRuntimeCleanupStatus,
|
|
||||||
} from './napcat-runtime-cleanup.entity';
|
|
||||||
|
|
||||||
type NapcatLoginStoreCache = Record<string, QqbotLoginScanSession>;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class NapcatLoginStateStoreService {
|
|
||||||
private readonly cache: NapcatLoginStoreCache = {};
|
|
||||||
private readonly pendingSessionWrites: Record<string, Promise<void> | undefined> =
|
|
||||||
{};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Optional()
|
|
||||||
@InjectRepository(NapcatLoginSession)
|
|
||||||
private readonly loginSessionRepository?: Repository<NapcatLoginSession>,
|
|
||||||
@Optional()
|
|
||||||
@InjectRepository(NapcatLoginChallengeEntity)
|
|
||||||
private readonly loginChallengeRepository?: Repository<NapcatLoginChallengeEntity>,
|
|
||||||
@Optional()
|
|
||||||
@InjectRepository(NapcatRuntimeCleanup)
|
|
||||||
private readonly runtimeCleanupRepository?: Repository<NapcatRuntimeCleanup>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getCached(sessionId: string) {
|
|
||||||
return this.cache[sessionId];
|
|
||||||
}
|
|
||||||
|
|
||||||
has(sessionId: string) {
|
|
||||||
return !!this.cache[sessionId];
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
Object.keys(this.cache).forEach((sessionId) => {
|
|
||||||
delete this.cache[sessionId];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(sessionId: string) {
|
|
||||||
const cached = this.getCached(sessionId);
|
|
||||||
if (cached) return cached;
|
|
||||||
if (!this.loginSessionRepository) return undefined;
|
|
||||||
|
|
||||||
const persisted = await this.loginSessionRepository.findOne({
|
|
||||||
where: { sessionKey: sessionId },
|
|
||||||
});
|
|
||||||
const session = persisted?.sessionPayload;
|
|
||||||
if (session) this.cache[session.id] = session;
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(session: QqbotLoginScanSession) {
|
|
||||||
this.cache[session.id] = session;
|
|
||||||
this.enqueueSessionWrite(session.id, () => this.persistSession(session));
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(sessionId: string) {
|
|
||||||
delete this.cache[sessionId];
|
|
||||||
this.enqueueSessionWrite(sessionId, () => this.markCompleted(sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(
|
|
||||||
iterator: (session: QqbotLoginScanSession, sessionId: string) => void,
|
|
||||||
) {
|
|
||||||
Object.entries(this.cache).forEach(([sessionId, session]) =>
|
|
||||||
iterator(session, sessionId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
recordCaptchaChallenge(session: QqbotLoginScanSession) {
|
|
||||||
if (!session.captchaUrl) return;
|
|
||||||
void this.saveChallenge({
|
|
||||||
challengePayload: {
|
|
||||||
expectedSelfId: session.expectedSelfId,
|
|
||||||
passwordMd5Present: !!session.passwordMd5,
|
|
||||||
},
|
|
||||||
challengeType: 'captcha',
|
|
||||||
challengeUrl: session.captchaUrl,
|
|
||||||
session,
|
|
||||||
status: 'pending',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
recordNewDeviceChallenge(session: QqbotLoginScanSession) {
|
|
||||||
if (!session.newDeviceStatus) return;
|
|
||||||
void this.saveChallenge({
|
|
||||||
challengePayload: {
|
|
||||||
deviceVerifyUrl: session.deviceVerifyUrl,
|
|
||||||
newDevicePullQrCodeSig: session.newDevicePullQrCodeSig,
|
|
||||||
newDeviceQrcode: session.newDeviceQrcode,
|
|
||||||
},
|
|
||||||
challengeType: 'new-device',
|
|
||||||
challengeUrl: session.deviceVerifyUrl || session.newDeviceQrcode || null,
|
|
||||||
session,
|
|
||||||
status: session.newDeviceStatus,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
recordRuntimeCleanup(
|
|
||||||
session: QqbotLoginScanSession,
|
|
||||||
input: {
|
|
||||||
cleanupType: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
status: NapcatRuntimeCleanupStatus;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (!this.runtimeCleanupRepository) return;
|
|
||||||
const cleanup = this.runtimeCleanupRepository.create({
|
|
||||||
cleanupType: input.cleanupType,
|
|
||||||
errorMessage: input.errorMessage || null,
|
|
||||||
sessionId: session.id,
|
|
||||||
status: input.status,
|
|
||||||
});
|
|
||||||
void this.runtimeCleanupRepository.save(cleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
async flushSessionWrites(sessionId?: string) {
|
|
||||||
if (sessionId) {
|
|
||||||
await this.pendingSessionWrites[sessionId];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await Promise.all(
|
|
||||||
Object.values(this.pendingSessionWrites).filter(
|
|
||||||
(write): write is Promise<void> => !!write,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private enqueueSessionWrite(sessionId: string, writer: () => Promise<void>) {
|
|
||||||
const previous = this.pendingSessionWrites[sessionId] || Promise.resolve();
|
|
||||||
const queued = previous.catch(() => undefined).then(writer);
|
|
||||||
const tracked = queued.finally(() => {
|
|
||||||
if (this.pendingSessionWrites[sessionId] === tracked) {
|
|
||||||
delete this.pendingSessionWrites[sessionId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.pendingSessionWrites[sessionId] = tracked;
|
|
||||||
void tracked.catch(() => undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async persistSession(session: QqbotLoginScanSession) {
|
|
||||||
if (!this.loginSessionRepository) return;
|
|
||||||
const current = await this.loginSessionRepository.findOne({
|
|
||||||
where: { sessionKey: session.id },
|
|
||||||
});
|
|
||||||
const entity = this.loginSessionRepository.create({
|
|
||||||
...(current || {}),
|
|
||||||
accountId: session.accountId || null,
|
|
||||||
completedAt:
|
|
||||||
session.status === 'pending'
|
|
||||||
? null
|
|
||||||
: (new Date() as NapcatLoginSession['completedAt']),
|
|
||||||
expiresAt: new Date(session.expiresAt) as NapcatLoginSession['expiresAt'],
|
|
||||||
loginStage: this.pickLoginStage(session),
|
|
||||||
progressMessage: session.errorMessage || this.pickProgressMessage(session),
|
|
||||||
sessionKey: session.id,
|
|
||||||
sessionPayload: session,
|
|
||||||
status: session.status,
|
|
||||||
});
|
|
||||||
await this.loginSessionRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async markCompleted(sessionId: string) {
|
|
||||||
if (!this.loginSessionRepository) return;
|
|
||||||
await this.loginSessionRepository.update(
|
|
||||||
{ sessionKey: sessionId },
|
|
||||||
{
|
|
||||||
completedAt: new Date() as NapcatLoginSession['completedAt'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveChallenge(input: {
|
|
||||||
challengePayload: null | Record<string, unknown>;
|
|
||||||
challengeType: NapcatLoginChallengeType;
|
|
||||||
challengeUrl: null | string;
|
|
||||||
session: QqbotLoginScanSession;
|
|
||||||
status: string;
|
|
||||||
}) {
|
|
||||||
if (!this.loginChallengeRepository) return;
|
|
||||||
const entity = this.loginChallengeRepository.create({
|
|
||||||
challengePayload: input.challengePayload,
|
|
||||||
challengeType: input.challengeType,
|
|
||||||
challengeUrl: input.challengeUrl,
|
|
||||||
resolvedAt: this.isResolvedChallenge(input.status)
|
|
||||||
? (new Date() as NapcatLoginChallengeEntity['resolvedAt'])
|
|
||||||
: null,
|
|
||||||
sessionId: input.session.id,
|
|
||||||
status: input.status,
|
|
||||||
});
|
|
||||||
await this.loginChallengeRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isResolvedChallenge(status: string) {
|
|
||||||
return ['failed', 'expired', 'verified'].includes(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
private pickLoginStage(session: QqbotLoginScanSession) {
|
|
||||||
if (session.newDeviceStatus) return 'new-device';
|
|
||||||
if (session.captchaUrl) return 'captcha';
|
|
||||||
if (session.passwordMd5) return 'password';
|
|
||||||
if (session.preparingRelogin) return 'quick';
|
|
||||||
if (session.qrcode) return 'manual-qr';
|
|
||||||
return session.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
private pickProgressMessage(session: QqbotLoginScanSession) {
|
|
||||||
if (session.status === 'success') return '登录成功';
|
|
||||||
if (session.status === 'error') return '登录失败';
|
|
||||||
if (session.status === 'expired') return '扫码会话已过期';
|
|
||||||
if (session.newDeviceStatus) return '需要新设备验证二维码';
|
|
||||||
if (session.captchaUrl) return '需要验证码';
|
|
||||||
if (session.qrcode) return '正在生成手动二维码';
|
|
||||||
return '登录处理中';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
|
|
||||||
export type NapcatRuntimeCleanupStatus = 'failed' | 'pending' | 'success';
|
|
||||||
|
|
||||||
@Entity('napcat_runtime_cleanup')
|
|
||||||
@Index('idx_napcat_runtime_cleanup_session', ['sessionId'])
|
|
||||||
export class NapcatRuntimeCleanup {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'session_id', type: 'bigint' })
|
|
||||||
sessionId: string;
|
|
||||||
|
|
||||||
@Column({ length: 64, name: 'cleanup_type' })
|
|
||||||
cleanupType: string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: NapcatRuntimeCleanupStatus;
|
|
||||||
|
|
||||||
@Column({ default: null, name: 'error_message', nullable: true, type: 'text' })
|
|
||||||
errorMessage: null | string;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import * as QRCode from 'qrcode';
|
|
||||||
|
|
||||||
export type NewDeviceQrStatus =
|
export type NewDeviceQrStatus =
|
||||||
| 'confirming'
|
| 'confirming'
|
||||||
| 'expired'
|
| 'expired'
|
||||||
@ -9,7 +7,6 @@ export type NewDeviceQrStatus =
|
|||||||
| 'verified';
|
| 'verified';
|
||||||
|
|
||||||
export type NewDeviceQrCode = {
|
export type NewDeviceQrCode = {
|
||||||
deviceVerifyUrl?: string;
|
|
||||||
pullQrCodeSig?: string;
|
pullQrCodeSig?: string;
|
||||||
qrcodeUrl: string;
|
qrcodeUrl: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@ -47,16 +44,13 @@ export class NapcatLoginApiClient {
|
|||||||
data.qrcode,
|
data.qrcode,
|
||||||
data.url,
|
data.url,
|
||||||
);
|
);
|
||||||
const jumpUrl = this.pickString(data.jumpUrl, data.verifyUrl);
|
if (!qrcodeUrl) {
|
||||||
const newDeviceQrcodeUrl = qrcodeUrl || (await this.createQrcode(jumpUrl));
|
|
||||||
if (!newDeviceQrcodeUrl) {
|
|
||||||
throw new Error('NapCat 未返回新设备验证二维码');
|
throw new Error('NapCat 未返回新设备验证二维码');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deviceVerifyUrl: jumpUrl || undefined,
|
|
||||||
pullQrCodeSig: this.pickString(data.newDevicePullQrCodeSig, data.sig),
|
pullQrCodeSig: this.pickString(data.newDevicePullQrCodeSig, data.sig),
|
||||||
qrcodeUrl: newDeviceQrcodeUrl,
|
qrcodeUrl,
|
||||||
sessionId,
|
sessionId,
|
||||||
status: 'qr-pending',
|
status: 'qr-pending',
|
||||||
};
|
};
|
||||||
@ -112,14 +106,4 @@ export class NapcatLoginApiClient {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createQrcode(text: string) {
|
|
||||||
if (!text) return '';
|
|
||||||
return QRCode.toDataURL(text, {
|
|
||||||
errorCorrectionLevel: 'M',
|
|
||||||
margin: 1,
|
|
||||||
scale: 6,
|
|
||||||
type: 'image/png',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user