refactor: 迁移QQBot核心模块

This commit is contained in:
sunlei 2026-06-15 04:05:19 +08:00
parent 679aabb40d
commit 7c32cda506
16 changed files with 1305 additions and 268 deletions

View File

@ -10,16 +10,16 @@
## Domains
| Domain | Tables | Owner Batch | Notes |
| --- | --- | --- | --- |
| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Login, menu, role, permission, department. |
| Platform Config | `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Dict and component template split from legacy Admin misc. |
| Blog Content | `blog_post`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_import_job` | Batch 3 | Categories and tags use relation table. |
| WordPress Mirror | `wordpress_site`, `wordpress_auth_session`, `wordpress_remote_post`, `wordpress_remote_term`, `wordpress_sync_job`, `wordpress_sync_mapping` | Batch 3 | Remote state separate from local Blog content. |
| Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. |
| System Event | `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | MySQL stores actionable events; Loki remains log query source. |
| Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. |
| QQBot Core | `qqbot_account`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_command`, `qqbot_command_alias`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event` | Batch 4 | Account, connection, permission, command, message, send queue. |
| NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Device identity and login challenge state. |
| QQBot Plugin Platform | `qqbot_plugin`, `qqbot_plugin_version`, `qqbot_plugin_installation`, `qqbot_plugin_operation`, `qqbot_plugin_event_handler`, `qqbot_plugin_account_binding`, `qqbot_plugin_config`, `qqbot_plugin_asset`, `qqbot_plugin_runtime_event` | Batch 5 | Manifest, install lifecycle, runtime health, bindings. |
| Plugin-Owned Data | plugin namespace tables | Batch 6 | Table names start with registered plugin namespace. |
| Domain | Tables | Owner Batch | Notes |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Login, menu, role, permission, department. |
| Platform Config | `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Dict and component template split from legacy Admin misc. |
| Blog Content | `blog_post`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_import_job` | Batch 3 | Categories and tags use relation table. |
| WordPress Mirror | `wordpress_site`, `wordpress_auth_session`, `wordpress_remote_post`, `wordpress_remote_term`, `wordpress_sync_job`, `wordpress_sync_mapping` | Batch 3 | Remote state separate from local Blog content. |
| Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. |
| System Event | `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | MySQL stores actionable events; Loki remains log query source. |
| Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. |
| QQBot Core | `qqbot_account`, `qqbot_account_ability`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_allowlist`, `qqbot_blocklist`, `qqbot_command`, `qqbot_command_alias`, `qqbot_command_log`, `qqbot_config`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event`, `qqbot_dedupe`, `qqbot_account_napcat`, `qqbot_napcat_container` | Batch 4 | Account, connection, permission, command, message, send queue, and transitional registered-entity compatibility tables until Batches 5-7 replace plugin/NapCat internals. |
| NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Device identity and login challenge state. |
| QQBot Plugin Platform | `qqbot_plugin`, `qqbot_plugin_version`, `qqbot_plugin_installation`, `qqbot_plugin_operation`, `qqbot_plugin_event_handler`, `qqbot_plugin_account_binding`, `qqbot_plugin_config`, `qqbot_plugin_asset`, `qqbot_plugin_runtime_event` | Batch 5 | Manifest, install lifecycle, runtime health, bindings. |
| Plugin-Owned Data | plugin namespace tables | Batch 6 | Table names start with registered plugin namespace. |

View File

