diff --git a/docs/refactor-v3/schema-map.md b/docs/refactor-v3/schema-map.md index a8c652d..f532060 100644 --- a/docs/refactor-v3/schema-map.md +++ b/docs/refactor-v3/schema-map.md @@ -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. | diff --git a/docs/superpowers/plans/2026-06-15-api-admin-full-refactor-v3.md b/docs/superpowers/plans/2026-06-15-api-admin-full-refactor-v3.md index 913da47..ebd687c 100644 --- a/docs/superpowers/plans/2026-06-15-api-admin-full-refactor-v3.md +++ b/docs/superpowers/plans/2026-06-15-api-admin-full-refactor-v3.md @@ -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:/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:/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 diff --git a/sql/refactor-v3/00-full-schema.sql b/sql/refactor-v3/00-full-schema.sql index d6e6076..82afacc 100644 --- a/sql/refactor-v3/00-full-schema.sql +++ b/sql/refactor-v3/00-full-schema.sql @@ -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, diff --git a/src/app.module.ts b/src/app.module.ts index 2f07bd3..f66feb5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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, diff --git a/src/modules/qqbot/core/qqbot-core.contract.ts b/src/modules/qqbot/core/qqbot-core.contract.ts new file mode 100644 index 0000000..62ed557 --- /dev/null +++ b/src/modules/qqbot/core/qqbot-core.contract.ts @@ -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; diff --git a/src/modules/qqbot/core/qqbot-core.module.ts b/src/modules/qqbot/core/qqbot-core.module.ts new file mode 100644 index 0000000..e0d7190 --- /dev/null +++ b/src/modules/qqbot/core/qqbot-core.module.ts @@ -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 {} diff --git a/src/qqbot/qqbot.module.ts b/src/qqbot/qqbot.module.ts index e4df845..1d10d12 100644 --- a/src/qqbot/qqbot.module.ts +++ b/src/qqbot/qqbot.module.ts @@ -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 {} diff --git a/test/helpers/sql-schema.helper.ts b/test/helpers/sql-schema.helper.ts index dac5276..8ff5a8b 100644 --- a/test/helpers/sql-schema.helper.ts +++ b/test/helpers/sql-schema.helper.ts @@ -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>(); + const tables = new Map(); 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]), ); }, diff --git a/test/modules/asset/asset-module-contract.spec.ts b/test/modules/asset/asset-module-contract.spec.ts index 316f4eb..7cbd3a0 100644 --- a/test/modules/asset/asset-module-contract.spec.ts +++ b/test/modules/asset/asset-module-contract.spec.ts @@ -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'; diff --git a/test/modules/blog/blog-module-contract.spec.ts b/test/modules/blog/blog-module-contract.spec.ts index a94aa2a..ce34d13 100644 --- a/test/modules/blog/blog-module-contract.spec.ts +++ b/test/modules/blog/blog-module-contract.spec.ts @@ -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'; diff --git a/test/modules/qqbot/core/qqbot-core-command-contract.spec.ts b/test/modules/qqbot/core/qqbot-core-command-contract.spec.ts new file mode 100644 index 0000000..bf0262a --- /dev/null +++ b/test/modules/qqbot/core/qqbot-core-command-contract.spec.ts @@ -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', + ]); + }); +}); diff --git a/test/modules/qqbot/core/qqbot-core-command-smoke.spec.ts b/test/modules/qqbot/core/qqbot-core-command-smoke.spec.ts new file mode 100644 index 0000000..27bb7c4 --- /dev/null +++ b/test/modules/qqbot/core/qqbot-core-command-smoke.spec.ts @@ -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, + }), + ); + }); +}); diff --git a/test/modules/qqbot/core/qqbot-core-module-contract.spec.ts b/test/modules/qqbot/core/qqbot-core-module-contract.spec.ts new file mode 100644 index 0000000..b0eb096 --- /dev/null +++ b/test/modules/qqbot/core/qqbot-core-module-contract.spec.ts @@ -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 = (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); + } + } + }); +}); diff --git a/test/modules/qqbot/core/qqbot-core-send-contract.spec.ts b/test/modules/qqbot/core/qqbot-core-send-contract.spec.ts new file mode 100644 index 0000000..82dd3d9 --- /dev/null +++ b/test/modules/qqbot/core/qqbot-core-send-contract.spec.ts @@ -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', + ]); + }); +}); diff --git a/test/modules/qqbot/core/qqbot-core-status-contract.spec.ts b/test/modules/qqbot/core/qqbot-core-status-contract.spec.ts new file mode 100644 index 0000000..4f5eca7 --- /dev/null +++ b/test/modules/qqbot/core/qqbot-core-status-contract.spec.ts @@ -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', + ]); + }); +}); diff --git a/test/modules/wordpress/wordpress-module-contract.spec.ts b/test/modules/wordpress/wordpress-module-contract.spec.ts index 9d4c828..3a61644 100644 --- a/test/modules/wordpress/wordpress-module-contract.spec.ts +++ b/test/modules/wordpress/wordpress-module-contract.spec.ts @@ -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';