@ -43,67 +43,68 @@ Every batch must leave the product in a locally verifiable state. Batch 8 is the
### API Files To Create
| Path | Responsibility |
| --- | --- |
| `docs/refactor-v3/schema-map.md` | Batch 0 full schema ownership map. |
| `docs/refactor-v3/api-admin-contract-matrix.md` | API route, DTO, SSE, and Admin caller/page matrix. |
| `docs/refactor-v3/breaking-changes.md` | Approved route, DTO, schema, seed, and Admin contract breaks. |
| `docs/refactor-v3/rebuild-runbook.md` | Local dry run, online backup, schema rebuild, smoke, and rollback runbook. |
| `sql/refactor-v3/00-full-schema.sql` | Full new schema, no historical patch accumulation. |
| `sql/refactor-v3/01-seed-core.sql` | Initial seed for Admin, menus, platform settings, QQBot core, and plugin metadata. |
| `sql/refactor-v3/99-verify.sql` | SQL checks that prove required tables, seed rows, and indexes exist. |
| `scripts/refactor-v3/db-dry-run.ps1` | Local empty DB rebuild smoke wrapper. |
| `scripts/refactor-v3/db-backup-online.ps1` | Online backup command wrapper without secrets. |
| `scripts/refactor-v3/db-restore-online.ps1` | Online rollback wrapper without secrets. |
| `scripts/refactor-v3/local-smoke.ps1` | Bounded local smoke entry for API contract checks. |
| `src/modules/admin/**` | New Admin/Auth/Platform Config module. |
| `src/modules/blog/**` | New Blog content module. |
| `src/modules/wordpress/**` | New WordPress mirror/sync module. |
| `src/modules/asset/**` | New Asset/MinIO ownership module. |
| `src/modules/qqbot/core/**` | New QQBot account, command, message, permission, send queue core. |
| `src/modules/qqbot/plugin-platform/**` | Manifest, registry, CLI, worker runtime, RPC, installation lifecycle. |
| `src/modules/qqbot/plugins/**` | Rewritten BangDream, FF14 Market, FFLogs, Repeater plugin packages. |
| `src/modules/qqbot/napcat/**` | NapCat container, device identity, login session, challenge, cleanup runtime. |
| `test/refactor-v3/**` | Cross-batch schema, contract, smoke, and release guard tests. |
| `test/modules/**` | New module-scoped tests matching `src/modules/**`. |
| Path | Responsibility |
| ----------------------------------------------- | ---------------------------------------------------------------------------------- |
| `docs/refactor-v3/schema-map.md` | Batch 0 full schema ownership map. |
| `docs/refactor-v3/api-admin-contract-matrix.md` | API route, DTO, SSE, and Admin caller/page matrix. |
| `docs/refactor-v3/breaking-changes.md` | Approved route, DTO, schema, seed, and Admin contract breaks. |
| `docs/refactor-v3/rebuild-runbook.md` | Local dry run, online backup, schema rebuild, smoke, and rollback runbook. |
| `sql/refactor-v3/00-full-schema.sql` | Full new schema, no historical patch accumulation. |
| `sql/refactor-v3/01-seed-core.sql` | Initial seed for Admin, menus, platform settings, QQBot core, and plugin metadata. |
| `sql/refactor-v3/99-verify.sql` | SQL checks that prove required tables, seed rows, and indexes exist. |
| `scripts/refactor-v3/db-dry-run.ps1` | Local empty DB rebuild smoke wrapper. |
| `scripts/refactor-v3/db-backup-online.ps1` | Online backup command wrapper without secrets. |
| `scripts/refactor-v3/db-restore-online.ps1` | Online rollback wrapper without secrets. |
| `scripts/refactor-v3/local-smoke.ps1` | Bounded local smoke entry for API contract checks. |
| `src/modules/admin/**` | New Admin/Auth/Platform Config module. |
| `src/modules/blog/**` | New Blog content module. |
| `src/modules/wordpress/**` | New WordPress mirror/sync module. |
| `src/modules/asset/**` | New Asset/MinIO ownership module. |
| `src/modules/qqbot/core/**` | New QQBot account, command, message, permission, send queue core. |
| `src/modules/qqbot/plugin-platform/**` | Manifest, registry, CLI, worker runtime, RPC, installation lifecycle. |
| `src/modules/qqbot/plugins/**` | Rewritten BangDream, FF14 Market, FFLogs, Repeater plugin packages. |
| `src/modules/qqbot/napcat/**` | NapCat container, device identity, login session, challenge, cleanup runtime. |
| `test/refactor-v3/**` | Cross-batch schema, contract, smoke, and release guard tests. |
| `test/modules/**` | New module-scoped tests matching `src/modules/**`. |
### API Files To Modify
| Path | Responsibility |
| --- | --- |
| Path | Responsibility |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `src/app.module.ts` | Replace legacy module imports with new `src/modules/**` modules when each batch is ready. |
| `src/runtime/**` | Keep `/health/runtime` stable, add runtime clients/adapters needed by modules. |
| `src/common/**` | Keep only shared response, error, time, Snowflake, logger, decorator helpers. |
| `src/admin/**` | Legacy module removed after Batch 2 compatibility tests pass. |
| `src/blog/**` | Legacy module removed after Batch 3 compatibility tests pass. |
| `src/wordpress/**` | Legacy module removed after Batch 3 compatibility tests pass. |
| `src/minio/**` | Replaced by `src/modules/asset/**` after Batch 3. |
| `src/qqbot/**` | Legacy core/plugin/NapCat modules removed after Batches 4-7. |
| `API.md` | API contract changes and breaking changes summary. |
| `README.md` | Refactor/development command entry updates. |
| `src/runtime/**` | Keep `/health/runtime` stable, add runtime clients/adapters needed by modules. |
| `src/common/**` | Keep only shared response, error, time, Snowflake, logger, decorator helpers. |
| `src/admin/**` | Legacy module removed after Batch 2 compatibility tests pass. |
| `src/blog/**` | Legacy module removed after Batch 3 compatibility tests pass. |
| `src/wordpress/**` | Legacy module removed after Batch 3 compatibility tests pass. |
| `src/minio/**` | Replaced by `src/modules/asset/**` after Batch 3. |
| `src/qqbot/**` | Legacy core/plugin/NapCat modules removed after Batches 4-7. |
| `API.md` | API contract changes and breaking changes summary. |
| `README.md` | Refactor/development command entry updates. |
### Admin Files To Create Or Modify
| Path | Responsibility |
| --- | --- |
| `apps/web-antdv-next/src/api/system/*.ts` | Sync Admin/Auth/Platform Config API contracts. |
| `apps/web-antdv-next/src/api/blog/*.ts` | Sync Blog/WordPress/Asset API contracts. |
| `apps/web-antdv-next/src/api/qqbot/index.ts` | Replace old QQBot API types and callers. |
| `apps/web-antdv-next/src/api/qqbot/plugin.ts` | Plugin platform API caller. |
| `apps/web-antdv-next/src/api/qqbot/napcat.ts` | NapCat login/device/SSE caller. |
| `apps/web-antdv-next/src/views/system/**` | Identity, menu, role, dept, dict, setting, notice pages. |
| `apps/web-antdv-next/src/views/blog/**` | Blog, WordPress, Asset management pages. |
| `apps/web-antdv-next/src/views/qqbot/**` | QQBot account, command, rule, message, send queue, plugin, NapCat pages. |
| `apps/web-antdv-next/src/router/routes/modules/system.ts` | Menu route sync if frontend routes change. |
| `apps/web-antdv-next/src/router/routes/modules/blog.ts` | Blog route sync. |
| `apps/web-antdv-next/src/router/routes/modules/qqbot.ts` | QQBot/plugin/NapCat route sync. |
| `apps/web-antdv-next/src/locales/langs/zh-CN/system.json` | Visible Chinese labels for new states and pages. |
| Path | Responsibility |
| --------------------------------------------------------- | ------------------------------------------------------------------------ |
| `apps/web-antdv-next/src/api/system/*.ts` | Sync Admin/Auth/Platform Config API contracts. |
| `apps/web-antdv-next/src/api/blog/*.ts` | Sync Blog/WordPress/Asset API contracts. |
| `apps/web-antdv-next/src/api/qqbot/index.ts` | Replace old QQBot API types and callers. |
| `apps/web-antdv-next/src/api/qqbot/plugin.ts` | Plugin platform API caller. |
| `apps/web-antdv-next/src/api/qqbot/napcat.ts` | NapCat login/device/SSE caller. |
| `apps/web-antdv-next/src/views/system/**` | Identity, menu, role, dept, dict, setting, notice pages. |
| `apps/web-antdv-next/src/views/blog/**` | Blog, WordPress, Asset management pages. |
| `apps/web-antdv-next/src/views/qqbot/**` | QQBot account, command, rule, message, send queue, plugin, NapCat pages. |
| `apps/web-antdv-next/src/router/routes/modules/system.ts` | Menu route sync if frontend routes change. |
| `apps/web-antdv-next/src/router/routes/modules/blog.ts` | Blog route sync. |
| `apps/web-antdv-next/src/router/routes/modules/qqbot.ts` | QQBot/plugin/NapCat route sync. |
| `apps/web-antdv-next/src/locales/langs/zh-CN/system.json` | Visible Chinese labels for new states and pages. |
## Batch 0: Workspace And Migration Preparation
### Task 0.1: Prepare API branch
**Files:**
- No file edits in this task.
- [x] **Step 1: Verify API worktree is clean**
@ -153,6 +154,7 @@ Actual note: `dev/api-full-refactor-v3` could not be created because the API rep
### Task 0.2: Clean authorized Admin old artifacts and prepare branch
**Files:**
- Restore only:
- `D:\MyFiles\KT\Vue\kt-template-admin\README.md`
- `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\index.ts`
@ -219,6 +221,7 @@ Switched to a new branch 'dev-admin-full-refactor-v3'
### Task 0.3: Create migration control documents
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\docs\refactor-v3\schema-map.md`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\docs\refactor-v3\api-admin-contract-matrix.md`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\docs\refactor-v3\breaking-changes.md`
@ -241,19 +244,19 @@ Use this content:
## Domains
| Domain | Tables | Owner Batch | Notes |
| --- | --- | --- | --- |
| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Login, menu, role, permission, department. |
| Platform Config | `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Dict and component template split from legacy Admin misc. |
| Blog Content | `blog_post`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_import_job` | Batch 3 | Categories and tags use relation table. |
| WordPress Mirror | `wordpress_site`, `wordpress_auth_session`, `wordpress_remote_post`, `wordpress_remote_term`, `wordpress_sync_job`, `wordpress_sync_mapping` | Batch 3 | Remote state separate from local Blog content. |
| Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. |
| System Event | `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | MySQL stores actionable events; Loki remains log query source. |
| Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. |
| QQBot Core | `qqbot_account`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_command`, `qqbot_command_alias`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event` | Batch 4 | Account, connection, permission, command, message, send queue. |
| NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Device identity and login challenge state. |
| QQBot Plugin Platform | `qqbot_plugin`, `qqbot_plugin_version`, `qqbot_plugin_installation`, `qqbot_plugin_operation`, `qqbot_plugin_event_handler`, `qqbot_plugin_account_binding`, `qqbot_plugin_config`, `qqbot_plugin_asset`, `qqbot_plugin_runtime_event` | Batch 5 | Manifest, install lifecycle, runtime health, bindings. |
| Plugin-Owned Data | plugin namespace tables | Batch 6 | Table names start with registered plugin namespace. |
| Domain | Tables | Owner Batch | Notes |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------- |
| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Login, menu, role, permission, department. |
| Platform Config | `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Dict and component template split from legacy Admin misc. |
| Blog Content | `blog_post`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_import_job` | Batch 3 | Categories and tags use relation table. |
| WordPress Mirror | `wordpress_site`, `wordpress_auth_session`, `wordpress_remote_post`, `wordpress_remote_term`, `wordpress_sync_job`, `wordpress_sync_mapping` | Batch 3 | Remote state separate from local Blog content. |
| Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. |
| System Event | `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | MySQL stores actionable events; Loki remains log query source. |
| Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. |
| QQBot Core | `qqbot_account`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_command`, `qqbot_command_alias`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event` | Batch 4 | Account, connection, permission, command, message, send queue. |
| NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Device identity and login challenge state. |
| QQBot Plugin Platform | `qqbot_plugin`, `qqbot_plugin_version`, `qqbot_plugin_installation`, `qqbot_plugin_operation`, `qqbot_plugin_event_handler`, `qqbot_plugin_account_binding`, `qqbot_plugin_config`, `qqbot_plugin_asset`, `qqbot_plugin_runtime_event` | Batch 5 | Manifest, install lifecycle, runtime health, bindings. |
| Plugin-Owned Data | plugin namespace tables | Batch 6 | Table names start with registered plugin namespace. |
```
- [x] **Step 2: Add `api-admin-contract-matrix.md`**
@ -263,16 +266,16 @@ Use this content:
```markdown
# API/Admin Contract Matrix
| Batch | API Contract | Admin Surface | Smoke Evidence |
| --- | --- | --- | --- |
| 1 | `GET /health/runtime` plain JSON, runtime adapter internals | Runtime status remains available | `curl http://localhost:<port>/health/runtime` |
| 2 | `/auth/*`, `/admin/user/*`, `/admin/menu/*`, `/admin/role/*`, `/admin/dept/*`, `/admin/dict/*`, `/admin/notice/*` | Login, menu, system pages | Login request, menu load, route render |
| 3 | `/blog/*`, `/wordpress/*`, `/asset/*` | Blog, WordPress, Asset pages | Public blog request, Admin list request, asset upload smoke |
| 4 | `/qqbot/account/*`, `/qqbot/command/*`, `/qqbot/rule/*`, `/qqbot/message/*`, `/qqbot/send/*` | QQBot core pages | `/qqbot/command/test` local request |
| 5 | `/qqbot/plugin-platform/*` | Plugin upload/install/enable/config/health pages | local test plugin install and enable |
| 6 | plugin operations exposed through QQBot command/event routing | Existing plugin pages and operation views | BangDream, FF14, FFLogs, Repeater smoke |
| 7 | `/qqbot/napcat/*`, login SSE events | NapCat device/login progress pages | simulated captcha and new-device session |
| 8 | public deployed URLs | deployed Admin | online smoke bundle |
| Batch | API Contract | Admin Surface | Smoke Evidence |
| ----- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------- |
| 1 | `GET /health/runtime` plain JSON, runtime adapter internals | Runtime status remains available | `curl http://localhost:<port>/health/runtime` |
| 2 | `/auth/*`, `/admin/user/*`, `/admin/menu/*`, `/admin/role/*`, `/admin/dept/*`, `/admin/dict/*`, `/admin/notice/*` | Login, menu, system pages | Login request, menu load, route render |
| 3 | `/blog/*`, `/wordpress/*`, `/asset/*` | Blog, WordPress, Asset pages | Public blog request, Admin list request, asset upload smoke |
| 4 | `/qqbot/account/*`, `/qqbot/command/*`, `/qqbot/rule/*`, `/qqbot/message/*`, `/qqbot/send/*` | QQBot core pages | `/qqbot/command/test` local request |
| 5 | `/qqbot/plugin-platform/*` | Plugin upload/install/enable/config/health pages | local test plugin install and enable |
| 6 | plugin operations exposed through QQBot command/event routing | Existing plugin pages and operation views | BangDream, FF14, FFLogs, Repeater smoke |
| 7 | `/qqbot/napcat/*`, login SSE events | NapCat device/login progress pages | simulated captcha and new-device session |
| 8 | public deployed URLs | deployed Admin | online smoke bundle |
```
- [x] **Step 3: Add `breaking-changes.md`**
@ -284,11 +287,11 @@ Use this content:
## Approved
| Area | Change | Reason | First Batch |
| --- | --- | --- | --- |
| Database | Rebuild all API-owned tables from new full schema | Current data is not important and old schema blocks clean module ownership | Batch 0 |
| SQL init | Replace historical patch scripts with `sql/refactor-v3/*` | Avoid accumulated `ALTER TABLE` history | Batch 0 |
| Admin QQBot API types | Replace old QQBot caller types with new contract | New QQBot Core, Plugin Platform, and NapCat state model | Batch 4 |
| Area | Change | Reason | First Batch |
| --------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------- | ----------- |
| Database | Rebuild all API-owned tables from new full schema | Current data is not important and old schema blocks clean module ownership | Batch 0 |
| SQL init | Replace historical patch scripts with `sql/refactor-v3/*` | Avoid accumulated `ALTER TABLE` history | Batch 0 |
| Admin QQBot API types | Replace old QQBot caller types with new contract | New QQBot Core, Plugin Platform, and NapCat state model | Batch 4 |
## Protected Behavior
@ -363,6 +366,7 @@ True
### Task 0.4: Add schema skeleton and RED schema test
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\00-full-schema.sql`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\01-seed-core.sql`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\sql\refactor-v3\99-verify.sql`
@ -464,6 +468,7 @@ Expected: PASS.
### Task 0.5: Add DB dry-run wrappers
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\refactor-v3\db-dry-run.ps1`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\refactor-v3\db-backup-online.ps1`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\refactor-v3\db-restore-online.ps1`
@ -590,6 +595,7 @@ ok
### Task 0.6: Batch 0 verification and commits
**Files:**
- Modify: `D:\MyFiles\KT\TASKS.md`
- API commit includes only Batch 0 API files.
- Root commit includes only `TASKS.md`.
@ -648,6 +654,7 @@ git -C D:\MyFiles\KT commit -m "docs: 记录第三期迁移准备"
### Task 1.1: Add runtime adapter contracts
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\runtime\client\runtime-http-client.types.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\runtime\client\runtime-process-client.types.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\runtime\client\runtime-docker-client.types.ts`
@ -695,6 +702,7 @@ Expected: PASS.
### Task 1.2: Keep common narrow and stable
**Files:**
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\common\index.ts`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\common\common.module.ts`
- Test: existing `test/common/*.spec.ts`
@ -727,6 +735,7 @@ Expected: PASS.
### Task 1.3: Commit Batch 1
**Files:**
- Modify: `D:\MyFiles\KT\TASKS.md`
- API and root commits.
@ -762,6 +771,7 @@ and assert that the imported controller is not also registered directly.
### Task 2.1: Build new Admin module boundary
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\admin\admin.module.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\admin\identity\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\admin\platform-config\**`
@ -796,6 +806,7 @@ Expected: PASS.
### Task 2.2: Sync Admin frontend system callers and pages
**Files:**
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\core\auth.ts`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\core\menu.ts`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\system\*.ts`
@ -849,6 +860,7 @@ Actual: Admin repo remained clean after Task 2.2 verification, so Batch 2 create
### Task 3.1: Build Blog, WordPress, and Asset modules
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\blog\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\wordpress\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\asset\**`
@ -889,6 +901,7 @@ Actual note: Batch 3 moved API ownership to transitional `BlogContentModule`, `W
### Task 3.2: Sync Admin Blog/WordPress/Asset pages
**Files:**
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\blog\index.ts`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\blog\wordpress.ts`
- Create: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\blog\asset.ts`
@ -931,27 +944,28 @@ git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "refactor: 同步博客与
### Task 4.1: Build QQBot core module
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\core\**`
- Modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\app.module.ts`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\core\**`
- [ ] **Step 1: Write RED status-separation tests**
- [x] **Step 1: Write RED status-separation tests**
Cover OneBot connection, container status, WebUI status, and QQ login status as separate fields.
- [ ] **Step 2: Write RED command tests**
- [x] **Step 2: Write RED command tests**
Cover operation key lookup, command ID usage, aliases, cooldown, permission policy, and parser validation.
- [ ] **Step 3: Write RED send queue tests**
- [x] **Step 3: Write RED send queue tests**
Cover queue reservation, rate limit, send log, and dedupe event behavior.
- [ ] **Step 4: Implement core**
- [x] **Step 4: Implement core**
Move account, connection, command, rule, message, permission, send, dedupe, and dashboard logic into `src/modules/qqbot/core`.
- [ ] **Step 5: Verify**
- [x] **Step 5: Verify**
Run:
@ -962,9 +976,12 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
Expected: PASS.
Actual note: `QqbotCoreModule` now owns the QQBot Nest boundary directly with Config/AdminAuthGuard/Dict/TypeORM imports, full controller/provider/entity/export lists, and `src/qqbot/qqbot.module.ts` kept only as a compatibility shim. The RED contract tests failed first on missing `QQBOT_CORE_ENTITIES` / `QQBOT_CORE_EXPORTS` and missing `qqbot-core.contract`; after implementation, Turing review found that registered legacy TypeORM entities were not fully represented in `sql/refactor-v3/00-full-schema.sql`. The follow-up RED/GREEN guard now checks every registered QQBot Core entity table/column against the real v3 SQL, checks nullable entity columns, and rejects SQL-only required insert columns. The schema map and full SQL include transitional compatibility tables/columns for currently registered legacy QQBot, plugin-adjacent, and NapCat-adjacent entities; Batches 5-7 will replace those internals rather than treating this as the final plugin/NapCat design.
### Task 4.2: Sync Admin QQBot core pages
**Files:**
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\index.ts`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\account\**`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\command\list.tsx`
@ -972,15 +989,15 @@ Expected: PASS.
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\message\list.tsx`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\sendLog\list.tsx`
- [ ] **Step 1: Split status fields in the API types**
- [x] **Step 1: Split status fields in the API types**
Define `oneBotStatus`, `containerStatus`, `webuiStatus`, and `qqLoginStatus` separately.
- [ ] **Step 2: Keep account page single-root**
- [x] **Step 2: Keep account page single-root**
If modal/drawer components are siblings, wrap the route in one root container.
- [ ] **Step 3: Verify**
- [x] **Step 3: Verify**
Run:
@ -990,6 +1007,8 @@ pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typec
Expected: PASS.
Actual note: QQBot account API types now expose `oneBotStatus`, `containerStatus`, `webuiStatus`, and `qqLoginStatus` as top-level split fields while preserving nested `napcat` fallbacks. The account page keeps its existing single route root and derives display/actions from the split status helpers, including disabling disconnect by OneBot status instead of the old collapsed connection status. Command/rule/message/sendLog pages did not need visible changes in this slice because their contracts did not consume the status fields. Admin typecheck passed.
### Task 4.3: Run local QQBot command smoke
Run API locally, query enabled command by `operationKey`, pass `commandId`, and include full command text.
@ -1002,6 +1021,8 @@ Expected evidence:
- response status
- sanitized response summary without `replyText`
Actual evidence: local Nest TestingModule HTTP smoke calls `GET /qqbot/command/list?operationKey=bangdream.song.search&enabled=true&pageNo=1&pageSize=10`, obtains command ID `cmd-bangdream-song-search`, then calls `POST /qqbot/command/test` with `commandId` and full text `/查歌 FIRE BIRD`. Response status is 200 and the asserted summary strips `replyText` while proving real `QqbotCommandService.page`, `QqbotCommandEngineService.preview`, parser, reply template, and plugin execution boundaries were exercised with mocked repository/plugin edges. Main-thread verification passed `pnpm exec jest --runInBand --runTestsByPath test/refactor-v3/schema-map.spec.ts ... test/modules/qqbot/core/qqbot-core-command-smoke.spec.ts` with 13 suites / 28 tests, API `pnpm run typecheck`, and Admin `pnpm -F @vben/web-antdv-next run typecheck`.
### Task 4.4: Commit Batch 4
Run:
@ -1018,6 +1039,7 @@ git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "refactor: 同步QQBot核
### Task 5.1: Build plugin platform schema and manifest contracts
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\manifest\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\persistence\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugin-platform\manifest.spec.ts`
@ -1043,6 +1065,7 @@ Expected: PASS.
### Task 5.2: Build plugin CLI
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\qqbot-plugin\cli.ts`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\qqbot-plugin\templates\basic\plugin.json`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\scripts\qqbot-plugin\templates\basic\src\index.ts`
@ -1089,6 +1112,7 @@ Expected: commands exit `0`, package includes content hash.
### Task 5.3: Build worker runtime and lifecycle
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\runtime\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugin-platform\sdk\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugin-platform\worker-runtime.spec.ts`
@ -1119,6 +1143,7 @@ Expected: PASS.
### Task 5.4: Sync Admin plugin platform page
**Files:**
- Create: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\plugin.ts`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\plugin\list.tsx`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\router\routes\modules\qqbot.ts`
@ -1157,6 +1182,7 @@ git -C D:\MyFiles\KT\Vue\kt-template-admin commit -m "feat: 增加QQBot插件管
### Task 6.1: Rewrite BangDream as platform plugin
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangDream\plugin.json`
- Create/modify: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\bangDream\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangDream\**`
@ -1183,6 +1209,7 @@ Expected: PASS. Event stage smoke keeps `imageCount=5`.
### Task 6.2: Rewrite FF14 Market, FFLogs, and Repeater
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\ff14Market\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\fflogs\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\plugins\repeater\**`
@ -1227,6 +1254,7 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 重写现
### Task 7.1: Build NapCat device persistence model
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\device\**`
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\container\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\device-identity.spec.ts`
@ -1252,6 +1280,7 @@ Expected: PASS.
### Task 7.2: Build NapCat login state machine
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\login\**`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\login-state-machine.spec.ts`
@ -1282,6 +1311,7 @@ Expected: PASS.
### Task 7.3: Implement new-device NapCat API flow
**Files:**
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\src\modules\qqbot\napcat\integration\napcat-login-api.client.ts`
- Test: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\new-device-flow.spec.ts`
@ -1322,6 +1352,7 @@ Expected: PASS.
### Task 7.4: Sync Admin NapCat login page and SSE Chinese progress
**Files:**
- Create: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\api\qqbot\napcat.ts`
- Modify: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\account\**`
- Create: `D:\MyFiles\KT\Vue\kt-template-admin\apps\web-antdv-next\src\views\qqbot\napcat\**`
@ -1503,22 +1534,22 @@ If review finds a real Important issue, fix it and re-review before commit.
### Spec Coverage
| Spec Requirement | Plan Coverage |
| --- | --- |
| API/Admin dual-repo refactor | Tasks 0.1, 0.2, 2.2, 3.2, 4.2, 5.4, 7.4, 8.1-8.7 |
| Clean Admin authorized old artifacts during implementation preparation | Task 0.2 |
| Full database redesign and destructive rebuild with rollback | Tasks 0.3-0.5, 8.5 |
| Batch 0-8 local-first rhythm | Batch sections 0-8 and Cross-Batch Review Gates |
| Runtime/Common migration | Batch 1 |
| Admin/Auth/Platform Config migration | Batch 2 |
| Blog/WordPress/Asset migration | Batch 3 |
| QQBot Core migration | Batch 4 |
| QQBot Plugin Platform | Batch 5 |
| Existing plugin rewrite | Batch 6 |
| NapCat device persistence and login state machine | Batch 7 |
| New-device flow `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` | Task 7.3 |
| SSE/Admin Chinese progress | Task 7.4 |
| Unified push, Jenkins/K8s, DB rebuild, online smoke | Batch 8 |
| Spec Requirement | Plan Coverage |
| ------------------------------------------------------------------------- | ------------------------------------------------ |
| API/Admin dual-repo refactor | Tasks 0.1, 0.2, 2.2, 3.2, 4.2, 5.4, 7.4, 8.1-8.7 |
| Clean Admin authorized old artifacts during implementation preparation | Task 0.2 |
| Full database redesign and destructive rebuild with rollback | Tasks 0.3-0.5, 8.5 |
| Batch 0-8 local-first rhythm | Batch sections 0-8 and Cross-Batch Review Gates |
| Runtime/Common migration | Batch 1 |
| Admin/Auth/Platform Config migration | Batch 2 |
| Blog/WordPress/Asset migration | Batch 3 |
| QQBot Core migration | Batch 4 |
| QQBot Plugin Platform | Batch 5 |
| Existing plugin rewrite | Batch 6 |
| NapCat device persistence and login state machine | Batch 7 |
| New-device flow `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin` | Task 7.3 |
| SSE/Admin Chinese progress | Task 7.4 |
| Unified push, Jenkins/K8s, DB rebuild, online smoke | Batch 8 |
### Type And Path Consistency

View File

@ -368,17 +368,41 @@ CREATE TABLE IF NOT EXISTS runtime_evidence_index (
CREATE TABLE IF NOT EXISTS qqbot_account (
id BIGINT NOT NULL PRIMARY KEY,
self_id VARCHAR(64) NOT NULL,
connection_mode VARCHAR(32) NOT NULL DEFAULT 'reverse-ws',
name VARCHAR(120) NOT NULL DEFAULT '',
display_name VARCHAR(128) NULL,
onebot_status VARCHAR(32) NOT NULL,
container_status VARCHAR(32) NOT NULL,
webui_status VARCHAR(32) NOT NULL,
qq_login_status VARCHAR(32) NOT NULL,
access_token VARCHAR(255) NULL,
enabled TINYINT NOT NULL DEFAULT 1,
connect_status VARCHAR(32) NOT NULL DEFAULT 'offline',
onebot_status VARCHAR(32) NOT NULL DEFAULT 'offline',
container_status VARCHAR(32) NOT NULL DEFAULT 'unknown',
webui_status VARCHAR(32) NOT NULL DEFAULT 'unknown',
qq_login_status VARCHAR(32) NOT NULL DEFAULT 'unknown',
client_role VARCHAR(32) NULL,
last_connected_at DATETIME NULL,
last_heartbeat_at DATETIME NULL,
last_error TEXT NULL,
napcat_login_password_secret VARCHAR(1024) NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_account_self_id (self_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_account_ability (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
self_id VARCHAR(64) NOT NULL,
ability_type VARCHAR(32) NOT NULL,
ability_key VARCHAR(128) NOT NULL,
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_account_ability (account_id, ability_type, ability_key),
KEY idx_qqbot_account_ability_self (self_id, ability_type, is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_connection_session (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
@ -412,13 +436,55 @@ CREATE TABLE IF NOT EXISTS qqbot_permission_policy (
UNIQUE KEY uk_qqbot_permission_policy (policy_key, scope_type, scope_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_allowlist (
id BIGINT NOT NULL PRIMARY KEY,
self_id VARCHAR(64) NOT NULL DEFAULT '',
target_type VARCHAR(32) NOT NULL DEFAULT 'qq',
target_id VARCHAR(64) NOT NULL DEFAULT '',
user_id VARCHAR(64) NOT NULL DEFAULT '',
precise_user TINYINT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_blocklist (
id BIGINT NOT NULL PRIMARY KEY,
self_id VARCHAR(64) NOT NULL DEFAULT '',
target_type VARCHAR(32) NOT NULL DEFAULT 'qq',
target_id VARCHAR(64) NOT NULL DEFAULT '',
user_id VARCHAR(64) NOT NULL DEFAULT '',
precise_user TINYINT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_command (
id BIGINT NOT NULL PRIMARY KEY,
operation_key VARCHAR(128) NOT NULL,
command_key VARCHAR(128) NOT NULL,
command_key VARCHAR(128) NULL,
code VARCHAR(80) NOT NULL,
name VARCHAR(120) NOT NULL DEFAULT '',
aliases TEXT NOT NULL,
prefixes VARCHAR(120) NOT NULL DEFAULT '/,!,',
plugin_key VARCHAR(128) NULL,
parser_key VARCHAR(40) NOT NULL DEFAULT 'plain',
target_type VARCHAR(32) NOT NULL DEFAULT 'all',
default_params TEXT NULL,
reply_template TEXT NULL,
error_template TEXT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
priority INT NOT NULL DEFAULT 0,
cooldown_ms INT NOT NULL DEFAULT 1500,
cooldown_seconds INT NOT NULL DEFAULT 0,
last_hit_at DATETIME NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_command_key (command_key),
@ -433,14 +499,53 @@ CREATE TABLE IF NOT EXISTS qqbot_command_alias (
UNIQUE KEY uk_qqbot_command_alias (command_id, alias_text)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_command_log (
id BIGINT NOT NULL PRIMARY KEY,
command_id VARCHAR(64) NOT NULL,
command_code VARCHAR(80) NOT NULL DEFAULT '',
plugin_key VARCHAR(80) NOT NULL,
operation_key VARCHAR(120) NOT NULL,
self_id VARCHAR(64) NOT NULL DEFAULT '',
target_type VARCHAR(32) NOT NULL DEFAULT 'private',
target_id VARCHAR(64) NOT NULL DEFAULT '',
user_id VARCHAR(64) NOT NULL DEFAULT '',
raw_message TEXT NOT NULL,
input TEXT NULL,
output TEXT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'success',
error_message TEXT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_config (
id BIGINT NOT NULL PRIMARY KEY,
config_key VARCHAR(120) NOT NULL,
config_value TEXT NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_config_key (config_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_rule (
id BIGINT NOT NULL PRIMARY KEY,
rule_key VARCHAR(128) NOT NULL,
rule_key VARCHAR(128) NULL,
name VARCHAR(120) NOT NULL DEFAULT '',
match_type VARCHAR(32) NOT NULL DEFAULT 'keyword',
keyword VARCHAR(500) NOT NULL,
target_type VARCHAR(32) NOT NULL DEFAULT 'all',
reply_content TEXT NOT NULL,
account_id BIGINT NULL,
command_id BIGINT NULL,
matcher_json JSON NOT NULL,
action_json JSON NOT NULL,
matcher_json JSON NULL,
action_json JSON NULL,
enabled TINYINT NOT NULL DEFAULT 1,
priority INT NOT NULL DEFAULT 0,
cooldown_ms INT NOT NULL DEFAULT 1500,
last_hit_at DATETIME NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_rule_key (rule_key)
@ -448,10 +553,19 @@ CREATE TABLE IF NOT EXISTS qqbot_rule (
CREATE TABLE IF NOT EXISTS qqbot_conversation (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
conversation_type VARCHAR(64) NOT NULL,
conversation_key VARCHAR(128) NOT NULL,
account_id BIGINT NULL,
self_id VARCHAR(64) NOT NULL,
conversation_type VARCHAR(64) NULL,
conversation_key VARCHAR(128) NULL,
target_type VARCHAR(32) NOT NULL,
target_id VARCHAR(64) NOT NULL,
display_name VARCHAR(255) NULL,
target_name VARCHAR(120) NOT NULL DEFAULT '',
last_message_id VARCHAR(64) NULL,
last_message_text TEXT NOT NULL,
last_message_time DATETIME NULL,
message_count INT NOT NULL DEFAULT 0,
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_conversation (account_id, conversation_type, conversation_key)
@ -459,21 +573,31 @@ CREATE TABLE IF NOT EXISTS qqbot_conversation (
CREATE TABLE IF NOT EXISTS qqbot_message (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
account_id BIGINT NULL,
self_id VARCHAR(64) NOT NULL,
conversation_id BIGINT NULL,
message_id VARCHAR(128) NOT NULL,
message_id VARCHAR(128) NULL,
direction VARCHAR(32) NOT NULL,
message_type VARCHAR(64) NOT NULL,
target_id VARCHAR(64) NOT NULL,
group_id VARCHAR(64) NULL,
user_id VARCHAR(64) NOT NULL,
sender_nickname VARCHAR(120) NOT NULL DEFAULT '',
raw_message TEXT NOT NULL,
message_text TEXT NOT NULL,
summary TEXT NULL,
raw_payload JSON NULL,
raw_event JSON NULL,
event_time DATETIME NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_message (account_id, message_id),
KEY idx_qqbot_message_conversation (conversation_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_send_task (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
account_id BIGINT NULL,
conversation_id BIGINT NULL,
task_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
@ -490,11 +614,21 @@ CREATE TABLE IF NOT EXISTS qqbot_send_task (
CREATE TABLE IF NOT EXISTS qqbot_send_log (
id BIGINT NOT NULL PRIMARY KEY,
task_id BIGINT NULL,
account_id BIGINT NOT NULL,
account_id BIGINT NULL,
self_id VARCHAR(64) NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_id VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
message_text TEXT NOT NULL,
params JSON NULL,
status VARCHAR(32) NOT NULL,
echo VARCHAR(80) NULL,
message_id VARCHAR(64) NULL,
safe_summary JSON NULL,
error_message TEXT NULL,
response JSON NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_qqbot_send_log_task (task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@ -507,6 +641,15 @@ CREATE TABLE IF NOT EXISTS qqbot_dedupe_event (
UNIQUE KEY uk_qqbot_dedupe_event_key (dedupe_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_dedupe (
id BIGINT NOT NULL PRIMARY KEY,
event_key VARCHAR(255) NOT NULL,
expire_at DATETIME NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_dedupe_event_key (event_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_plugin (
id BIGINT NOT NULL PRIMARY KEY,
plugin_key VARCHAR(128) NOT NULL,
@ -612,6 +755,42 @@ CREATE TABLE IF NOT EXISTS napcat_container (
UNIQUE KEY uk_napcat_container_name (container_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_account_napcat (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,
container_id BIGINT NOT NULL,
bind_status VARCHAR(32) NOT NULL DEFAULT 'pending',
is_primary TINYINT NOT NULL DEFAULT 1,
last_login_at DATETIME NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_qqbot_account_napcat_account (account_id, is_deleted),
KEY idx_qqbot_account_napcat_container (container_id, is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS qqbot_napcat_container (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(120) NOT NULL,
base_url VARCHAR(255) NOT NULL,
webui_port INT NULL,
webui_token VARCHAR(255) NULL,
image VARCHAR(255) NOT NULL DEFAULT '',
data_dir VARCHAR(500) NOT NULL DEFAULT '',
reverse_ws_url VARCHAR(500) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'creating',
last_started_at DATETIME NULL,
last_checked_at DATETIME NULL,
last_error VARCHAR(500) NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
is_deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_qqbot_napcat_container_name (name),
KEY idx_qqbot_napcat_container_status (status, is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS napcat_device_identity (
id BIGINT NOT NULL PRIMARY KEY,
account_id BIGINT NOT NULL,

View File

@ -16,8 +16,8 @@ import {
import { AdminModule } from './modules/admin/admin.module';
import { AssetModule } from './modules/asset/asset.module';
import { BlogContentModule } from './modules/blog/blog-content.module';
import { QqbotCoreModule } from './modules/qqbot/core/qqbot-core.module';
import { WordpressMirrorModule } from './modules/wordpress/wordpress-mirror.module';
import { QqbotModule } from './qqbot/qqbot.module';
import { RuntimeModule } from './runtime';
@Module({
@ -69,7 +69,7 @@ import { RuntimeModule } from './runtime';
BlogContentModule,
WordpressMirrorModule,
AssetModule,
QqbotModule,
QqbotCoreModule,
],
providers: [
AppService,

View File

@ -0,0 +1,74 @@
export const QQBOT_CORE_DOMAIN_CONTRACT = {
tables: [
'qqbot_account',
'qqbot_connection_session',
'qqbot_capability_binding',
'qqbot_permission_policy',
'qqbot_command',
'qqbot_command_alias',
'qqbot_rule',
'qqbot_conversation',
'qqbot_message',
'qqbot_send_task',
'qqbot_send_log',
'qqbot_dedupe_event',
],
status: {
accountTable: 'qqbot_account',
oneBotField: 'onebot_status',
containerField: 'container_status',
webuiField: 'webui_status',
qqLoginField: 'qq_login_status',
lastErrorField: 'last_error',
connectionSessionTable: 'qqbot_connection_session',
sessionStatusField: 'status',
closeReasonField: 'close_reason',
},
command: {
commandTable: 'qqbot_command',
commandIdField: 'id',
commandKeyField: 'command_key',
operationKeyField: 'operation_key',
pluginKeyField: 'plugin_key',
enabledField: 'enabled',
cooldownField: 'cooldown_seconds',
aliasTable: 'qqbot_command_alias',
aliasCommandField: 'command_id',
aliasTextField: 'alias_text',
accountBindingTable: 'qqbot_capability_binding',
accountBindingCommandField: 'capability_key',
parserValidation: {
route: 'POST /qqbot/command/test',
commandIdField: 'commandId',
fullCommandTextField: 'text',
},
},
permission: {
policyTable: 'qqbot_permission_policy',
policyKeyField: 'policy_key',
scopeFields: ['scope_type', 'scope_value'],
effectField: 'effect',
ruleTable: 'qqbot_rule',
ruleCommandField: 'command_id',
matcherField: 'matcher_json',
actionField: 'action_json',
enabledField: 'enabled',
},
messageSend: {
conversationTable: 'qqbot_conversation',
messageTable: 'qqbot_message',
sendQueueTable: 'qqbot_send_task',
queueTaskKeyField: 'task_key',
queueStatusField: 'status',
queuePayloadField: 'payload_json',
queueReservedAtField: 'reserved_at',
queueSentAtField: 'sent_at',
sendLogTable: 'qqbot_send_log',
sendLogStatusField: 'status',
sendLogSafeSummaryField: 'safe_summary',
dedupeTable: 'qqbot_dedupe_event',
dedupeKeyField: 'dedupe_key',
dedupeExpiresAtField: 'expires_at',
rateLimitProvider: 'QqbotRateLimitService',
},
} as const;

View File

@ -0,0 +1,142 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { DictModule } from '@/admin/dict/dict.module';
import { QqbotAccountAbility } from '@/qqbot/account/qqbot-account-ability.entity';
import { QqbotAccountController } from '@/qqbot/account/qqbot-account.controller';
import { QqbotAccount } from '@/qqbot/account/qqbot-account.entity';
import { QqbotAccountService } from '@/qqbot/account/qqbot-account.service';
import { QqbotNapcatLoginService } from '@/qqbot/account/qqbot-napcat-login.service';
import { QqbotNapcatWatchdogService } from '@/qqbot/account/qqbot-napcat-watchdog.service';
import { QqbotCommandController } from '@/qqbot/command/qqbot-command.controller';
import { QqbotCommand } from '@/qqbot/command/qqbot-command.entity';
import { QqbotCommandEngineService } from '@/qqbot/command/qqbot-command-engine.service';
import { QqbotCommandLog } from '@/qqbot/command/qqbot-command-log.entity';
import { QqbotCommandParserService } from '@/qqbot/command/qqbot-command-parser.service';
import { QqbotCommandService } from '@/qqbot/command/qqbot-command.service';
import { QqbotReplyTemplateService } from '@/qqbot/command/qqbot-reply-template.service';
import { QqbotReverseWsService } from '@/qqbot/connection/qqbot-reverse-ws.service';
import { QqbotConfig } from '@/qqbot/config/qqbot-config.entity';
import { QqbotConfigService } from '@/qqbot/config/qqbot-config.service';
import { QqbotDashboardController } from '@/qqbot/dashboard/qqbot-dashboard.controller';
import { QqbotDashboardService } from '@/qqbot/dashboard/qqbot-dashboard.service';
import { QqbotDedupe } from '@/qqbot/dedupe/qqbot-dedupe.entity';
import { QqbotDedupeService } from '@/qqbot/dedupe/qqbot-dedupe.service';
import { QqbotEventService } from '@/qqbot/event/qqbot-event.service';
import { QqbotConversation } from '@/qqbot/message/qqbot-conversation.entity';
import { QqbotMessageController } from '@/qqbot/message/qqbot-message.controller';
import { QqbotMessage } from '@/qqbot/message/qqbot-message.entity';
import { QqbotMessageService } from '@/qqbot/message/qqbot-message.service';
import { QqbotBusService } from '@/qqbot/mqtt/qqbot-bus.service';
import { QqbotAccountNapcat } from '@/qqbot/napcat/qqbot-account-napcat.entity';
import { QqbotNapcatContainer } from '@/qqbot/napcat/qqbot-napcat-container.entity';
import { QqbotNapcatContainerService } from '@/qqbot/napcat/qqbot-napcat-container.service';
import { QqbotAllowlist } from '@/qqbot/permission/qqbot-allowlist.entity';
import { QqbotBlocklist } from '@/qqbot/permission/qqbot-blocklist.entity';
import { QqbotPermissionController } from '@/qqbot/permission/qqbot-permission.controller';
import { QqbotPermissionService } from '@/qqbot/permission/qqbot-permission.service';
import { QqbotEventPluginRegistryService } from '@/qqbot/plugin/qqbot-event-plugin-registry.service';
import { QqbotPluginController } from '@/qqbot/plugin/qqbot-plugin.controller';
import { QqbotPluginRegistryService } from '@/qqbot/plugin/qqbot-plugin-registry.service';
import { QqbotBangDreamClientService } from '@/qqbot/plugins/bangDream/application/bangdream-client.service';
import { TsuguApplicationService } from '@/qqbot/plugins/bangDream/application/bangdream-application.service';
import { QqbotBangDreamRendererService } from '@/qqbot/plugins/bangDream/application/bangdream-renderer.facade';
import { QqbotBangDreamPluginService } from '@/qqbot/plugins/bangDream/qqbot-bangdream.plugin';
import { QqbotFf14ClientService } from '@/qqbot/plugins/ff14Market/qqbot-ff14-client.service';
import { QqbotFf14MarketPluginService } from '@/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin';
import { QqbotFflogsClientService } from '@/qqbot/plugins/fflogs/qqbot-fflogs-client.service';
import { QqbotFflogsPluginService } from '@/qqbot/plugins/fflogs/qqbot-fflogs.plugin';
import { QqbotRepeaterPluginService } from '@/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 './qqbot-core.contract';
export const QQBOT_CORE_ENTITIES = [
QqbotAccount,
QqbotAccountAbility,
QqbotAllowlist,
QqbotBlocklist,
QqbotCommand,
QqbotCommandLog,
QqbotConfig,
QqbotConversation,
QqbotDedupe,
QqbotMessage,
QqbotAccountNapcat,
QqbotNapcatContainer,
QqbotRule,
QqbotSendLog,
];
export const QQBOT_CORE_CONTROLLERS = [
QqbotAccountController,
QqbotCommandController,
QqbotDashboardController,
QqbotMessageController,
QqbotPermissionController,
QqbotPluginController,
QqbotRuleController,
QqbotSendController,
];
export const QQBOT_CORE_PROVIDERS = [
QqbotAccountService,
QqbotBusService,
QqbotCommandEngineService,
QqbotCommandParserService,
QqbotCommandService,
QqbotConfigService,
QqbotDashboardService,
QqbotDedupeService,
QqbotEventService,
QqbotBangDreamClientService,
QqbotBangDreamPluginService,
QqbotBangDreamRendererService,
TsuguApplicationService,
QqbotFf14ClientService,
QqbotFf14MarketPluginService,
QqbotFflogsClientService,
QqbotFflogsPluginService,
QqbotMessageService,
QqbotNapcatLoginService,
QqbotNapcatWatchdogService,
QqbotNapcatContainerService,
QqbotPermissionService,
QqbotEventPluginRegistryService,
QqbotPluginRegistryService,
QqbotRepeaterPluginService,
QqbotRateLimitService,
QqbotReplyTemplateService,
QqbotReverseWsService,
QqbotRuleEngineService,
QqbotRuleService,
QqbotSendService,
];
export const QQBOT_CORE_EXPORTS = [
QqbotAccountService,
QqbotNapcatLoginService,
QqbotNapcatContainerService,
QqbotReverseWsService,
];
@Module({
imports: [
ConfigModule,
AdminAuthGuardModule,
DictModule,
TypeOrmModule.forFeature(QQBOT_CORE_ENTITIES),
],
controllers: QQBOT_CORE_CONTROLLERS,
providers: QQBOT_CORE_PROVIDERS,
exports: QQBOT_CORE_EXPORTS,
})
export class QqbotCoreModule {}

View File

@ -1,132 +1,8 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
import { DictModule } from '@/admin/dict/dict.module';
import { QqbotAccountController } from './account/qqbot-account.controller';
import { QqbotAccountAbility } from './account/qqbot-account-ability.entity';
import { QqbotAccount } from './account/qqbot-account.entity';
import { QqbotAccountService } from './account/qqbot-account.service';
import { QqbotNapcatLoginService } from './account/qqbot-napcat-login.service';
import { QqbotNapcatWatchdogService } from './account/qqbot-napcat-watchdog.service';
import { QqbotCommandController } from './command/qqbot-command.controller';
import { QqbotCommand } from './command/qqbot-command.entity';
import { QqbotCommandEngineService } from './command/qqbot-command-engine.service';
import { QqbotCommandLog } from './command/qqbot-command-log.entity';
import { QqbotCommandParserService } from './command/qqbot-command-parser.service';
import { QqbotCommandService } from './command/qqbot-command.service';
import { QqbotReplyTemplateService } from './command/qqbot-reply-template.service';
import { QqbotReverseWsService } from './connection/qqbot-reverse-ws.service';
import { QqbotConfig } from './config/qqbot-config.entity';
import { QqbotConfigService } from './config/qqbot-config.service';
import { QqbotDashboardController } from './dashboard/qqbot-dashboard.controller';
import { QqbotDashboardService } from './dashboard/qqbot-dashboard.service';
import { QqbotDedupe } from './dedupe/qqbot-dedupe.entity';
import { QqbotDedupeService } from './dedupe/qqbot-dedupe.service';
import { QqbotEventService } from './event/qqbot-event.service';
import { QqbotConversation } from './message/qqbot-conversation.entity';
import { QqbotMessageController } from './message/qqbot-message.controller';
import { QqbotMessage } from './message/qqbot-message.entity';
import { QqbotMessageService } from './message/qqbot-message.service';
import { QqbotBusService } from './mqtt/qqbot-bus.service';
import { QqbotAccountNapcat } from './napcat/qqbot-account-napcat.entity';
import { QqbotNapcatContainer } from './napcat/qqbot-napcat-container.entity';
import { QqbotNapcatContainerService } from './napcat/qqbot-napcat-container.service';
import { QqbotAllowlist } from './permission/qqbot-allowlist.entity';
import { QqbotBlocklist } from './permission/qqbot-blocklist.entity';
import { QqbotPermissionController } from './permission/qqbot-permission.controller';
import { QqbotPermissionService } from './permission/qqbot-permission.service';
import { QqbotEventPluginRegistryService } from './plugin/qqbot-event-plugin-registry.service';
import { QqbotPluginController } from './plugin/qqbot-plugin.controller';
import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.service';
import { QqbotBangDreamClientService } from './plugins/bangDream/application/bangdream-client.service';
import { QqbotBangDreamRendererService } from './plugins/bangDream/application/bangdream-renderer.facade';
import { TsuguApplicationService } from './plugins/bangDream/application/bangdream-application.service';
import { QqbotBangDreamPluginService } from './plugins/bangDream/qqbot-bangdream.plugin';
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
import { QqbotFflogsClientService } from './plugins/fflogs/qqbot-fflogs-client.service';
import { QqbotFflogsPluginService } from './plugins/fflogs/qqbot-fflogs.plugin';
import { QqbotRepeaterPluginService } from './plugins/repeater/qqbot-repeater.plugin';
import { QqbotRuleController } from './rule/qqbot-rule.controller';
import { QqbotRule } from './rule/qqbot-rule.entity';
import { QqbotRuleEngineService } from './rule/qqbot-rule-engine.service';
import { QqbotRuleService } from './rule/qqbot-rule.service';
import { QqbotRateLimitService } from './send/qqbot-rate-limit.service';
import { QqbotSendController } from './send/qqbot-send.controller';
import { QqbotSendLog } from './send/qqbot-send-log.entity';
import { QqbotSendService } from './send/qqbot-send.service';
import { QqbotCoreModule } from '@/modules/qqbot/core/qqbot-core.module';
@Module({
imports: [
ConfigModule,
AdminAuthGuardModule,
DictModule,
TypeOrmModule.forFeature([
QqbotAccount,
QqbotAccountAbility,
QqbotAllowlist,
QqbotBlocklist,
QqbotCommand,
QqbotCommandLog,
QqbotConfig,
QqbotConversation,
QqbotDedupe,
QqbotMessage,
QqbotAccountNapcat,
QqbotNapcatContainer,
QqbotRule,
QqbotSendLog,
]),
],
controllers: [
QqbotAccountController,
QqbotCommandController,
QqbotDashboardController,
QqbotMessageController,
QqbotPermissionController,
QqbotPluginController,
QqbotRuleController,
QqbotSendController,
],
providers: [
QqbotAccountService,
QqbotBusService,
QqbotCommandEngineService,
QqbotCommandParserService,
QqbotCommandService,
QqbotConfigService,
QqbotDashboardService,
QqbotDedupeService,
QqbotEventService,
QqbotBangDreamClientService,
QqbotBangDreamPluginService,
QqbotBangDreamRendererService,
TsuguApplicationService,
QqbotFf14ClientService,
QqbotFf14MarketPluginService,
QqbotFflogsClientService,
QqbotFflogsPluginService,
QqbotMessageService,
QqbotNapcatLoginService,
QqbotNapcatWatchdogService,
QqbotNapcatContainerService,
QqbotPermissionService,
QqbotEventPluginRegistryService,
QqbotPluginRegistryService,
QqbotRepeaterPluginService,
QqbotRateLimitService,
QqbotReplyTemplateService,
QqbotReverseWsService,
QqbotRuleEngineService,
QqbotRuleService,
QqbotSendService,
],
exports: [
QqbotAccountService,
QqbotNapcatLoginService,
QqbotNapcatContainerService,
QqbotReverseWsService,
],
imports: [QqbotCoreModule],
exports: [QqbotCoreModule],
})
export class QqbotModule {}

View File

@ -14,10 +14,16 @@ const nonColumnTokens = new Set([
export type SqlSchemaContract = {
hasTable(tableName: string): boolean;
getTableColumns(tableName: string): SqlSchemaColumn[];
expectTableColumns(tableName: string, columns: readonly string[]): void;
};
const parseColumnNames = (tableBlock: string) => {
export type SqlSchemaColumn = {
definition: string;
name: string;
};
const parseColumns = (tableBlock: string): SqlSchemaColumn[] => {
return tableBlock
.split(/\r?\n/)
.map((line) => line.trim().replace(/,$/, ''))
@ -28,16 +34,21 @@ const parseColumnNames = (tableBlock: string) => {
return [];
}
return [match[1]];
return [
{
definition: line,
name: match[1],
},
];
});
};
const parseSchema = (sql: string) => {
const tables = new Map<string, Set<string>>();
const tables = new Map<string, SqlSchemaColumn[]>();
for (const match of sql.matchAll(createTableRegex)) {
const [, tableName, tableBlock] = match;
tables.set(tableName, new Set(parseColumnNames(tableBlock)));
tables.set(tableName, parseColumns(tableBlock));
}
return tables;
@ -55,12 +66,13 @@ export const readRefactorV3SqlSchema = (): SqlSchemaContract => {
const tables = parseSchema(fs.readFileSync(schemaPath, 'utf8'));
return {
getTableColumns: (tableName) => tables.get(tableName) || [],
hasTable: (tableName) => tables.has(tableName),
expectTableColumns: (tableName, columns) => {
const tableColumns = tables.get(tableName);
expect(tableColumns).toBeDefined();
expect(Array.from(tableColumns || [])).toEqual(
expect((tableColumns || []).map((column) => column.name)).toEqual(
expect.arrayContaining([...columns]),
);
},

View File

@ -1,5 +1,5 @@
jest.mock('../../../src/qqbot/qqbot.module', () => ({
QqbotModule: class QqbotModule {},
jest.mock('../../../src/modules/qqbot/core/qqbot-core.module', () => ({
QqbotCoreModule: class QqbotCoreModule {},
}));
import { MODULE_METADATA } from '@nestjs/common/constants';

View File

@ -1,5 +1,5 @@
jest.mock('../../../src/qqbot/qqbot.module', () => ({
QqbotModule: class QqbotModule {},
jest.mock('../../../src/modules/qqbot/core/qqbot-core.module', () => ({
QqbotCoreModule: class QqbotCoreModule {},
}));
import { MODULE_METADATA } from '@nestjs/common/constants';

View File

@ -0,0 +1,94 @@
jest.mock('@/qqbot/qqbot.module', () => ({
QqbotModule: class QqbotModule {},
}));
jest.mock('@/qqbot/plugin/qqbot-event-plugin-registry.service', () => ({
QqbotEventPluginRegistryService: class QqbotEventPluginRegistryService {},
}));
jest.mock(
'../../../../src/qqbot/plugin/qqbot-event-plugin-registry.service',
() => ({
QqbotEventPluginRegistryService: class QqbotEventPluginRegistryService {},
}),
);
jest.mock('@/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
jest.mock('../../../../src/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
import { QQBOT_CORE_DOMAIN_CONTRACT } from '../../../../src/modules/qqbot/core/qqbot-core.contract';
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
describe('QQBot core command contract', () => {
const schema = readRefactorV3SqlSchema();
it('keeps operation lookup, command ID binding, aliases, cooldown and parser validation explicit', () => {
expect(QQBOT_CORE_DOMAIN_CONTRACT.command).toEqual({
commandTable: 'qqbot_command',
commandIdField: 'id',
commandKeyField: 'command_key',
operationKeyField: 'operation_key',
pluginKeyField: 'plugin_key',
enabledField: 'enabled',
cooldownField: 'cooldown_seconds',
aliasTable: 'qqbot_command_alias',
aliasCommandField: 'command_id',
aliasTextField: 'alias_text',
accountBindingTable: 'qqbot_capability_binding',
accountBindingCommandField: 'capability_key',
parserValidation: {
route: 'POST /qqbot/command/test',
commandIdField: 'commandId',
fullCommandTextField: 'text',
},
});
expect(QQBOT_CORE_DOMAIN_CONTRACT.permission).toEqual({
policyTable: 'qqbot_permission_policy',
policyKeyField: 'policy_key',
scopeFields: ['scope_type', 'scope_value'],
effectField: 'effect',
ruleTable: 'qqbot_rule',
ruleCommandField: 'command_id',
matcherField: 'matcher_json',
actionField: 'action_json',
enabledField: 'enabled',
});
schema.expectTableColumns('qqbot_command', [
'id',
'operation_key',
'command_key',
'plugin_key',
'enabled',
'cooldown_seconds',
]);
schema.expectTableColumns('qqbot_command_alias', [
'id',
'command_id',
'alias_text',
]);
schema.expectTableColumns('qqbot_capability_binding', [
'id',
'account_id',
'capability_key',
'enabled',
]);
schema.expectTableColumns('qqbot_permission_policy', [
'id',
'policy_key',
'scope_type',
'scope_value',
'effect',
]);
schema.expectTableColumns('qqbot_rule', [
'id',
'rule_key',
'account_id',
'command_id',
'matcher_json',
'action_json',
'enabled',
]);
});
});

View File

@ -0,0 +1,205 @@
jest.mock('@/admin/auth/jwt-auth.guard', () => ({
JwtAuthGuard: class {
canActivate() {
return true;
}
},
}));
jest.mock('@/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
jest.mock('../../../../src/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
import type { INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { DictService } from '../../../../src/admin/dict/dict.service';
import { ToolsService } from '../../../../src/common';
import { QqbotAccountService } from '../../../../src/qqbot/account/qqbot-account.service';
import { QqbotCommandController } from '../../../../src/qqbot/command/qqbot-command.controller';
import { QqbotCommand } from '../../../../src/qqbot/command/qqbot-command.entity';
import { QqbotCommandEngineService } from '../../../../src/qqbot/command/qqbot-command-engine.service';
import { QqbotCommandLog } from '../../../../src/qqbot/command/qqbot-command-log.entity';
import { QqbotCommandParserService } from '../../../../src/qqbot/command/qqbot-command-parser.service';
import { QqbotCommandService } from '../../../../src/qqbot/command/qqbot-command.service';
import { QqbotReplyTemplateService } from '../../../../src/qqbot/command/qqbot-reply-template.service';
import { QqbotPluginRegistryService } from '../../../../src/qqbot/plugin/qqbot-plugin-registry.service';
import { QqbotSendService } from '../../../../src/qqbot/send/qqbot-send.service';
describe('QQBot core command local smoke', () => {
let app: INestApplication;
const command = {
aliases: '["查歌"]',
code: 'bangdream-song-search',
cooldownMs: 5000,
defaultParams: null,
enabled: true,
errorTemplate: null,
id: 'cmd-bangdream-song-search',
isDeleted: false,
lastHitAt: null,
name: 'BangDream 查歌',
operationKey: 'bangdream.song.search',
parserKey: 'plain',
pluginKey: 'bangdream',
prefixes: '["/"]',
priority: 100,
replyTemplate: '找到歌曲:{{output.title}}',
targetType: 'all',
} as QqbotCommand;
const queryBuilder = {
addOrderBy: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn().mockResolvedValue([[command], 1]),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
};
const commandRepository = {
createQueryBuilder: jest.fn(() => queryBuilder),
findOne: jest.fn().mockResolvedValue(command),
};
const commandLogRepository = {
create: jest.fn((payload) => payload),
save: jest.fn(),
};
const pluginRegistry = {
execute: jest.fn().mockResolvedValue({
title: 'FIRE BIRD',
type: 'text',
}),
};
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [QqbotCommandController],
providers: [
ToolsService,
QqbotCommandEngineService,
QqbotCommandParserService,
QqbotCommandService,
QqbotReplyTemplateService,
{
provide: getRepositoryToken(QqbotCommand),
useValue: commandRepository,
},
{
provide: getRepositoryToken(QqbotCommandLog),
useValue: commandLogRepository,
},
{
provide: QqbotAccountService,
useValue: {
getBoundCommandIds: jest.fn().mockResolvedValue([command.id]),
},
},
{
provide: QqbotPluginRegistryService,
useValue: pluginRegistry,
},
{
provide: QqbotSendService,
useValue: {
sendText: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('5000'),
},
},
{
provide: DictService,
useValue: {
getDictItemsByKey: jest.fn().mockResolvedValue([]),
relationTree: jest.fn().mockResolvedValue([]),
},
},
],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(() => {
jest.clearAllMocks();
});
it('queries enabled command by operationKey before previewing a full command text with commandId', async () => {
const listResponse = await request(app.getHttpServer())
.get('/qqbot/command/list')
.query({
enabled: true,
operationKey: command.operationKey,
pageNo: 1,
pageSize: 10,
})
.expect(200);
const commandId = listResponse.body.data.list[0].id;
const previewResponse = await request(app.getHttpServer())
.post('/qqbot/command/test')
.send({
commandId,
selfId: 'preview',
targetId: '10000',
targetType: 'group',
text: '/查歌 FIRE BIRD',
userId: '10000',
})
.expect(200);
const sanitizedPreview = { ...previewResponse.body.data };
delete sanitizedPreview.replyText;
expect(sanitizedPreview).toMatchObject({
command: {
id: command.id,
operationKey: command.operationKey,
},
input: {
text: 'FIRE BIRD',
},
matched: true,
status: 'success',
});
expect(queryBuilder.andWhere).toHaveBeenCalledWith(
'command.operationKey = :operationKey',
{ operationKey: command.operationKey },
);
expect(queryBuilder.andWhere).toHaveBeenCalledWith(
'command.enabled = :enabled',
{ enabled: true },
);
expect(commandRepository.findOne).toHaveBeenCalledWith({
where: { id: command.id, isDeleted: false },
});
expect(pluginRegistry.execute).toHaveBeenCalledWith(
command.pluginKey,
command.operationKey,
expect.objectContaining({
text: 'FIRE BIRD',
}),
expect.objectContaining({
command,
}),
);
});
});

View File

@ -0,0 +1,280 @@
const mockQqbotEventPluginRegistryService = class QqbotEventPluginRegistryService {};
const mockQqbotPluginRegistryService = class QqbotPluginRegistryService {};
jest.mock('@/qqbot/plugin/qqbot-event-plugin-registry.service', () => ({
QqbotEventPluginRegistryService: mockQqbotEventPluginRegistryService,
}));
jest.mock(
'../../../../src/qqbot/plugin/qqbot-event-plugin-registry.service',
() => ({
QqbotEventPluginRegistryService: mockQqbotEventPluginRegistryService,
}),
);
jest.mock('@/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: mockQqbotPluginRegistryService,
}));
jest.mock('../../../../src/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: mockQqbotPluginRegistryService,
}));
jest.mock(
'@/qqbot/plugins/bangDream/application/bangdream-client.service',
() => ({
QqbotBangDreamClientService: class QqbotBangDreamClientService {},
}),
);
jest.mock(
'@/qqbot/plugins/bangDream/application/bangdream-renderer.facade',
() => ({
QqbotBangDreamRendererService: class QqbotBangDreamRendererService {},
}),
);
jest.mock(
'@/qqbot/plugins/bangDream/application/bangdream-application.service',
() => ({
TsuguApplicationService: class TsuguApplicationService {},
}),
);
jest.mock('@/qqbot/plugins/bangDream/qqbot-bangdream.plugin', () => ({
QqbotBangDreamPluginService: class QqbotBangDreamPluginService {},
}));
jest.mock('@/qqbot/plugins/ff14Market/qqbot-ff14-client.service', () => ({
QqbotFf14ClientService: class QqbotFf14ClientService {},
}));
jest.mock('@/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin', () => ({
QqbotFf14MarketPluginService: class QqbotFf14MarketPluginService {},
}));
jest.mock('@/qqbot/plugins/fflogs/qqbot-fflogs-client.service', () => ({
QqbotFflogsClientService: class QqbotFflogsClientService {},
}));
jest.mock('@/qqbot/plugins/fflogs/qqbot-fflogs.plugin', () => ({
QqbotFflogsPluginService: class QqbotFflogsPluginService {},
}));
jest.mock('@/qqbot/plugins/repeater/qqbot-repeater.plugin', () => ({
QqbotRepeaterPluginService: class QqbotRepeaterPluginService {},
}));
import { MODULE_METADATA } from '@nestjs/common/constants';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm';
import { AdminAuthGuardModule } from '../../../../src/admin/auth/admin-auth-guard.module';
import { DictModule } from '../../../../src/admin/dict/dict.module';
import { AppModule } from '../../../../src/app.module';
import { QqbotAccountController } from '../../../../src/qqbot/account/qqbot-account.controller';
import { QqbotCommandController } from '../../../../src/qqbot/command/qqbot-command.controller';
import { QqbotDashboardController } from '../../../../src/qqbot/dashboard/qqbot-dashboard.controller';
import { QqbotMessageController } from '../../../../src/qqbot/message/qqbot-message.controller';
import { QqbotPermissionController } from '../../../../src/qqbot/permission/qqbot-permission.controller';
import { QqbotPluginController } from '../../../../src/qqbot/plugin/qqbot-plugin.controller';
import { QqbotModule } from '../../../../src/qqbot/qqbot.module';
import { QqbotRuleController } from '../../../../src/qqbot/rule/qqbot-rule.controller';
import { QqbotSendController } from '../../../../src/qqbot/send/qqbot-send.controller';
import {
QQBOT_CORE_CONTROLLERS,
QQBOT_CORE_ENTITIES,
QQBOT_CORE_EXPORTS,
QQBOT_CORE_PROVIDERS,
QqbotCoreModule,
} from '../../../../src/modules/qqbot/core/qqbot-core.module';
import {
collectControllerRoutes,
routeKey,
} from '../../../helpers/controller-route.helper';
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
const getModuleMetadata = <T>(moduleClass: unknown, key: string): T[] => {
return Reflect.getMetadata(key, moduleClass) || [];
};
const getNames = (items: unknown[]) =>
items.map((item) => (item as { name?: string }).name || `${item}`);
type EntityClass = new (...args: never[]) => unknown;
const getEntityTableName = (entity: EntityClass) => {
return getMetadataArgsStorage().tables.find(
(table) => table.target === entity,
)?.name;
};
const getEntityColumnNames = (entity: EntityClass) => {
return getMetadataArgsStorage()
.columns.filter((column) => column.target === entity)
.map((column) => `${column.options.name || column.propertyName}`);
};
const getEntityNullableColumnNames = (entity: EntityClass) => {
return getMetadataArgsStorage()
.columns.filter((column) => column.target === entity)
.filter((column) => column.options.nullable === true)
.map((column) => `${column.options.name || column.propertyName}`);
};
const isOptionalSqlColumnForEntityInsert = (definition: string) => {
return (
!/\bNOT\s+NULL\b/i.test(definition) ||
/\bDEFAULT\b/i.test(definition) ||
/\bAUTO_INCREMENT\b/i.test(definition)
);
};
describe('QQBot core module contract', () => {
const schema = readRefactorV3SqlSchema();
it('keeps QQBot Admin and runtime routes compatible through the core boundary', () => {
const routes = collectControllerRoutes(QQBOT_CORE_CONTROLLERS);
expect(routes.map(routeKey)).toEqual(
expect.arrayContaining([
'GET /qqbot/account/list',
'GET /qqbot/account/enabled',
'POST /qqbot/account/scan/create',
'GET /qqbot/account/scan/status',
'POST /qqbot/account/bind/command',
'POST /qqbot/account/unbind/command',
'POST /qqbot/account/kick',
'GET /qqbot/command/list',
'POST /qqbot/command/test',
'GET /qqbot/conversation/list',
'GET /qqbot/message/list',
'GET /qqbot/permission/config',
'GET /qqbot/permission/allowlist',
'GET /qqbot/permission/blocklist',
'GET /qqbot/plugin/list',
'GET /qqbot/rule/list',
'GET /qqbot/send/log/list',
'POST /qqbot/send/private',
'POST /qqbot/send/group',
'GET /qqbot/dashboard/summary',
]),
);
});
it('routes QQBot through the core module as the owning Nest boundary', () => {
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).toEqual(
expect.arrayContaining([QqbotCoreModule]),
);
expect(getModuleMetadata(AppModule, MODULE_METADATA.IMPORTS)).not.toEqual(
expect.arrayContaining([QqbotModule]),
);
const coreImports = getModuleMetadata(
QqbotCoreModule,
MODULE_METADATA.IMPORTS,
);
expect(coreImports).toEqual(
expect.arrayContaining([ConfigModule, AdminAuthGuardModule, DictModule]),
);
expect(
coreImports.some(
(item) => (item as { module?: unknown }).module === TypeOrmModule,
),
).toBe(true);
expect(coreImports).not.toEqual(expect.arrayContaining([QqbotModule]));
expect(
getModuleMetadata(QqbotCoreModule, MODULE_METADATA.CONTROLLERS),
).toEqual(expect.arrayContaining(QQBOT_CORE_CONTROLLERS));
expect(
getModuleMetadata(QqbotCoreModule, MODULE_METADATA.PROVIDERS),
).toEqual(expect.arrayContaining(QQBOT_CORE_PROVIDERS));
expect(getModuleMetadata(QqbotCoreModule, MODULE_METADATA.EXPORTS)).toEqual(
expect.arrayContaining(QQBOT_CORE_EXPORTS),
);
expect(getModuleMetadata(QqbotModule, MODULE_METADATA.IMPORTS)).toEqual(
expect.arrayContaining([QqbotCoreModule]),
);
expect(getModuleMetadata(QqbotModule, MODULE_METADATA.EXPORTS)).toEqual(
expect.arrayContaining([QqbotCoreModule]),
);
});
it('makes the legacy QQBot controllers, providers and entities explicit', () => {
expect(QQBOT_CORE_CONTROLLERS).toEqual(
expect.arrayContaining([
QqbotAccountController,
QqbotCommandController,
QqbotDashboardController,
QqbotMessageController,
QqbotPermissionController,
QqbotPluginController,
QqbotRuleController,
QqbotSendController,
]),
);
expect(getNames(QQBOT_CORE_PROVIDERS)).toEqual(
expect.arrayContaining([
'QqbotAccountService',
'QqbotBusService',
'QqbotCommandEngineService',
'QqbotCommandParserService',
'QqbotCommandService',
'QqbotConfigService',
'QqbotDashboardService',
'QqbotDedupeService',
'QqbotEventPluginRegistryService',
'QqbotEventService',
'QqbotMessageService',
'QqbotNapcatContainerService',
'QqbotNapcatLoginService',
'QqbotNapcatWatchdogService',
'QqbotPermissionService',
'QqbotPluginRegistryService',
'QqbotRateLimitService',
'QqbotReplyTemplateService',
'QqbotReverseWsService',
'QqbotRuleEngineService',
'QqbotRuleService',
'QqbotSendService',
]),
);
expect(getNames(QQBOT_CORE_ENTITIES)).toEqual(
expect.arrayContaining([
'QqbotAccount',
'QqbotAccountAbility',
'QqbotAccountNapcat',
'QqbotAllowlist',
'QqbotBlocklist',
'QqbotCommand',
'QqbotCommandLog',
'QqbotConfig',
'QqbotConversation',
'QqbotDedupe',
'QqbotMessage',
'QqbotNapcatContainer',
'QqbotRule',
'QqbotSendLog',
]),
);
});
it('keeps every registered QQBot core entity mapped to the refactor-v3 schema', () => {
for (const entity of QQBOT_CORE_ENTITIES) {
const tableName = getEntityTableName(entity);
const columns = getEntityColumnNames(entity);
const nullableColumns = getEntityNullableColumnNames(entity);
expect(tableName).toBeTruthy();
expect(schema.hasTable(tableName || '')).toBe(true);
schema.expectTableColumns(tableName || '', columns);
const sqlColumns = schema.getTableColumns(tableName || '');
const entityColumnNames = new Set(columns);
const requiredSqlOnlyColumns = sqlColumns
.filter((column) => !entityColumnNames.has(column.name))
.filter(
(column) => !isOptionalSqlColumnForEntityInsert(column.definition),
)
.map((column) => `${tableName}.${column.name}`);
expect(requiredSqlOnlyColumns).toEqual([]);
for (const nullableColumn of nullableColumns) {
const sqlColumn = sqlColumns.find(
(column) => column.name === nullableColumn,
);
expect(sqlColumn?.definition).not.toMatch(/\bNOT\s+NULL\b/i);
}
}
});
});

View File

@ -0,0 +1,86 @@
jest.mock('@/qqbot/qqbot.module', () => ({
QqbotModule: class QqbotModule {},
}));
jest.mock('@/qqbot/plugin/qqbot-event-plugin-registry.service', () => ({
QqbotEventPluginRegistryService: class QqbotEventPluginRegistryService {},
}));
jest.mock(
'../../../../src/qqbot/plugin/qqbot-event-plugin-registry.service',
() => ({
QqbotEventPluginRegistryService: class QqbotEventPluginRegistryService {},
}),
);
jest.mock('@/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
jest.mock('../../../../src/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
import { QQBOT_CORE_DOMAIN_CONTRACT } from '../../../../src/modules/qqbot/core/qqbot-core.contract';
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
describe('QQBot core send contract', () => {
const schema = readRefactorV3SqlSchema();
it('keeps send queue reservation, rate limit, send log and dedupe event contracts explicit', () => {
expect(QQBOT_CORE_DOMAIN_CONTRACT.messageSend).toEqual({
conversationTable: 'qqbot_conversation',
messageTable: 'qqbot_message',
sendQueueTable: 'qqbot_send_task',
queueTaskKeyField: 'task_key',
queueStatusField: 'status',
queuePayloadField: 'payload_json',
queueReservedAtField: 'reserved_at',
queueSentAtField: 'sent_at',
sendLogTable: 'qqbot_send_log',
sendLogStatusField: 'status',
sendLogSafeSummaryField: 'safe_summary',
dedupeTable: 'qqbot_dedupe_event',
dedupeKeyField: 'dedupe_key',
dedupeExpiresAtField: 'expires_at',
rateLimitProvider: 'QqbotRateLimitService',
});
schema.expectTableColumns('qqbot_conversation', [
'id',
'account_id',
'conversation_type',
'conversation_key',
]);
schema.expectTableColumns('qqbot_message', [
'id',
'account_id',
'conversation_id',
'message_id',
'direction',
'message_type',
'raw_payload',
]);
schema.expectTableColumns('qqbot_send_task', [
'id',
'account_id',
'conversation_id',
'task_key',
'status',
'payload_json',
'reserved_at',
'sent_at',
'last_error',
]);
schema.expectTableColumns('qqbot_send_log', [
'id',
'task_id',
'account_id',
'status',
'safe_summary',
'error_message',
]);
schema.expectTableColumns('qqbot_dedupe_event', [
'id',
'dedupe_key',
'account_id',
'expires_at',
]);
});
});

View File

@ -0,0 +1,58 @@
jest.mock('@/qqbot/qqbot.module', () => ({
QqbotModule: class QqbotModule {},
}));
jest.mock('@/qqbot/plugin/qqbot-event-plugin-registry.service', () => ({
QqbotEventPluginRegistryService: class QqbotEventPluginRegistryService {},
}));
jest.mock(
'../../../../src/qqbot/plugin/qqbot-event-plugin-registry.service',
() => ({
QqbotEventPluginRegistryService: class QqbotEventPluginRegistryService {},
}),
);
jest.mock('@/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
jest.mock('../../../../src/qqbot/plugin/qqbot-plugin-registry.service', () => ({
QqbotPluginRegistryService: class QqbotPluginRegistryService {},
}));
import { QQBOT_CORE_DOMAIN_CONTRACT } from '../../../../src/modules/qqbot/core/qqbot-core.contract';
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
describe('QQBot core status contract', () => {
const schema = readRefactorV3SqlSchema();
it('keeps OneBot connection, container, WebUI and QQ login status as separate account fields', () => {
expect(QQBOT_CORE_DOMAIN_CONTRACT.status).toEqual({
accountTable: 'qqbot_account',
oneBotField: 'onebot_status',
containerField: 'container_status',
webuiField: 'webui_status',
qqLoginField: 'qq_login_status',
lastErrorField: 'last_error',
connectionSessionTable: 'qqbot_connection_session',
sessionStatusField: 'status',
closeReasonField: 'close_reason',
});
schema.expectTableColumns('qqbot_account', [
'id',
'self_id',
'onebot_status',
'container_status',
'webui_status',
'qq_login_status',
'last_error',
]);
schema.expectTableColumns('qqbot_connection_session', [
'id',
'account_id',
'session_key',
'status',
'connected_at',
'disconnected_at',
'close_reason',
]);
});
});

View File

@ -1,5 +1,5 @@
jest.mock('../../../src/qqbot/qqbot.module', () => ({
QqbotModule: class QqbotModule {},
jest.mock('../../../src/modules/qqbot/core/qqbot-core.module', () => ({
QqbotCoreModule: class QqbotCoreModule {},
}));
import { MODULE_METADATA } from '@nestjs/common/constants';