Compare commits
No commits in common. "fd22836c23e8443ddf924aa6a9433c5d0f42dad1" and "cde9638227600a5af429ba86fa10e0a13953d626" have entirely different histories.
fd22836c23
...
cde9638227
80
API.md
@ -52,20 +52,20 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
|
|
||||||
## Runtime Health
|
## Runtime Health
|
||||||
|
|
||||||
| 方法 | 路径 | 认证 | 说明 |
|
| 方法 | 路径 | 认证 | 说明 |
|
||||||
| ----- | ----------------- | ---- | ---------------------------- |
|
| ----- | ----------------- | ---- | ---------------------------------- |
|
||||||
| `GET` | `/health/runtime` | 否 | API 运行时健康和配置检查状态 |
|
| `GET` | `/health/runtime` | 否 | API 运行时健康和配置检查状态 |
|
||||||
|
|
||||||
该接口返回 plain JSON,不使用 Vben 响应包装,供本地 smoke、Jenkins/K8s 和 ktWorkflow 观测脚本直接读取。接口位于 Swagger 基础能力分组 `/api/basic`。
|
该接口返回 plain JSON,不使用 Vben 响应包装,供本地 smoke、Jenkins/K8s 和 ktWorkflow 观测脚本直接读取。接口位于 Swagger 基础能力分组 `/api/basic`。
|
||||||
|
|
||||||
顶层字段:
|
顶层字段:
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
| ----------- | ---------------------------------------- |
|
| ----------- | ------------------------------------------------ |
|
||||||
| `service` | 固定为 `kt-template-online-api` |
|
| `service` | 固定为 `kt-template-online-api` |
|
||||||
| `checkedAt` | ISO 时间字符串 |
|
| `checkedAt` | ISO 时间字符串 |
|
||||||
| `status` | `live`、`ready`、`degraded` 或 `blocked` |
|
| `status` | `live`、`ready`、`degraded` 或 `blocked` |
|
||||||
| `checks` | 进程和配置检查列表 |
|
| `checks` | 进程和配置检查列表 |
|
||||||
|
|
||||||
公开响应不返回数据库、WordPress、Loki、NapCat SSH 等运行拓扑配置快照;配置检查只暴露 key 级别、是否存在和缺失说明。
|
公开响应不返回数据库、WordPress、Loki、NapCat SSH 等运行拓扑配置快照;配置检查只暴露 key 级别、是否存在和缺失说明。
|
||||||
|
|
||||||
@ -78,19 +78,19 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
|
|
||||||
## 环境变量分组
|
## 环境变量分组
|
||||||
|
|
||||||
| 分组 | 关键变量 |
|
| 分组 | 关键变量 |
|
||||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| MySQL | `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`、`DB_DATABASE`、`DB_SYNC` |
|
| MySQL | `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`、`DB_DATABASE`、`DB_SYNC` |
|
||||||
| MinIO | `MINIO_ENDPOINT`、`MINIO_PORT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`MINIO_BUCKET` |
|
| MinIO | `MINIO_ENDPOINT`、`MINIO_PORT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`MINIO_BUCKET` |
|
||||||
| Admin | `ADMIN_TOKEN_SECRET`、`ADMIN_COOKIE_SECURE`、`SNOWFLAKE_WORKER_ID`、`SNOWFLAKE_DATACENTER_ID` |
|
| Admin | `ADMIN_TOKEN_SECRET`、`ADMIN_COOKIE_SECURE`、`SNOWFLAKE_WORKER_ID`、`SNOWFLAKE_DATACENTER_ID` |
|
||||||
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD` |
|
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD` |
|
||||||
| Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_QUERY_SELECTOR` |
|
| Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_QUERY_SELECTOR` |
|
||||||
| QQBot | `QQBOT_ENABLED`、`QQBOT_ACCOUNT_SECRET_KEY`、`QQBOT_REVERSE_WS_PATH`、`QQBOT_REVERSE_WS_TOKEN`、`QQBOT_EVENT_BUS`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*` |
|
| QQBot | `QQBOT_ENABLED`、`QQBOT_ACCOUNT_SECRET_KEY`、`QQBOT_REVERSE_WS_PATH`、`QQBOT_REVERSE_WS_TOKEN`、`QQBOT_EVENT_BUS`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*` |
|
||||||
| NapCat | `NAPCAT_WEBUI_BASE_URL`、`NAPCAT_WEBUI_TOKEN`、`QQBOT_NAPCAT_*` |
|
| NapCat | `NAPCAT_WEBUI_BASE_URL`、`NAPCAT_WEBUI_TOKEN`、`QQBOT_NAPCAT_*` |
|
||||||
| MQTT | `MQTT_URL`、`MQTT_USERNAME`、`MQTT_PASSWORD`、`MQTT_CLIENT_ID` |
|
| MQTT | `MQTT_URL`、`MQTT_USERNAME`、`MQTT_PASSWORD`、`MQTT_CLIENT_ID` |
|
||||||
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
||||||
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_DEFAULT_WORLD` |
|
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_DEFAULT_WORLD` |
|
||||||
| FFLogs | `FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
| FFLogs | `FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
||||||
|
|
||||||
真实密码、Token、OAuth secret 和生产 env 不提交到 Git。
|
真实密码、Token、OAuth secret 和生产 env 不提交到 Git。
|
||||||
|
|
||||||
@ -345,12 +345,8 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
|||||||
|
|
||||||
扫码链路返回 `sessionId`,前端应使用 SSE 查看步骤进度,而不是等待长 HTTP 请求完成。已有账号的更新登录会先重启目标 NapCat 容器尝试 `ACCOUNT`/`-q` 快速登录;目标账号在线则直接完成会话。没有历史登录态的新容器会跳过快速登录,优先尝试保存的登录密码;快速登录失败后,如果账号保存了登录密码,会临时注入 `NAPCAT_QUICK_PASSWORD` 并按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询密码登录结果,准备阶段的扫码会话会持续续期,避免后台密码登录未完成时前端先判过期。密码登录触发 QQ 安全验证时,接口返回的 `captchaUrl` 只用于前端拉起腾讯验证码;前端必须把腾讯验证码返回的 `ticket`、`randstr`、`sid` 连同 `sessionId` 提交到 `/qqbot/account/scan/captcha/submit`,后端再代理到同一 NapCat 容器的 `/api/QQLogin/CaptchaLogin` 继续密码登录第二步。会话已有 `captchaUrl` 后,`/qqbot/account/scan/status` 遇到 NapCat 继续返回“需要验证码/继续完成验证/安全验证”但不带 URL 时仍保持 `pending` 和原 `captchaUrl`。密码登录成功后会重建容器移除该运行态密码,清理失败则本次登录失败;密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,再进入重置登录态和二维码兜底流程。看门狗自动登录使用同样的 quick -> password 顺序,但不会自动进入扫码阶段。
|
扫码链路返回 `sessionId`,前端应使用 SSE 查看步骤进度,而不是等待长 HTTP 请求完成。已有账号的更新登录会先重启目标 NapCat 容器尝试 `ACCOUNT`/`-q` 快速登录;目标账号在线则直接完成会话。没有历史登录态的新容器会跳过快速登录,优先尝试保存的登录密码;快速登录失败后,如果账号保存了登录密码,会临时注入 `NAPCAT_QUICK_PASSWORD` 并按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询密码登录结果,准备阶段的扫码会话会持续续期,避免后台密码登录未完成时前端先判过期。密码登录触发 QQ 安全验证时,接口返回的 `captchaUrl` 只用于前端拉起腾讯验证码;前端必须把腾讯验证码返回的 `ticket`、`randstr`、`sid` 连同 `sessionId` 提交到 `/qqbot/account/scan/captcha/submit`,后端再代理到同一 NapCat 容器的 `/api/QQLogin/CaptchaLogin` 继续密码登录第二步。会话已有 `captchaUrl` 后,`/qqbot/account/scan/status` 遇到 NapCat 继续返回“需要验证码/继续完成验证/安全验证”但不带 URL 时仍保持 `pending` 和原 `captchaUrl`。密码登录成功后会重建容器移除该运行态密码,清理失败则本次登录失败;密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,再进入重置登录态和二维码兜底流程。看门狗自动登录使用同样的 quick -> password 顺序,但不会自动进入扫码阶段。
|
||||||
|
|
||||||
密码验证码通过后如果 NapCat 返回 `needNewDevice`,后端不会只把 `jumpUrl` 透给 Admin,而是在同一会话中继续调用 `/api/QQLogin/GetNewDeviceQRCode` 生成新设备验证二维码;`/qqbot/account/scan/status` 后续轮询会代理 `/api/QQLogin/PollNewDeviceQR`,状态映射为 `newDeviceStatus=qr-pending|scanned|confirming|verified|expired|failed`,进入确认态后再调用 `/api/QQLogin/NewDeviceLogin` 并回到密码登录完成检查。扫码会话结果新增 `newDeviceQrcode`、`newDeviceStatus`、`deviceVerifyUrl` 字段;`captchaUrl` 和 `newDeviceQrcode` 分别表示腾讯安全验证码和 QQ 新设备验证二维码,前端必须分开展示。SSE 进度文案包含快速登录、密码登录、验证码、新设备二维码、已扫码、确认中、登录成功/失败和运行态清理失败。
|
|
||||||
|
|
||||||
同一 QQ 账号只保留一个有效 NapCat 主容器。扫码后如果已有账号绑定到新容器,后端会释放旧绑定和未共享的旧容器,避免同账号多实例互相挤下线。OneBot notice 只有机器人下线、登录失效、`KickedOffLine` 等账号级信号才会记录 QQ 登录态异常并生成 `qqbot.account.offline` 站内信,普通群成员 kick 不属于账号离线信号。下线原因写入 `lastError` 前按 `last_error` 500 字符列宽截断;后续无错误的普通断连只更新 OneBot 连接状态,不清空该原因。账号列表会按近期缓存检查绑定 NapCat 容器的最新登录状态日志,日志检测默认 5 秒超时;`isOnline:false` 属于 QQ 登录态离线信号;心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态。托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像。
|
同一 QQ 账号只保留一个有效 NapCat 主容器。扫码后如果已有账号绑定到新容器,后端会释放旧绑定和未共享的旧容器,避免同账号多实例互相挤下线。OneBot notice 只有机器人下线、登录失效、`KickedOffLine` 等账号级信号才会记录 QQ 登录态异常并生成 `qqbot.account.offline` 站内信,普通群成员 kick 不属于账号离线信号。下线原因写入 `lastError` 前按 `last_error` 500 字符列宽截断;后续无错误的普通断连只更新 OneBot 连接状态,不清空该原因。账号列表会按近期缓存检查绑定 NapCat 容器的最新登录状态日志,日志检测默认 5 秒超时;`isOnline:false` 属于 QQ 登录态离线信号;心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态。托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像。
|
||||||
|
|
||||||
托管 NapCat 容器按账号持久化设备身份,`napcat_device_identity` 保存账号对应的数据目录、hostname、machine-id 路径、MAC 地址、验证状态和最近登录证据。重建同一账号容器时会复用这些设备字段,并在 Docker run 中注入 `--hostname`、`--mac-address` 和只读 `/etc/machine-id` 挂载,降低每次重建都被 QQ 判定为新设备的概率。
|
|
||||||
|
|
||||||
外发消息不直接抢发:后端会按 `QQBOT_SEND_GLOBAL_INTERVAL_MS`、`QQBOT_SEND_TARGET_INTERVAL_MS` 和 `QQBOT_SEND_JITTER_MS` 预约发送窗口,默认全局 2500ms、同会话 8000ms、抖动 0-800ms;如果等待超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS`,本次发送会在下发前被拒绝。在线命令和自动回复规则会叠加运行时保底冷却,默认命令 5000ms、规则 30000ms;复读机默认连续 4 次相同普通文本才触发,同一会话默认 10 分钟内只复读一次,并限制普通文本长度,减少自动行为被风控识别的概率。
|
外发消息不直接抢发:后端会按 `QQBOT_SEND_GLOBAL_INTERVAL_MS`、`QQBOT_SEND_TARGET_INTERVAL_MS` 和 `QQBOT_SEND_JITTER_MS` 预约发送窗口,默认全局 2500ms、同会话 8000ms、抖动 0-800ms;如果等待超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS`,本次发送会在下发前被拒绝。在线命令和自动回复规则会叠加运行时保底冷却,默认命令 5000ms、规则 30000ms;复读机默认连续 4 次相同普通文本才触发,同一会话默认 10 分钟内只复读一次,并限制普通文本长度,减少自动行为被风控识别的概率。
|
||||||
|
|
||||||
### Command / Rule / Permission
|
### Command / Rule / Permission
|
||||||
@ -411,36 +407,6 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
|||||||
| `GET` | `/qqbot/conversation/list` | 会话列表 |
|
| `GET` | `/qqbot/conversation/list` | 会话列表 |
|
||||||
| `GET` | `/qqbot/message/list` | 消息列表 |
|
| `GET` | `/qqbot/message/list` | 消息列表 |
|
||||||
|
|
||||||
### Plugin Platform
|
|
||||||
|
|
||||||
插件平台使用统一 `plugin.json` manifest 描述插件 key、版本、入口、操作、事件、权限和运行预算;后端会校验路径必须留在插件包内,权限必须命中白名单,安装包 content hash 必须与 manifest 匹配。CLI 入口:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm qqbot-plugin create <pluginKey>
|
|
||||||
pnpm qqbot-plugin validate <pluginDir>
|
|
||||||
pnpm qqbot-plugin pack <pluginDir>
|
|
||||||
pnpm qqbot-plugin install-local <packageFile>
|
|
||||||
```
|
|
||||||
|
|
||||||
平台管理接口:
|
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
|
||||||
| ------ | ----------------------------------------- | ---------------------------------- |
|
|
||||||
| `GET` | `/qqbot/plugin-platform/installations` | 插件安装记录,支持 key/status 过滤 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/upload` | 上传插件包并返回校验摘要 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/validate` | 校验 manifest JSON |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/install` | 按上传包安装插件版本 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/install-local` | 按本地包路径安装插件版本 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/enable` | 启用插件安装 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/disable` | 禁用插件安装 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/upgrade` | 升级插件安装版本 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/uninstall` | 卸载插件安装 |
|
|
||||||
| `POST` | `/qqbot/plugin-platform/config` | 保存插件配置 |
|
|
||||||
| `GET` | `/qqbot/plugin-platform/runtime-events` | 查询插件运行事件 |
|
|
||||||
| `GET` | `/qqbot/plugin-platform/account-bindings` | 查询插件账号绑定 |
|
|
||||||
|
|
||||||
`src/modules/qqbot/plugin-platform/runtime` 当前提供 host-side driver 边界和超时/崩溃事件归档;实际 worker/child-process driver 可以在后续批次接入,但插件侧只能通过受控 SDK 访问发送队列、配置、存储、HTTP、资产和事件上下文。
|
|
||||||
|
|
||||||
### OneBot Reverse WebSocket
|
### OneBot Reverse WebSocket
|
||||||
|
|
||||||
`QQBOT_REVERSE_WS_PATH` 默认是 `/qqbot/onebot/reverse`。NapCat 通过反向 WS 连接 API,token 使用 `QQBOT_REVERSE_WS_TOKEN`。
|
`QQBOT_REVERSE_WS_PATH` 默认是 `/qqbot/onebot/reverse`。NapCat 通过反向 WS 连接 API,token 使用 `QQBOT_REVERSE_WS_TOKEN`。
|
||||||
@ -449,7 +415,7 @@ pnpm qqbot-plugin install-local <packageFile>
|
|||||||
|
|
||||||
### BangDream
|
### BangDream
|
||||||
|
|
||||||
插件 key:`bangdream`。旧 `bangDream` 作为兼容别名仍可解析;当前源码根目录为 `src/modules/qqbot/plugins/bangDream`,不再使用旧 `tsugu` 子目录。
|
插件 key:`bangDream`。当前源码根目录为 `src/qqbot/plugins/bangDream`,不再使用旧 `tsugu` 子目录。
|
||||||
|
|
||||||
| operation key | 命令 | 说明 |
|
| operation key | 命令 | 说明 |
|
||||||
| ----------------------------- | ------------- | ------------------------ |
|
| ----------------------------- | ------------- | ------------------------ |
|
||||||
@ -473,7 +439,7 @@ pnpm qqbot-plugin install-local <packageFile>
|
|||||||
|
|
||||||
### FF14 Market
|
### FF14 Market
|
||||||
|
|
||||||
插件 key:`ff14-market`。旧 `ff14Market` 作为兼容别名仍可解析。
|
插件 key:`ff14Market`。
|
||||||
|
|
||||||
| operation key | 说明 |
|
| operation key | 说明 |
|
||||||
| ------------------- | ------------------------------------------ |
|
| ------------------- | ------------------------------------------ |
|
||||||
|
|||||||
55
README.md
@ -16,18 +16,17 @@
|
|||||||
|
|
||||||
## 功能模块
|
## 功能模块
|
||||||
|
|
||||||
| 模块 | 说明 |
|
| 模块 | 说明 |
|
||||||
| ------------------------------- | ------------------------------------------------------------------------------------------ |
|
| -------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| `admin` | Vben Admin 认证、用户、菜单、角色、部门、时区、字典、组件模板、系统日志 |
|
| `admin` | Vben Admin 认证、用户、菜单、角色、部门、时区、字典、组件模板、系统日志 |
|
||||||
| `blog` | 本地博客文章、分类、标签、Argon 主题配置和 WordPress 导入 |
|
| `blog` | 本地博客文章、分类、标签、Argon 主题配置和 WordPress 导入 |
|
||||||
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
| `wordpress` | WordPress REST 代理、登录态透传、文章/分类/标签/主题配置 |
|
||||||
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志和插件平台 |
|
| `qqbot` | QQBot 账号、NapCat 扫码登录、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志 |
|
||||||
| `modules/qqbot/plugin-platform` | QQBot 插件 manifest 校验、版本安装、运行事件、受控 SDK 和 CLI 脚手架 |
|
| `qqbot/plugins/bangDream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
|
||||||
| `qqbot/plugins/bangDream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 |
|
| `qqbot/plugins/ff14Market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
|
||||||
| `qqbot/plugins/ff14Market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 |
|
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
|
||||||
| `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 |
|
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
|
||||||
| `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 |
|
| `common` | 响应封装、异常过滤、请求日志、日期格式化、字典解码、Snowflake、工具服务 |
|
||||||
| `common` | 响应封装、异常过滤、请求日志、日期格式化、字典解码、Snowflake、工具服务 |
|
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
@ -37,7 +36,6 @@ src/
|
|||||||
blog/ 本地博客内容与主题配置
|
blog/ 本地博客内容与主题配置
|
||||||
common/ 全局装饰器、过滤器、拦截器、logger、工具和类型
|
common/ 全局装饰器、过滤器、拦截器、logger、工具和类型
|
||||||
minio/ MinIO 文件服务
|
minio/ MinIO 文件服务
|
||||||
modules/ 第三期重构后的业务边界模块
|
|
||||||
qqbot/ QQBot 运行态、管理接口和插件生态
|
qqbot/ QQBot 运行态、管理接口和插件生态
|
||||||
wordpress/ WordPress REST 代理
|
wordpress/ WordPress REST 代理
|
||||||
app.module.ts
|
app.module.ts
|
||||||
@ -55,17 +53,17 @@ ci/ Jenkins Agent/Docker 辅助文件
|
|||||||
|
|
||||||
主要配置分组:
|
主要配置分组:
|
||||||
|
|
||||||
| 分组 | 变量 |
|
| 分组 | 变量 |
|
||||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| MySQL | `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`、`DB_DATABASE`、`DB_SYNC` |
|
| MySQL | `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`、`DB_DATABASE`、`DB_SYNC` |
|
||||||
| MinIO | `MINIO_ENDPOINT`、`MINIO_PORT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`MINIO_BUCKET` |
|
| MinIO | `MINIO_ENDPOINT`、`MINIO_PORT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`MINIO_BUCKET` |
|
||||||
| Admin | `ADMIN_TOKEN_SECRET`、`ADMIN_COOKIE_SECURE`、`SNOWFLAKE_WORKER_ID`、`SNOWFLAKE_DATACENTER_ID` |
|
| Admin | `ADMIN_TOKEN_SECRET`、`ADMIN_COOKIE_SECURE`、`SNOWFLAKE_WORKER_ID`、`SNOWFLAKE_DATACENTER_ID` |
|
||||||
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD`、`WORDPRESS_*_TIMEOUT_MS` |
|
| WordPress | `WORDPRESS_BASE_URL`、`WORDPRESS_HOST_HEADER`、`WORDPRESS_ADMIN_USERNAME`、`WORDPRESS_ADMIN_PASSWORD`、`WORDPRESS_*_TIMEOUT_MS` |
|
||||||
| Logging/Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_*` |
|
| Logging/Loki | `LOG_LEVEL`、`LOG_APP_NAME`、`LOKI_URL`、`LOKI_QUERY_HOST`、`LOKI_*` |
|
||||||
| QQBot/NapCat | `QQBOT_ENABLED`、`QQBOT_ACCOUNT_SECRET_KEY`、`QQBOT_REVERSE_WS_*`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*`、`NAPCAT_*`、`QQBOT_NAPCAT_*`、`MQTT_*` |
|
| QQBot/NapCat | `QQBOT_ENABLED`、`QQBOT_ACCOUNT_SECRET_KEY`、`QQBOT_REVERSE_WS_*`、`QQBOT_SEND_*`、`QQBOT_COMMAND_MIN_COOLDOWN_MS`、`QQBOT_RULE_MIN_COOLDOWN_MS`、`QQBOT_REPEATER_*`、`NAPCAT_*`、`QQBOT_NAPCAT_*`、`MQTT_*` |
|
||||||
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
| BangDream | `BANGDREAM_TSUGU_MAIN_SERVER`、`BANGDREAM_TSUGU_DISPLAYED_SERVERS`、`BANGDREAM_TSUGU_CACHE_ROOT` |
|
||||||
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_MARKET_CACHE_TTL_MS` |
|
| FF14 Market | `FF14_XIVAPI_BASE_URL`、`FF14_UNIVERSALIS_BASE_URL`、`FF14_MARKET_CACHE_TTL_MS` |
|
||||||
| FFLogs | `FFLOGS_BASE_URL`、`FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
| FFLogs | `FFLOGS_BASE_URL`、`FFLOGS_GRAPHQL_URL`、`FFLOGS_TOKEN_URL`、`FFLOGS_CLIENT_ID`、`FFLOGS_CLIENT_SECRET` |
|
||||||
|
|
||||||
`DB_SYNC=true` 只适合本地开发或明确允许自动同步表结构的环境;生产应关闭并使用 SQL/迁移脚本。
|
`DB_SYNC=true` 只适合本地开发或明确允许自动同步表结构的环境;生产应关闭并使用 SQL/迁移脚本。
|
||||||
|
|
||||||
@ -87,10 +85,6 @@ pnpm run typecheck
|
|||||||
pnpm run lint
|
pnpm run lint
|
||||||
pnpm test
|
pnpm test
|
||||||
pnpm run build
|
pnpm run build
|
||||||
pnpm qqbot-plugin create <pluginKey>
|
|
||||||
pnpm qqbot-plugin validate <pluginDir>
|
|
||||||
pnpm qqbot-plugin pack <pluginDir>
|
|
||||||
pnpm qqbot-plugin install-local <packageFile>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Jest 只扫描 `test/**/*.spec.ts`。如果在 Windows 下指定测试文件,使用:
|
Jest 只扫描 `test/**/*.spec.ts`。如果在 Windows 下指定测试文件,使用:
|
||||||
@ -152,15 +146,12 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow
|
|||||||
- QQBot 外发统一走发送排队:默认全局间隔 `2500ms`、同会话间隔 `8000ms`、排队抖动 `0-800ms`,超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS` 时拒绝本次发送,避免高频自动回复形成突发流量。
|
- QQBot 外发统一走发送排队:默认全局间隔 `2500ms`、同会话间隔 `8000ms`、排队抖动 `0-800ms`,超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS` 时拒绝本次发送,避免高频自动回复形成突发流量。
|
||||||
- QQBot 在线命令和自动回复规则都有运行时保底冷却:默认命令 `5000ms`、规则 `30000ms`;即使数据库里旧数据冷却值更低,也按保底值判定,降低频繁触发风控的概率。
|
- QQBot 在线命令和自动回复规则都有运行时保底冷却:默认命令 `5000ms`、规则 `30000ms`;即使数据库里旧数据冷却值更低,也按保底值判定,降低频繁触发风控的概率。
|
||||||
- QQBot 复读机默认阈值为 4,同一会话默认 10 分钟只复读一次,默认只复读 120 字以内普通文本,避免群聊重复内容导致机器人过于频繁地模拟真人发言。
|
- QQBot 复读机默认阈值为 4,同一会话默认 10 分钟只复读一次,默认只复读 120 字以内普通文本,避免群聊重复内容导致机器人过于频繁地模拟真人发言。
|
||||||
- QQBot 插件平台统一使用 `plugin.json` manifest 描述插件 key、版本、操作、事件、权限、运行预算和包入口;CLI 负责 create/validate/pack/install-local,后端只暴露受控 SDK 能力并通过插件维度记录安装、配置、账号绑定和运行事件。
|
|
||||||
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表拆开展示 OneBot、容器、WebUI 和 QQ 登录态,心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态;`qqLoginMessage` 只展示 QQ 登录态消息,WebUI 配置或请求错误留在 `lastError`。
|
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表拆开展示 OneBot、容器、WebUI 和 QQ 登录态,心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态;`qqLoginMessage` 只展示 QQ 登录态消息,WebUI 配置或请求错误留在 `lastError`。
|
||||||
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。
|
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。
|
||||||
- NapCat 账号新增/编辑支持可选 QQ 登录密码:Admin 只提交 RSA-OAEP 加密后的 `encryptedLoginPassword`,后端解密后必须用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密保存到 `qqbot_account.napcat_login_password_secret`;空值、`change-me` 和历史公开默认值会被拒绝;列表和详情不回显密码,日志会脱敏密码字段。
|
- NapCat 账号新增/编辑支持可选 QQ 登录密码:Admin 只提交 RSA-OAEP 加密后的 `encryptedLoginPassword`,后端解密后必须用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密保存到 `qqbot_account.napcat_login_password_secret`;空值、`change-me` 和历史公开默认值会被拒绝;列表和详情不回显密码,日志会脱敏密码字段。
|
||||||
- NapCat 容器为已知 `selfId` 创建/重建时会注入 `ACCOUNT` 环境变量启用 `-q` 快速登录:容器重启(崩溃/重启策略/宿主重启)能从持久化会话免扫码自动重登;硬踢 `登录已失效` 会话作废仍需扫码。已绑定但缺少 `ACCOUNT` 的旧容器在下一次「更新登录」时原地重建一次补齐(保留 QQ 数据卷),`docker inspect` 已带 `ACCOUNT` 则跳过,重建失败不阻断登录。`ACCOUNT` 只负责指定快速登录账号;如果数据卷内没有该 QQ 的历史登录记录,后端会跳过快速登录并优先尝试账号保存的登录密码,密码登录会按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询结果,准备中的扫码会话会续期,避免后台密码登录未结束时前端先过期;如果 NapCat 返回 `proofWaterUrl` 或日志出现“需要验证码”,会保持会话 pending 并把腾讯验证码结果 `ticket`/`randstr`/`sid` 通过 `/qqbot/account/scan/captcha/submit` 回交到同一容器的 `/api/QQLogin/CaptchaLogin`,不是让用户只打开外链;密码登录成功后会移除运行态 `NAPCAT_QUICK_PASSWORD`,清理失败则本次登录失败,避免成功态残留明文 env;密码登录也失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时才重置登录态并生成二维码。Admin「更新登录」的 SSE 步骤顺序按实际路径为 `quick-login-*`(已有历史会话)-> `password-login-*` / `password-login-captcha` -> `password-env-cleanup` -> `relogin-reset/qrcode/waiting-scan`。
|
- NapCat 容器为已知 `selfId` 创建/重建时会注入 `ACCOUNT` 环境变量启用 `-q` 快速登录:容器重启(崩溃/重启策略/宿主重启)能从持久化会话免扫码自动重登;硬踢 `登录已失效` 会话作废仍需扫码。已绑定但缺少 `ACCOUNT` 的旧容器在下一次「更新登录」时原地重建一次补齐(保留 QQ 数据卷),`docker inspect` 已带 `ACCOUNT` 则跳过,重建失败不阻断登录。`ACCOUNT` 只负责指定快速登录账号;如果数据卷内没有该 QQ 的历史登录记录,后端会跳过快速登录并优先尝试账号保存的登录密码,密码登录会按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询结果,准备中的扫码会话会续期,避免后台密码登录未结束时前端先过期;如果 NapCat 返回 `proofWaterUrl` 或日志出现“需要验证码”,会保持会话 pending 并把腾讯验证码结果 `ticket`/`randstr`/`sid` 通过 `/qqbot/account/scan/captcha/submit` 回交到同一容器的 `/api/QQLogin/CaptchaLogin`,不是让用户只打开外链;密码登录成功后会移除运行态 `NAPCAT_QUICK_PASSWORD`,清理失败则本次登录失败,避免成功态残留明文 env;密码登录也失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时才重置登录态并生成二维码。Admin「更新登录」的 SSE 步骤顺序按实际路径为 `quick-login-*`(已有历史会话)-> `password-login-*` / `password-login-captcha` -> `password-env-cleanup` -> `relogin-reset/qrcode/waiting-scan`。
|
||||||
- NapCat 设备身份按账号持久化到 `napcat_device_identity`:同一账号重建容器会复用数据目录、hostname、machine-id 和 MAC,并在 Docker run 中注入 `--hostname`、`--mac-address`、只读 `/etc/machine-id`,避免频繁重建被 QQ 判定为全新设备。
|
|
||||||
- NapCat 新设备验证走同一 scan session:`CaptchaLogin` 返回 `needNewDevice` 后,后端继续调用 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`,Admin/SSE 分开展示 `captchaUrl`、`newDeviceQrcode`、已扫码、确认中、验证成功、登录成功/失败等中文进度,不把 `jumpUrl` 当作唯一完成入口。
|
|
||||||
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000`,`QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,使掉线/被踢无需管理员打开列表页即可及时发现;检测到离线后先尝试 `ACCOUNT` 历史会话快速登录,再尝试账号保存的登录密码,仍失败时写入离线原因并复用 `super` 站内信告警;看门狗不自动进入扫码阶段。
|
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000`,`QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,使掉线/被踢无需管理员打开列表页即可及时发现;检测到离线后先尝试 `ACCOUNT` 历史会话快速登录,再尝试账号保存的登录密码,仍失败时写入离线原因并复用 `super` 站内信告警;看门狗不自动进入扫码阶段。
|
||||||
- BangDream 当前源码根目录是 `src/modules/qqbot/plugins/bangDream`;不要恢复旧 `tsugu` 层级或旧大桶目录。
|
- BangDream 当前源码根目录是 `src/qqbot/plugins/bangDream`;不要恢复旧 `tsugu` 层级或旧大桶目录。
|
||||||
- BangDream 在线命令以 `registry/operation-registry.ts` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 registry/command-SQL 测试。
|
- BangDream 在线命令以 `registry/operation-registry.ts` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 registry/command-SQL 测试。
|
||||||
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
|
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
|
||||||
|
|
||||||
@ -196,4 +187,4 @@ BangDream 图片能力改动:
|
|||||||
|
|
||||||
| 一级来源 | 使用方式 | License |
|
| 一级来源 | 使用方式 | License |
|
||||||
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------- |
|
||||||
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/modules/qqbot/plugins/bangDream`,保留本地 `TSUGU-LICENSE` | MIT |
|
| [Tsugu BangDream Bot](https://github.com/Yamamoto-2/tsugu-bangdream-bot) | BangDream QQBot 后端能力已重构合入 `src/qqbot/plugins/bangDream`,保留本地 `TSUGU-LICENSE` | MIT |
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
# API/Admin Contract Matrix
|
|
||||||
|
|
||||||
| Batch | API Contract | Admin Surface | Smoke Evidence |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 1 | `GET /health/runtime` plain JSON, runtime adapter internals | Runtime status remains available | `curl http://localhost:<port>/health/runtime` |
|
|
||||||
| 2 | `/auth/*`, `/admin/user/*`, `/admin/menu/*`, `/admin/role/*`, `/admin/dept/*`, `/admin/dict/*`, `/admin/notice/*` | Login, menu, system pages | Login request, menu load, route render |
|
|
||||||
| 3 | `/blog/*`, `/wordpress/*`, `/asset/*` | Blog, WordPress, Asset pages | Public blog request, Admin list request, asset upload smoke |
|
|
||||||
| 4 | `/qqbot/account/*`, `/qqbot/command/*`, `/qqbot/rule/*`, `/qqbot/message/*`, `/qqbot/send/*` | QQBot core pages | `/qqbot/command/test` local request |
|
|
||||||
| 5 | `/qqbot/plugin-platform/*` | Plugin upload/install/enable/config/health pages | local test plugin install and enable |
|
|
||||||
| 6 | plugin operations exposed through QQBot command/event routing | Existing plugin pages and operation views | BangDream, FF14, FFLogs, Repeater smoke |
|
|
||||||
| 7 | `/qqbot/napcat/*`, login SSE events | NapCat device/login progress pages | simulated captcha and new-device session |
|
|
||||||
| 8 | public deployed URLs | deployed Admin | online smoke bundle |
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
# Refactor V3 Breaking Changes
|
|
||||||
|
|
||||||
## 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 |
|
|
||||||
|
|
||||||
## Protected Behavior
|
|
||||||
|
|
||||||
- Admin login succeeds.
|
|
||||||
- Admin menu loads.
|
|
||||||
- Vben success/error wrappers remain stable for Admin APIs.
|
|
||||||
- `/health/runtime` remains plain JSON.
|
|
||||||
- Blog public list/detail remain available.
|
|
||||||
- QQBot command test remains available.
|
|
||||||
- QQBot status keeps OneBot, container, WebUI, and QQ login state separate.
|
|
||||||
- NapCat cleanup failure blocks success.
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# Refactor V3 Rebuild Runbook
|
|
||||||
|
|
||||||
## Local Dry Run
|
|
||||||
|
|
||||||
1. Create an empty local database dedicated to refactor V3.
|
|
||||||
2. Apply `sql/refactor-v3/00-full-schema.sql`.
|
|
||||||
3. Apply `sql/refactor-v3/01-seed-core.sql`.
|
|
||||||
4. Run `sql/refactor-v3/99-verify.sql`.
|
|
||||||
5. Start API against the dry-run database.
|
|
||||||
6. Run `scripts/refactor-v3/local-smoke.ps1`.
|
|
||||||
|
|
||||||
## Online Backup
|
|
||||||
|
|
||||||
1. Confirm the exact API image tag and current database name.
|
|
||||||
2. Run `scripts/refactor-v3/db-backup-online.ps1`.
|
|
||||||
3. Record backup path, timestamp, source database, and restore command.
|
|
||||||
|
|
||||||
## Online Rebuild
|
|
||||||
|
|
||||||
1. Stop or limit API write traffic.
|
|
||||||
2. Apply full schema and seed scripts.
|
|
||||||
3. Run verify SQL.
|
|
||||||
4. Deploy API/Admin image versions bound to this schema.
|
|
||||||
5. Run online smoke bundle.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
1. Stop write traffic.
|
|
||||||
2. Restore the recorded backup or previous schema bundle.
|
|
||||||
3. Roll back API/Admin images.
|
|
||||||
4. Re-run smoke for the restored version.
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
# Refactor V3 Schema Map
|
|
||||||
|
|
||||||
## Global Rules
|
|
||||||
|
|
||||||
- Primary keys are Snowflake `BIGINT`; API/Admin boundary treats IDs as strings.
|
|
||||||
- Table names use lower snake case.
|
|
||||||
- Queryable values are structured columns, not JSON-only fields.
|
|
||||||
- Event/log tables are append-only and have retention strategy.
|
|
||||||
- New schema is full initialization SQL, not historical `ALTER TABLE` patches.
|
|
||||||
|
|
||||||
## Domains
|
|
||||||
|
|
||||||
| Domain | Tables | Owner Batch | Notes |
|
|
||||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_dept`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Runtime login/menu/role/dept tables plus future permission and department boundary tables. |
|
|
||||||
| Platform Config | `admin_dict`, `admin_component`, `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Runtime Admin dict/component compatibility plus new platform dict/component template split. |
|
|
||||||
| Blog Content | `blog_post`, `blog_article`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_theme_config`, `blog_import_job` | Batch 3 | `blog_article`/`blog_theme_config` keep current runtime compatibility; `blog_term` carries both runtime category/tag fields and nullable taxonomy planning fields. |
|
|
||||||
| 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 | `admin_notice`, `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | Admin notices and MySQL 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. |
|
|
||||||
@ -1,405 +0,0 @@
|
|||||||
# API 全模块架构、全库表设计与 QQBot 插件平台设计
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
`kt-template-online-api` 已经从一个相对紧凑的 NestJS 后端,演进为承接
|
|
||||||
Admin、Blog、WordPress、MinIO、QQBot、NapCat、Loki 运行期事件和线上部署
|
|
||||||
观测的多领域服务。第一期 Runtime Foundation 已经补上 `/health/runtime`、
|
|
||||||
运行时配置检查和证据格式,为后续重构提供了基础观测能力。
|
|
||||||
|
|
||||||
第二期不是继续只修 QQBot 或 NapCat,而是进入 API 全模块架构规划。当前线上
|
|
||||||
数据不重要,因此后续实现阶段允许从头设计数据库表,允许重命名表、删除旧字段、
|
|
||||||
拆分或合并旧表,并重建初始化 SQL。即便允许破坏式重建,实现阶段仍必须保留
|
|
||||||
备份、回滚和线上 smoke,避免线上动作不可追踪。
|
|
||||||
|
|
||||||
## 范围决策
|
|
||||||
|
|
||||||
本阶段是用户确认的 B 阶段:先完成全局规划,不写业务实现代码。
|
|
||||||
|
|
||||||
规划范围:
|
|
||||||
|
|
||||||
1. 定义 API 目标模块架构。
|
|
||||||
2. 从头设计全库 schema。
|
|
||||||
3. 设计 QQBot 插件平台,支持 worker/child process 隔离、线上安装、热插拔和
|
|
||||||
CLI 脚手架。
|
|
||||||
4. 定义后续 C 阶段的迁移矩阵和实施顺序。
|
|
||||||
|
|
||||||
本文件不实现代码、SQL、Admin 页面或运行时行为。用户 review 通过后,下一步
|
|
||||||
进入 Superpowers `writing-plans`。完整 C 阶段迁移开始前,必须重新走一轮
|
|
||||||
Superpowers brainstorming。
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
- 明确每个 controller、use case、entity、repository、外部集成和测试的模块归属。
|
|
||||||
- 用干净的新 schema 替代当前补丁式表历史。
|
|
||||||
- 默认保持外部 API 兼容,确实需要破坏兼容时提前列入 breaking changes。
|
|
||||||
- 将 QQBot 插件从硬编码 Nest provider 集合升级为统一插件平台。
|
|
||||||
- 通过 CLI 脚手架让插件创建、校验、打包和本地安装可重复。
|
|
||||||
- 支持线上插件安装、校验、启用、禁用、升级和卸载,不重启整个 API 进程。
|
|
||||||
- 将现有 BangDream、FF14 Market、FFLogs、Repeater 按统一插件契约重写。
|
|
||||||
- 继承第一期 Runtime Foundation 的可靠性和证据模型,作为后续模块迁移的验证底座。
|
|
||||||
|
|
||||||
## 非目标
|
|
||||||
|
|
||||||
- 本规划阶段不写实现代码。
|
|
||||||
- 不为了旧数据迁移方便而保留旧表结构。
|
|
||||||
- 不把 Jenkins/K8s rollout 成功当作功能成功。
|
|
||||||
- 本阶段不改 Admin 前端。Admin 联动只在本文规划,后续实现前重新 brainstorming。
|
|
||||||
- 不绕过 QQ 或腾讯安全校验。NapCat 验证码和新设备验证仍然由用户驱动。
|
|
||||||
- 不允许线上安装的插件代码直接访问后端真实密钥、TypeORM Repository 或任意 Nest DI。
|
|
||||||
|
|
||||||
## 目标架构
|
|
||||||
|
|
||||||
目标源码结构按平台基础、通用能力和业务模块拆分:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
runtime/
|
|
||||||
common/
|
|
||||||
modules/
|
|
||||||
admin/
|
|
||||||
blog/
|
|
||||||
wordpress/
|
|
||||||
asset/
|
|
||||||
qqbot/
|
|
||||||
```
|
|
||||||
|
|
||||||
`runtime` 负责运行时基础能力:
|
|
||||||
|
|
||||||
- 类型化运行时配置 profile。
|
|
||||||
- 带超时、安全摘要和错误分类的 HTTP/process client。
|
|
||||||
- 健康检查和 `/health/runtime`。
|
|
||||||
- 运行证据记录。
|
|
||||||
- 清理语义。
|
|
||||||
|
|
||||||
`common` 只保留稳定通用能力:
|
|
||||||
|
|
||||||
- Vben 响应和错误工具。
|
|
||||||
- 全局 filter/interceptor。
|
|
||||||
- 时间字段装饰器和序列化工具。
|
|
||||||
- Snowflake ID。
|
|
||||||
- 低层文本、对象工具。
|
|
||||||
- 非业务专属的日志基础能力。
|
|
||||||
|
|
||||||
业务模块内部统一结构:
|
|
||||||
|
|
||||||
```text
|
|
||||||
module/
|
|
||||||
contract/
|
|
||||||
application/
|
|
||||||
domain/
|
|
||||||
infrastructure/
|
|
||||||
persistence/
|
|
||||||
integration/
|
|
||||||
schema/
|
|
||||||
tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 分层职责
|
|
||||||
|
|
||||||
`contract`:Controller、请求 DTO、响应 DTO、Swagger 元信息和现有路由兼容适配。
|
|
||||||
|
|
||||||
`application`:用例编排、事务边界、权限检查和状态流转。可以依赖 domain、
|
|
||||||
repository 和 integration port,但不能直接拼外部 HTTP、Docker 脚本或临时 SQL。
|
|
||||||
|
|
||||||
`domain`:纯业务规则、状态机、策略和值对象。Domain 代码应能脱离 Nest、TypeORM、
|
|
||||||
Docker、HTTP 和外部凭据做单元测试。
|
|
||||||
|
|
||||||
`infrastructure/persistence`:TypeORM Entity、Repository 实现、查询模型和 schema mapper。
|
|
||||||
|
|
||||||
`infrastructure/integration`:WordPress HTTP、MinIO SDK、Loki query、OneBot WebSocket、
|
|
||||||
NapCat WebUI、插件 worker RPC 和 runtime process 适配器。
|
|
||||||
|
|
||||||
`schema`:模块表设计说明、初始化 SQL 归属、重建脚本和验证 SQL。
|
|
||||||
|
|
||||||
`tests`:模块级 unit、contract、repository 和 integration smoke。跨模块用户流程测试可以
|
|
||||||
继续放在根 `test/` 下。
|
|
||||||
|
|
||||||
### 兼容规则
|
|
||||||
|
|
||||||
- 外部 route path 默认保持稳定,除非实现计划明确列入 breaking changes。
|
|
||||||
- Admin-facing 业务接口继续使用当前 Vben success/error wrapper,stream、file、WebSocket
|
|
||||||
和 plain JSON 接口除外。
|
|
||||||
- `/health/runtime` 继续返回 plain JSON。
|
|
||||||
- Snowflake ID 在 MySQL 中继续使用 `BIGINT`,在 JavaScript/API 边界按字符串语义处理。
|
|
||||||
- 时间字段继续使用 `KtDateTime extends Date` 和 KT 时间装饰器。
|
|
||||||
- 后续实现阶段可以替换旧 SQL 文件为全量新库初始化文件,但本规划阶段不改 SQL 实现。
|
|
||||||
|
|
||||||
## 全库表设计
|
|
||||||
|
|
||||||
后续实现阶段允许从头重建 schema。由于当前数据不重要,目标 schema 优先服务清晰
|
|
||||||
数据域和模块所有权,而不是旧表兼容。实现阶段仍必须在破坏式数据库动作前备份,
|
|
||||||
并提供回滚或恢复命令。
|
|
||||||
|
|
||||||
### 全局表约定
|
|
||||||
|
|
||||||
- 主键使用 Snowflake `BIGINT`。
|
|
||||||
- 外键列命名为 `*_id` 并建立索引。
|
|
||||||
- 稳定关系表使用组合唯一约束。
|
|
||||||
- 高频日志和事件表避免硬数据库外键,减少 retention 和清理成本。
|
|
||||||
- 只有可恢复、可隐藏的业务配置使用软删。
|
|
||||||
- 运行事件、命令日志、消息日志、插件运行事件使用 append-only 加 retention。
|
|
||||||
- 通用时间字段使用 `create_time`、`update_time`,需要软删时加 `delete_time`。
|
|
||||||
- 状态字段使用明确的 varchar 枚举,并在 schema 说明中列出允许值。
|
|
||||||
- JSON 字段只承载外部原始 payload、插件 metadata、低频配置或证据详情;可查询字段必须结构化。
|
|
||||||
- 有 UI 或存储长度限制的文本字段必须写明截断规则。
|
|
||||||
- 新初始化 SQL 应是干净的全量 schema,不再累积历史 `ALTER TABLE` 补丁。
|
|
||||||
|
|
||||||
### 数据域表规划
|
|
||||||
|
|
||||||
| 数据域 | 目标表 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Admin Identity | `admin_user`、`admin_role`、`admin_permission`、`admin_menu`、`admin_department`、`admin_user_role`、`admin_role_permission`、`admin_role_menu` | 保持 Admin 登录和菜单行为稳定,同时拆清路由菜单与权限原子。部门为树结构。头像、时区和首页路径属于用户 profile。 |
|
|
||||||
| Platform Config | `platform_dict_group`、`platform_dict_item`、`platform_component_template`、`platform_setting` | 字典和组件模板从 Admin 杂项中抽离。字典用 group/item 替代当前扁平重载行。 |
|
|
||||||
| Blog Content | `blog_post`、`blog_taxonomy`、`blog_term`、`blog_post_term`、`blog_theme_profile`、`blog_import_job` | 分类、标签使用关系表,不再依赖逗号或长文本字段。主题配置改为可命名 profile。 |
|
|
||||||
| WordPress Mirror | `wordpress_site`、`wordpress_auth_session`、`wordpress_remote_post`、`wordpress_remote_term`、`wordpress_sync_job`、`wordpress_sync_mapping` | 远端 WordPress 状态与本地 Blog 内容分离。mapping 表连接远端 ID 和本地 post/term。 |
|
|
||||||
| Asset/MinIO | `asset_bucket`、`asset_object`、`asset_reference`、`asset_access_grant` | 统一记录对象归属、来源模块、MIME/type 元数据和临时访问授权,避免 MinIO URL 散落在业务表。 |
|
|
||||||
| System Event | `system_notice`、`system_event`、`system_event_dedupe`、`system_event_delivery` | Loki 仍是日志查询源。MySQL 只保存可处理通知、去重状态和通知投递状态。 |
|
|
||||||
| Runtime Evidence | `runtime_evidence_index` | 只存重要运行证据文件的安全索引,不存完整日志和 secrets。大 JSON 证据仍保留在 `.kt-workspace/test-artifacts` 或部署产物存储。 |
|
|
||||||
| 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` | 拆分账号身份、连接态、权限、路由、会话、消息历史和发送队列/历史。 |
|
|
||||||
| NapCat Runtime | `napcat_container`、`napcat_device_identity`、`napcat_account_binding`、`napcat_login_session`、`napcat_login_challenge`、`napcat_runtime_cleanup` | 设备身份是一等表,记录 MAC、hostname、machine-id 路径、data dir 和验证状态。登录 challenge 覆盖验证码和新设备验证。清理记录用于阻断假成功。 |
|
|
||||||
| 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` | 插件 metadata、版本、operation、event、账号绑定、配置、资产和运行事件属于平台,不属于某个插件硬编码。 |
|
|
||||||
| Plugin-Owned Data | `qqbot_plugin_data_*` 或插件命名空间表 | 插件 migration 可创建命名空间表。表名必须以 plugin key 或已注册 namespace 开头,避免冲突。 |
|
|
||||||
|
|
||||||
### 破坏式重建策略
|
|
||||||
|
|
||||||
实现阶段可按以下流程重建数据库:
|
|
||||||
|
|
||||||
1. 对线上数据库做带时间戳的备份。
|
|
||||||
2. 在破坏式 schema 动作期间停止或限制 API 写流量。
|
|
||||||
3. 按已批准脚本 drop 或 rename 旧 API 表。
|
|
||||||
4. 应用新的全量初始化 SQL。
|
|
||||||
5. 初始化必要 Admin 用户、角色、菜单、平台设置、字典、核心 QQBot 插件 metadata 和默认在线命令。
|
|
||||||
6. 让 API 使用新 schema 启动。
|
|
||||||
7. 运行本地或线上 smoke:Admin 登录、菜单加载、Blog 公开读取、MinIO check、QQBot command registry、plugin registry、NapCat 账号状态、`/health/runtime`。
|
|
||||||
8. 关键 smoke 失败时,恢复备份或重新应用旧 schema bundle,并回滚到上一个 API 镜像。
|
|
||||||
|
|
||||||
## QQBot 插件平台
|
|
||||||
|
|
||||||
QQBot 插件升级为平台能力,不再是硬编码 Nest service 注册。命令插件、事件插件、
|
|
||||||
渲染插件、外部查询插件和自动化插件使用同一套 contract。
|
|
||||||
|
|
||||||
### 插件包结构
|
|
||||||
|
|
||||||
```text
|
|
||||||
plugins/<pluginKey>/
|
|
||||||
plugin.json
|
|
||||||
src/
|
|
||||||
index.ts
|
|
||||||
operations/
|
|
||||||
events/
|
|
||||||
config/
|
|
||||||
migrations/
|
|
||||||
assets/
|
|
||||||
tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
`plugin.json` 是插件元信息的单一来源:
|
|
||||||
|
|
||||||
- plugin key、名称、描述、版本、作者、license 和主页。
|
|
||||||
- 最低 API plugin SDK 版本。
|
|
||||||
- 请求的 host 权限。
|
|
||||||
- operations 和 event handlers。
|
|
||||||
- config schema 和默认值。
|
|
||||||
- assets 声明。
|
|
||||||
- migrations 声明。
|
|
||||||
- runtime 要求,例如 timeout、memory limit 和 worker type。
|
|
||||||
|
|
||||||
### Worker 隔离运行时
|
|
||||||
|
|
||||||
已确认的运行模型是进程隔离:
|
|
||||||
|
|
||||||
- API 主进程负责安装包、校验 manifest、维护 registry、路由 command/event。
|
|
||||||
- 插件代码运行在独立 worker 或 child process 中。
|
|
||||||
- 插件崩溃只将插件实例标记为 `degraded` 或 `offline`,不拖垮 API 主进程。
|
|
||||||
- 启用、禁用、升级、卸载会启动或停止 worker,并在运行时刷新 registry。
|
|
||||||
- Worker 和 API 通过窄 RPC 协议通信。
|
|
||||||
|
|
||||||
RPC 协议至少支持:
|
|
||||||
|
|
||||||
- `load`:加载 manifest 和编译入口。
|
|
||||||
- `activate`:初始化运行态。
|
|
||||||
- `deactivate`:停止接收新任务。
|
|
||||||
- `executeOperation`:执行命令或查询能力。
|
|
||||||
- `handleEvent`:处理消息或账号事件。
|
|
||||||
- `health`:返回插件健康状态。
|
|
||||||
- `dispose`:关闭前释放资源。
|
|
||||||
|
|
||||||
RPC 消息携带 operation ID、correlation ID、timeout budget、脱敏后的输入和结构化输出。
|
|
||||||
超时由 host 统一控制。
|
|
||||||
|
|
||||||
### 插件 SDK 边界
|
|
||||||
|
|
||||||
插件代码不能访问 Nest DI、TypeORM 原始 Repository、后端原始环境变量或任意文件路径。
|
|
||||||
插件只能拿到受控 SDK:
|
|
||||||
|
|
||||||
- 通过 host 发送队列发送 QQBot 消息。
|
|
||||||
- 通过插件配置服务读写插件配置。
|
|
||||||
- 读写插件自有 storage。
|
|
||||||
- 发送插件运行事件。
|
|
||||||
- 请求 host 提供的 runtime HTTP 能力,带 timeout 和安全证据。
|
|
||||||
- 从插件 asset root 加载已声明静态资源。
|
|
||||||
- 读取当前 operation 或 event context。
|
|
||||||
|
|
||||||
插件必须在安装前声明权限。Manifest 请求未支持权限,或代码包 hash 与校验记录不一致时,
|
|
||||||
host 必须拒绝安装或启用。
|
|
||||||
|
|
||||||
### CLI 脚手架
|
|
||||||
|
|
||||||
仓库应新增插件作者 CLI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm qqbot-plugin create <pluginKey>
|
|
||||||
pnpm qqbot-plugin validate <path>
|
|
||||||
pnpm qqbot-plugin pack <path>
|
|
||||||
pnpm qqbot-plugin install-local <package>
|
|
||||||
```
|
|
||||||
|
|
||||||
`create` 自动生成:
|
|
||||||
|
|
||||||
- `plugin.json`。
|
|
||||||
- 带 `createPlugin()` 的 `src/index.ts`。
|
|
||||||
- 一个 operation handler 模板。
|
|
||||||
- 一个 event handler 模板。
|
|
||||||
- config schema 和默认值。
|
|
||||||
- migration/schema 模板。
|
|
||||||
- contract tests。
|
|
||||||
- package metadata。
|
|
||||||
- README/API 片段草稿。
|
|
||||||
|
|
||||||
`validate` 校验 manifest shape、operation key、event key、权限、schema 文件、migrations、
|
|
||||||
包大小、禁用路径和基础测试是否存在。
|
|
||||||
|
|
||||||
`pack` 输出带版本和 content hash 的插件包。
|
|
||||||
|
|
||||||
`install-local` 使用与线上安装相同的校验链路,将插件包安装到本地开发插件根目录。
|
|
||||||
|
|
||||||
### 线上安装和热插拔状态机
|
|
||||||
|
|
||||||
插件安装状态流转:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uploaded -> validated -> installed -> enabled
|
|
||||||
enabled -> disabled
|
|
||||||
installed -> uninstalled
|
|
||||||
enabled -> upgrading -> enabled
|
|
||||||
enabled -> failed
|
|
||||||
```
|
|
||||||
|
|
||||||
Admin 上传插件包后,API 校验 manifest、包 hash、版本兼容、权限、migration 和 assets。
|
|
||||||
插件包保存到受控运行时插件目录,不能写入 `src/`。
|
|
||||||
|
|
||||||
启用插件时启动 worker,注册 operations 和 event handlers,并记录运行状态。禁用插件时
|
|
||||||
停止 worker,并从路由中移除 active operation/event,不删除数据。卸载插件时停止 worker、
|
|
||||||
移除 registry、解绑 command/event,然后按数据策略处理插件数据:默认保留,只有管理员
|
|
||||||
明确选择清理时才删除。
|
|
||||||
|
|
||||||
### 现有插件重写计划
|
|
||||||
|
|
||||||
BangDream 作为大型参考插件。现有业务能力拆分可以保留在插件包内:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bangDream/
|
|
||||||
operations/
|
|
||||||
song/
|
|
||||||
card/
|
|
||||||
character/
|
|
||||||
event/
|
|
||||||
gacha/
|
|
||||||
player/
|
|
||||||
cutoff/
|
|
||||||
provider/
|
|
||||||
renderer/
|
|
||||||
theme/
|
|
||||||
assets/
|
|
||||||
```
|
|
||||||
|
|
||||||
现有 BangDream operation registry 改为由 manifest 支撑 operation metadata。
|
|
||||||
handlerName 仍作为 worker 内部实现细节。现有图片 smoke 预期继续保留,包括
|
|
||||||
event stage 拆图输出行为。
|
|
||||||
|
|
||||||
FF14 Market 改为外部查询插件,通过 host runtime HTTP SDK 调用 XIVAPI 和 Universalis。
|
|
||||||
|
|
||||||
FFLogs 改为外部查询插件,通过 host runtime HTTP SDK 调用 GraphQL/token,并通过插件
|
|
||||||
config 引用客户端凭据。
|
|
||||||
|
|
||||||
Repeater 改为事件插件,由 host 管理账号绑定,由插件持有 transient state,且必须走 host
|
|
||||||
发送队列,不能直接绕过限流和发送排队。
|
|
||||||
|
|
||||||
## 模块迁移矩阵
|
|
||||||
|
|
||||||
后续实现阶段拆成可审计批次:
|
|
||||||
|
|
||||||
| 批次 | 范围 | 结果 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 0 | 迁移准备 | 新 schema map、全量初始化 SQL 计划、破坏式重建脚本计划、备份/恢复命令、验证 SQL、模块模板和 breaking-change 清单。 |
|
|
||||||
| 1 | Runtime/Common | 保持 runtime foundation 稳定,补齐模块 adapter 需要的 runtime client/evidence 原语,收缩 common 到稳定共享能力。 |
|
|
||||||
| 2 | Admin/Auth/Platform Config | 重建身份、菜单、权限、字典、组件模板和系统通知模型。Admin 登录和菜单加载必须先通过。 |
|
|
||||||
| 3 | Blog/WordPress/Asset | 重建 Blog 内容关系、WordPress mirror/sync 和 MinIO asset 归属。Blog 公开读取和 Admin Blog 管理必须通过。 |
|
|
||||||
| 4 | QQBot Core | 重建账号、连接、权限、命令、规则、会话、消息和发送队列模型。命令匹配和发送队列测试必须通过。 |
|
|
||||||
| 5 | QQBot Plugin Platform | 新增插件 manifest 校验、数据库 registry、CLI 脚手架、worker runtime、RPC、线上安装、热插拔状态和 Admin/API 管理契约。 |
|
|
||||||
| 6 | Existing Plugin Rewrite | 将 BangDream、FF14 Market、FFLogs、Repeater 重写为隔离插件包。现有命令 smoke 行为必须继续可用。 |
|
|
||||||
| 7 | NapCat Runtime | 在新 QQBot/NapCat 模型上实现容器设备持久化、登录 session 状态机、验证码/新设备流程和清理证据。 |
|
|
||||||
| 8 | 线上闭环 | 发布后观察 Jenkins/K8s,运行 `/health/runtime`、Admin smoke、Blog smoke、插件安装/启用 smoke、QQBot command smoke 和真实 NapCat 账号登录 smoke。 |
|
|
||||||
|
|
||||||
NapCat 故意排在 QQBot Core 和 Plugin Platform 后面。它依赖新账号模型,并和 command/event
|
|
||||||
路由共同参与线上验证,但它本身不是插件。
|
|
||||||
|
|
||||||
## 契约和破坏兼容策略
|
|
||||||
|
|
||||||
默认保持外部 API 兼容。任何实现任务如果改变 route path、请求字段、响应字段、状态语义、
|
|
||||||
SSE 事件名、Admin API wrapper 或 SQL seed 标识,都必须在编码前写入 breaking-change 表。
|
|
||||||
|
|
||||||
允许的计划内破坏:
|
|
||||||
|
|
||||||
- 数据库表名、字段、索引和初始化 SQL 可以自由改变,因为 schema 从头设计。
|
|
||||||
- Admin 页面可能需要在实现阶段同步 API wrapper。
|
|
||||||
- 插件管理接口是新增能力,可以使用新的 route shape。
|
|
||||||
|
|
||||||
必须保护的行为:
|
|
||||||
|
|
||||||
- Admin 登录和菜单加载。
|
|
||||||
- 业务 API 的 Vben 响应 wrapper。
|
|
||||||
- `/health/runtime` plain JSON。
|
|
||||||
- Blog public article list/detail。
|
|
||||||
- QQBot command test flow。
|
|
||||||
- QQBot 账号状态继续区分 OneBot 连接、容器、WebUI 和 QQ 登录态。
|
|
||||||
- NapCat 登录安全校验、验证码回交、新设备验证和运行态密码清理失败阻断成功语义。
|
|
||||||
|
|
||||||
## 验证策略
|
|
||||||
|
|
||||||
本规划阶段验证:
|
|
||||||
|
|
||||||
- spec 自检:未决标记、内部矛盾和模糊范围。
|
|
||||||
- `git diff --check`。
|
|
||||||
- KT 文档同步检查。
|
|
||||||
- KT global review 扫描变更文件。
|
|
||||||
|
|
||||||
后续实现阶段必须包含:
|
|
||||||
|
|
||||||
- domain policy 和状态机的定向单测。
|
|
||||||
- 新 SQL 和 TypeORM mapping 的 repository/schema 测试。
|
|
||||||
- CLI 生成的插件 contract tests。
|
|
||||||
- worker runtime 的 load、activate、execute、health、deactivate 和崩溃隔离测试。
|
|
||||||
- 变更接口的真实本地 API 请求。
|
|
||||||
- Admin caller 变化时的 Admin UI smoke。
|
|
||||||
- 破坏式线上数据库动作前,本地数据库重建 dry run。
|
|
||||||
- 线上备份、重建、恢复路径、Jenkins/K8s 观测和功能 smoke。
|
|
||||||
|
|
||||||
## 本规划阶段验收标准
|
|
||||||
|
|
||||||
本阶段完成条件:
|
|
||||||
|
|
||||||
1. 设计文档写入 `docs/superpowers/specs/`。
|
|
||||||
2. 设计文档已提交。
|
|
||||||
3. 用户 review 已提交文档。
|
|
||||||
4. 下一步进入 Superpowers `writing-plans`,不是直接实现代码。
|
|
||||||
5. 后续完整迁移开始前,重新走 Superpowers brainstorming。
|
|
||||||
|
|
||||||
## 交接到下一阶段
|
|
||||||
|
|
||||||
用户 review 通过后,`writing-plans` 阶段应产出计划文档,把本文转换成可执行 work packages、
|
|
||||||
schema 文档、插件平台任务和验证清单。
|
|
||||||
|
|
||||||
后续完整实现必须重新 brainstorming,因为它会涉及破坏式 schema 工作、Admin/API 契约决策、
|
|
||||||
插件运行时安全边界和线上闭环。
|
|
||||||
@ -1,521 +0,0 @@
|
|||||||
# API/Admin 第三期全量重构设计
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
第一期已经完成 Runtime Foundation 与线上部署观测铺底,第二期完成了 API 全模块架构、全库表设计、QQBot 插件平台和 NapCat Runtime 的中文规划。第三期不再只修 QQBot,也不是继续追加局部补丁,而是进入用户确认的 C 阶段:按第二期规划完整迁移 API 全模块,并同步重构 Admin 管理端。
|
|
||||||
|
|
||||||
当前业务数据不重要,因此第三期允许从头设计数据库表,允许破坏式重建 schema,允许重写旧初始化 SQL 和旧模块结构。破坏式不等于无保护:本阶段必须把备份、回滚、验证 SQL、本地 dry run、线上发布观测和真实功能 smoke 作为同一条闭环的一部分。
|
|
||||||
|
|
||||||
本文件是 Superpowers brainstorming 的第三期设计落地文档。它只固化设计和执行边界,不创建实现分支,不清理 Admin 旧产物,不写业务实现。用户 review 本文件后,下一步进入 Superpowers `writing-plans`。
|
|
||||||
|
|
||||||
## 已确认决策
|
|
||||||
|
|
||||||
- 第三期范围是完整 C 阶段迁移,必须覆盖 API 全模块,不只做 Batch 0-2。
|
|
||||||
- API 与 Admin 一起重构。API 仓库使用 `dev/api-full-refactor-v3`,Admin 仓库使用 `dev/admin-full-refactor-v3`。
|
|
||||||
- 当前 Admin 仓库未提交改动属于旧产物,用户已授权在实现准备阶段放弃;设计阶段先不清理。
|
|
||||||
- 每个批次在本地完成 TDD、验证、review 和提交,不按批次推送。
|
|
||||||
- Batch 0-8 全部完成并通过本地闭环后,再统一推送、重建数据库、观察 Jenkins/K8s、执行线上完整 smoke。
|
|
||||||
- 数据库按全新 schema 设计,可破坏式重建,但必须有备份、恢复路径、验证 SQL 和 API 镜像回滚策略。
|
|
||||||
- QQBot 插件平台是统一平台能力,必须支持 manifest、CLI 脚手架、线上安装、热插拔、worker 或 child process 隔离、配置、健康、运行事件和账号绑定。
|
|
||||||
- NapCat Runtime 不是插件。它属于 QQBot/NapCat 基础运行时,必须独立实现设备身份持久化和登录状态机。
|
|
||||||
- NapCat 新设备验证必须实现 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`,不能只把 `jumpUrl` 透给 Admin。
|
|
||||||
- SSE/Admin 必须展示完整中文进度:快速登录、密码登录、验证码、新设备二维码、已扫码、确认中、登录成功、登录失败。
|
|
||||||
|
|
||||||
## 范围
|
|
||||||
|
|
||||||
### API 仓库
|
|
||||||
|
|
||||||
第三期 API 重构覆盖:
|
|
||||||
|
|
||||||
- `runtime` 和 `common` 基础层。
|
|
||||||
- Admin/Auth/Platform Config。
|
|
||||||
- Blog/WordPress/Asset。
|
|
||||||
- QQBot Core。
|
|
||||||
- QQBot Plugin Platform。
|
|
||||||
- 现有 QQBot 插件重写。
|
|
||||||
- NapCat Runtime。
|
|
||||||
- 全量初始化 SQL、schema 文档、验证 SQL、备份和恢复脚本。
|
|
||||||
- 本地、发布、线上 smoke 和 KT/Superpowers review 证据。
|
|
||||||
|
|
||||||
### Admin 仓库
|
|
||||||
|
|
||||||
第三期 Admin 重构覆盖:
|
|
||||||
|
|
||||||
- 登录、菜单、权限、用户、角色、部门等身份和权限页面。
|
|
||||||
- 平台设置、字典、组件模板、系统通知。
|
|
||||||
- Blog/WordPress/Asset 管理页。
|
|
||||||
- QQBot 账号、连接、命令、规则、消息、发送队列管理页。
|
|
||||||
- QQBot 插件管理页:上传、校验、安装、启用、禁用、升级、卸载、配置、健康、运行事件、账号绑定。
|
|
||||||
- NapCat 登录管理页:设备身份、登录 session、验证码、新设备二维码、SSE 中文进度、清理失败提示。
|
|
||||||
|
|
||||||
Admin 不做独立产品改版。UI 调整服务于新 API 契约、状态语义和管理流程。
|
|
||||||
|
|
||||||
## 非目标
|
|
||||||
|
|
||||||
- 本设计阶段不写实现代码。
|
|
||||||
- 本设计阶段不创建 `dev/api-full-refactor-v3` 或 `dev/admin-full-refactor-v3` 分支。
|
|
||||||
- 本设计阶段不清理 Admin 旧未提交改动。
|
|
||||||
- 不保留旧表结构来迁就旧数据。
|
|
||||||
- 不把 NapCat 登录安全校验做成自动绕过。
|
|
||||||
- 不允许线上插件直接访问后端真实密钥、TypeORM Repository、Nest DI 或任意宿主文件路径。
|
|
||||||
- 不把 Jenkins/K8s 成功当成功能完成。线上真实 smoke 是完成条件的一部分。
|
|
||||||
|
|
||||||
## 分支和仓库策略
|
|
||||||
|
|
||||||
实现准备阶段按顺序处理:
|
|
||||||
|
|
||||||
1. 确认 API 仓库状态,基于当前 `main` 创建 `dev/api-full-refactor-v3`。
|
|
||||||
2. 确认 Admin 仓库状态,放弃用户已授权的旧未提交产物后,基于当前 `main` 创建 `dev/admin-full-refactor-v3`。
|
|
||||||
3. API 与 Admin 分别提交,提交信息使用英文 type 前缀加中文说明。
|
|
||||||
4. 每个批次可以产生 API 提交、Admin 提交或两者都有的提交。
|
|
||||||
5. Batch 8 本地闭环完成前不推送。
|
|
||||||
6. 统一推送前再次确认两仓 diff、提交序列、数据库破坏式操作范围和回滚入口。
|
|
||||||
|
|
||||||
跨仓依赖以 API 契约为中心:API 批次先定义 contract、schema 和状态语义,Admin 同批次或紧随其后完成 caller、页面状态和 smoke。任何 breaking change 必须在批次开始前写入 breaking-change 清单。
|
|
||||||
|
|
||||||
## 目标模块架构
|
|
||||||
|
|
||||||
API 目标结构继承第二期规划:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
runtime/
|
|
||||||
common/
|
|
||||||
modules/
|
|
||||||
admin/
|
|
||||||
blog/
|
|
||||||
wordpress/
|
|
||||||
asset/
|
|
||||||
qqbot/
|
|
||||||
```
|
|
||||||
|
|
||||||
模块内部使用稳定分层:
|
|
||||||
|
|
||||||
```text
|
|
||||||
module/
|
|
||||||
contract/
|
|
||||||
application/
|
|
||||||
domain/
|
|
||||||
infrastructure/
|
|
||||||
persistence/
|
|
||||||
integration/
|
|
||||||
schema/
|
|
||||||
tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
- `contract`:Controller、DTO、Swagger、SSE 事件和外部兼容适配。
|
|
||||||
- `application`:用例编排、事务、权限、状态流转和跨端契约。
|
|
||||||
- `domain`:纯规则、状态机、策略和值对象,不依赖 Nest、TypeORM、Docker 或 HTTP。
|
|
||||||
- `infrastructure/persistence`:Entity、Repository、查询模型和 schema mapper。
|
|
||||||
- `infrastructure/integration`:WordPress、MinIO、Loki、OneBot、NapCat WebUI、Docker、插件 worker RPC。
|
|
||||||
- `schema`:表设计、初始化 SQL、验证 SQL、备份恢复说明和批次迁移记录。
|
|
||||||
- `tests`:unit、contract、repository、integration smoke 和回归用例。
|
|
||||||
|
|
||||||
Admin 目标结构以功能域组织 API caller、页面、组件和状态适配,避免把 QQBot、插件平台、NapCat 状态继续塞进单一大页面。
|
|
||||||
|
|
||||||
## 全库表重建设计
|
|
||||||
|
|
||||||
第三期以全新 schema 为目标,不做旧表兼容迁移。表设计必须在 Batch 0 冻结第一版,并在每个后续批次按模块补齐实现。
|
|
||||||
|
|
||||||
全局约定:
|
|
||||||
|
|
||||||
- 主键使用 Snowflake `BIGINT`,API 边界按字符串语义处理。
|
|
||||||
- 外键列使用 `*_id`,查询路径必须建索引。
|
|
||||||
- 业务强关系使用唯一约束,日志和事件类表避免强外键。
|
|
||||||
- 时间字段使用 `create_time`、`update_time`,软删使用 `delete_time`。
|
|
||||||
- 状态字段使用明确 varchar 枚举,允许值写入 schema 文档。
|
|
||||||
- JSON 字段只放外部原始 payload、低频配置、插件 metadata 或证据详情;需要查询的字段必须结构化。
|
|
||||||
- 初始化 SQL 是全量干净 schema,不继续堆叠历史 `ALTER TABLE`。
|
|
||||||
|
|
||||||
核心数据域:
|
|
||||||
|
|
||||||
| 数据域 | 表设计方向 |
|
|
||||||
| --- | --- |
|
|
||||||
| Admin Identity | 用户、角色、权限、菜单、部门、用户角色、角色权限、角色菜单分离,菜单路由和权限原子拆清。 |
|
|
||||||
| Platform Config | 字典组、字典项、组件模板、平台设置、系统通知独立成域。 |
|
|
||||||
| Blog Content | 文章、分类法、术语、文章术语关系、主题 profile、导入 job 分离。 |
|
|
||||||
| WordPress Mirror | 站点、认证会话、远端文章、远端术语、同步 job、远近端 mapping 分离。 |
|
|
||||||
| Asset/MinIO | bucket、object、reference、access grant 记录对象归属、MIME、来源模块和临时授权。 |
|
|
||||||
| System Event | notice、event、dedupe、delivery 保存可处理事件和投递状态,Loki 仍是日志查询源。 |
|
|
||||||
| Runtime Evidence | 保存重要运行证据索引,不把大日志和 secrets 放入 MySQL。 |
|
|
||||||
| QQBot Core | 账号、连接 session、能力绑定、权限策略、命令、别名、规则、会话、消息、发送任务、发送日志、去重事件分离。 |
|
|
||||||
| NapCat Runtime | 容器、设备身份、账号绑定、登录 session、登录 challenge、清理记录分离。 |
|
|
||||||
| QQBot Plugin Platform | 插件、版本、安装、operation、event handler、账号绑定、配置、资产、运行事件分离。 |
|
|
||||||
| Plugin-Owned Data | 插件自有表必须带 plugin key 或注册 namespace 前缀。 |
|
|
||||||
|
|
||||||
破坏式重建流程必须包含:
|
|
||||||
|
|
||||||
1. 本地空库 dry run:应用全量 SQL、seed、验证 SQL、API 启动、关键接口 smoke。
|
|
||||||
2. 线上备份:备份当前 API 数据库,记录备份路径、时间、库名和恢复命令。
|
|
||||||
3. 写流量限制:在 drop 或 rename 旧表前停止或限制 API 写流量。
|
|
||||||
4. 应用新 schema:按批准脚本重建表、索引、seed、插件 metadata 和菜单权限。
|
|
||||||
5. 绑定 API 镜像:schema 版本和 API 镜像作为同一回滚单元。
|
|
||||||
6. 线上验证:Admin、Blog、Asset、QQBot、插件平台、NapCat、`/health/runtime` smoke。
|
|
||||||
7. 失败恢复:恢复备份或切回旧 schema bundle,并回滚 API/Admin 镜像。
|
|
||||||
|
|
||||||
## QQBot 插件平台设计
|
|
||||||
|
|
||||||
插件平台是第三期独立核心能力。它不是把现有插件目录挪位置,而是建立统一 contract、安装链路、运行隔离和 Admin 管理面。
|
|
||||||
|
|
||||||
### 插件包结构
|
|
||||||
|
|
||||||
```text
|
|
||||||
plugins/<pluginKey>/
|
|
||||||
plugin.json
|
|
||||||
src/
|
|
||||||
index.ts
|
|
||||||
operations/
|
|
||||||
events/
|
|
||||||
config/
|
|
||||||
migrations/
|
|
||||||
assets/
|
|
||||||
tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
`plugin.json` 是单一事实来源,包含:
|
|
||||||
|
|
||||||
- plugin key、名称、描述、版本、作者、license、主页。
|
|
||||||
- 最低 API plugin SDK 版本。
|
|
||||||
- 权限声明。
|
|
||||||
- operations、event handlers、config schema、assets、migrations。
|
|
||||||
- runtime 要求:timeout、memory limit、worker type、并发策略。
|
|
||||||
|
|
||||||
### CLI 脚手架
|
|
||||||
|
|
||||||
API 仓库提供插件 CLI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm qqbot-plugin create <pluginKey>
|
|
||||||
pnpm qqbot-plugin validate <path>
|
|
||||||
pnpm qqbot-plugin pack <path>
|
|
||||||
pnpm qqbot-plugin install-local <package>
|
|
||||||
```
|
|
||||||
|
|
||||||
`create` 生成初始插件模块结构、manifest、入口、operation 模板、event 模板、config schema、migration 模板、contract tests 和说明草稿。
|
|
||||||
|
|
||||||
`validate` 校验 manifest shape、operation key、event key、权限声明、migration、assets、包大小、禁用路径和基础测试。
|
|
||||||
|
|
||||||
`pack` 输出带版本和 content hash 的插件包。
|
|
||||||
|
|
||||||
`install-local` 走与线上安装相同的校验链路,避免本地与线上行为分叉。
|
|
||||||
|
|
||||||
### 运行隔离
|
|
||||||
|
|
||||||
插件代码运行在 worker 或 child process 中。API 主进程只负责安装包、校验、registry、路由、权限和受控 SDK。
|
|
||||||
|
|
||||||
RPC 协议必须支持:
|
|
||||||
|
|
||||||
- `load`
|
|
||||||
- `activate`
|
|
||||||
- `deactivate`
|
|
||||||
- `executeOperation`
|
|
||||||
- `handleEvent`
|
|
||||||
- `health`
|
|
||||||
- `dispose`
|
|
||||||
|
|
||||||
插件崩溃、超时或健康失败只影响对应插件 installation,不能拖垮 API 主进程。Host 记录插件 runtime event,并把状态暴露给 Admin。
|
|
||||||
|
|
||||||
插件 SDK 只能提供受控能力:
|
|
||||||
|
|
||||||
- 走 host 发送队列发送 QQBot 消息。
|
|
||||||
- 读写插件配置。
|
|
||||||
- 读写插件自有 storage。
|
|
||||||
- 发送插件运行事件。
|
|
||||||
- 通过 host runtime HTTP client 调外部 API。
|
|
||||||
- 从声明 asset root 读取资源。
|
|
||||||
- 读取当前 operation 或 event context。
|
|
||||||
|
|
||||||
插件不得读取真实环境变量、任意宿主文件、Nest DI、TypeORM 原始 Repository 或跨插件私有数据。
|
|
||||||
|
|
||||||
### 线上安装和热插拔
|
|
||||||
|
|
||||||
状态机:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uploaded -> validated -> installed -> enabled
|
|
||||||
enabled -> disabled
|
|
||||||
installed -> uninstalled
|
|
||||||
enabled -> upgrading -> enabled
|
|
||||||
enabled -> failed
|
|
||||||
```
|
|
||||||
|
|
||||||
Admin 上传插件包后,API 负责校验 manifest、hash、版本兼容、权限、migration 和 assets。启用时启动 worker 并注册 operation/event。禁用时停止 worker 并从 active registry 移除。卸载时停止 worker、移除 registry 和绑定,默认保留插件数据,只有管理员明确选择清理才删除插件自有数据。
|
|
||||||
|
|
||||||
### 现有插件重写
|
|
||||||
|
|
||||||
- BangDream:作为大型参考插件重写,保留 song、card、character、event、gacha、player、cutoff、provider、renderer、theme、assets 等能力边界,operation metadata 由 manifest 支撑,图片 smoke 行为必须保留。
|
|
||||||
- FF14 Market:外部查询插件,通过 host runtime HTTP SDK 调用 XIVAPI 和 Universalis。
|
|
||||||
- FFLogs:外部查询插件,通过 host runtime HTTP SDK 调 GraphQL/token,凭据来自插件 config 的受控引用。
|
|
||||||
- Repeater:事件插件,由 host 管理账号绑定和发送队列,插件只维护自身策略状态,不能绕过限流和队列。
|
|
||||||
|
|
||||||
## NapCat Runtime 设计
|
|
||||||
|
|
||||||
NapCat Runtime 排在 QQBot Core 和插件平台之后实现,但它不是插件。它属于账号、容器、设备身份和登录安全流程的基础运行时。
|
|
||||||
|
|
||||||
目标:
|
|
||||||
|
|
||||||
- Docker 设备状态持久化,避免每次重建容器都被 QQ 识别成新设备。
|
|
||||||
- 设备身份是一等数据:容器名、data dir、hostname、machine-id、MAC、账号绑定、验证状态和最后登录证据必须可追踪。
|
|
||||||
- 登录状态机保持清晰:快速登录、密码登录、验证码、新设备验证、二维码 fallback、成功、失败、清理失败各自独立。
|
|
||||||
- 新设备验证按 NapCat 上游流程完成:`GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`。
|
|
||||||
- SSE/Admin 全中文进度可观察,用户能知道当前卡在扫码、确认、验证码、清理失败还是最终失败。
|
|
||||||
- 运行态密码清理失败必须阻断成功或二维码 fallback,并写入明确错误,不被普通离线原因覆盖。
|
|
||||||
|
|
||||||
登录顺序:
|
|
||||||
|
|
||||||
```text
|
|
||||||
quick login -> saved password login -> captcha challenge -> new device QR challenge -> manual QR fallback
|
|
||||||
```
|
|
||||||
|
|
||||||
验证码处理:
|
|
||||||
|
|
||||||
- `proofWaterUrl` 只作为用户完成腾讯验证码的入口。
|
|
||||||
- 用户提交 `ticket`、`randstr`、`sid` 后,API 调 NapCat `CaptchaLogin`。
|
|
||||||
- 不伪造验证码票据,不绕过腾讯安全校验。
|
|
||||||
- 一旦 session 进入 captcha pending,后续状态缺少 URL 也不能直接失败,必须保留 challenge 语义。
|
|
||||||
|
|
||||||
新设备验证处理:
|
|
||||||
|
|
||||||
- `CaptchaLogin` 或登录状态返回 `needNewDevice` 时,API 创建或更新 new-device challenge。
|
|
||||||
- API 调 NapCat `GetNewDeviceQRCode` 获取新设备二维码,不只透传 `jumpUrl`。
|
|
||||||
- API 周期调用 `PollNewDeviceQR`,把未扫码、已扫码、确认中、过期、失败状态映射为中文 SSE/Admin 状态。
|
|
||||||
- 用户确认后 API 调 `NewDeviceLogin`,同一登录 session 继续完成登录。
|
|
||||||
- 新设备验证成功后保留设备身份证据,后续容器重建复用持久化设备状态。
|
|
||||||
|
|
||||||
SSE/Admin 状态至少覆盖:
|
|
||||||
|
|
||||||
- 正在快速登录。
|
|
||||||
- 快速登录失败,进入密码登录。
|
|
||||||
- 正在密码登录。
|
|
||||||
- 需要验证码。
|
|
||||||
- 验证码已提交,等待 NapCat 确认。
|
|
||||||
- 需要新设备验证二维码。
|
|
||||||
- 新设备二维码待扫码。
|
|
||||||
- 新设备二维码已扫码。
|
|
||||||
- 新设备确认中。
|
|
||||||
- 新设备验证成功,继续登录。
|
|
||||||
- 正在生成手动二维码。
|
|
||||||
- 登录成功。
|
|
||||||
- 登录失败。
|
|
||||||
- 运行态清理失败。
|
|
||||||
|
|
||||||
## 执行批次
|
|
||||||
|
|
||||||
### Batch 0:迁移准备
|
|
||||||
|
|
||||||
产物:
|
|
||||||
|
|
||||||
- API/Admin 双仓分支准备方案。
|
|
||||||
- Admin 旧产物清理记录。
|
|
||||||
- 全库 schema map。
|
|
||||||
- 全量初始化 SQL 草案。
|
|
||||||
- 破坏式重建脚本设计。
|
|
||||||
- 备份和恢复命令。
|
|
||||||
- 验证 SQL。
|
|
||||||
- 模块模板。
|
|
||||||
- breaking-change 清单。
|
|
||||||
- API/Admin 契约矩阵。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 空库 schema dry run 可执行。
|
|
||||||
- 初始 seed 覆盖 Admin 登录、菜单、基础设置、插件 metadata 和 QQBot 基础命令。
|
|
||||||
- writing-plans 输出的任务能够映射到 Batch 1-8。
|
|
||||||
|
|
||||||
### Batch 1:Runtime/Common
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- 稳定 runtime profile、runtime client、evidence、timeout、process/Docker adapter、错误分类。
|
|
||||||
- 收缩 common,只保留通用响应、错误、时间、Snowflake、日志基础能力。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- `/health/runtime` 行为稳定。
|
|
||||||
- 现有 runtime tests 继续通过。
|
|
||||||
- 下游模块可以通过统一 adapter 接入外部 HTTP、process、Docker 和证据记录。
|
|
||||||
|
|
||||||
### Batch 2:Admin/Auth/Platform Config
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- 重建身份、角色、权限、菜单、部门、字典、组件模板、平台设置和系统通知模型。
|
|
||||||
- Admin 登录、菜单、权限页面同步新 contract。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 本地真实登录接口通过。
|
|
||||||
- Admin 菜单加载和页面路由通过。
|
|
||||||
- 权限、菜单、字典、设置的增删改查通过 scoped smoke。
|
|
||||||
|
|
||||||
### Batch 3:Blog/WordPress/Asset
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- 重建 Blog 内容、分类法、术语关系、主题 profile。
|
|
||||||
- 重建 WordPress mirror、sync job、mapping。
|
|
||||||
- 重建 MinIO asset 归属、引用和临时授权。
|
|
||||||
- Admin Blog、WordPress、Asset 页面同步新 contract。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- Blog public list/detail 本地真实请求通过。
|
|
||||||
- Admin Blog 管理 smoke 通过。
|
|
||||||
- WordPress 同步 dry run 或受控 smoke 通过。
|
|
||||||
- Asset 上传、引用和读取 smoke 通过。
|
|
||||||
|
|
||||||
### Batch 4:QQBot Core
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- 重建账号、连接 session、能力绑定、权限策略、命令、别名、规则、会话、消息、发送任务、发送日志和去重事件。
|
|
||||||
- 拆清 OneBot 连接、容器、WebUI 和 QQ 登录态。
|
|
||||||
- Admin QQBot 基础管理页同步新 contract。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 命令 registry 和 command SQL 测试通过。
|
|
||||||
- 发送队列、去重、权限策略单测通过。
|
|
||||||
- `/qqbot/command/test` 本地真实请求通过。
|
|
||||||
- Admin QQBot 账号和命令页面 smoke 通过。
|
|
||||||
|
|
||||||
### Batch 5:QQBot Plugin Platform
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- 插件 manifest schema。
|
|
||||||
- 插件 registry 数据库。
|
|
||||||
- CLI `create/validate/pack/install-local`。
|
|
||||||
- worker 或 child process runtime。
|
|
||||||
- RPC 协议。
|
|
||||||
- 线上安装、启用、禁用、升级、卸载。
|
|
||||||
- 插件配置、健康、运行事件、账号绑定。
|
|
||||||
- Admin 插件管理页面。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- CLI 创建插件并通过 validate。
|
|
||||||
- pack 产物含 hash,install-local 走统一校验链路。
|
|
||||||
- worker load、activate、execute、health、deactivate、崩溃隔离测试通过。
|
|
||||||
- Admin 插件上传、安装、启用、禁用 smoke 通过。
|
|
||||||
|
|
||||||
### Batch 6:现有插件重写
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- BangDream 重写为平台插件。
|
|
||||||
- FF14 Market 重写为平台插件。
|
|
||||||
- FFLogs 重写为平台插件。
|
|
||||||
- Repeater 重写为平台插件。
|
|
||||||
- 旧硬编码插件 registry 退出。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- BangDream 现有核心命令和图片 smoke 通过。
|
|
||||||
- FF14 Market 查询 smoke 通过。
|
|
||||||
- FFLogs 查询 smoke 通过。
|
|
||||||
- Repeater 事件策略和发送队列 smoke 通过。
|
|
||||||
- Admin 能查看插件 operation、配置、健康和运行事件。
|
|
||||||
|
|
||||||
### Batch 7:NapCat Runtime
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- Docker 设备状态持久化。
|
|
||||||
- NapCat 容器、设备身份、账号绑定、登录 session、challenge、cleanup 记录。
|
|
||||||
- 快速登录、密码登录、验证码、新设备验证、二维码 fallback 状态机。
|
|
||||||
- `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`。
|
|
||||||
- SSE/Admin 中文进度。
|
|
||||||
- 清理失败阻断成功。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- 本地 NapCat login session 状态机单测通过。
|
|
||||||
- API 真实本地请求覆盖验证码和新设备 challenge 模拟。
|
|
||||||
- Admin 登录页面进度 smoke 通过。
|
|
||||||
- 容器重建后设备身份持久化证据可查询。
|
|
||||||
|
|
||||||
### Batch 8:统一上线闭环
|
|
||||||
|
|
||||||
范围:
|
|
||||||
|
|
||||||
- API/Admin 最终本地验证。
|
|
||||||
- 两仓统一推送。
|
|
||||||
- 数据库备份、重建、seed、验证 SQL。
|
|
||||||
- Jenkins/K8s 观测。
|
|
||||||
- 线上完整 smoke。
|
|
||||||
- 真实 QQBot/NapCat 账号闭环验证。
|
|
||||||
|
|
||||||
验收:
|
|
||||||
|
|
||||||
- Jenkins build number、commit hash、镜像 tag、Deployment generation、Pod image、restart count 和日志证据齐全。
|
|
||||||
- `/health/runtime` 线上通过。
|
|
||||||
- Admin 登录、菜单、关键管理页线上通过。
|
|
||||||
- Blog public 线上通过。
|
|
||||||
- 插件安装、启用、健康、命令线上通过。
|
|
||||||
- `/qqbot/command/test` 线上按 operationKey 查询 commandId 后通过。
|
|
||||||
- NapCat 真实账号完成登录闭环,包含设备持久化和新设备验证链路。
|
|
||||||
|
|
||||||
## 每批验证和提交节奏
|
|
||||||
|
|
||||||
每个批次必须按同一节奏执行:
|
|
||||||
|
|
||||||
1. RED:先写或定位失败检查,包括单测、schema 验证、contract smoke 或 Admin smoke。
|
|
||||||
2. GREEN:做最小足够实现。
|
|
||||||
3. 本地验证:按批次风险运行 typecheck、scoped lint、scoped Jest、真实 API 请求和 Admin smoke。
|
|
||||||
4. 文档同步:更新 schema、README/API、TASKS 或相关 docs。
|
|
||||||
5. KT global review。
|
|
||||||
6. Superpowers code review。
|
|
||||||
7. 批次提交,API/Admin 分仓提交。
|
|
||||||
|
|
||||||
接口变化必须真实本地调用一次。发布或线上功能必须通过对应线上 self-test 后才能声明完成。
|
|
||||||
|
|
||||||
## 推送和上线策略
|
|
||||||
|
|
||||||
第三期本地实现期间不推送。Batch 0-8 全部本地完成后,统一执行:
|
|
||||||
|
|
||||||
1. 双仓最终 `git status` 和 diff 复核。
|
|
||||||
2. 本地完整轻量验证矩阵。
|
|
||||||
3. 用户确认推送和破坏式数据库动作窗口。
|
|
||||||
4. 推送 API 和 Admin 分支或目标分支。
|
|
||||||
5. 确认 Jenkins build number 和 commit hash。
|
|
||||||
6. 观察 K8s Deployment、Pod、image tag、restart count 和日志。
|
|
||||||
7. 线上数据库备份、重建、seed、验证 SQL。
|
|
||||||
8. 线上 API/Admin/Blog/QQBot/插件/NapCat smoke。
|
|
||||||
|
|
||||||
如果线上 smoke 失败,按 schema 与 API 镜像绑定回滚:恢复数据库备份或旧 schema bundle,回滚 API/Admin 镜像,记录失败点和稳定解法。
|
|
||||||
|
|
||||||
## 风险和控制
|
|
||||||
|
|
||||||
- 范围过大:通过 Batch 0-8 拆分,每批有独立 RED/GREEN、review、commit 和验收。
|
|
||||||
- 数据库破坏式重建风险:本地空库 dry run、线上备份、恢复命令、验证 SQL 和镜像绑定回滚。
|
|
||||||
- API/Admin 契约漂移:每批维护 contract matrix,接口变化必须真实调用,Admin 同步 smoke。
|
|
||||||
- 插件安全风险:manifest 权限、hash 校验、受控 SDK、worker 隔离、运行事件和禁用路径校验。
|
|
||||||
- NapCat 状态混淆:QQ 登录态、OneBot 连接、容器、WebUI、验证码、新设备、清理失败分别建模。
|
|
||||||
- 线上成功误判:Jenkins/K8s 只算部署证据,功能完成必须有线上 smoke。
|
|
||||||
|
|
||||||
## 本设计验收标准
|
|
||||||
|
|
||||||
本设计阶段完成条件:
|
|
||||||
|
|
||||||
1. 本文件写入 `docs/superpowers/specs/`。
|
|
||||||
2. 本文件通过占位、矛盾、范围和模糊性自检。
|
|
||||||
3. `TASKS.md` 记录第三期设计上下文。
|
|
||||||
4. API 和根仓库文档变更分别提交。
|
|
||||||
5. 用户 review 本文件。
|
|
||||||
6. 用户确认后进入 Superpowers `writing-plans`,再产出可执行计划。
|
|
||||||
|
|
||||||
## 交接到 writing-plans
|
|
||||||
|
|
||||||
`writing-plans` 必须把本设计转换为可执行计划,至少包含:
|
|
||||||
|
|
||||||
- Batch 0-8 逐批任务清单。
|
|
||||||
- 每批 RED/GREEN 测试入口。
|
|
||||||
- API/Admin 双仓文件范围。
|
|
||||||
- 表设计和初始化 SQL 产物路径。
|
|
||||||
- 破坏式重建、备份、恢复、验证 SQL 细节。
|
|
||||||
- 插件 CLI、worker runtime、Admin 插件管理页任务拆分。
|
|
||||||
- NapCat 设备持久化和新设备验证任务拆分。
|
|
||||||
- 每批提交、review、清理、文档同步和最终上线闭环门禁。
|
|
||||||
|
|
||||||
在用户 review 本文件前,不进入实现,也不进入分支清理或 Admin 旧产物处理。
|
|
||||||
@ -22,7 +22,6 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||||
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"qqbot-plugin": "ts-node -r tsconfig-paths/register scripts/qqbot-plugin/cli.ts",
|
|
||||||
"verify:staged": "node scripts/husky-fast-check.mjs",
|
"verify:staged": "node scripts/husky-fast-check.mjs",
|
||||||
"verify:commit": "pnpm run lint && pnpm run typecheck",
|
"verify:commit": "pnpm run lint && pnpm run typecheck",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
|||||||
@ -48,8 +48,8 @@ const path = require("path");
|
|||||||
const { ConfigService } = require("@nestjs/config");
|
const { ConfigService } = require("@nestjs/config");
|
||||||
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
|
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
|
||||||
const { ToolsService } = require("./src/common/services/tool.service");
|
const { ToolsService } = require("./src/common/services/tool.service");
|
||||||
const { QqbotBangDreamRendererService } = require("./src/modules/qqbot/plugins/bangDream/application/bangdream-renderer.facade");
|
const { QqbotBangDreamRendererService } = require("./src/qqbot/plugins/bangDream/application/bangdream-renderer.facade");
|
||||||
const { TsuguApplicationService } = require("./src/modules/qqbot/plugins/bangDream/application/bangdream-application.service");
|
const { TsuguApplicationService } = require("./src/qqbot/plugins/bangDream/application/bangdream-application.service");
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const renderer = new QqbotBangDreamRendererService(new ConfigService({}), undefined);
|
const renderer = new QqbotBangDreamRendererService(new ConfigService({}), undefined);
|
||||||
|
|||||||
@ -1,312 +0,0 @@
|
|||||||
import * as crypto from 'crypto';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import {
|
|
||||||
parseQqbotPluginManifest,
|
|
||||||
type QqbotPluginManifest,
|
|
||||||
} from '../../src/modules/qqbot/plugin-platform/manifest';
|
|
||||||
|
|
||||||
export type QqbotPluginCliCommand =
|
|
||||||
| 'create'
|
|
||||||
| 'install-local'
|
|
||||||
| 'pack'
|
|
||||||
| 'validate';
|
|
||||||
|
|
||||||
export type QqbotPluginCliOptions = {
|
|
||||||
cwd?: string;
|
|
||||||
stderr?: (message: string) => void;
|
|
||||||
stdout?: (message: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginCliResult = {
|
|
||||||
command: QqbotPluginCliCommand;
|
|
||||||
exitCode: number;
|
|
||||||
packageHash?: string;
|
|
||||||
packagePath?: string;
|
|
||||||
pluginKey?: string;
|
|
||||||
pluginRoot?: string;
|
|
||||||
version?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PackedPluginFile = {
|
|
||||||
path: string;
|
|
||||||
sha256: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PackedPlugin = {
|
|
||||||
contentHash: string;
|
|
||||||
files: PackedPluginFile[];
|
|
||||||
manifest: QqbotPluginManifest;
|
|
||||||
};
|
|
||||||
|
|
||||||
const templateRoot = path.join(__dirname, 'templates', 'basic');
|
|
||||||
const packageExtension = '.qqbot-plugin.json';
|
|
||||||
|
|
||||||
const sha256 = (content: Buffer | string) =>
|
|
||||||
crypto.createHash('sha256').update(content).digest('hex');
|
|
||||||
|
|
||||||
const stableStringify = (value: unknown): string => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
||||||
}
|
|
||||||
if (value && typeof value === 'object') {
|
|
||||||
const record = value as Record<string, unknown>;
|
|
||||||
return `{${Object.keys(record)
|
|
||||||
.filter((key) => record[key] !== undefined)
|
|
||||||
.sort()
|
|
||||||
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
||||||
.join(',')}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPluginName = (pluginKey: string) => {
|
|
||||||
return pluginKey
|
|
||||||
.split('-')
|
|
||||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
const readManifest = (pluginRoot: string) => {
|
|
||||||
const manifestPath = path.join(pluginRoot, 'plugin.json');
|
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
|
|
||||||
|
|
||||||
return parseQqbotPluginManifest(manifest, { pluginRoot });
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyTemplateFile = (
|
|
||||||
sourcePath: string,
|
|
||||||
targetPath: string,
|
|
||||||
replacements: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
const content = fs.readFileSync(sourcePath);
|
|
||||||
const textContent = content.toString('utf8');
|
|
||||||
const rendered = Object.entries(replacements).reduce(
|
|
||||||
(result, [key, value]) => result.replaceAll(key, value),
|
|
||||||
textContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
||||||
fs.writeFileSync(targetPath, rendered);
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyTemplateDirectory = (
|
|
||||||
sourceRoot: string,
|
|
||||||
targetRoot: string,
|
|
||||||
replacements: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
||||||
const sourcePath = path.join(sourceRoot, entry.name);
|
|
||||||
const targetPath = path.join(targetRoot, entry.name);
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
copyTemplateDirectory(sourcePath, targetPath, replacements);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyTemplateFile(sourcePath, targetPath, replacements);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const listPackageFiles = (pluginRoot: string): PackedPluginFile[] => {
|
|
||||||
const files: PackedPluginFile[] = [];
|
|
||||||
const ignoredRoots = new Set(['dist', 'node_modules']);
|
|
||||||
|
|
||||||
const visit = (current: string) => {
|
|
||||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
||||||
if (current === pluginRoot && ignoredRoots.has(entry.name)) continue;
|
|
||||||
|
|
||||||
const absolutePath = path.join(current, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
visit(absolutePath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativePath = path
|
|
||||||
.relative(pluginRoot, absolutePath)
|
|
||||||
.replace(/\\/g, '/');
|
|
||||||
files.push({
|
|
||||||
path: relativePath,
|
|
||||||
sha256: sha256(fs.readFileSync(absolutePath)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(pluginRoot);
|
|
||||||
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildPackedPlugin = (pluginRoot: string): PackedPlugin => {
|
|
||||||
const manifest = readManifest(pluginRoot);
|
|
||||||
const files = listPackageFiles(pluginRoot);
|
|
||||||
const contentHash = sha256(
|
|
||||||
stableStringify({
|
|
||||||
files,
|
|
||||||
manifest,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentHash,
|
|
||||||
files,
|
|
||||||
manifest,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const assertPackageIntegrity = (packedPlugin: PackedPlugin) => {
|
|
||||||
const expectedHash = sha256(
|
|
||||||
stableStringify({
|
|
||||||
files: packedPlugin.files,
|
|
||||||
manifest: packedPlugin.manifest,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (packedPlugin.contentHash !== expectedHash) {
|
|
||||||
throw new Error('QQBot plugin package content hash mismatch.');
|
|
||||||
}
|
|
||||||
|
|
||||||
parseQqbotPluginManifest(packedPlugin.manifest);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPlugin = (
|
|
||||||
pluginKey: string | undefined,
|
|
||||||
options: Required<QqbotPluginCliOptions>,
|
|
||||||
): QqbotPluginCliResult => {
|
|
||||||
if (!pluginKey) throw new Error('Usage: qqbot-plugin create <pluginKey>');
|
|
||||||
|
|
||||||
const pluginRoot = path.join(options.cwd, 'plugins', pluginKey);
|
|
||||||
if (fs.existsSync(pluginRoot)) {
|
|
||||||
throw new Error(`Plugin already exists: ${pluginRoot}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyTemplateDirectory(templateRoot, pluginRoot, {
|
|
||||||
__PLUGIN_KEY__: pluginKey,
|
|
||||||
__PLUGIN_NAME__: formatPluginName(pluginKey),
|
|
||||||
});
|
|
||||||
fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true });
|
|
||||||
fs.mkdirSync(path.join(pluginRoot, 'migrations'), { recursive: true });
|
|
||||||
fs.mkdirSync(path.join(pluginRoot, 'tests'), { recursive: true });
|
|
||||||
readManifest(pluginRoot);
|
|
||||||
|
|
||||||
options.stdout(`Created QQBot plugin: ${pluginRoot}`);
|
|
||||||
return {
|
|
||||||
command: 'create',
|
|
||||||
exitCode: 0,
|
|
||||||
pluginKey,
|
|
||||||
pluginRoot,
|
|
||||||
version: '0.1.0',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const validatePlugin = (
|
|
||||||
pluginRootArg: string | undefined,
|
|
||||||
options: Required<QqbotPluginCliOptions>,
|
|
||||||
): QqbotPluginCliResult => {
|
|
||||||
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin validate <path>');
|
|
||||||
|
|
||||||
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
|
||||||
const manifest = readManifest(pluginRoot);
|
|
||||||
options.stdout(
|
|
||||||
`Validated QQBot plugin: ${manifest.pluginKey}@${manifest.version}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
command: 'validate',
|
|
||||||
exitCode: 0,
|
|
||||||
pluginKey: manifest.pluginKey,
|
|
||||||
pluginRoot,
|
|
||||||
version: manifest.version,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const packPlugin = (
|
|
||||||
pluginRootArg: string | undefined,
|
|
||||||
options: Required<QqbotPluginCliOptions>,
|
|
||||||
): QqbotPluginCliResult => {
|
|
||||||
if (!pluginRootArg) throw new Error('Usage: qqbot-plugin pack <path>');
|
|
||||||
|
|
||||||
const pluginRoot = path.resolve(options.cwd, pluginRootArg);
|
|
||||||
const packedPlugin = buildPackedPlugin(pluginRoot);
|
|
||||||
const shortHash = packedPlugin.contentHash.slice(0, 12);
|
|
||||||
const packagePath = path.join(
|
|
||||||
pluginRoot,
|
|
||||||
'dist',
|
|
||||||
`${packedPlugin.manifest.pluginKey}-${packedPlugin.manifest.version}-${shortHash}${packageExtension}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(packagePath), { recursive: true });
|
|
||||||
fs.writeFileSync(packagePath, `${JSON.stringify(packedPlugin, null, 2)}\n`);
|
|
||||||
options.stdout(`Packed QQBot plugin: ${packagePath}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
command: 'pack',
|
|
||||||
exitCode: 0,
|
|
||||||
packageHash: packedPlugin.contentHash,
|
|
||||||
packagePath,
|
|
||||||
pluginKey: packedPlugin.manifest.pluginKey,
|
|
||||||
pluginRoot,
|
|
||||||
version: packedPlugin.manifest.version,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const installLocalPlugin = (
|
|
||||||
packagePathArg: string | undefined,
|
|
||||||
options: Required<QqbotPluginCliOptions>,
|
|
||||||
): QqbotPluginCliResult => {
|
|
||||||
if (!packagePathArg) {
|
|
||||||
throw new Error('Usage: qqbot-plugin install-local <package>');
|
|
||||||
}
|
|
||||||
|
|
||||||
const packagePath = path.resolve(options.cwd, packagePathArg);
|
|
||||||
const packedPlugin = JSON.parse(
|
|
||||||
fs.readFileSync(packagePath, 'utf8'),
|
|
||||||
) as PackedPlugin;
|
|
||||||
assertPackageIntegrity(packedPlugin);
|
|
||||||
options.stdout(
|
|
||||||
`Installed local QQBot plugin package: ${packedPlugin.manifest.pluginKey}@${packedPlugin.manifest.version}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
command: 'install-local',
|
|
||||||
exitCode: 0,
|
|
||||||
packageHash: packedPlugin.contentHash,
|
|
||||||
packagePath,
|
|
||||||
pluginKey: packedPlugin.manifest.pluginKey,
|
|
||||||
version: packedPlugin.manifest.version,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const runQqbotPluginCli = async (
|
|
||||||
argv: string[],
|
|
||||||
options: QqbotPluginCliOptions = {},
|
|
||||||
): Promise<QqbotPluginCliResult> => {
|
|
||||||
const command = argv[0] as QqbotPluginCliCommand | undefined;
|
|
||||||
const resolvedOptions: Required<QqbotPluginCliOptions> = {
|
|
||||||
cwd: options.cwd || process.cwd(),
|
|
||||||
stderr: options.stderr || ((message) => console.error(message)),
|
|
||||||
stdout: options.stdout || ((message) => console.log(message)),
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case 'create':
|
|
||||||
return createPlugin(argv[1], resolvedOptions);
|
|
||||||
case 'install-local':
|
|
||||||
return installLocalPlugin(argv[1], resolvedOptions);
|
|
||||||
case 'pack':
|
|
||||||
return packPlugin(argv[1], resolvedOptions);
|
|
||||||
case 'validate':
|
|
||||||
return validatePlugin(argv[1], resolvedOptions);
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
'Usage: qqbot-plugin <create|validate|pack|install-local> [args]',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
runQqbotPluginCli(process.argv.slice(2)).catch((error) => {
|
|
||||||
console.error(error instanceof Error ? error.message : error);
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginKey": "__PLUGIN_KEY__",
|
|
||||||
"name": "__PLUGIN_NAME__",
|
|
||||||
"description": "Generated QQBot plugin",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"author": "KT",
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"homepage": "",
|
|
||||||
"minApiSdkVersion": "1.0.0",
|
|
||||||
"entry": "src/index.ts",
|
|
||||||
"permissions": ["qqbot.send", "plugin.config.read", "plugin.storage.write"],
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"key": "__PLUGIN_KEY__.echo",
|
|
||||||
"name": "Echo",
|
|
||||||
"description": "Echo input text",
|
|
||||||
"handlerName": "echo",
|
|
||||||
"aliases": ["/__PLUGIN_KEY__"],
|
|
||||||
"permissions": ["qqbot.send"],
|
|
||||||
"timeoutMs": 3000,
|
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"text": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"outputSchema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"events": [
|
|
||||||
{
|
|
||||||
"key": "__PLUGIN_KEY__.message",
|
|
||||||
"name": "Message Event",
|
|
||||||
"eventName": "message.created",
|
|
||||||
"handlerName": "onMessage"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
},
|
|
||||||
"assets": [],
|
|
||||||
"migrations": [],
|
|
||||||
"runtime": {
|
|
||||||
"workerType": "node-worker",
|
|
||||||
"timeoutMs": 5000,
|
|
||||||
"memoryMb": 128,
|
|
||||||
"maxConcurrency": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
export async function echo(input: { text?: string }) {
|
|
||||||
return {
|
|
||||||
replyText: input.text || 'pong',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function onMessage() {
|
|
||||||
return {
|
|
||||||
handled: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Database,
|
|
||||||
[Parameter(Mandatory = $true)][string]$OutputDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
if ($Database -notmatch '^[A-Za-z0-9_]+$') {
|
|
||||||
throw "Database must match ^[A-Za-z0-9_]+$"
|
|
||||||
}
|
|
||||||
|
|
||||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
||||||
$target = Join-Path $OutputDirectory "$Database-refactor-v3-$stamp.sql"
|
|
||||||
mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers --default-character-set=utf8mb4 "--result-file=$target" $Database
|
|
||||||
Write-Output $target
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Database,
|
|
||||||
[string]$HostName = "127.0.0.1",
|
|
||||||
[int]$Port = 3306,
|
|
||||||
[string]$User = "root"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
if ($Database -notmatch '^[A-Za-z0-9_]+$') {
|
|
||||||
throw "Database must match ^[A-Za-z0-9_]+$"
|
|
||||||
}
|
|
||||||
|
|
||||||
$root = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
|
||||||
$schema = Join-Path $root "sql\refactor-v3\00-full-schema.sql"
|
|
||||||
$seed = Join-Path $root "sql\refactor-v3\01-seed-core.sql"
|
|
||||||
$verify = Join-Path $root "sql\refactor-v3\99-verify.sql"
|
|
||||||
|
|
||||||
function Invoke-Mysql {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string[]]$Arguments
|
|
||||||
)
|
|
||||||
|
|
||||||
mysql --default-character-set=utf8mb4 @Arguments
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "mysql failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-MysqlSource {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Path
|
|
||||||
)
|
|
||||||
|
|
||||||
$sourcePath = (Resolve-Path -LiteralPath $Path).Path.Replace("\", "/")
|
|
||||||
Invoke-Mysql -Arguments @("-h", $HostName, "-P", "$Port", "-u", $User, $Database, "--execute=source $sourcePath")
|
|
||||||
}
|
|
||||||
|
|
||||||
Invoke-Mysql -Arguments @("-h", $HostName, "-P", "$Port", "-u", $User, "-e", "DROP DATABASE IF EXISTS ``$Database``; CREATE DATABASE ``$Database`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;")
|
|
||||||
Invoke-MysqlSource -Path $schema
|
|
||||||
Invoke-MysqlSource -Path $seed
|
|
||||||
Invoke-MysqlSource -Path $verify
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Database,
|
|
||||||
[Parameter(Mandatory = $true)][string]$BackupFile
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
if ($Database -notmatch '^[A-Za-z0-9_]+$') {
|
|
||||||
throw "Database must match ^[A-Za-z0-9_]+$"
|
|
||||||
}
|
|
||||||
if (-not (Test-Path -LiteralPath $BackupFile -PathType Leaf)) {
|
|
||||||
throw "BackupFile does not exist"
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourcePath = (Resolve-Path -LiteralPath $BackupFile).Path.Replace("\", "/")
|
|
||||||
mysql -e "DROP DATABASE IF EXISTS ``$Database``; CREATE DATABASE ``$Database`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
|
||||||
mysql --default-character-set=utf8mb4 $Database --execute="source $sourcePath"
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
param(
|
|
||||||
[string]$BaseUrl = "http://127.0.0.1:48085"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$runtime = Invoke-RestMethod -Method Get -Uri "$BaseUrl/health/runtime" -TimeoutSec 10
|
|
||||||
if (-not $runtime.service) {
|
|
||||||
throw "Runtime health response did not include service"
|
|
||||||
}
|
|
||||||
Write-Output "runtime.service=$($runtime.service)"
|
|
||||||
@ -1,987 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS admin_menu (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
pid BIGINT NOT NULL DEFAULT 0,
|
|
||||||
name VARCHAR(120) NOT NULL,
|
|
||||||
path VARCHAR(255) NULL,
|
|
||||||
component VARCHAR(255) NULL,
|
|
||||||
redirect VARCHAR(255) NULL,
|
|
||||||
auth_code VARCHAR(120) NULL,
|
|
||||||
type VARCHAR(32) NOT NULL DEFAULT 'menu',
|
|
||||||
meta LONGTEXT NULL,
|
|
||||||
status INT NOT NULL DEFAULT 1,
|
|
||||||
sort INT NOT NULL DEFAULT 0,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
UNIQUE KEY uk_admin_menu_name (name),
|
|
||||||
KEY idx_admin_menu_pid (pid),
|
|
||||||
KEY idx_admin_menu_path (path),
|
|
||||||
KEY idx_admin_menu_auth_code (auth_code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_role (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
role_code VARCHAR(255) NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
status INT NOT NULL DEFAULT 1,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
UNIQUE KEY uk_admin_role_code (role_code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_user (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
username VARCHAR(255) NOT NULL,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
real_name VARCHAR(255) NOT NULL,
|
|
||||||
avatar VARCHAR(1024) NOT NULL DEFAULT '',
|
|
||||||
dept_id BIGINT NULL,
|
|
||||||
home_path VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
timezone VARCHAR(255) NOT NULL DEFAULT 'Asia/Shanghai',
|
|
||||||
status INT NOT NULL DEFAULT 1,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
UNIQUE KEY uk_admin_user_username (username),
|
|
||||||
KEY idx_admin_user_dept_id (dept_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_dept (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
pid BIGINT NOT NULL DEFAULT 0,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
status INT NOT NULL DEFAULT 1,
|
|
||||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
KEY idx_admin_dept_pid (pid)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_dict (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
dict_code VARCHAR(255) NOT NULL,
|
|
||||||
label VARCHAR(255) NOT NULL,
|
|
||||||
value VARCHAR(255) NOT NULL,
|
|
||||||
children_code VARCHAR(255) NULL,
|
|
||||||
sort INT NOT NULL DEFAULT 0,
|
|
||||||
status INT NOT NULL DEFAULT 1,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
UNIQUE KEY uk_admin_dict_code_value (dict_code, value),
|
|
||||||
KEY idx_admin_dict_code (dict_code),
|
|
||||||
KEY idx_admin_dict_children_code (children_code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_component (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
type INT NOT NULL,
|
|
||||||
component_type INT NOT NULL,
|
|
||||||
image MEDIUMTEXT NOT NULL,
|
|
||||||
template MEDIUMTEXT NOT NULL,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
KEY idx_admin_component_type (type),
|
|
||||||
KEY idx_admin_component_component_type (component_type),
|
|
||||||
KEY idx_admin_component_deleted (is_deleted)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_notice (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
content LONGTEXT NOT NULL,
|
|
||||||
summary TEXT NULL,
|
|
||||||
level INT NOT NULL DEFAULT 1,
|
|
||||||
status INT NOT NULL DEFAULT 1,
|
|
||||||
severity VARCHAR(16) NOT NULL DEFAULT 'info',
|
|
||||||
source VARCHAR(64) NOT NULL DEFAULT 'system',
|
|
||||||
event_type VARCHAR(120) NOT NULL DEFAULT 'system.event',
|
|
||||||
dedupe_key VARCHAR(255) NULL,
|
|
||||||
occurrence_count INT NOT NULL DEFAULT 1,
|
|
||||||
notify_role_code VARCHAR(64) NOT NULL DEFAULT 'super',
|
|
||||||
metadata JSON NULL,
|
|
||||||
is_top TINYINT NOT NULL DEFAULT 0,
|
|
||||||
notify_users TEXT NULL,
|
|
||||||
created_by BIGINT NULL,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
active_dedupe_key VARCHAR(255) GENERATED ALWAYS AS (CASE WHEN is_deleted = 0 AND dedupe_key IS NOT NULL THEN dedupe_key ELSE NULL END) VIRTUAL,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
first_seen_at DATETIME NULL,
|
|
||||||
last_seen_at DATETIME NULL,
|
|
||||||
UNIQUE KEY uk_admin_notice_active_dedupe_key (active_dedupe_key),
|
|
||||||
KEY idx_admin_notice_status (status),
|
|
||||||
KEY idx_admin_notice_severity (severity),
|
|
||||||
KEY idx_admin_notice_source_event (source, event_type),
|
|
||||||
KEY idx_admin_notice_dedupe_key (dedupe_key),
|
|
||||||
KEY idx_admin_notice_notify_role (notify_role_code),
|
|
||||||
KEY idx_admin_notice_is_top (is_top),
|
|
||||||
KEY idx_admin_notice_is_deleted (is_deleted),
|
|
||||||
KEY idx_admin_notice_create_time (create_time),
|
|
||||||
KEY idx_admin_notice_last_seen (last_seen_at)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_user_role (
|
|
||||||
user_id BIGINT NOT NULL,
|
|
||||||
role_id BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (user_id, role_id),
|
|
||||||
KEY idx_admin_user_role_role_id (role_id),
|
|
||||||
CONSTRAINT fk_admin_user_role_user FOREIGN KEY (user_id) REFERENCES admin_user (id) ON DELETE CASCADE,
|
|
||||||
CONSTRAINT fk_admin_user_role_role FOREIGN KEY (role_id) REFERENCES admin_role (id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_role_menu (
|
|
||||||
role_id BIGINT NOT NULL,
|
|
||||||
menu_id BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (role_id, menu_id),
|
|
||||||
KEY idx_admin_role_menu_menu_id (menu_id),
|
|
||||||
CONSTRAINT fk_admin_role_menu_role FOREIGN KEY (role_id) REFERENCES admin_role (id) ON DELETE CASCADE,
|
|
||||||
CONSTRAINT fk_admin_role_menu_menu FOREIGN KEY (menu_id) REFERENCES admin_menu (id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_permission (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
permission_key VARCHAR(128) NOT NULL,
|
|
||||||
permission_name VARCHAR(128) NOT NULL,
|
|
||||||
module_key VARCHAR(64) 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_admin_permission_key (permission_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_department (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
parent_id BIGINT NULL,
|
|
||||||
dept_name VARCHAR(128) NOT NULL,
|
|
||||||
sort_no INT NOT NULL DEFAULT 0,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_admin_department_parent (parent_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_role_permission (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
role_id BIGINT NOT NULL,
|
|
||||||
permission_id BIGINT NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_admin_role_permission (role_id, permission_id),
|
|
||||||
KEY idx_admin_role_permission_permission (permission_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS platform_dict_group (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
group_key VARCHAR(128) NOT NULL,
|
|
||||||
group_name VARCHAR(128) NOT NULL,
|
|
||||||
status VARCHAR(32) 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_platform_dict_group_key (group_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS platform_dict_item (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
group_id BIGINT NOT NULL,
|
|
||||||
item_key VARCHAR(128) NOT NULL,
|
|
||||||
item_label VARCHAR(128) NOT NULL,
|
|
||||||
item_value VARCHAR(255) NOT NULL,
|
|
||||||
sort_no INT NOT NULL DEFAULT 0,
|
|
||||||
status VARCHAR(32) 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_platform_dict_item (group_id, item_key),
|
|
||||||
KEY idx_platform_dict_item_group (group_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS platform_component_template (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
template_key VARCHAR(128) NOT NULL,
|
|
||||||
template_name VARCHAR(128) NOT NULL,
|
|
||||||
schema_json JSON NULL,
|
|
||||||
status VARCHAR(32) 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_platform_component_template_key (template_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS platform_setting (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
setting_key VARCHAR(128) NOT NULL,
|
|
||||||
setting_value TEXT NULL,
|
|
||||||
value_type VARCHAR(32) 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_platform_setting_key (setting_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_post (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
slug VARCHAR(255) NOT NULL,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
summary TEXT NULL,
|
|
||||||
content_markdown LONGTEXT NULL,
|
|
||||||
content_html LONGTEXT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
publish_time 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_blog_post_slug (slug),
|
|
||||||
KEY idx_blog_post_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_article (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
slug VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
|
||||||
excerpt TEXT NULL,
|
|
||||||
content_markdown MEDIUMTEXT NULL,
|
|
||||||
content_html MEDIUMTEXT NULL,
|
|
||||||
cover TEXT NULL,
|
|
||||||
author_name VARCHAR(255) NOT NULL DEFAULT 'KwiTsukasa',
|
|
||||||
category_items TEXT NULL,
|
|
||||||
tag_items TEXT NULL,
|
|
||||||
views INT NOT NULL DEFAULT 0,
|
|
||||||
comments INT NOT NULL DEFAULT 0,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
publish_time DATETIME NULL,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
KEY idx_blog_article_slug (slug),
|
|
||||||
KEY idx_blog_article_status (status),
|
|
||||||
KEY idx_blog_article_publish_time (publish_time),
|
|
||||||
KEY idx_blog_article_is_deleted (is_deleted)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_taxonomy (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
taxonomy_key VARCHAR(64) NOT NULL,
|
|
||||||
taxonomy_name VARCHAR(128) 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_blog_taxonomy_key (taxonomy_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_term (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
taxonomy_id BIGINT NULL,
|
|
||||||
kind VARCHAR(32) NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
slug VARCHAR(255) NOT NULL,
|
|
||||||
term_name VARCHAR(128) NULL,
|
|
||||||
description TEXT NULL,
|
|
||||||
parent_id VARCHAR(64) NULL,
|
|
||||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
|
||||||
KEY idx_blog_term_taxonomy (taxonomy_id),
|
|
||||||
KEY idx_blog_term_kind_slug (kind, slug),
|
|
||||||
KEY idx_blog_term_parent_id (parent_id),
|
|
||||||
KEY idx_blog_term_is_deleted (is_deleted)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_post_term (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
post_id BIGINT NOT NULL,
|
|
||||||
term_id BIGINT NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_blog_post_term (post_id, term_id),
|
|
||||||
KEY idx_blog_post_term_term (term_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_theme_profile (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
profile_key VARCHAR(128) NOT NULL,
|
|
||||||
profile_name VARCHAR(128) NOT NULL,
|
|
||||||
config_json JSON NOT NULL,
|
|
||||||
enabled 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_blog_theme_profile_key (profile_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_theme_config (
|
|
||||||
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
|
||||||
config LONGTEXT NOT NULL,
|
|
||||||
source VARCHAR(255) NOT NULL DEFAULT 'local',
|
|
||||||
create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
||||||
update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blog_import_job (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
source_key VARCHAR(64) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
started_at DATETIME NULL,
|
|
||||||
finished_at DATETIME NULL,
|
|
||||||
summary_json JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_blog_import_job_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wordpress_site (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
site_key VARCHAR(128) NOT NULL,
|
|
||||||
base_url VARCHAR(512) NOT NULL,
|
|
||||||
status VARCHAR(32) 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_wordpress_site_key (site_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wordpress_auth_session (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
site_id BIGINT NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
expires_at DATETIME NULL,
|
|
||||||
safe_summary JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_wordpress_auth_session_site (site_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wordpress_remote_post (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
site_id BIGINT NOT NULL,
|
|
||||||
remote_id VARCHAR(64) NOT NULL,
|
|
||||||
slug VARCHAR(255) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
raw_payload JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_wordpress_remote_post (site_id, remote_id),
|
|
||||||
KEY idx_wordpress_remote_post_slug (slug)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wordpress_remote_term (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
site_id BIGINT NOT NULL,
|
|
||||||
remote_id VARCHAR(64) NOT NULL,
|
|
||||||
taxonomy_key VARCHAR(64) NOT NULL,
|
|
||||||
slug VARCHAR(255) NOT NULL,
|
|
||||||
raw_payload JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_wordpress_remote_term (site_id, taxonomy_key, remote_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wordpress_sync_job (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
site_id BIGINT NOT NULL,
|
|
||||||
job_type VARCHAR(64) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
started_at DATETIME NULL,
|
|
||||||
finished_at DATETIME NULL,
|
|
||||||
summary_json JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_wordpress_sync_job_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wordpress_sync_mapping (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
site_id BIGINT NOT NULL,
|
|
||||||
remote_type VARCHAR(64) NOT NULL,
|
|
||||||
remote_id VARCHAR(64) NOT NULL,
|
|
||||||
local_type VARCHAR(64) NOT NULL,
|
|
||||||
local_id BIGINT 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_wordpress_sync_mapping (site_id, remote_type, remote_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS asset_bucket (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
bucket_key VARCHAR(128) NOT NULL,
|
|
||||||
bucket_name VARCHAR(128) NOT NULL,
|
|
||||||
provider VARCHAR(64) NOT NULL,
|
|
||||||
status VARCHAR(32) 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_asset_bucket_key (bucket_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS asset_object (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
bucket_id BIGINT NOT NULL,
|
|
||||||
object_key VARCHAR(512) NOT NULL,
|
|
||||||
source_module VARCHAR(64) NOT NULL,
|
|
||||||
mime_type VARCHAR(128) NULL,
|
|
||||||
size_bytes BIGINT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
metadata_json JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_asset_object_key (bucket_id, object_key),
|
|
||||||
KEY idx_asset_object_source (source_module)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS asset_reference (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
object_id BIGINT NOT NULL,
|
|
||||||
owner_module VARCHAR(64) NOT NULL,
|
|
||||||
owner_type VARCHAR(64) NOT NULL,
|
|
||||||
owner_id BIGINT NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_asset_reference (object_id, owner_module, owner_type, owner_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS asset_access_grant (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
object_id BIGINT NOT NULL,
|
|
||||||
grant_token VARCHAR(128) NOT NULL,
|
|
||||||
expires_at DATETIME NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_asset_access_grant_token (grant_token),
|
|
||||||
KEY idx_asset_access_grant_object (object_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_notice (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
notice_key VARCHAR(128) NOT NULL,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
level VARCHAR(32) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_system_notice_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_event (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
event_key VARCHAR(128) NOT NULL,
|
|
||||||
source_module VARCHAR(64) NOT NULL,
|
|
||||||
event_type VARCHAR(64) NOT NULL,
|
|
||||||
payload_json JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_system_event_key (event_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_event_dedupe (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
dedupe_key VARCHAR(255) NOT NULL,
|
|
||||||
first_seen_at DATETIME NOT NULL,
|
|
||||||
last_seen_at DATETIME NOT NULL,
|
|
||||||
hit_count INT NOT NULL DEFAULT 1,
|
|
||||||
UNIQUE KEY uk_system_event_dedupe_key (dedupe_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS system_event_delivery (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
event_id BIGINT NOT NULL,
|
|
||||||
delivery_target VARCHAR(128) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
attempt_count INT NOT NULL DEFAULT 0,
|
|
||||||
last_error TEXT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_system_event_delivery_event (event_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS runtime_evidence_index (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
evidence_key VARCHAR(128) NOT NULL,
|
|
||||||
evidence_type VARCHAR(64) NOT NULL,
|
|
||||||
artifact_path VARCHAR(512) NOT NULL,
|
|
||||||
safe_summary JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_runtime_evidence_key (evidence_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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,
|
|
||||||
session_key VARCHAR(128) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
connected_at DATETIME NULL,
|
|
||||||
disconnected_at DATETIME NULL,
|
|
||||||
close_reason TEXT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_connection_session_key (session_key),
|
|
||||||
KEY idx_qqbot_connection_session_account (account_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_capability_binding (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
account_id BIGINT NOT NULL,
|
|
||||||
capability_key VARCHAR(128) NOT NULL,
|
|
||||||
enabled TINYINT NOT NULL DEFAULT 1,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_capability_binding (account_id, capability_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_permission_policy (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
policy_key VARCHAR(128) NOT NULL,
|
|
||||||
scope_type VARCHAR(64) NOT NULL,
|
|
||||||
scope_value VARCHAR(128) NOT NULL,
|
|
||||||
effect VARCHAR(32) 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_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) 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),
|
|
||||||
KEY idx_qqbot_command_operation (operation_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_command_alias (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
command_id BIGINT NOT NULL,
|
|
||||||
alias_text VARCHAR(128) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
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) 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 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)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_conversation (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
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)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_message (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
account_id BIGINT NULL,
|
|
||||||
self_id VARCHAR(64) NOT NULL,
|
|
||||||
conversation_id BIGINT 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 NULL,
|
|
||||||
conversation_id BIGINT NULL,
|
|
||||||
task_key VARCHAR(128) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
payload_json JSON NOT NULL,
|
|
||||||
reserved_at DATETIME NULL,
|
|
||||||
sent_at DATETIME NULL,
|
|
||||||
last_error TEXT 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_send_task_key (task_key),
|
|
||||||
KEY idx_qqbot_send_task_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_send_log (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
task_id BIGINT 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;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_dedupe_event (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
dedupe_key VARCHAR(255) NOT NULL,
|
|
||||||
account_id BIGINT NULL,
|
|
||||||
expires_at DATETIME NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
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,
|
|
||||||
plugin_name VARCHAR(128) NOT NULL,
|
|
||||||
description TEXT NULL,
|
|
||||||
status VARCHAR(32) 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_plugin_key (plugin_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_version (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
version VARCHAR(64) NOT NULL,
|
|
||||||
package_hash VARCHAR(128) NOT NULL,
|
|
||||||
manifest_json JSON NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_plugin_version (plugin_id, version)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_installation (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
version_id BIGINT NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
runtime_status VARCHAR(32) NOT NULL,
|
|
||||||
installed_path VARCHAR(512) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_qqbot_plugin_installation_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_operation (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
operation_key VARCHAR(128) NOT NULL,
|
|
||||||
operation_name VARCHAR(128) NOT NULL,
|
|
||||||
handler_name VARCHAR(128) NOT NULL,
|
|
||||||
enabled TINYINT NOT NULL DEFAULT 1,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_plugin_operation (plugin_id, operation_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_event_handler (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
event_key VARCHAR(128) NOT NULL,
|
|
||||||
handler_name VARCHAR(128) NOT NULL,
|
|
||||||
enabled TINYINT NOT NULL DEFAULT 1,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_plugin_event_handler (plugin_id, event_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_account_binding (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
account_id BIGINT NOT NULL,
|
|
||||||
enabled TINYINT NOT NULL DEFAULT 1,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_plugin_account_binding (plugin_id, account_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_config (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
config_key VARCHAR(128) NOT NULL,
|
|
||||||
config_value JSON 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_plugin_config (plugin_id, config_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_asset (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
asset_key VARCHAR(255) NOT NULL,
|
|
||||||
asset_path VARCHAR(512) NOT NULL,
|
|
||||||
content_hash VARCHAR(128) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_qqbot_plugin_asset (plugin_id, asset_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_runtime_event (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
plugin_id BIGINT NOT NULL,
|
|
||||||
installation_id BIGINT NULL,
|
|
||||||
event_type VARCHAR(64) NOT NULL,
|
|
||||||
level VARCHAR(32) NOT NULL,
|
|
||||||
safe_summary JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_qqbot_plugin_runtime_event_plugin (plugin_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_container (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
account_id BIGINT NOT NULL,
|
|
||||||
container_name VARCHAR(128) NOT NULL,
|
|
||||||
image_name VARCHAR(255) NOT NULL,
|
|
||||||
status VARCHAR(32) 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_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,
|
|
||||||
container_id BIGINT NULL,
|
|
||||||
data_dir VARCHAR(512) NOT NULL,
|
|
||||||
hostname VARCHAR(128) NOT NULL,
|
|
||||||
machine_id_path VARCHAR(512) NOT NULL,
|
|
||||||
mac_address VARCHAR(64) NOT NULL,
|
|
||||||
verification_status VARCHAR(32) NOT NULL,
|
|
||||||
last_login_evidence JSON NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_napcat_device_identity_account (account_id),
|
|
||||||
KEY idx_napcat_device_identity_container (container_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_account_binding (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
account_id BIGINT NOT NULL,
|
|
||||||
container_id BIGINT NOT NULL,
|
|
||||||
device_identity_id BIGINT NOT NULL,
|
|
||||||
status VARCHAR(32) 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_napcat_account_binding_account (account_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_login_session (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
account_id BIGINT NOT NULL,
|
|
||||||
session_key VARCHAR(128) NOT NULL,
|
|
||||||
login_stage VARCHAR(64) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
progress_message VARCHAR(255) 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_napcat_login_session_key (session_key),
|
|
||||||
KEY idx_napcat_login_session_account (account_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_login_challenge (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
session_id BIGINT NOT NULL,
|
|
||||||
challenge_type VARCHAR(64) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
challenge_url TEXT NULL,
|
|
||||||
challenge_payload JSON NULL,
|
|
||||||
resolved_at DATETIME NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_napcat_login_challenge_session (session_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_runtime_cleanup (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
session_id BIGINT NOT NULL,
|
|
||||||
cleanup_type VARCHAR(64) NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
error_message TEXT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_napcat_runtime_cleanup_session (session_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
INSERT INTO admin_role (
|
|
||||||
id,
|
|
||||||
role_code,
|
|
||||||
name,
|
|
||||||
remark,
|
|
||||||
status
|
|
||||||
) VALUES (
|
|
||||||
2041700000000010001,
|
|
||||||
'super',
|
|
||||||
'超级管理员',
|
|
||||||
'拥有所有后台权限',
|
|
||||||
1
|
|
||||||
) ON DUPLICATE KEY UPDATE
|
|
||||||
name = VALUES(name),
|
|
||||||
remark = VALUES(remark),
|
|
||||||
status = VALUES(status),
|
|
||||||
is_deleted = 0;
|
|
||||||
|
|
||||||
INSERT INTO admin_dept (
|
|
||||||
id,
|
|
||||||
pid,
|
|
||||||
name,
|
|
||||||
status,
|
|
||||||
remark
|
|
||||||
) VALUES (
|
|
||||||
2041700000000200001,
|
|
||||||
0,
|
|
||||||
'KT 总部',
|
|
||||||
1,
|
|
||||||
'根部门'
|
|
||||||
) ON DUPLICATE KEY UPDATE
|
|
||||||
pid = VALUES(pid),
|
|
||||||
name = VALUES(name),
|
|
||||||
status = VALUES(status),
|
|
||||||
remark = VALUES(remark),
|
|
||||||
is_deleted = 0;
|
|
||||||
|
|
||||||
INSERT INTO admin_user (
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
real_name,
|
|
||||||
avatar,
|
|
||||||
dept_id,
|
|
||||||
home_path,
|
|
||||||
timezone,
|
|
||||||
status
|
|
||||||
) VALUES (
|
|
||||||
2041700000000000002,
|
|
||||||
'admin',
|
|
||||||
'123456',
|
|
||||||
'Admin',
|
|
||||||
'',
|
|
||||||
2041700000000200001,
|
|
||||||
'/workspace',
|
|
||||||
'Asia/Shanghai',
|
|
||||||
1
|
|
||||||
) ON DUPLICATE KEY UPDATE
|
|
||||||
password = VALUES(password),
|
|
||||||
real_name = VALUES(real_name),
|
|
||||||
avatar = VALUES(avatar),
|
|
||||||
dept_id = VALUES(dept_id),
|
|
||||||
home_path = VALUES(home_path),
|
|
||||||
timezone = VALUES(timezone),
|
|
||||||
status = VALUES(status),
|
|
||||||
is_deleted = 0;
|
|
||||||
|
|
||||||
INSERT INTO admin_menu (
|
|
||||||
id,
|
|
||||||
pid,
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
component,
|
|
||||||
redirect,
|
|
||||||
auth_code,
|
|
||||||
type,
|
|
||||||
meta,
|
|
||||||
status,
|
|
||||||
sort
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
2041700000000100001,
|
|
||||||
0,
|
|
||||||
'Dashboard',
|
|
||||||
'/dashboard',
|
|
||||||
NULL,
|
|
||||||
'/workspace',
|
|
||||||
NULL,
|
|
||||||
'catalog',
|
|
||||||
'{"order":-1,"title":"page.dashboard.title"}',
|
|
||||||
1,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2041700000000100102,
|
|
||||||
2041700000000100001,
|
|
||||||
'Workspace',
|
|
||||||
'/workspace',
|
|
||||||
'/dashboard/workspace/index',
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
'menu',
|
|
||||||
'{"icon":"carbon:workspace","title":"page.dashboard.workspace"}',
|
|
||||||
1,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2041700000000100400,
|
|
||||||
0,
|
|
||||||
'QqBot',
|
|
||||||
'/qqbot',
|
|
||||||
NULL,
|
|
||||||
'/qqbot/dashboard',
|
|
||||||
NULL,
|
|
||||||
'catalog',
|
|
||||||
'{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}',
|
|
||||||
1,
|
|
||||||
110
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2041700000000100402,
|
|
||||||
2041700000000100400,
|
|
||||||
'QqBotAccount',
|
|
||||||
'/qqbot/account',
|
|
||||||
'/qqbot/account/list',
|
|
||||||
NULL,
|
|
||||||
'QqBot:Account:List',
|
|
||||||
'menu',
|
|
||||||
'{"icon":"lucide:radio-receiver","title":"账号连接"}',
|
|
||||||
1,
|
|
||||||
1
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2041700000000100408,
|
|
||||||
2041700000000100400,
|
|
||||||
'QqBotCommand',
|
|
||||||
'/qqbot/command',
|
|
||||||
'/qqbot/command/list',
|
|
||||||
NULL,
|
|
||||||
'QqBot:Command:List',
|
|
||||||
'menu',
|
|
||||||
'{"icon":"lucide:square-terminal","title":"在线命令"}',
|
|
||||||
1,
|
|
||||||
3
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2041700000000100409,
|
|
||||||
2041700000000100400,
|
|
||||||
'QqBotPlugin',
|
|
||||||
'/qqbot/plugin',
|
|
||||||
'/qqbot/plugin/list',
|
|
||||||
NULL,
|
|
||||||
'QqBot:Plugin:List',
|
|
||||||
'menu',
|
|
||||||
'{"icon":"lucide:plug","title":"插件能力"}',
|
|
||||||
1,
|
|
||||||
4
|
|
||||||
)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
pid = VALUES(pid),
|
|
||||||
path = VALUES(path),
|
|
||||||
component = VALUES(component),
|
|
||||||
redirect = VALUES(redirect),
|
|
||||||
auth_code = VALUES(auth_code),
|
|
||||||
type = VALUES(type),
|
|
||||||
meta = VALUES(meta),
|
|
||||||
status = VALUES(status),
|
|
||||||
sort = VALUES(sort),
|
|
||||||
is_deleted = 0;
|
|
||||||
|
|
||||||
INSERT INTO admin_dict (
|
|
||||||
id,
|
|
||||||
dict_code,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
children_code,
|
|
||||||
sort,
|
|
||||||
status
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
2041700000000300001,
|
|
||||||
'COMPONENT_TYPE',
|
|
||||||
'图表',
|
|
||||||
'1',
|
|
||||||
'CHART',
|
|
||||||
1,
|
|
||||||
1
|
|
||||||
),
|
|
||||||
(
|
|
||||||
2041700000000300101,
|
|
||||||
'CHART',
|
|
||||||
'未分类',
|
|
||||||
'-1',
|
|
||||||
NULL,
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
label = VALUES(label),
|
|
||||||
children_code = VALUES(children_code),
|
|
||||||
sort = VALUES(sort),
|
|
||||||
status = VALUES(status),
|
|
||||||
is_deleted = 0;
|
|
||||||
|
|
||||||
INSERT IGNORE INTO admin_user_role (user_id, role_id)
|
|
||||||
VALUES (2041700000000000002, 2041700000000010001);
|
|
||||||
|
|
||||||
INSERT IGNORE INTO admin_role_menu (role_id, menu_id)
|
|
||||||
SELECT 2041700000000010001, id
|
|
||||||
FROM admin_menu
|
|
||||||
WHERE is_deleted = 0;
|
|
||||||
|
|
||||||
INSERT INTO platform_setting (
|
|
||||||
id,
|
|
||||||
setting_key,
|
|
||||||
setting_value,
|
|
||||||
value_type
|
|
||||||
) VALUES (
|
|
||||||
1000000000000000005,
|
|
||||||
'schema.version',
|
|
||||||
'refactor-v3',
|
|
||||||
'string'
|
|
||||||
) ON DUPLICATE KEY UPDATE
|
|
||||||
setting_value = VALUES(setting_value),
|
|
||||||
value_type = VALUES(value_type);
|
|
||||||
|
|
||||||
INSERT INTO qqbot_plugin (
|
|
||||||
id,
|
|
||||||
plugin_key,
|
|
||||||
plugin_name,
|
|
||||||
description,
|
|
||||||
status
|
|
||||||
) VALUES
|
|
||||||
(
|
|
||||||
1000000000000000101,
|
|
||||||
'bangdream',
|
|
||||||
'BangDream',
|
|
||||||
'Built-in BangDream command plugin metadata.',
|
|
||||||
'installed'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1000000000000000102,
|
|
||||||
'ff14-market',
|
|
||||||
'FF14 Market',
|
|
||||||
'Built-in FF14 market command plugin metadata.',
|
|
||||||
'installed'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1000000000000000103,
|
|
||||||
'fflogs',
|
|
||||||
'FFLogs',
|
|
||||||
'Built-in FFLogs command plugin metadata.',
|
|
||||||
'installed'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1000000000000000104,
|
|
||||||
'repeater',
|
|
||||||
'Repeater',
|
|
||||||
'Built-in repeater event plugin metadata.',
|
|
||||||
'installed'
|
|
||||||
)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
plugin_name = VALUES(plugin_name),
|
|
||||||
description = VALUES(description),
|
|
||||||
status = VALUES(status);
|
|
||||||
|
|
||||||
INSERT INTO qqbot_command (
|
|
||||||
id,
|
|
||||||
operation_key,
|
|
||||||
command_key,
|
|
||||||
code,
|
|
||||||
name,
|
|
||||||
aliases,
|
|
||||||
plugin_key,
|
|
||||||
enabled,
|
|
||||||
cooldown_seconds
|
|
||||||
) VALUES (
|
|
||||||
1000000000000000201,
|
|
||||||
'bangdream.song.search',
|
|
||||||
'bangdream_song',
|
|
||||||
'bd',
|
|
||||||
'BangDream 查歌',
|
|
||||||
'["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]',
|
|
||||||
'bangdream',
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
) ON DUPLICATE KEY UPDATE
|
|
||||||
operation_key = VALUES(operation_key),
|
|
||||||
code = VALUES(code),
|
|
||||||
name = VALUES(name),
|
|
||||||
aliases = VALUES(aliases),
|
|
||||||
plugin_key = VALUES(plugin_key),
|
|
||||||
enabled = VALUES(enabled),
|
|
||||||
cooldown_seconds = VALUES(cooldown_seconds);
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
SELECT 'admin_user' AS table_name, COUNT(*) AS row_count FROM admin_user;
|
|
||||||
SELECT 'admin_role' AS table_name, COUNT(*) AS row_count FROM admin_role;
|
|
||||||
SELECT 'admin_menu' AS table_name, COUNT(*) AS row_count FROM admin_menu;
|
|
||||||
SELECT 'platform_setting' AS table_name, COUNT(*) AS row_count FROM platform_setting;
|
|
||||||
SELECT 'admin_dict' AS table_name, COUNT(*) AS row_count FROM admin_dict;
|
|
||||||
SELECT 'qqbot_command' AS table_name, COUNT(*) AS row_count FROM qqbot_command;
|
|
||||||
SELECT 'qqbot_plugin' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin;
|
|
||||||
SELECT 'napcat_device_identity' AS table_name, COUNT(*) AS row_count FROM napcat_device_identity;
|
|
||||||
|
|
||||||
SELECT 'seed_admin_user' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM admin_user
|
|
||||||
WHERE username = 'admin'
|
|
||||||
AND password <> ''
|
|
||||||
AND status = 1
|
|
||||||
AND is_deleted = 0;
|
|
||||||
|
|
||||||
SELECT 'seed_platform_schema_version' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM platform_setting
|
|
||||||
WHERE setting_key = 'schema.version'
|
|
||||||
AND setting_value = 'refactor-v3';
|
|
||||||
|
|
||||||
SELECT 'seed_qqbot_plugin_bangdream' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM qqbot_plugin
|
|
||||||
WHERE plugin_key = 'bangdream'
|
|
||||||
AND status = 'installed';
|
|
||||||
|
|
||||||
SELECT 'seed_qqbot_command_bangdream_song' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM qqbot_command
|
|
||||||
WHERE command_key = 'bangdream_song'
|
|
||||||
AND operation_key = 'bangdream.song.search'
|
|
||||||
AND plugin_key = 'bangdream'
|
|
||||||
AND enabled = 1;
|
|
||||||
|
|
||||||
SELECT 'index_admin_user_username' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM information_schema.statistics
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'admin_user'
|
|
||||||
AND index_name = 'uk_admin_user_username';
|
|
||||||
|
|
||||||
SELECT 'index_platform_setting_key' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM information_schema.statistics
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'platform_setting'
|
|
||||||
AND index_name = 'uk_platform_setting_key';
|
|
||||||
|
|
||||||
SELECT 'index_admin_dict_code_value' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM information_schema.statistics
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'admin_dict'
|
|
||||||
AND index_name = 'uk_admin_dict_code_value';
|
|
||||||
|
|
||||||
SELECT 'index_qqbot_command_key' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM information_schema.statistics
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'qqbot_command'
|
|
||||||
AND index_name = 'uk_qqbot_command_key';
|
|
||||||
|
|
||||||
SELECT 'index_napcat_device_identity_account' AS check_name, COUNT(*) AS matched_rows
|
|
||||||
FROM information_schema.statistics
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'napcat_device_identity'
|
|
||||||
AND index_name = 'uk_napcat_device_identity_account';
|
|
||||||
@ -6,6 +6,7 @@ import { LoggerModule } from 'nestjs-pino';
|
|||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { MinioModule } from 'nestjs-minio-client';
|
import { MinioModule } from 'nestjs-minio-client';
|
||||||
|
import { MinioClientModule } from './minio/minio.module';
|
||||||
import {
|
import {
|
||||||
ApiRequestLogInterceptor,
|
ApiRequestLogInterceptor,
|
||||||
ApiExceptionFilter,
|
ApiExceptionFilter,
|
||||||
@ -13,12 +14,10 @@ import {
|
|||||||
createPinoLoggerParams,
|
createPinoLoggerParams,
|
||||||
SaveBodyInterceptor,
|
SaveBodyInterceptor,
|
||||||
} from './common';
|
} from './common';
|
||||||
import { AdminModule } from './modules/admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AssetModule } from './modules/asset/asset.module';
|
import { BlogModule } from './blog/blog.module';
|
||||||
import { BlogContentModule } from './modules/blog/blog-content.module';
|
import { WordpressModule } from './wordpress/wordpress.module';
|
||||||
import { QqbotCoreModule } from './modules/qqbot/core/qqbot-core.module';
|
import { QqbotModule } from './qqbot/qqbot.module';
|
||||||
import { QqbotPluginPlatformModule } from './modules/qqbot/plugin-platform/plugin-platform.module';
|
|
||||||
import { WordpressMirrorModule } from './modules/wordpress/wordpress-mirror.module';
|
|
||||||
import { RuntimeModule } from './runtime';
|
import { RuntimeModule } from './runtime';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -44,10 +43,7 @@ import { RuntimeModule } from './runtime';
|
|||||||
password: configService.get('DB_PASSWORD'),
|
password: configService.get('DB_PASSWORD'),
|
||||||
database: configService.get('DB_DATABASE'),
|
database: configService.get('DB_DATABASE'),
|
||||||
synchronize: configService.get<string>('DB_SYNC') === 'true',
|
synchronize: configService.get<string>('DB_SYNC') === 'true',
|
||||||
entities: [
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
__dirname + '/**/*.entity{.ts,.js}',
|
|
||||||
__dirname + '/**/*.entities{.ts,.js}',
|
|
||||||
],
|
|
||||||
subscribers: [__dirname + '/**/*.subscriber{.ts,.js}'],
|
subscribers: [__dirname + '/**/*.subscriber{.ts,.js}'],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -67,14 +63,13 @@ import { RuntimeModule } from './runtime';
|
|||||||
},
|
},
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
MinioClientModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RuntimeModule,
|
RuntimeModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
BlogContentModule,
|
BlogModule,
|
||||||
WordpressMirrorModule,
|
WordpressModule,
|
||||||
AssetModule,
|
QqbotModule,
|
||||||
QqbotCoreModule,
|
|
||||||
QqbotPluginPlatformModule,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { AdminIdentityModule } from './identity/admin-identity.module';
|
|
||||||
import { AdminPlatformConfigModule } from './platform-config/admin-platform-config.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [AdminIdentityModule, AdminPlatformConfigModule],
|
|
||||||
})
|
|
||||||
export class AdminModule {}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
|
||||||
import { AdminAuthController } from '@/admin/auth/admin-auth.controller';
|
|
||||||
import { AdminDeptController } from '@/admin/dept/admin-dept.controller';
|
|
||||||
import { AdminDept } from '@/admin/dept/admin-dept.entity';
|
|
||||||
import { AdminDeptService } from '@/admin/dept/admin-dept.service';
|
|
||||||
import { AdminMenuController } from '@/admin/menu/admin-menu.controller';
|
|
||||||
import { AdminMenu } from '@/admin/menu/admin-menu.entity';
|
|
||||||
import { AdminMenuService } from '@/admin/menu/admin-menu.service';
|
|
||||||
import { AdminRoleController } from '@/admin/role/admin-role.controller';
|
|
||||||
import { AdminRole } from '@/admin/role/admin-role.entity';
|
|
||||||
import { AdminRoleService } from '@/admin/role/admin-role.service';
|
|
||||||
import { AdminUserManageController } from '@/admin/user/admin-user-manage.controller';
|
|
||||||
import { AdminUserController } from '@/admin/user/admin-user.controller';
|
|
||||||
import { AdminUser } from '@/admin/user/admin-user.entity';
|
|
||||||
import { AdminUserService } from '@/admin/user/admin-user.service';
|
|
||||||
import { WordpressMirrorModule } from '@/modules/wordpress/wordpress-mirror.module';
|
|
||||||
|
|
||||||
export const ADMIN_IDENTITY_CONTROLLERS = [
|
|
||||||
AdminAuthController,
|
|
||||||
AdminUserController,
|
|
||||||
AdminUserManageController,
|
|
||||||
AdminMenuController,
|
|
||||||
AdminRoleController,
|
|
||||||
AdminDeptController,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ADMIN_IDENTITY_PROVIDERS = [
|
|
||||||
AdminUserService,
|
|
||||||
AdminMenuService,
|
|
||||||
AdminRoleService,
|
|
||||||
AdminDeptService,
|
|
||||||
];
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([AdminUser, AdminRole, AdminMenu, AdminDept]),
|
|
||||||
AdminAuthGuardModule,
|
|
||||||
WordpressMirrorModule,
|
|
||||||
],
|
|
||||||
controllers: ADMIN_IDENTITY_CONTROLLERS,
|
|
||||||
providers: ADMIN_IDENTITY_PROVIDERS,
|
|
||||||
})
|
|
||||||
export class AdminIdentityModule {}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
|
||||||
import { ComponentController } from '@/admin/component/component.controller';
|
|
||||||
import { Component } from '@/admin/component/component.entity';
|
|
||||||
import { ComponentService } from '@/admin/component/component.service';
|
|
||||||
import { DictController } from '@/admin/dict/dict.controller';
|
|
||||||
import { DictModule } from '@/admin/dict/dict.module';
|
|
||||||
import { AdminNoticeController } from '@/admin/notice/admin-notice.controller';
|
|
||||||
import { NoticeModule } from '@/admin/notice/notice.module';
|
|
||||||
import { SystemLogController } from '@/admin/system-log/system-log.controller';
|
|
||||||
import { SystemLogService } from '@/admin/system-log/system-log.service';
|
|
||||||
import { AdminTimezoneController } from '@/admin/timezone/admin-timezone.controller';
|
|
||||||
import { AdminTimezoneService } from '@/admin/timezone/admin-timezone.service';
|
|
||||||
import { AdminUser } from '@/admin/user/admin-user.entity';
|
|
||||||
import { AdminExampleController } from '@/admin/example/admin-example.controller';
|
|
||||||
import { AssetModule } from '@/modules/asset/asset.module';
|
|
||||||
|
|
||||||
export const ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS = [
|
|
||||||
ComponentController,
|
|
||||||
SystemLogController,
|
|
||||||
AdminTimezoneController,
|
|
||||||
AdminExampleController,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ADMIN_PLATFORM_CONFIG_IMPORTED_CONTROLLERS = [
|
|
||||||
DictController,
|
|
||||||
AdminNoticeController,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ADMIN_PLATFORM_CONFIG_CONTROLLERS = [
|
|
||||||
...ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS,
|
|
||||||
...ADMIN_PLATFORM_CONFIG_IMPORTED_CONTROLLERS,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ADMIN_PLATFORM_CONFIG_PROVIDERS = [
|
|
||||||
ComponentService,
|
|
||||||
SystemLogService,
|
|
||||||
AdminTimezoneService,
|
|
||||||
];
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([Component, AdminUser]),
|
|
||||||
AdminAuthGuardModule,
|
|
||||||
DictModule,
|
|
||||||
NoticeModule,
|
|
||||||
AssetModule,
|
|
||||||
],
|
|
||||||
controllers: ADMIN_PLATFORM_CONFIG_DIRECT_CONTROLLERS,
|
|
||||||
providers: ADMIN_PLATFORM_CONFIG_PROVIDERS,
|
|
||||||
})
|
|
||||||
export class AdminPlatformConfigModule {}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { MinioClientController } from '@/minio/minio.controller';
|
|
||||||
import { MinioClientModule } from '@/minio/minio.module';
|
|
||||||
import { MinioClientService } from '@/minio/minio.service';
|
|
||||||
|
|
||||||
export const ASSET_CONTROLLERS = [MinioClientController];
|
|
||||||
|
|
||||||
export const ASSET_PROVIDERS = [MinioClientService];
|
|
||||||
|
|
||||||
export const ASSET_DOMAIN_CONTRACT = {
|
|
||||||
tables: [
|
|
||||||
'asset_bucket',
|
|
||||||
'asset_object',
|
|
||||||
'asset_reference',
|
|
||||||
'asset_access_grant',
|
|
||||||
],
|
|
||||||
objectOwnership: {
|
|
||||||
objectTable: 'asset_object',
|
|
||||||
bucketTable: 'asset_bucket',
|
|
||||||
ownerModuleField: 'source_module',
|
|
||||||
objectKeyField: 'object_key',
|
|
||||||
},
|
|
||||||
mimeMetadata: {
|
|
||||||
objectTable: 'asset_object',
|
|
||||||
mimeField: 'mime_type',
|
|
||||||
sizeField: 'size_bytes',
|
|
||||||
metadataField: 'metadata_json',
|
|
||||||
},
|
|
||||||
reference: {
|
|
||||||
table: 'asset_reference',
|
|
||||||
objectKey: 'object_id',
|
|
||||||
ownerFields: ['owner_module', 'owner_type', 'owner_id'],
|
|
||||||
},
|
|
||||||
accessGrant: {
|
|
||||||
table: 'asset_access_grant',
|
|
||||||
objectKey: 'object_id',
|
|
||||||
tokenField: 'grant_token',
|
|
||||||
expiresAtField: 'expires_at',
|
|
||||||
statusField: 'status',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [MinioClientModule],
|
|
||||||
exports: [MinioClientModule],
|
|
||||||
})
|
|
||||||
export class AssetModule {}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { BlogArticleController } from '@/blog/blog-article.controller';
|
|
||||||
import { BlogArticleService } from '@/blog/blog-article.service';
|
|
||||||
import { BlogModule } from '@/blog/blog.module';
|
|
||||||
import { BlogTermController } from '@/blog/blog-term.controller';
|
|
||||||
import { BlogTermService } from '@/blog/blog-term.service';
|
|
||||||
import { BlogThemeConfigController } from '@/blog/blog-theme-config.controller';
|
|
||||||
import { BlogThemeConfigService } from '@/blog/blog-theme-config.service';
|
|
||||||
|
|
||||||
export const BLOG_CONTENT_CONTROLLERS = [
|
|
||||||
BlogArticleController,
|
|
||||||
BlogTermController,
|
|
||||||
BlogThemeConfigController,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const BLOG_CONTENT_PROVIDERS = [
|
|
||||||
BlogArticleService,
|
|
||||||
BlogTermService,
|
|
||||||
BlogThemeConfigService,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const BLOG_CONTENT_DOMAIN_CONTRACT = {
|
|
||||||
tables: [
|
|
||||||
'blog_post',
|
|
||||||
'blog_taxonomy',
|
|
||||||
'blog_term',
|
|
||||||
'blog_post_term',
|
|
||||||
'blog_theme_profile',
|
|
||||||
'blog_import_job',
|
|
||||||
],
|
|
||||||
publicArticleList: {
|
|
||||||
route: 'GET /blog/article/public/list',
|
|
||||||
sourceTable: 'blog_post',
|
|
||||||
statusField: 'status',
|
|
||||||
publishTimeField: 'publish_time',
|
|
||||||
},
|
|
||||||
publicArticleDetail: {
|
|
||||||
route: 'GET /blog/article/public/detail',
|
|
||||||
sourceTable: 'blog_post',
|
|
||||||
lookupFields: ['id', 'slug'],
|
|
||||||
},
|
|
||||||
termRelation: {
|
|
||||||
relationTable: 'blog_post_term',
|
|
||||||
postKey: 'post_id',
|
|
||||||
termKey: 'term_id',
|
|
||||||
taxonomyTables: ['blog_taxonomy', 'blog_term'],
|
|
||||||
},
|
|
||||||
themeProfile: {
|
|
||||||
table: 'blog_theme_profile',
|
|
||||||
profileKey: 'profile_key',
|
|
||||||
configField: 'config_json',
|
|
||||||
enabledField: 'enabled',
|
|
||||||
},
|
|
||||||
importJob: {
|
|
||||||
table: 'blog_import_job',
|
|
||||||
sourceField: 'source_key',
|
|
||||||
statusField: 'status',
|
|
||||||
summaryField: 'summary_json',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [BlogModule],
|
|
||||||
exports: [BlogModule],
|
|
||||||
})
|
|
||||||
export class BlogContentModule {}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
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 { QqbotPluginHttpClientService } from '@/modules/qqbot/plugin-platform/sdk';
|
|
||||||
import {
|
|
||||||
NapcatDeviceIdentity,
|
|
||||||
NapcatDeviceIdentityService,
|
|
||||||
} from '@/modules/qqbot/napcat';
|
|
||||||
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 '@/modules/qqbot/plugins/bangDream/application/bangdream-client.service';
|
|
||||||
import { TsuguApplicationService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-application.service';
|
|
||||||
import { QqbotBangDreamRendererService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-renderer.facade';
|
|
||||||
import { QqbotBangDreamPluginService } from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.plugin';
|
|
||||||
import { QqbotFf14ClientService } from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-client.service';
|
|
||||||
import { QqbotFf14MarketPluginService } from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin';
|
|
||||||
import { QqbotFflogsClientService } from '@/modules/qqbot/plugins/fflogs/qqbot-fflogs-client.service';
|
|
||||||
import { QqbotFflogsPluginService } from '@/modules/qqbot/plugins/fflogs/qqbot-fflogs.plugin';
|
|
||||||
import { QqbotRepeaterPluginService } from '@/modules/qqbot/plugins/repeater/qqbot-repeater.plugin';
|
|
||||||
import { QqbotRuleController } from '@/qqbot/rule/qqbot-rule.controller';
|
|
||||||
import { QqbotRule } from '@/qqbot/rule/qqbot-rule.entity';
|
|
||||||
import { QqbotRuleEngineService } from '@/qqbot/rule/qqbot-rule-engine.service';
|
|
||||||
import { QqbotRuleService } from '@/qqbot/rule/qqbot-rule.service';
|
|
||||||
import { QqbotRateLimitService } from '@/qqbot/send/qqbot-rate-limit.service';
|
|
||||||
import { QqbotSendController } from '@/qqbot/send/qqbot-send.controller';
|
|
||||||
import { QqbotSendLog } from '@/qqbot/send/qqbot-send-log.entity';
|
|
||||||
import { QqbotSendService } from '@/qqbot/send/qqbot-send.service';
|
|
||||||
|
|
||||||
export { QQBOT_CORE_DOMAIN_CONTRACT } from './qqbot-core.contract';
|
|
||||||
|
|
||||||
export const QQBOT_CORE_ENTITIES = [
|
|
||||||
QqbotAccount,
|
|
||||||
QqbotAccountAbility,
|
|
||||||
QqbotAllowlist,
|
|
||||||
QqbotBlocklist,
|
|
||||||
QqbotCommand,
|
|
||||||
QqbotCommandLog,
|
|
||||||
QqbotConfig,
|
|
||||||
QqbotConversation,
|
|
||||||
QqbotDedupe,
|
|
||||||
QqbotMessage,
|
|
||||||
NapcatDeviceIdentity,
|
|
||||||
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,
|
|
||||||
QqbotPluginHttpClientService,
|
|
||||||
QqbotBangDreamClientService,
|
|
||||||
QqbotBangDreamPluginService,
|
|
||||||
QqbotBangDreamRendererService,
|
|
||||||
TsuguApplicationService,
|
|
||||||
QqbotFf14ClientService,
|
|
||||||
QqbotFf14MarketPluginService,
|
|
||||||
QqbotFflogsClientService,
|
|
||||||
QqbotFflogsPluginService,
|
|
||||||
QqbotMessageService,
|
|
||||||
NapcatDeviceIdentityService,
|
|
||||||
QqbotNapcatLoginService,
|
|
||||||
QqbotNapcatWatchdogService,
|
|
||||||
QqbotNapcatContainerService,
|
|
||||||
QqbotPermissionService,
|
|
||||||
QqbotEventPluginRegistryService,
|
|
||||||
QqbotPluginRegistryService,
|
|
||||||
QqbotRepeaterPluginService,
|
|
||||||
QqbotRateLimitService,
|
|
||||||
QqbotReplyTemplateService,
|
|
||||||
QqbotReverseWsService,
|
|
||||||
QqbotRuleEngineService,
|
|
||||||
QqbotRuleService,
|
|
||||||
QqbotSendService,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const QQBOT_CORE_EXPORTS = [
|
|
||||||
QqbotAccountService,
|
|
||||||
NapcatDeviceIdentityService,
|
|
||||||
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 {}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import type { NapcatDeviceIdentity } from '../device/napcat-device-identity.entity';
|
|
||||||
|
|
||||||
export type NapcatDockerDeviceOptions = {
|
|
||||||
dataDir: string;
|
|
||||||
deviceEnvPath: string;
|
|
||||||
hostname: string;
|
|
||||||
machineIdPath: string;
|
|
||||||
macAddress: string;
|
|
||||||
runFlags: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function toNapcatDockerDeviceOptions(
|
|
||||||
identity: Pick<
|
|
||||||
NapcatDeviceIdentity,
|
|
||||||
'dataDir' | 'hostname' | 'machineIdPath' | 'macAddress'
|
|
||||||
>,
|
|
||||||
): NapcatDockerDeviceOptions {
|
|
||||||
return {
|
|
||||||
dataDir: identity.dataDir,
|
|
||||||
deviceEnvPath: `${identity.dataDir}/device.env`,
|
|
||||||
hostname: identity.hostname,
|
|
||||||
machineIdPath: identity.machineIdPath,
|
|
||||||
macAddress: identity.macAddress,
|
|
||||||
runFlags: [
|
|
||||||
'--hostname "$NAPCAT_HOSTNAME"',
|
|
||||||
'--mac-address "$NAPCAT_MAC_ADDRESS"',
|
|
||||||
'-v "$MACHINE_ID_PATH:/etc/machine-id:ro"',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
|
|
||||||
export type NapcatDeviceVerificationStatus =
|
|
||||||
| 'pending'
|
|
||||||
| 'trusted'
|
|
||||||
| 'unknown'
|
|
||||||
| 'unverified';
|
|
||||||
|
|
||||||
@Entity('napcat_device_identity')
|
|
||||||
@Index('uk_napcat_device_identity_account', ['accountId'], { unique: true })
|
|
||||||
@Index('idx_napcat_device_identity_container', ['containerId'])
|
|
||||||
export class NapcatDeviceIdentity {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', type: 'bigint' })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ default: null, name: 'container_id', nullable: true, type: 'bigint' })
|
|
||||||
containerId: null | string;
|
|
||||||
|
|
||||||
@Column({ length: 512, name: 'data_dir' })
|
|
||||||
dataDir: string;
|
|
||||||
|
|
||||||
@Column({ length: 128 })
|
|
||||||
hostname: string;
|
|
||||||
|
|
||||||
@Column({ length: 512, name: 'machine_id_path' })
|
|
||||||
machineIdPath: string;
|
|
||||||
|
|
||||||
@Column({ length: 64, name: 'mac_address' })
|
|
||||||
macAddress: string;
|
|
||||||
|
|
||||||
@Column({ length: 32, name: 'verification_status' })
|
|
||||||
verificationStatus: NapcatDeviceVerificationStatus;
|
|
||||||
|
|
||||||
@Column({ default: null, name: 'last_login_evidence', nullable: true, type: 'json' })
|
|
||||||
lastLoginEvidence: null | Record<string, unknown>;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
import { createHash } from 'crypto';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { ensureSnowflakeId } from '@/common';
|
|
||||||
import { NapcatDeviceIdentity } from './napcat-device-identity.entity';
|
|
||||||
|
|
||||||
type ResolveNapcatDeviceIdentityInput = {
|
|
||||||
accountId: string;
|
|
||||||
containerId?: string;
|
|
||||||
selfId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class NapcatDeviceIdentityService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(NapcatDeviceIdentity)
|
|
||||||
private readonly identityRepository: Repository<NapcatDeviceIdentity>,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async resolveForAccount(input: ResolveNapcatDeviceIdentityInput) {
|
|
||||||
const accountId = `${input.accountId}`.trim();
|
|
||||||
const existing = await this.identityRepository.findOne({
|
|
||||||
where: { accountId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
const containerId = input.containerId || null;
|
|
||||||
if (containerId && existing.containerId !== containerId) {
|
|
||||||
await this.identityRepository.update(
|
|
||||||
{ id: existing.id },
|
|
||||||
{ containerId },
|
|
||||||
);
|
|
||||||
existing.containerId = containerId;
|
|
||||||
}
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerName = this.buildContainerName(input.selfId || accountId);
|
|
||||||
const dataDir = `${this.getRootDir()}/${containerName}`;
|
|
||||||
const identity = this.identityRepository.create({
|
|
||||||
accountId,
|
|
||||||
containerId: input.containerId || null,
|
|
||||||
dataDir,
|
|
||||||
hostname: this.buildHostname(containerName),
|
|
||||||
lastLoginEvidence: null,
|
|
||||||
macAddress: this.buildMacAddress(accountId, containerName),
|
|
||||||
machineIdPath: `${dataDir}/machine-id`,
|
|
||||||
verificationStatus: 'pending',
|
|
||||||
});
|
|
||||||
ensureSnowflakeId(identity);
|
|
||||||
|
|
||||||
return this.identityRepository.save(identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildContainerName(seed: string) {
|
|
||||||
const prefix = this.getConfig(
|
|
||||||
'QQBOT_NAPCAT_CONTAINER_PREFIX',
|
|
||||||
'kt-qqbot-napcat',
|
|
||||||
);
|
|
||||||
const suffix = `${seed || 'unknown'}`
|
|
||||||
.replace(/[^a-zA-Z0-9_.-]/g, '-')
|
|
||||||
.toLowerCase();
|
|
||||||
return `${prefix}-${suffix}`.replace(/-+/g, '-').slice(0, 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildHostname(containerName: string) {
|
|
||||||
const normalized = containerName
|
|
||||||
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
||||||
.replace(/-+/g, '-')
|
|
||||||
.replace(/^-|-$/g, '');
|
|
||||||
if (normalized.length <= 63) return normalized;
|
|
||||||
|
|
||||||
const hash = createHash('sha256').update(containerName).digest('hex');
|
|
||||||
return `${normalized.slice(0, 50)}-${hash.slice(0, 12)}`.slice(0, 63);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildMacAddress(accountId: string, containerName: string) {
|
|
||||||
const hash = createHash('sha256')
|
|
||||||
.update(`${accountId}:${containerName}`)
|
|
||||||
.digest('hex');
|
|
||||||
return ['02', '42', hash.slice(0, 2), hash.slice(2, 4), hash.slice(4, 6), hash.slice(6, 8)].join(
|
|
||||||
':',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRootDir() {
|
|
||||||
return (
|
|
||||||
this.getConfig(
|
|
||||||
'QQBOT_NAPCAT_ROOT',
|
|
||||||
'/vol1/docker/kt-qqbot/napcat-instances',
|
|
||||||
)
|
|
||||||
).replace(/[\\/]+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getConfig(key: string, defaultValue = '') {
|
|
||||||
return `${this.configService.get<string>(key) || defaultValue}`.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export * from './container/napcat-docker-device-options';
|
|
||||||
export * from './device/napcat-device-identity.entity';
|
|
||||||
export * from './device/napcat-device-identity.service';
|
|
||||||
export * from './integration/napcat-login-api.client';
|
|
||||||
export * from './login/napcat-login-state-machine';
|
|
||||||
export * from './persistence';
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
export type NewDeviceQrStatus =
|
|
||||||
| 'confirming'
|
|
||||||
| 'expired'
|
|
||||||
| 'failed'
|
|
||||||
| 'qr-pending'
|
|
||||||
| 'scanned'
|
|
||||||
| 'verified';
|
|
||||||
|
|
||||||
export type NewDeviceQrCode = {
|
|
||||||
pullQrCodeSig?: string;
|
|
||||||
qrcodeUrl: string;
|
|
||||||
sessionId: string;
|
|
||||||
status: 'qr-pending';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NewDeviceQrPollResult = {
|
|
||||||
message?: string;
|
|
||||||
sessionId: string;
|
|
||||||
status: Exclude<NewDeviceQrStatus, 'verified'>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NewDeviceLoginResult = {
|
|
||||||
message?: string;
|
|
||||||
sessionId: string;
|
|
||||||
status: 'failed' | 'verified';
|
|
||||||
success: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NapcatLoginApiTransport = {
|
|
||||||
post(path: string, body: Record<string, unknown>): Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NapcatLoginApiClient {
|
|
||||||
constructor(private readonly transport: NapcatLoginApiTransport) {}
|
|
||||||
|
|
||||||
async getNewDeviceQRCode(sessionId: string): Promise<NewDeviceQrCode> {
|
|
||||||
const data = (await this.transport.post(
|
|
||||||
'/api/QQLogin/GetNewDeviceQRCode',
|
|
||||||
{ sessionId },
|
|
||||||
)) as Record<string, unknown>;
|
|
||||||
const qrcodeUrl = this.pickString(
|
|
||||||
data.qrcodeUrl,
|
|
||||||
data.qrcodeurl,
|
|
||||||
data.qrcode,
|
|
||||||
data.url,
|
|
||||||
);
|
|
||||||
if (!qrcodeUrl) {
|
|
||||||
throw new Error('NapCat 未返回新设备验证二维码');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
pullQrCodeSig: this.pickString(data.newDevicePullQrCodeSig, data.sig),
|
|
||||||
qrcodeUrl,
|
|
||||||
sessionId,
|
|
||||||
status: 'qr-pending',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async pollNewDeviceQR(sessionId: string): Promise<NewDeviceQrPollResult> {
|
|
||||||
const data = (await this.transport.post(
|
|
||||||
'/api/QQLogin/PollNewDeviceQR',
|
|
||||||
{ sessionId },
|
|
||||||
)) as Record<string, unknown>;
|
|
||||||
const status = this.normalizePollStatus(
|
|
||||||
this.pickString(data.status, data.state, data.result),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: this.pickString(data.message, data.reason) || undefined,
|
|
||||||
sessionId,
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async newDeviceLogin(sessionId: string): Promise<NewDeviceLoginResult> {
|
|
||||||
const data = (await this.transport.post(
|
|
||||||
'/api/QQLogin/NewDeviceLogin',
|
|
||||||
{ sessionId },
|
|
||||||
)) as Record<string, unknown>;
|
|
||||||
const success = data.success !== false;
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: this.pickString(data.message, data.reason) || undefined,
|
|
||||||
sessionId,
|
|
||||||
status: success ? 'verified' : 'failed',
|
|
||||||
success,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizePollStatus(status: string): NewDeviceQrPollResult['status'] {
|
|
||||||
const normalized = status.toLowerCase().replace(/[_\s-]+/g, '');
|
|
||||||
if (['scan', 'scanned'].includes(normalized)) return 'scanned';
|
|
||||||
if (['confirm', 'confirming'].includes(normalized)) return 'confirming';
|
|
||||||
if (['expire', 'expired', 'timeout'].includes(normalized)) return 'expired';
|
|
||||||
if (['fail', 'failed', 'error', 'denied'].includes(normalized)) {
|
|
||||||
return 'failed';
|
|
||||||
}
|
|
||||||
return 'qr-pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
private pickString(...values: unknown[]) {
|
|
||||||
for (const value of values) {
|
|
||||||
if (typeof value !== 'string') continue;
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (trimmed) return trimmed;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,277 +0,0 @@
|
|||||||
export type NapcatLoginSessionStatus = 'failure' | 'pending' | 'success';
|
|
||||||
|
|
||||||
export type NapcatLoginStage =
|
|
||||||
| 'captcha'
|
|
||||||
| 'cleanup-failed'
|
|
||||||
| 'failure'
|
|
||||||
| 'manual-qr'
|
|
||||||
| 'new-device'
|
|
||||||
| 'password-login'
|
|
||||||
| 'quick-login'
|
|
||||||
| 'success';
|
|
||||||
|
|
||||||
export type NapcatLoginChallenge =
|
|
||||||
| {
|
|
||||||
reason?: string;
|
|
||||||
status: 'failed' | 'pending' | 'submitted';
|
|
||||||
type: 'captcha';
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
pullQrCodeSig?: string;
|
|
||||||
qrcodeUrl?: string;
|
|
||||||
reason?: string;
|
|
||||||
status: 'confirming' | 'expired' | 'failed' | 'qr-pending' | 'scanned';
|
|
||||||
type: 'new-device';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NapcatLoginSessionState = {
|
|
||||||
accountId: string;
|
|
||||||
challenge?: NapcatLoginChallenge;
|
|
||||||
hasHistoricalSession: boolean;
|
|
||||||
hasSavedPassword: boolean;
|
|
||||||
progressMessage: string;
|
|
||||||
selfId?: string;
|
|
||||||
sessionId: string;
|
|
||||||
stage: NapcatLoginStage;
|
|
||||||
status: NapcatLoginSessionStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateNapcatLoginSessionInput = {
|
|
||||||
accountId: string;
|
|
||||||
hasHistoricalSession: boolean;
|
|
||||||
hasSavedPassword: boolean;
|
|
||||||
sessionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NapcatLoginStateEvent =
|
|
||||||
| { type: 'captcha-still-required' }
|
|
||||||
| { type: 'captcha-submitted' }
|
|
||||||
| { newDevicePullQrCodeSig?: string; type: 'captcha-new-device-required' }
|
|
||||||
| { reason?: string; type: 'login-failed' }
|
|
||||||
| { selfId?: string; type: 'login-success' }
|
|
||||||
| { type: 'manual-qr-required' }
|
|
||||||
| { captchaUrl: string; type: 'password-login-captcha-required' }
|
|
||||||
| { type: 'password-login-failed' }
|
|
||||||
| { type: 'quick-login-failed' }
|
|
||||||
| { qrcodeUrl?: string; type: 'new-device-qr-ready' }
|
|
||||||
| { type: 'new-device-scanned' }
|
|
||||||
| { type: 'new-device-confirming' }
|
|
||||||
| { type: 'new-device-poll-pending' }
|
|
||||||
| { type: 'new-device-verified' }
|
|
||||||
| { reason?: string; type: 'new-device-expired' }
|
|
||||||
| { reason?: string; type: 'new-device-failed' }
|
|
||||||
| { reason?: string; type: 'runtime-cleanup-failed' };
|
|
||||||
|
|
||||||
export const NAPCAT_LOGIN_PROGRESS_MESSAGES = {
|
|
||||||
captchaNeeded: '需要验证码',
|
|
||||||
captchaSubmitted: '验证码已提交,等待确认',
|
|
||||||
cleanupFailed: '运行态清理失败',
|
|
||||||
failed: '登录失败',
|
|
||||||
manualQr: '正在生成手动二维码',
|
|
||||||
newDeviceConfirming: '新设备确认中',
|
|
||||||
newDeviceNeeded: '需要新设备验证二维码',
|
|
||||||
newDeviceQrPending: '新设备二维码待扫码',
|
|
||||||
newDeviceScanned: '新设备二维码已扫码',
|
|
||||||
newDeviceVerified: '新设备验证成功,继续登录',
|
|
||||||
passwordLogin: '正在密码登录',
|
|
||||||
quickLogin: '正在快速登录',
|
|
||||||
quickToPassword: '快速登录失败,进入密码登录',
|
|
||||||
success: '登录成功',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function createNapcatLoginSession(
|
|
||||||
input: CreateNapcatLoginSessionInput,
|
|
||||||
): NapcatLoginSessionState {
|
|
||||||
const stage = input.hasHistoricalSession ? 'quick-login' : 'password-login';
|
|
||||||
|
|
||||||
return {
|
|
||||||
accountId: input.accountId,
|
|
||||||
hasHistoricalSession: input.hasHistoricalSession,
|
|
||||||
hasSavedPassword: input.hasSavedPassword,
|
|
||||||
progressMessage: input.hasHistoricalSession
|
|
||||||
? NAPCAT_LOGIN_PROGRESS_MESSAGES.quickLogin
|
|
||||||
: NAPCAT_LOGIN_PROGRESS_MESSAGES.passwordLogin,
|
|
||||||
sessionId: input.sessionId,
|
|
||||||
stage,
|
|
||||||
status: 'pending',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NapcatLoginStateMachine {
|
|
||||||
advance(
|
|
||||||
session: NapcatLoginSessionState,
|
|
||||||
event: NapcatLoginStateEvent,
|
|
||||||
): NapcatLoginSessionState {
|
|
||||||
if (session.status === 'failure') return session;
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case 'quick-login-failed':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
progressMessage: session.hasSavedPassword
|
|
||||||
? NAPCAT_LOGIN_PROGRESS_MESSAGES.quickToPassword
|
|
||||||
: NAPCAT_LOGIN_PROGRESS_MESSAGES.manualQr,
|
|
||||||
stage: session.hasSavedPassword ? 'password-login' : 'manual-qr',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'password-login-failed':
|
|
||||||
case 'manual-qr-required':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.manualQr,
|
|
||||||
stage: 'manual-qr',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'password-login-captcha-required':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
challenge: {
|
|
||||||
status: 'pending',
|
|
||||||
type: 'captcha',
|
|
||||||
url: event.captchaUrl,
|
|
||||||
},
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.captchaNeeded,
|
|
||||||
stage: 'captcha',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'captcha-still-required':
|
|
||||||
return session.challenge?.type === 'captcha'
|
|
||||||
? {
|
|
||||||
...session,
|
|
||||||
challenge: {
|
|
||||||
...session.challenge,
|
|
||||||
status:
|
|
||||||
session.challenge.status === 'submitted'
|
|
||||||
? 'submitted'
|
|
||||||
: 'pending',
|
|
||||||
},
|
|
||||||
progressMessage:
|
|
||||||
session.challenge.status === 'submitted'
|
|
||||||
? NAPCAT_LOGIN_PROGRESS_MESSAGES.captchaSubmitted
|
|
||||||
: NAPCAT_LOGIN_PROGRESS_MESSAGES.captchaNeeded,
|
|
||||||
stage: 'captcha',
|
|
||||||
}
|
|
||||||
: session;
|
|
||||||
|
|
||||||
case 'captcha-submitted':
|
|
||||||
return session.challenge?.type === 'captcha'
|
|
||||||
? {
|
|
||||||
...session,
|
|
||||||
challenge: {
|
|
||||||
...session.challenge,
|
|
||||||
status: 'submitted',
|
|
||||||
},
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.captchaSubmitted,
|
|
||||||
stage: 'captcha',
|
|
||||||
}
|
|
||||||
: session;
|
|
||||||
|
|
||||||
case 'captcha-new-device-required':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
challenge: {
|
|
||||||
pullQrCodeSig: event.newDevicePullQrCodeSig,
|
|
||||||
status: 'qr-pending',
|
|
||||||
type: 'new-device',
|
|
||||||
},
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.newDeviceNeeded,
|
|
||||||
stage: 'new-device',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'new-device-qr-ready':
|
|
||||||
case 'new-device-poll-pending':
|
|
||||||
return this.updateNewDeviceChallenge(session, {
|
|
||||||
qrcodeUrl:
|
|
||||||
event.type === 'new-device-qr-ready'
|
|
||||||
? event.qrcodeUrl
|
|
||||||
: undefined,
|
|
||||||
status: 'qr-pending',
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.newDeviceQrPending,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'new-device-scanned':
|
|
||||||
return this.updateNewDeviceChallenge(session, {
|
|
||||||
status: 'scanned',
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.newDeviceScanned,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'new-device-confirming':
|
|
||||||
return this.updateNewDeviceChallenge(session, {
|
|
||||||
status: 'confirming',
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.newDeviceConfirming,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'new-device-expired':
|
|
||||||
return this.updateNewDeviceChallenge(session, {
|
|
||||||
reason: event.reason,
|
|
||||||
status: 'expired',
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.newDeviceQrPending,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'new-device-failed':
|
|
||||||
return this.updateNewDeviceChallenge(session, {
|
|
||||||
reason: event.reason,
|
|
||||||
status: 'failed',
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.failed,
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'new-device-verified':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
challenge: undefined,
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.newDeviceVerified,
|
|
||||||
stage: 'password-login',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'runtime-cleanup-failed':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.cleanupFailed,
|
|
||||||
stage: 'cleanup-failed',
|
|
||||||
status: 'failure',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'login-success':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
progressMessage: NAPCAT_LOGIN_PROGRESS_MESSAGES.success,
|
|
||||||
selfId: event.selfId,
|
|
||||||
stage: 'success',
|
|
||||||
status: 'success',
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'login-failed':
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
progressMessage: event.reason || NAPCAT_LOGIN_PROGRESS_MESSAGES.failed,
|
|
||||||
stage: 'failure',
|
|
||||||
status: 'failure',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateNewDeviceChallenge(
|
|
||||||
session: NapcatLoginSessionState,
|
|
||||||
patch: {
|
|
||||||
progressMessage: string;
|
|
||||||
qrcodeUrl?: string;
|
|
||||||
reason?: string;
|
|
||||||
status: Extract<NapcatLoginChallenge, { type: 'new-device' }>['status'];
|
|
||||||
},
|
|
||||||
): NapcatLoginSessionState {
|
|
||||||
if (session.challenge?.type !== 'new-device') return session;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...session,
|
|
||||||
challenge: {
|
|
||||||
...session.challenge,
|
|
||||||
qrcodeUrl: patch.qrcodeUrl || session.challenge.qrcodeUrl,
|
|
||||||
reason: patch.reason || session.challenge.reason,
|
|
||||||
status: patch.status,
|
|
||||||
},
|
|
||||||
progressMessage: patch.progressMessage,
|
|
||||||
stage: 'new-device',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { NapcatDeviceIdentity } from './device/napcat-device-identity.entity';
|
|
||||||
|
|
||||||
export const NAPCAT_RUNTIME_DOMAIN_CONTRACT = {
|
|
||||||
tables: [
|
|
||||||
'napcat_container',
|
|
||||||
'napcat_device_identity',
|
|
||||||
'napcat_account_binding',
|
|
||||||
'napcat_login_session',
|
|
||||||
'napcat_login_challenge',
|
|
||||||
'napcat_runtime_cleanup',
|
|
||||||
],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const NAPCAT_RUNTIME_ENTITIES = [NapcatDeviceIdentity];
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './manifest.parser';
|
|
||||||
export * from './manifest.types';
|
|
||||||
@ -1,395 +0,0 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import {
|
|
||||||
QQBOT_PLUGIN_ALLOWED_PERMISSIONS,
|
|
||||||
QQBOT_PLUGIN_WORKER_TYPES,
|
|
||||||
type QqbotPluginAssetManifest,
|
|
||||||
type QqbotPluginEventManifest,
|
|
||||||
type QqbotPluginManifest,
|
|
||||||
type QqbotPluginManifestParseOptions,
|
|
||||||
type QqbotPluginManifestValidationIssue,
|
|
||||||
type QqbotPluginMigrationManifest,
|
|
||||||
type QqbotPluginOperationManifest,
|
|
||||||
type QqbotPluginPermission,
|
|
||||||
type QqbotPluginRuntimeManifest,
|
|
||||||
type QqbotPluginWorkerType,
|
|
||||||
} from './manifest.types';
|
|
||||||
|
|
||||||
const pluginKeyPattern = /^[a-z][a-z0-9-]{2,63}$/;
|
|
||||||
const semanticVersionPattern = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
||||||
const capabilityKeyPattern = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
|
|
||||||
const windowsAbsolutePathPattern = /^[a-zA-Z]:[\\/]/;
|
|
||||||
|
|
||||||
const allowedPermissionSet = new Set<string>(QQBOT_PLUGIN_ALLOWED_PERMISSIONS);
|
|
||||||
const allowedWorkerTypeSet = new Set<string>(QQBOT_PLUGIN_WORKER_TYPES);
|
|
||||||
|
|
||||||
export class QqbotPluginManifestValidationError extends Error {
|
|
||||||
constructor(readonly issues: QqbotPluginManifestValidationIssue[]) {
|
|
||||||
super(
|
|
||||||
`QQBot plugin manifest validation failed: ${issues
|
|
||||||
.map((issue) => `${issue.path}:${issue.code}`)
|
|
||||||
.join(', ')}`,
|
|
||||||
);
|
|
||||||
this.name = 'QqbotPluginManifestValidationError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
||||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getString = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
): string | undefined => {
|
|
||||||
const value = source[key];
|
|
||||||
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStringArray = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
): string[] => {
|
|
||||||
const value = source[key];
|
|
||||||
if (!Array.isArray(value)) return [];
|
|
||||||
|
|
||||||
return value
|
|
||||||
.filter((item): item is string => typeof item === 'string')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNumber = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
): number | undefined => {
|
|
||||||
const value = source[key];
|
|
||||||
return typeof value === 'number' && Number.isFinite(value)
|
|
||||||
? value
|
|
||||||
: undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pushIssue = (
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
code: string,
|
|
||||||
pathName: string,
|
|
||||||
message: string,
|
|
||||||
) => {
|
|
||||||
issues.push({ code, message, path: pathName });
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizePackagePath = (
|
|
||||||
value: unknown,
|
|
||||||
pathName: string,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): string => {
|
|
||||||
if (typeof value !== 'string' || !value.trim()) {
|
|
||||||
pushIssue(issues, 'REQUIRED_PATH', pathName, 'Package path is required.');
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidate = value.trim().replace(/\\/g, '/');
|
|
||||||
const normalized = path.posix.normalize(candidate);
|
|
||||||
const outsideRoot =
|
|
||||||
candidate.includes('\0') ||
|
|
||||||
normalized === '..' ||
|
|
||||||
normalized.startsWith('../') ||
|
|
||||||
normalized.startsWith('/') ||
|
|
||||||
path.posix.isAbsolute(candidate) ||
|
|
||||||
windowsAbsolutePathPattern.test(candidate) ||
|
|
||||||
normalized.split('/').includes('..');
|
|
||||||
|
|
||||||
if (outsideRoot) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'PATH_OUTSIDE_PLUGIN_ROOT',
|
|
||||||
pathName,
|
|
||||||
'Package paths must stay inside the plugin root.',
|
|
||||||
);
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizePermissions = (
|
|
||||||
value: unknown,
|
|
||||||
pathName: string,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): QqbotPluginPermission[] => {
|
|
||||||
if (!Array.isArray(value)) return [];
|
|
||||||
|
|
||||||
const result: QqbotPluginPermission[] = [];
|
|
||||||
value.forEach((item, index) => {
|
|
||||||
if (typeof item !== 'string' || !item.trim()) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'INVALID_PERMISSION',
|
|
||||||
`${pathName}[${index}]`,
|
|
||||||
'Permission must be a non-empty string.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = item.trim();
|
|
||||||
if (!allowedPermissionSet.has(permission)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'UNKNOWN_PERMISSION',
|
|
||||||
`${pathName}[${index}]`,
|
|
||||||
`Unknown permission: ${permission}.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.includes(permission as QqbotPluginPermission)) {
|
|
||||||
result.push(permission as QqbotPluginPermission);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const requireSemver = (
|
|
||||||
value: string | undefined,
|
|
||||||
pathName: string,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
) => {
|
|
||||||
if (!value || !semanticVersionPattern.test(value)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'INVALID_SEMVER',
|
|
||||||
pathName,
|
|
||||||
'Version must use semantic version format.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const requireKey = (
|
|
||||||
value: string | undefined,
|
|
||||||
pathName: string,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
) => {
|
|
||||||
if (!value || !capabilityKeyPattern.test(value)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'INVALID_CAPABILITY_KEY',
|
|
||||||
pathName,
|
|
||||||
'Capability key must be dot-separated lower-case segments.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseRuntime = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): QqbotPluginRuntimeManifest => {
|
|
||||||
const runtime = isPlainObject(source.runtime) ? source.runtime : {};
|
|
||||||
const timeoutMs = getNumber(runtime, 'timeoutMs');
|
|
||||||
const memoryMb = getNumber(runtime, 'memoryMb');
|
|
||||||
const maxConcurrency = getNumber(runtime, 'maxConcurrency');
|
|
||||||
const workerType = getString(runtime, 'workerType');
|
|
||||||
|
|
||||||
if (!timeoutMs || !memoryMb || !maxConcurrency) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'MISSING_RUNTIME_BUDGET',
|
|
||||||
'runtime',
|
|
||||||
'Runtime must declare timeoutMs, memoryMb, and maxConcurrency.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!workerType || !allowedWorkerTypeSet.has(workerType)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'INVALID_WORKER_TYPE',
|
|
||||||
'runtime.workerType',
|
|
||||||
'Runtime workerType is not supported.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
maxConcurrency: maxConcurrency || 1,
|
|
||||||
memoryMb: memoryMb || 128,
|
|
||||||
timeoutMs: timeoutMs || 1000,
|
|
||||||
workerType: (workerType || 'node-worker') as QqbotPluginWorkerType,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseOperations = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): QqbotPluginOperationManifest[] => {
|
|
||||||
const operations = Array.isArray(source.operations) ? source.operations : [];
|
|
||||||
const seenKeys = new Set<string>();
|
|
||||||
|
|
||||||
return operations.filter(isPlainObject).map((operation, index) => {
|
|
||||||
const pathPrefix = `operations[${index}]`;
|
|
||||||
const key = getString(operation, 'key') || '';
|
|
||||||
const timeoutMs = getNumber(operation, 'timeoutMs');
|
|
||||||
|
|
||||||
requireKey(key, `${pathPrefix}.key`, issues);
|
|
||||||
if (seenKeys.has(key)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'DUPLICATE_OPERATION_KEY',
|
|
||||||
pathPrefix,
|
|
||||||
`Duplicate operation key: ${key}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
seenKeys.add(key);
|
|
||||||
|
|
||||||
if (!timeoutMs) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'MISSING_OPERATION_TIMEOUT',
|
|
||||||
`${pathPrefix}.timeoutMs`,
|
|
||||||
'Operation timeoutMs is required.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
aliases: getStringArray(operation, 'aliases'),
|
|
||||||
description: getString(operation, 'description'),
|
|
||||||
handlerName: getString(operation, 'handlerName') || '',
|
|
||||||
inputSchema: isPlainObject(operation.inputSchema)
|
|
||||||
? operation.inputSchema
|
|
||||||
: undefined,
|
|
||||||
key,
|
|
||||||
name: getString(operation, 'name') || key,
|
|
||||||
outputSchema: isPlainObject(operation.outputSchema)
|
|
||||||
? operation.outputSchema
|
|
||||||
: undefined,
|
|
||||||
permissions: normalizePermissions(
|
|
||||||
operation.permissions,
|
|
||||||
`${pathPrefix}.permissions`,
|
|
||||||
issues,
|
|
||||||
),
|
|
||||||
timeoutMs: timeoutMs || 1000,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseEvents = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): QqbotPluginEventManifest[] => {
|
|
||||||
const events = Array.isArray(source.events) ? source.events : [];
|
|
||||||
const seenKeys = new Set<string>();
|
|
||||||
|
|
||||||
return events.filter(isPlainObject).map((event, index) => {
|
|
||||||
const pathPrefix = `events[${index}]`;
|
|
||||||
const key = getString(event, 'key') || '';
|
|
||||||
|
|
||||||
requireKey(key, `${pathPrefix}.key`, issues);
|
|
||||||
if (seenKeys.has(key)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'DUPLICATE_EVENT_KEY',
|
|
||||||
pathPrefix,
|
|
||||||
`Duplicate event key: ${key}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
seenKeys.add(key);
|
|
||||||
|
|
||||||
return {
|
|
||||||
description: getString(event, 'description'),
|
|
||||||
eventName: getString(event, 'eventName') || '',
|
|
||||||
handlerName: getString(event, 'handlerName') || '',
|
|
||||||
key,
|
|
||||||
name: getString(event, 'name') || key,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseAssets = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): QqbotPluginAssetManifest[] => {
|
|
||||||
const assets = Array.isArray(source.assets) ? source.assets : [];
|
|
||||||
|
|
||||||
return assets.filter(isPlainObject).map((asset, index) => ({
|
|
||||||
contentHash: getString(asset, 'contentHash'),
|
|
||||||
key: getString(asset, 'key') || '',
|
|
||||||
path: normalizePackagePath(asset.path, `assets[${index}].path`, issues),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseMigrations = (
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
issues: QqbotPluginManifestValidationIssue[],
|
|
||||||
): QqbotPluginMigrationManifest[] => {
|
|
||||||
const migrations = Array.isArray(source.migrations) ? source.migrations : [];
|
|
||||||
|
|
||||||
return migrations.filter(isPlainObject).map((migration, index) => ({
|
|
||||||
path: normalizePackagePath(
|
|
||||||
migration.path,
|
|
||||||
`migrations[${index}].path`,
|
|
||||||
issues,
|
|
||||||
),
|
|
||||||
version: getString(migration, 'version') || '',
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseQqbotPluginManifest = (
|
|
||||||
manifestLike: unknown,
|
|
||||||
options: QqbotPluginManifestParseOptions = {},
|
|
||||||
): QqbotPluginManifest => {
|
|
||||||
void options;
|
|
||||||
|
|
||||||
const issues: QqbotPluginManifestValidationIssue[] = [];
|
|
||||||
|
|
||||||
if (!isPlainObject(manifestLike)) {
|
|
||||||
throw new QqbotPluginManifestValidationError([
|
|
||||||
{
|
|
||||||
code: 'INVALID_MANIFEST',
|
|
||||||
message: 'Manifest must be a JSON object.',
|
|
||||||
path: '$',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginKey = getString(manifestLike, 'pluginKey') || '';
|
|
||||||
if (!pluginKeyPattern.test(pluginKey)) {
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
'INVALID_PLUGIN_KEY',
|
|
||||||
'pluginKey',
|
|
||||||
'Plugin key must be lower-case kebab-case.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = getString(manifestLike, 'version') || '';
|
|
||||||
const minApiSdkVersion = getString(manifestLike, 'minApiSdkVersion') || '';
|
|
||||||
requireSemver(version, 'version', issues);
|
|
||||||
requireSemver(minApiSdkVersion, 'minApiSdkVersion', issues);
|
|
||||||
|
|
||||||
const parsedManifest: QqbotPluginManifest = {
|
|
||||||
assets: parseAssets(manifestLike, issues),
|
|
||||||
author: getString(manifestLike, 'author'),
|
|
||||||
configSchema: isPlainObject(manifestLike.configSchema)
|
|
||||||
? manifestLike.configSchema
|
|
||||||
: {},
|
|
||||||
description: getString(manifestLike, 'description'),
|
|
||||||
entry: normalizePackagePath(manifestLike.entry, 'entry', issues),
|
|
||||||
events: parseEvents(manifestLike, issues),
|
|
||||||
homepage: getString(manifestLike, 'homepage'),
|
|
||||||
license: getString(manifestLike, 'license'),
|
|
||||||
migrations: parseMigrations(manifestLike, issues),
|
|
||||||
minApiSdkVersion,
|
|
||||||
name: getString(manifestLike, 'name') || pluginKey,
|
|
||||||
operations: parseOperations(manifestLike, issues),
|
|
||||||
permissions: normalizePermissions(
|
|
||||||
manifestLike.permissions,
|
|
||||||
'permissions',
|
|
||||||
issues,
|
|
||||||
),
|
|
||||||
pluginKey,
|
|
||||||
runtime: parseRuntime(manifestLike, issues),
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (issues.length > 0) {
|
|
||||||
throw new QqbotPluginManifestValidationError(issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedManifest;
|
|
||||||
};
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
export const QQBOT_PLUGIN_ALLOWED_PERMISSIONS = [
|
|
||||||
'asset.read',
|
|
||||||
'plugin.config.read',
|
|
||||||
'plugin.config.write',
|
|
||||||
'plugin.storage.read',
|
|
||||||
'plugin.storage.write',
|
|
||||||
'qqbot.command.read',
|
|
||||||
'qqbot.event.receive',
|
|
||||||
'qqbot.send',
|
|
||||||
'runtime.http',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const QQBOT_PLUGIN_WORKER_TYPES = [
|
|
||||||
'child-process',
|
|
||||||
'node-worker',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type QqbotPluginPermission =
|
|
||||||
(typeof QQBOT_PLUGIN_ALLOWED_PERMISSIONS)[number];
|
|
||||||
|
|
||||||
export type QqbotPluginWorkerType = (typeof QQBOT_PLUGIN_WORKER_TYPES)[number];
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeManifest = {
|
|
||||||
maxConcurrency: number;
|
|
||||||
memoryMb: number;
|
|
||||||
timeoutMs: number;
|
|
||||||
workerType: QqbotPluginWorkerType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginOperationManifest = {
|
|
||||||
aliases: string[];
|
|
||||||
description?: string;
|
|
||||||
handlerName: string;
|
|
||||||
inputSchema?: Record<string, unknown>;
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
outputSchema?: Record<string, unknown>;
|
|
||||||
permissions: QqbotPluginPermission[];
|
|
||||||
timeoutMs: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginEventManifest = {
|
|
||||||
description?: string;
|
|
||||||
eventName: string;
|
|
||||||
handlerName: string;
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginAssetManifest = {
|
|
||||||
contentHash?: string;
|
|
||||||
key: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginMigrationManifest = {
|
|
||||||
path: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginManifest = {
|
|
||||||
assets: QqbotPluginAssetManifest[];
|
|
||||||
author?: string;
|
|
||||||
configSchema: Record<string, unknown>;
|
|
||||||
description?: string;
|
|
||||||
entry: string;
|
|
||||||
events: QqbotPluginEventManifest[];
|
|
||||||
homepage?: string;
|
|
||||||
license?: string;
|
|
||||||
migrations: QqbotPluginMigrationManifest[];
|
|
||||||
minApiSdkVersion: string;
|
|
||||||
name: string;
|
|
||||||
operations: QqbotPluginOperationManifest[];
|
|
||||||
permissions: QqbotPluginPermission[];
|
|
||||||
pluginKey: string;
|
|
||||||
runtime: QqbotPluginRuntimeManifest;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginManifestValidationIssue = {
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginManifestParseOptions = {
|
|
||||||
pluginRoot?: string;
|
|
||||||
};
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './plugin-platform.contract';
|
|
||||||
export * from './plugin-platform.entities';
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
export const QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT = {
|
|
||||||
routes: {
|
|
||||||
accountBindings: '/qqbot/plugin-platform/account-bindings',
|
|
||||||
installLocal: '/qqbot/plugin-platform/install-local',
|
|
||||||
installations: '/qqbot/plugin-platform/installations',
|
|
||||||
runtimeEvents: '/qqbot/plugin-platform/runtime-events',
|
|
||||||
validate: '/qqbot/plugin-platform/validate',
|
|
||||||
},
|
|
||||||
tables: [
|
|
||||||
'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',
|
|
||||||
],
|
|
||||||
} as const;
|
|
||||||
@ -1,303 +0,0 @@
|
|||||||
import { BeforeInsert, Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
|
||||||
import {
|
|
||||||
ensureSnowflakeId,
|
|
||||||
KtCreateDateColumn,
|
|
||||||
KtDateTime,
|
|
||||||
KtUpdateDateColumn,
|
|
||||||
} from '@/common';
|
|
||||||
|
|
||||||
export type QqbotPluginInstallStatus =
|
|
||||||
| 'disabled'
|
|
||||||
| 'enabled'
|
|
||||||
| 'failed'
|
|
||||||
| 'installed'
|
|
||||||
| 'uninstalled'
|
|
||||||
| 'uploaded'
|
|
||||||
| 'validated';
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeStatus =
|
|
||||||
| 'crashed'
|
|
||||||
| 'healthy'
|
|
||||||
| 'starting'
|
|
||||||
| 'stopped'
|
|
||||||
| 'unhealthy';
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeEventLevel = 'error' | 'info' | 'warn';
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin')
|
|
||||||
@Index('uk_qqbot_plugin_key', ['pluginKey'], { unique: true })
|
|
||||||
export class QqbotPlugin {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'plugin_key' })
|
|
||||||
pluginKey: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'plugin_name' })
|
|
||||||
pluginName: string;
|
|
||||||
|
|
||||||
@Column({ name: 'description', nullable: true, type: 'text' })
|
|
||||||
description: null | string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: QqbotPluginInstallStatus;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_version')
|
|
||||||
@Index('uk_qqbot_plugin_version', ['pluginId', 'version'], { unique: true })
|
|
||||||
export class QqbotPluginVersion {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ length: 64 })
|
|
||||||
version: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'package_hash' })
|
|
||||||
packageHash: string;
|
|
||||||
|
|
||||||
@Column({ name: 'manifest_json', type: 'simple-json' })
|
|
||||||
manifestJson: Record<string, unknown>;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_installation')
|
|
||||||
@Index('idx_qqbot_plugin_installation_status', ['status'])
|
|
||||||
export class QqbotPluginInstallation {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'version_id', type: 'bigint' })
|
|
||||||
versionId: string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
status: QqbotPluginInstallStatus;
|
|
||||||
|
|
||||||
@Column({ length: 32, name: 'runtime_status' })
|
|
||||||
runtimeStatus: QqbotPluginRuntimeStatus;
|
|
||||||
|
|
||||||
@Column({ length: 512, name: 'installed_path' })
|
|
||||||
installedPath: string;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_operation')
|
|
||||||
@Index('uk_qqbot_plugin_operation', ['pluginId', 'operationKey'], {
|
|
||||||
unique: true,
|
|
||||||
})
|
|
||||||
export class QqbotPluginOperation {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'operation_key' })
|
|
||||||
operationKey: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'operation_name' })
|
|
||||||
operationName: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'handler_name' })
|
|
||||||
handlerName: string;
|
|
||||||
|
|
||||||
@Column({ default: true })
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_event_handler')
|
|
||||||
@Index('uk_qqbot_plugin_event_handler', ['pluginId', 'eventKey'], {
|
|
||||||
unique: true,
|
|
||||||
})
|
|
||||||
export class QqbotPluginEventHandler {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'event_key' })
|
|
||||||
eventKey: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'handler_name' })
|
|
||||||
handlerName: string;
|
|
||||||
|
|
||||||
@Column({ default: true })
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_account_binding')
|
|
||||||
@Index('uk_qqbot_plugin_account_binding', ['pluginId', 'accountId'], {
|
|
||||||
unique: true,
|
|
||||||
})
|
|
||||||
export class QqbotPluginAccountBinding {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', type: 'bigint' })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ default: true })
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_config')
|
|
||||||
@Index('uk_qqbot_plugin_config', ['pluginId', 'configKey'], { unique: true })
|
|
||||||
export class QqbotPluginConfig {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'config_key' })
|
|
||||||
configKey: string;
|
|
||||||
|
|
||||||
@Column({ name: 'config_value', nullable: true, type: 'simple-json' })
|
|
||||||
configValue: null | Record<string, unknown>;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@KtUpdateDateColumn({ name: 'update_time' })
|
|
||||||
updateTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_asset')
|
|
||||||
@Index('uk_qqbot_plugin_asset', ['pluginId', 'assetKey'], { unique: true })
|
|
||||||
export class QqbotPluginAsset {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({ length: 255, name: 'asset_key' })
|
|
||||||
assetKey: string;
|
|
||||||
|
|
||||||
@Column({ length: 512, name: 'asset_path' })
|
|
||||||
assetPath: string;
|
|
||||||
|
|
||||||
@Column({ length: 128, name: 'content_hash' })
|
|
||||||
contentHash: string;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity('qqbot_plugin_runtime_event')
|
|
||||||
@Index('idx_qqbot_plugin_runtime_event_plugin', ['pluginId'])
|
|
||||||
export class QqbotPluginRuntimeEvent {
|
|
||||||
@PrimaryColumn({ type: 'bigint' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
@Column({
|
|
||||||
default: null,
|
|
||||||
name: 'installation_id',
|
|
||||||
nullable: true,
|
|
||||||
type: 'bigint',
|
|
||||||
})
|
|
||||||
installationId: null | string;
|
|
||||||
|
|
||||||
@Column({ length: 64, name: 'event_type' })
|
|
||||||
eventType: string;
|
|
||||||
|
|
||||||
@Column({ length: 32 })
|
|
||||||
level: QqbotPluginRuntimeEventLevel;
|
|
||||||
|
|
||||||
@Column({ name: 'safe_summary', nullable: true, type: 'simple-json' })
|
|
||||||
safeSummary: null | Record<string, unknown>;
|
|
||||||
|
|
||||||
@KtCreateDateColumn({ name: 'create_time' })
|
|
||||||
createTime: KtDateTime;
|
|
||||||
|
|
||||||
@BeforeInsert()
|
|
||||||
createId() {
|
|
||||||
ensureSnowflakeId(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QQBOT_PLUGIN_PLATFORM_ENTITIES = [
|
|
||||||
QqbotPlugin,
|
|
||||||
QqbotPluginVersion,
|
|
||||||
QqbotPluginInstallation,
|
|
||||||
QqbotPluginOperation,
|
|
||||||
QqbotPluginEventHandler,
|
|
||||||
QqbotPluginAccountBinding,
|
|
||||||
QqbotPluginConfig,
|
|
||||||
QqbotPluginAsset,
|
|
||||||
QqbotPluginRuntimeEvent,
|
|
||||||
] as const;
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
|
||||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
|
||||||
import { vbenSuccess } from '@/common';
|
|
||||||
import { QqbotPluginPlatformService } from './plugin-platform.service';
|
|
||||||
|
|
||||||
@ApiTags('QQBot - 插件平台')
|
|
||||||
@Controller('qqbot/plugin-platform')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
export class QqbotPluginPlatformController {
|
|
||||||
constructor(private readonly service: QqbotPluginPlatformService) {}
|
|
||||||
|
|
||||||
@Get('installations')
|
|
||||||
@ApiOperation({ summary: '插件安装列表' })
|
|
||||||
async installations() {
|
|
||||||
return vbenSuccess(await this.service.listInstallations());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('validate')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '校验插件 manifest' })
|
|
||||||
async validate(@Body() body: { manifest?: unknown }) {
|
|
||||||
return vbenSuccess(this.service.validateManifest(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('upload')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '上传插件包' })
|
|
||||||
async upload(
|
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
manifest?: unknown;
|
|
||||||
packageHash?: string;
|
|
||||||
packagePath?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return vbenSuccess(this.service.validateManifest(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('install')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '安装插件包' })
|
|
||||||
async install(
|
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
manifest?: unknown;
|
|
||||||
packageHash?: string;
|
|
||||||
packagePath?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return vbenSuccess(await this.service.installLocal(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('install-local')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '本地安装插件包' })
|
|
||||||
async installLocal(
|
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
manifest?: unknown;
|
|
||||||
packageHash?: string;
|
|
||||||
packagePath?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return vbenSuccess(await this.service.installLocal(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('enable')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '启用插件' })
|
|
||||||
async enable(@Body() body: { id?: string }) {
|
|
||||||
return vbenSuccess(
|
|
||||||
await this.service.setInstallationStatus(body, 'enabled'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('disable')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '禁用插件' })
|
|
||||||
async disable(@Body() body: { id?: string }) {
|
|
||||||
return vbenSuccess(
|
|
||||||
await this.service.setInstallationStatus(body, 'disabled'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('upgrade')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '升级插件' })
|
|
||||||
async upgrade(@Body() body: { id?: string }) {
|
|
||||||
return vbenSuccess(
|
|
||||||
await this.service.setInstallationStatus(body, 'installed'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('uninstall')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '卸载插件' })
|
|
||||||
async uninstall(@Body() body: { id?: string }) {
|
|
||||||
return vbenSuccess(
|
|
||||||
await this.service.setInstallationStatus(body, 'uninstalled'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('config')
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@ApiOperation({ summary: '更新插件配置' })
|
|
||||||
async config(
|
|
||||||
@Body() body: { configKey?: string; pluginId?: string; value?: unknown },
|
|
||||||
) {
|
|
||||||
return vbenSuccess(await this.service.updateConfig(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('runtime-events')
|
|
||||||
@ApiOperation({ summary: '插件运行事件列表' })
|
|
||||||
@ApiQuery({ name: 'pluginId', required: false, type: String })
|
|
||||||
async runtimeEvents(@Query('pluginId') pluginId?: string) {
|
|
||||||
return vbenSuccess(await this.service.listRuntimeEvents(pluginId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('account-bindings')
|
|
||||||
@ApiOperation({ summary: '插件账号绑定列表' })
|
|
||||||
@ApiQuery({ name: 'pluginId', required: false, type: String })
|
|
||||||
async accountBindings(@Query('pluginId') pluginId?: string) {
|
|
||||||
return vbenSuccess(await this.service.listAccountBindings(pluginId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
|
||||||
import { QqbotPluginPlatformController } from './plugin-platform.controller';
|
|
||||||
import { QqbotPluginPlatformService } from './plugin-platform.service';
|
|
||||||
import { QQBOT_PLUGIN_PLATFORM_ENTITIES } from './persistence';
|
|
||||||
import { QqbotPluginHttpClientService } from './sdk';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [QqbotPluginPlatformController],
|
|
||||||
exports: [QqbotPluginHttpClientService, QqbotPluginPlatformService],
|
|
||||||
imports: [
|
|
||||||
AdminAuthGuardModule,
|
|
||||||
TypeOrmModule.forFeature([...QQBOT_PLUGIN_PLATFORM_ENTITIES]),
|
|
||||||
],
|
|
||||||
providers: [QqbotPluginHttpClientService, QqbotPluginPlatformService],
|
|
||||||
})
|
|
||||||
export class QqbotPluginPlatformModule {}
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { throwVbenError } from '@/common';
|
|
||||||
import { parseQqbotPluginManifest, type QqbotPluginManifest } from './manifest';
|
|
||||||
import {
|
|
||||||
QqbotPlugin,
|
|
||||||
QqbotPluginAccountBinding,
|
|
||||||
QqbotPluginAsset,
|
|
||||||
QqbotPluginConfig,
|
|
||||||
QqbotPluginEventHandler,
|
|
||||||
QqbotPluginInstallation,
|
|
||||||
QqbotPluginOperation,
|
|
||||||
QqbotPluginRuntimeEvent,
|
|
||||||
QqbotPluginVersion,
|
|
||||||
type QqbotPluginInstallStatus,
|
|
||||||
} from './persistence';
|
|
||||||
|
|
||||||
type ValidateManifestBody = {
|
|
||||||
manifest?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type InstallLocalBody = {
|
|
||||||
manifest?: unknown;
|
|
||||||
packageHash?: string;
|
|
||||||
packagePath?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type InstallationActionBody = {
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateConfigBody = {
|
|
||||||
configKey?: string;
|
|
||||||
pluginId?: string;
|
|
||||||
value?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class QqbotPluginPlatformService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(QqbotPlugin)
|
|
||||||
private readonly pluginRepository: Repository<QqbotPlugin>,
|
|
||||||
@InjectRepository(QqbotPluginVersion)
|
|
||||||
private readonly versionRepository: Repository<QqbotPluginVersion>,
|
|
||||||
@InjectRepository(QqbotPluginInstallation)
|
|
||||||
private readonly installationRepository: Repository<QqbotPluginInstallation>,
|
|
||||||
@InjectRepository(QqbotPluginOperation)
|
|
||||||
private readonly operationRepository: Repository<QqbotPluginOperation>,
|
|
||||||
@InjectRepository(QqbotPluginEventHandler)
|
|
||||||
private readonly eventHandlerRepository: Repository<QqbotPluginEventHandler>,
|
|
||||||
@InjectRepository(QqbotPluginAccountBinding)
|
|
||||||
private readonly accountBindingRepository: Repository<QqbotPluginAccountBinding>,
|
|
||||||
@InjectRepository(QqbotPluginConfig)
|
|
||||||
private readonly configRepository: Repository<QqbotPluginConfig>,
|
|
||||||
@InjectRepository(QqbotPluginAsset)
|
|
||||||
private readonly assetRepository: Repository<QqbotPluginAsset>,
|
|
||||||
@InjectRepository(QqbotPluginRuntimeEvent)
|
|
||||||
private readonly runtimeEventRepository: Repository<QqbotPluginRuntimeEvent>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async listInstallations() {
|
|
||||||
return this.installationRepository.find();
|
|
||||||
}
|
|
||||||
|
|
||||||
validateManifest(body: ValidateManifestBody) {
|
|
||||||
return {
|
|
||||||
manifest: parseQqbotPluginManifest(body.manifest),
|
|
||||||
valid: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async installLocal(body: InstallLocalBody) {
|
|
||||||
const manifest = parseQqbotPluginManifest(body.manifest);
|
|
||||||
const plugin = await this.pluginRepository.save({
|
|
||||||
pluginKey: manifest.pluginKey,
|
|
||||||
pluginName: manifest.name,
|
|
||||||
description: manifest.description || null,
|
|
||||||
status: 'installed',
|
|
||||||
});
|
|
||||||
const version = await this.versionRepository.save({
|
|
||||||
manifestJson: manifest,
|
|
||||||
packageHash: body.packageHash || 'local-dev-package',
|
|
||||||
pluginId: plugin.id,
|
|
||||||
version: manifest.version,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.persistManifestCapabilities(plugin.id, manifest);
|
|
||||||
|
|
||||||
return this.installationRepository.save({
|
|
||||||
installedPath: body.packagePath || '',
|
|
||||||
pluginId: plugin.id,
|
|
||||||
runtimeStatus: 'stopped',
|
|
||||||
status: 'installed',
|
|
||||||
versionId: version.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setInstallationStatus(
|
|
||||||
body: InstallationActionBody,
|
|
||||||
status: QqbotPluginInstallStatus,
|
|
||||||
) {
|
|
||||||
if (!body.id) throwVbenError('请选择插件安装记录');
|
|
||||||
|
|
||||||
await this.installationRepository.update({ id: body.id }, { status });
|
|
||||||
return {
|
|
||||||
id: body.id,
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateConfig(body: UpdateConfigBody) {
|
|
||||||
if (!body.pluginId || !body.configKey) {
|
|
||||||
throwVbenError('请选择插件和配置项');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.configRepository.save({
|
|
||||||
configKey: body.configKey,
|
|
||||||
configValue: body.value === undefined ? null : { value: body.value },
|
|
||||||
pluginId: body.pluginId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async listRuntimeEvents(pluginId?: string) {
|
|
||||||
return this.runtimeEventRepository.find({
|
|
||||||
where: pluginId ? { pluginId } : undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAccountBindings(pluginId?: string) {
|
|
||||||
return this.accountBindingRepository.find({
|
|
||||||
where: pluginId ? { pluginId } : undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async persistManifestCapabilities(
|
|
||||||
pluginId: string,
|
|
||||||
manifest: QqbotPluginManifest,
|
|
||||||
) {
|
|
||||||
await Promise.all([
|
|
||||||
...manifest.operations.map((operation) =>
|
|
||||||
this.operationRepository.save({
|
|
||||||
enabled: true,
|
|
||||||
handlerName: operation.handlerName,
|
|
||||||
operationKey: operation.key,
|
|
||||||
operationName: operation.name,
|
|
||||||
pluginId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...manifest.events.map((event) =>
|
|
||||||
this.eventHandlerRepository.save({
|
|
||||||
enabled: true,
|
|
||||||
eventKey: event.key,
|
|
||||||
handlerName: event.handlerName,
|
|
||||||
pluginId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...manifest.assets.map((asset) =>
|
|
||||||
this.assetRepository.save({
|
|
||||||
assetKey: asset.key,
|
|
||||||
assetPath: asset.path,
|
|
||||||
contentHash: asset.contentHash || '',
|
|
||||||
pluginId,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './worker-runtime';
|
|
||||||
export * from './worker-runtime.types';
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
import {
|
|
||||||
type QqbotPluginEventRequest,
|
|
||||||
type QqbotPluginOperationRequest,
|
|
||||||
type QqbotPluginRuntimeErrorCode,
|
|
||||||
type QqbotPluginRuntimeEvent,
|
|
||||||
type QqbotPluginRuntimeStatus,
|
|
||||||
type QqbotPluginSafeInputSummary,
|
|
||||||
type QqbotPluginWorkerDriver,
|
|
||||||
type QqbotPluginWorkerRequest,
|
|
||||||
type QqbotPluginWorkerRequestType,
|
|
||||||
type QqbotPluginWorkerRuntimeOptions,
|
|
||||||
} from './worker-runtime.types';
|
|
||||||
|
|
||||||
export class QqbotPluginRuntimeError extends Error {
|
|
||||||
constructor(
|
|
||||||
readonly code: QqbotPluginRuntimeErrorCode,
|
|
||||||
readonly pluginKey: string,
|
|
||||||
message: string,
|
|
||||||
readonly safeSummary: Record<string, unknown> = {},
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'QqbotPluginRuntimeError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCorrelationId = () => {
|
|
||||||
return `qqbot-plugin-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const summarizeInput = (
|
|
||||||
input: Record<string, unknown>,
|
|
||||||
): QqbotPluginSafeInputSummary => {
|
|
||||||
return {
|
|
||||||
fieldCount: Object.keys(input).length,
|
|
||||||
keys: Object.keys(input).sort(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class QqbotPluginWorkerRuntime {
|
|
||||||
private readonly runtimeEvents: QqbotPluginRuntimeEvent[] = [];
|
|
||||||
|
|
||||||
status: QqbotPluginRuntimeStatus = 'stopped';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly driver: QqbotPluginWorkerDriver,
|
|
||||||
private readonly options: QqbotPluginWorkerRuntimeOptions,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async load(manifest: unknown) {
|
|
||||||
const result = await this.request('load', {
|
|
||||||
manifest,
|
|
||||||
});
|
|
||||||
this.status = 'loaded';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async activate() {
|
|
||||||
const result = await this.request('activate');
|
|
||||||
this.status = 'active';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeOperation(request: QqbotPluginOperationRequest) {
|
|
||||||
return this.request(
|
|
||||||
'executeOperation',
|
|
||||||
{
|
|
||||||
input: request.input,
|
|
||||||
operationId: request.operationId,
|
|
||||||
operationKey: request.operationKey,
|
|
||||||
safeInputSummary: summarizeInput(request.input),
|
|
||||||
},
|
|
||||||
request.timeoutMs,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleEvent(request: QqbotPluginEventRequest) {
|
|
||||||
return this.request(
|
|
||||||
'handleEvent',
|
|
||||||
{
|
|
||||||
event: request.event,
|
|
||||||
eventKey: request.eventKey,
|
|
||||||
safeInputSummary: summarizeInput(request.event),
|
|
||||||
},
|
|
||||||
request.timeoutMs,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async health() {
|
|
||||||
return this.request('health');
|
|
||||||
}
|
|
||||||
|
|
||||||
async deactivate() {
|
|
||||||
const result = await this.request('deactivate');
|
|
||||||
this.status = 'stopped';
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispose() {
|
|
||||||
try {
|
|
||||||
await this.request('dispose');
|
|
||||||
} finally {
|
|
||||||
await this.driver.dispose();
|
|
||||||
this.status = 'stopped';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listRuntimeEvents() {
|
|
||||||
return [...this.runtimeEvents];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async request(
|
|
||||||
type: QqbotPluginWorkerRequestType,
|
|
||||||
payload: Partial<QqbotPluginWorkerRequest> = {},
|
|
||||||
timeoutMs = this.options.defaultTimeoutMs,
|
|
||||||
) {
|
|
||||||
const message: QqbotPluginWorkerRequest = {
|
|
||||||
correlationId: createCorrelationId(),
|
|
||||||
installationId: this.options.installationId,
|
|
||||||
pluginKey: this.options.pluginKey,
|
|
||||||
timeoutMs,
|
|
||||||
type,
|
|
||||||
...payload,
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestPromise = Promise.resolve().then(() =>
|
|
||||||
this.driver.request(message),
|
|
||||||
);
|
|
||||||
requestPromise.catch(() => undefined);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await Promise.race([
|
|
||||||
requestPromise,
|
|
||||||
this.createTimeoutPromise(type, message, timeoutMs),
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof QqbotPluginRuntimeError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = 'failed';
|
|
||||||
const runtimeError = new QqbotPluginRuntimeError(
|
|
||||||
'PLUGIN_WORKER_CRASH',
|
|
||||||
this.options.pluginKey,
|
|
||||||
'QQBot plugin worker crashed.',
|
|
||||||
{
|
|
||||||
correlationId: message.correlationId,
|
|
||||||
message: error instanceof Error ? error.message : `${error}`,
|
|
||||||
operationId: message.operationId,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.recordRuntimeEvent('worker-crash', runtimeError.safeSummary);
|
|
||||||
throw runtimeError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createTimeoutPromise(
|
|
||||||
type: QqbotPluginWorkerRequestType,
|
|
||||||
message: QqbotPluginWorkerRequest,
|
|
||||||
timeoutMs: number,
|
|
||||||
) {
|
|
||||||
return new Promise<never>((_, reject) => {
|
|
||||||
const timer = setTimeout(async () => {
|
|
||||||
this.status = 'failed';
|
|
||||||
const error = new QqbotPluginRuntimeError(
|
|
||||||
'PLUGIN_WORKER_TIMEOUT',
|
|
||||||
this.options.pluginKey,
|
|
||||||
'QQBot plugin worker timed out.',
|
|
||||||
{
|
|
||||||
correlationId: message.correlationId,
|
|
||||||
operationId: message.operationId,
|
|
||||||
timeoutMs,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.recordRuntimeEvent('worker-timeout', error.safeSummary);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.driver.dispose();
|
|
||||||
} catch {
|
|
||||||
// Timeout disposal is best-effort; the timeout error is the primary signal.
|
|
||||||
}
|
|
||||||
reject(error);
|
|
||||||
}, timeoutMs);
|
|
||||||
timer.unref?.();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private recordRuntimeEvent(
|
|
||||||
eventType: string,
|
|
||||||
safeSummary: Record<string, unknown>,
|
|
||||||
) {
|
|
||||||
this.runtimeEvents.push({
|
|
||||||
eventType,
|
|
||||||
level: 'error',
|
|
||||||
pluginKey: this.options.pluginKey,
|
|
||||||
safeSummary,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
export type QqbotPluginWorkerRequestType =
|
|
||||||
| 'activate'
|
|
||||||
| 'deactivate'
|
|
||||||
| 'dispose'
|
|
||||||
| 'executeOperation'
|
|
||||||
| 'handleEvent'
|
|
||||||
| 'health'
|
|
||||||
| 'load';
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeStatus =
|
|
||||||
| 'active'
|
|
||||||
| 'failed'
|
|
||||||
| 'loaded'
|
|
||||||
| 'stopped';
|
|
||||||
|
|
||||||
export type QqbotPluginSafeInputSummary = {
|
|
||||||
fieldCount: number;
|
|
||||||
keys: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginWorkerRequest = {
|
|
||||||
correlationId: string;
|
|
||||||
event?: unknown;
|
|
||||||
eventKey?: string;
|
|
||||||
installationId?: string;
|
|
||||||
manifest?: unknown;
|
|
||||||
operationId?: string;
|
|
||||||
operationKey?: string;
|
|
||||||
input?: unknown;
|
|
||||||
pluginKey: string;
|
|
||||||
safeInputSummary?: QqbotPluginSafeInputSummary;
|
|
||||||
timeoutMs: number;
|
|
||||||
type: QqbotPluginWorkerRequestType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginWorkerDriver = {
|
|
||||||
dispose(): Promise<void>;
|
|
||||||
request(message: QqbotPluginWorkerRequest): Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginWorkerRuntimeOptions = {
|
|
||||||
defaultTimeoutMs: number;
|
|
||||||
installationId: string;
|
|
||||||
pluginKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeEvent = {
|
|
||||||
eventType: string;
|
|
||||||
level: 'error' | 'info' | 'warn';
|
|
||||||
pluginKey: string;
|
|
||||||
safeSummary: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeErrorCode =
|
|
||||||
| 'PLUGIN_WORKER_CRASH'
|
|
||||||
| 'PLUGIN_WORKER_TIMEOUT';
|
|
||||||
|
|
||||||
export type QqbotPluginOperationRequest = {
|
|
||||||
input: Record<string, unknown>;
|
|
||||||
operationId: string;
|
|
||||||
operationKey: string;
|
|
||||||
timeoutMs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginEventRequest = {
|
|
||||||
event: Record<string, unknown>;
|
|
||||||
eventKey: string;
|
|
||||||
timeoutMs?: number;
|
|
||||||
};
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './plugin-sdk';
|
|
||||||
export * from './plugin-http-client.service';
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import * as http from 'node:http';
|
|
||||||
import * as https from 'node:https';
|
|
||||||
|
|
||||||
export type QqbotPluginHttpClientRequest = {
|
|
||||||
body?: Buffer | string;
|
|
||||||
context?: string;
|
|
||||||
failureMessage?: (statusCode: number) => string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
invalidJsonMessage?: string;
|
|
||||||
method?: string;
|
|
||||||
timeoutMessage?: string;
|
|
||||||
timeoutMs?: number;
|
|
||||||
url: string | URL;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class QqbotPluginHttpClientService {
|
|
||||||
async requestJson<T>(input: QqbotPluginHttpClientRequest): Promise<T> {
|
|
||||||
const body = await this.requestText(input);
|
|
||||||
try {
|
|
||||||
return JSON.parse(body) as T;
|
|
||||||
} catch {
|
|
||||||
throw new Error(
|
|
||||||
input.invalidJsonMessage ||
|
|
||||||
`${input.context || '插件 HTTP 接口'}返回不是合法 JSON`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestText(input: QqbotPluginHttpClientRequest): Promise<string> {
|
|
||||||
const url = input.url instanceof URL ? input.url : new URL(input.url);
|
|
||||||
const method = input.method || 'GET';
|
|
||||||
const timeoutMs = input.timeoutMs || 8000;
|
|
||||||
const context = input.context || '插件 HTTP 接口';
|
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
const client = url.protocol === 'http:' ? http : https;
|
|
||||||
const request = client.request(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'User-Agent': 'kt-template-online-api/qqbot-plugin',
|
|
||||||
...(input.headers || {}),
|
|
||||||
},
|
|
||||||
method,
|
|
||||||
timeout: timeoutMs,
|
|
||||||
},
|
|
||||||
(response) => {
|
|
||||||
let responseBody = '';
|
|
||||||
response.setEncoding('utf8');
|
|
||||||
response.on('data', (chunk) => {
|
|
||||||
responseBody += chunk;
|
|
||||||
});
|
|
||||||
response.on('end', () => {
|
|
||||||
const statusCode = response.statusCode || 500;
|
|
||||||
if (statusCode >= 400) {
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
input.failureMessage?.(statusCode) ||
|
|
||||||
`${context}请求失败:${statusCode}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(responseBody);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
request.on('timeout', () => {
|
|
||||||
request.destroy(
|
|
||||||
new Error(input.timeoutMessage || `${context}请求超时`),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
request.on('error', reject);
|
|
||||||
if (input.body) request.write(input.body);
|
|
||||||
request.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
export type QqbotPluginSendQueueSdk = {
|
|
||||||
sendMessage(input: Record<string, unknown>): Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginConfigSdk = {
|
|
||||||
getConfig(key: string): Promise<unknown>;
|
|
||||||
setConfig(key: string, value: unknown): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginStorageSdk = {
|
|
||||||
get(key: string): Promise<unknown>;
|
|
||||||
set(key: string, value: unknown): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginHttpSdk = {
|
|
||||||
request(input: Record<string, unknown>): Promise<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginAssetSdk = {
|
|
||||||
readAsset(assetKey: string): Promise<Buffer>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginRuntimeEventSdk = {
|
|
||||||
emitRuntimeEvent(input: Record<string, unknown>): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginSdkFactoryInput = {
|
|
||||||
assets: QqbotPluginAssetSdk;
|
|
||||||
config: QqbotPluginConfigSdk;
|
|
||||||
eventContext: Record<string, unknown>;
|
|
||||||
events: QqbotPluginRuntimeEventSdk;
|
|
||||||
http: QqbotPluginHttpSdk;
|
|
||||||
operationContext: Record<string, unknown>;
|
|
||||||
sendQueue: QqbotPluginSendQueueSdk;
|
|
||||||
storage: QqbotPluginStorageSdk;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QqbotPluginSdk = Readonly<QqbotPluginSdkFactoryInput>;
|
|
||||||
|
|
||||||
export const createQqbotPluginSdk = (
|
|
||||||
input: QqbotPluginSdkFactoryInput,
|
|
||||||
): QqbotPluginSdk => {
|
|
||||||
return Object.freeze({
|
|
||||||
assets: input.assets,
|
|
||||||
config: input.config,
|
|
||||||
eventContext: Object.freeze({ ...input.eventContext }),
|
|
||||||
events: input.events,
|
|
||||||
http: input.http,
|
|
||||||
operationContext: Object.freeze({ ...input.operationContext }),
|
|
||||||
sendQueue: input.sendQueue,
|
|
||||||
storage: input.storage,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,267 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginKey": "bangdream",
|
|
||||||
"name": "BangDream 查询",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"minApiSdkVersion": "1.0.0",
|
|
||||||
"description": "BanG Dream! Girls Band Party 查询、出图和档线能力。",
|
|
||||||
"author": "KT",
|
|
||||||
"license": "MIT",
|
|
||||||
"entry": "qqbot-bangdream.plugin.ts",
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"runtime": {
|
|
||||||
"workerType": "node-worker",
|
|
||||||
"timeoutMs": 30000,
|
|
||||||
"memoryMb": 512,
|
|
||||||
"maxConcurrency": 2
|
|
||||||
},
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"key": "bangdream.song.search",
|
|
||||||
"name": "查曲",
|
|
||||||
"handlerName": "searchSong",
|
|
||||||
"description": "按歌曲名、昵称、标签或歌曲 ID 查询歌曲信息图片。",
|
|
||||||
"aliases": ["查曲", "bd", "bangdream", "bandori", "邦邦", "邦邦查歌"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.song.chart",
|
|
||||||
"name": "查谱面",
|
|
||||||
"handlerName": "getSongChart",
|
|
||||||
"description": "按歌曲 ID 和难度查询谱面图片。",
|
|
||||||
"aliases": ["查谱面", "谱面", "bd谱面"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.song.random",
|
|
||||||
"name": "随机曲",
|
|
||||||
"handlerName": "randomSong",
|
|
||||||
"description": "按关键词随机抽取一首歌曲并输出歌曲图片。",
|
|
||||||
"aliases": ["随机曲", "随机", "bd随机"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.song.meta",
|
|
||||||
"name": "查询分数表",
|
|
||||||
"handlerName": "getSongMeta",
|
|
||||||
"description": "查询指定服务器歌曲分数榜图片。",
|
|
||||||
"aliases": [
|
|
||||||
"查询分数表",
|
|
||||||
"查分数表",
|
|
||||||
"查询分数榜",
|
|
||||||
"查分数榜",
|
|
||||||
"bd分数表"
|
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.card.search",
|
|
||||||
"name": "查卡",
|
|
||||||
"handlerName": "searchCard",
|
|
||||||
"description": "按卡牌关键词或卡牌 ID 查询卡牌信息图片。",
|
|
||||||
"aliases": ["查卡", "查卡牌", "bd查卡"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.card.illustration",
|
|
||||||
"name": "查卡面",
|
|
||||||
"handlerName": "getCardIllustration",
|
|
||||||
"description": "按卡牌 ID 查询卡面插画图片。",
|
|
||||||
"aliases": ["查卡面", "查卡插画", "查插画", "bd卡面"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.character.search",
|
|
||||||
"name": "查角色",
|
|
||||||
"handlerName": "searchCharacter",
|
|
||||||
"description": "按角色关键词或角色 ID 查询角色信息图片。",
|
|
||||||
"aliases": ["查角色", "bd角色"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.event.search",
|
|
||||||
"name": "查活动",
|
|
||||||
"handlerName": "searchEvent",
|
|
||||||
"description": "按活动关键词或活动 ID 查询活动信息图片。",
|
|
||||||
"aliases": ["查活动", "bd活动"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.event.stage",
|
|
||||||
"name": "查试炼",
|
|
||||||
"handlerName": "getEventStage",
|
|
||||||
"description": "查询当前或指定活动的试炼信息图片。",
|
|
||||||
"aliases": ["查试炼", "查stage", "查舞台", "查festival", "查5v5"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.player.search",
|
|
||||||
"name": "查玩家",
|
|
||||||
"handlerName": "searchPlayer",
|
|
||||||
"description": "按玩家 ID 和服务器查询玩家信息图片。",
|
|
||||||
"aliases": ["查玩家", "查询玩家", "bd玩家"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.gacha.search",
|
|
||||||
"name": "查卡池",
|
|
||||||
"handlerName": "searchGacha",
|
|
||||||
"description": "按卡池 ID 查询卡池信息图片。",
|
|
||||||
"aliases": ["查卡池", "bd卡池"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.gacha.simulate",
|
|
||||||
"name": "抽卡模拟",
|
|
||||||
"handlerName": "simulateGacha",
|
|
||||||
"description": "模拟指定次数抽卡并输出结果图片。",
|
|
||||||
"aliases": ["抽卡模拟", "bd抽卡"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.cutoff.detail",
|
|
||||||
"name": "ycx",
|
|
||||||
"handlerName": "getCutoffDetail",
|
|
||||||
"description": "查询指定档位的当前活动档线与预测线图片。",
|
|
||||||
"aliases": ["ycx", "预测线", "查档线", "bd档线"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.cutoff.all",
|
|
||||||
"name": "ycxall",
|
|
||||||
"handlerName": "getCutoffAll",
|
|
||||||
"description": "查询当前活动所有档位的档线与预测线图片。",
|
|
||||||
"aliases": ["ycxall", "myycx", "全部档线"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "bangdream.cutoff.recent",
|
|
||||||
"name": "lsycx",
|
|
||||||
"handlerName": "getCutoffRecent",
|
|
||||||
"description": "查询指定档位与最近同类型活动档线图片。",
|
|
||||||
"aliases": ["lsycx", "历史档线", "近期档线"],
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.send",
|
|
||||||
"runtime.http",
|
|
||||||
"asset.read",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read"
|
|
||||||
],
|
|
||||||
"timeoutMs": 30000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"events": [],
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"key": "bangdream-assets",
|
|
||||||
"path": "assets"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"migrations": []
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from '@/modules/qqbot/plugins/bangDream/shared/bangdream-protocol';
|
|
||||||
export * from '@/modules/qqbot/plugins/bangDream/dictionary/default-dictionary';
|
|
||||||
export * from '@/modules/qqbot/plugins/bangDream/config/runtime-options';
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginKey": "ff14-market",
|
|
||||||
"name": "FF14 市场价格",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"minApiSdkVersion": "1.0.0",
|
|
||||||
"description": "对接 XIVAPI v2 与 Universalis,提供 FF14 物品解析和市场价格查询能力。",
|
|
||||||
"author": "KT",
|
|
||||||
"entry": "qqbot-ff14-market.plugin.ts",
|
|
||||||
"permissions": ["runtime.http", "plugin.config.read"],
|
|
||||||
"runtime": {
|
|
||||||
"workerType": "node-worker",
|
|
||||||
"timeoutMs": 15000,
|
|
||||||
"memoryMb": 256,
|
|
||||||
"maxConcurrency": 4
|
|
||||||
},
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"xivapiBaseUrl": { "type": "string" },
|
|
||||||
"universalisBaseUrl": { "type": "string" },
|
|
||||||
"defaultWorld": { "type": "string" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"key": "ff14.item.resolve",
|
|
||||||
"name": "解析物品",
|
|
||||||
"handlerName": "resolveItem",
|
|
||||||
"description": "按物品名称或 ID 解析 XIVAPI 物品信息。",
|
|
||||||
"aliases": ["物品", "item", "ff14item"],
|
|
||||||
"permissions": ["runtime.http", "plugin.config.read"],
|
|
||||||
"timeoutMs": 15000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ff14.market.price",
|
|
||||||
"name": "市场查价",
|
|
||||||
"handlerName": "getPrice",
|
|
||||||
"description": "查询指定服务器的 FF14 市场最低价、均价与近期挂单。",
|
|
||||||
"aliases": ["查价", "price", "ff14price"],
|
|
||||||
"permissions": ["runtime.http", "plugin.config.read"],
|
|
||||||
"timeoutMs": 15000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"events": [],
|
|
||||||
"assets": [],
|
|
||||||
"migrations": []
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginKey": "fflogs",
|
|
||||||
"name": "FFLogs 查询",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"minApiSdkVersion": "1.0.0",
|
|
||||||
"description": "对接 FFLogs v2 GraphQL,提供 FF14 角色公开排名和指定高难最近记录查询能力。",
|
|
||||||
"author": "KT",
|
|
||||||
"entry": "qqbot-fflogs.plugin.ts",
|
|
||||||
"permissions": ["runtime.http", "plugin.config.read"],
|
|
||||||
"runtime": {
|
|
||||||
"workerType": "node-worker",
|
|
||||||
"timeoutMs": 20000,
|
|
||||||
"memoryMb": 256,
|
|
||||||
"maxConcurrency": 2
|
|
||||||
},
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"baseUrl": { "type": "string" },
|
|
||||||
"graphqlUrl": { "type": "string" },
|
|
||||||
"tokenUrl": { "type": "string" },
|
|
||||||
"clientIdRef": { "type": "string" },
|
|
||||||
"clientSecretRef": { "type": "string" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"key": "fflogs.character.summary",
|
|
||||||
"name": "角色排名摘要",
|
|
||||||
"handlerName": "getCharacterSummary",
|
|
||||||
"description": "查询指定角色的 FFLogs 公开排名摘要;传入 encounter/任务 后查询指定高难最近10次记录。",
|
|
||||||
"aliases": ["fflogs", "logs", "查logs", "查log"],
|
|
||||||
"permissions": ["runtime.http", "plugin.config.read"],
|
|
||||||
"timeoutMs": 20000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"events": [],
|
|
||||||
"assets": [],
|
|
||||||
"migrations": []
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"pluginKey": "repeater",
|
|
||||||
"name": "复读机",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"minApiSdkVersion": "1.0.0",
|
|
||||||
"description": "监听同一会话内连续重复的普通文本消息,达到阈值后通过宿主发送队列复读一次。",
|
|
||||||
"author": "KT",
|
|
||||||
"entry": "qqbot-repeater.plugin.ts",
|
|
||||||
"permissions": [
|
|
||||||
"qqbot.event.receive",
|
|
||||||
"qqbot.send",
|
|
||||||
"plugin.config.read",
|
|
||||||
"plugin.storage.read",
|
|
||||||
"plugin.storage.write"
|
|
||||||
],
|
|
||||||
"runtime": {
|
|
||||||
"workerType": "node-worker",
|
|
||||||
"timeoutMs": 5000,
|
|
||||||
"memoryMb": 128,
|
|
||||||
"maxConcurrency": 1
|
|
||||||
},
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"threshold": { "type": "number" },
|
|
||||||
"maxTextLength": { "type": "number" },
|
|
||||||
"minIntervalMs": { "type": "number" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"operations": [],
|
|
||||||
"events": [
|
|
||||||
{
|
|
||||||
"key": "repeater.message",
|
|
||||||
"name": "消息事件",
|
|
||||||
"eventName": "message",
|
|
||||||
"handlerName": "handleMessage",
|
|
||||||
"description": "同一会话连续重复普通文本达到阈值后复读。"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"assets": [],
|
|
||||||
"migrations": []
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { WordpressArticleController } from '@/wordpress/wordpress-article.controller';
|
|
||||||
import { WordpressAuthController } from '@/wordpress/wordpress-auth.controller';
|
|
||||||
import { WordpressCategoryController } from '@/wordpress/wordpress-category.controller';
|
|
||||||
import { WordpressModule } from '@/wordpress/wordpress.module';
|
|
||||||
import { WordpressService } from '@/wordpress/wordpress.service';
|
|
||||||
import { WordpressTagController } from '@/wordpress/wordpress-tag.controller';
|
|
||||||
import { WordpressThemeController } from '@/wordpress/wordpress-theme.controller';
|
|
||||||
|
|
||||||
export const WORDPRESS_MIRROR_CONTROLLERS = [
|
|
||||||
WordpressAuthController,
|
|
||||||
WordpressArticleController,
|
|
||||||
WordpressTagController,
|
|
||||||
WordpressCategoryController,
|
|
||||||
WordpressThemeController,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const WORDPRESS_MIRROR_PROVIDERS = [WordpressService];
|
|
||||||
|
|
||||||
export const WORDPRESS_MIRROR_DOMAIN_CONTRACT = {
|
|
||||||
tables: [
|
|
||||||
'wordpress_site',
|
|
||||||
'wordpress_auth_session',
|
|
||||||
'wordpress_remote_post',
|
|
||||||
'wordpress_remote_term',
|
|
||||||
'wordpress_sync_job',
|
|
||||||
'wordpress_sync_mapping',
|
|
||||||
],
|
|
||||||
remotePostMapping: {
|
|
||||||
remoteTable: 'wordpress_remote_post',
|
|
||||||
mappingTable: 'wordpress_sync_mapping',
|
|
||||||
remoteType: 'post',
|
|
||||||
localType: 'blog_post',
|
|
||||||
remoteKeys: ['site_id', 'remote_id'],
|
|
||||||
localKey: 'local_id',
|
|
||||||
},
|
|
||||||
remoteTermMapping: {
|
|
||||||
remoteTable: 'wordpress_remote_term',
|
|
||||||
mappingTable: 'wordpress_sync_mapping',
|
|
||||||
remoteType: 'term',
|
|
||||||
localType: 'blog_term',
|
|
||||||
remoteKeys: ['site_id', 'taxonomy_key', 'remote_id'],
|
|
||||||
localKey: 'local_id',
|
|
||||||
},
|
|
||||||
syncJob: {
|
|
||||||
table: 'wordpress_sync_job',
|
|
||||||
jobTypeField: 'job_type',
|
|
||||||
statusField: 'status',
|
|
||||||
startedAtField: 'started_at',
|
|
||||||
finishedAtField: 'finished_at',
|
|
||||||
summaryField: 'summary_json',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [WordpressModule],
|
|
||||||
exports: [WordpressModule],
|
|
||||||
})
|
|
||||||
export class WordpressMirrorModule {}
|
|
||||||
@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { throwVbenError, ToolsService } from '@/common';
|
import { throwVbenError, ToolsService } from '@/common';
|
||||||
import { NapcatLoginApiClient } from '@/modules/qqbot/napcat';
|
|
||||||
import type {
|
import type {
|
||||||
NapcatApiResponse,
|
NapcatApiResponse,
|
||||||
NapcatCaptchaLoginResult,
|
NapcatCaptchaLoginResult,
|
||||||
@ -170,9 +169,6 @@ export class QqbotNapcatLoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = await this.getSessionContainer(session);
|
const container = await this.getSessionContainer(session);
|
||||||
if (session.newDeviceStatus && session.newDeviceStatus !== 'verified') {
|
|
||||||
return this.pollNewDeviceVerification(session, container);
|
|
||||||
}
|
|
||||||
let status: NapcatLoginStatus;
|
let status: NapcatLoginStatus;
|
||||||
try {
|
try {
|
||||||
status = await this.getLoginStatus(container);
|
status = await this.getLoginStatus(container);
|
||||||
@ -289,19 +285,67 @@ export class QqbotNapcatLoginService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (captchaResult?.needNewDevice) {
|
if (captchaResult?.needNewDevice && captchaResult.jumpUrl) {
|
||||||
return this.startNewDeviceVerification(
|
return this.keepSessionPending(
|
||||||
session,
|
session,
|
||||||
container,
|
'验证码已通过,但 QQ 仍要求新设备验证,请在 NapCat WebUI 继续完成',
|
||||||
captchaResult,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.completePasswordLoginAfterChallenge(
|
const loginStatus = await this.waitForPasswordLoginStatus(container);
|
||||||
session,
|
if (!loginStatus.isLogin) {
|
||||||
container,
|
const captchaUrl = this.getCaptchaUrlFromStatus(loginStatus);
|
||||||
'验证码登录成功',
|
if (captchaUrl) {
|
||||||
);
|
return this.keepPasswordCaptchaPending(
|
||||||
|
session,
|
||||||
|
captchaUrl,
|
||||||
|
loginStatus.loginError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.failCaptchaLogin(
|
||||||
|
session,
|
||||||
|
container,
|
||||||
|
`验证码登录未完成:${loginStatus.loginError || 'NapCat 未返回登录成功'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginInfo = await this.getLoginInfo(container);
|
||||||
|
const selfId = this.toolsService.pickNapcatSelfId(loginInfo);
|
||||||
|
if (loginInfo.online === false || !selfId) {
|
||||||
|
return this.failCaptchaLogin(
|
||||||
|
session,
|
||||||
|
container,
|
||||||
|
loginInfo.online === false
|
||||||
|
? 'NapCat 当前账号已离线'
|
||||||
|
: 'NapCat 未返回 QQ 号',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (session.expectedSelfId && session.expectedSelfId !== selfId) {
|
||||||
|
await this.clearRuntimeLoginPasswordAfterFailedPassword(
|
||||||
|
session,
|
||||||
|
container,
|
||||||
|
selfId,
|
||||||
|
);
|
||||||
|
return this.failSession(
|
||||||
|
session,
|
||||||
|
`当前密码登录账号 ${selfId} 与目标账号 ${session.expectedSelfId} 不一致`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.clearRuntimeLoginPasswordAfterSuccess(
|
||||||
|
session,
|
||||||
|
container,
|
||||||
|
selfId,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return this.toResult(session);
|
||||||
|
}
|
||||||
|
return this.completeLogin(session, container, {
|
||||||
|
loginInfo,
|
||||||
|
successMessage: '验证码登录成功',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
events(sessionId: string) {
|
events(sessionId: string) {
|
||||||
@ -533,12 +577,9 @@ export class QqbotNapcatLoginService {
|
|||||||
captchaUrl: session.captchaUrl,
|
captchaUrl: session.captchaUrl,
|
||||||
containerId: session.containerId,
|
containerId: session.containerId,
|
||||||
containerName: session.containerName,
|
containerName: session.containerName,
|
||||||
deviceVerifyUrl: session.deviceVerifyUrl,
|
|
||||||
errorMessage: session.errorMessage,
|
errorMessage: session.errorMessage,
|
||||||
expiresAt: session.expiresAt,
|
expiresAt: session.expiresAt,
|
||||||
mode: session.mode,
|
mode: session.mode,
|
||||||
newDeviceQrcode: session.newDeviceQrcode,
|
|
||||||
newDeviceStatus: session.newDeviceStatus,
|
|
||||||
qrcode: session.qrcode,
|
qrcode: session.qrcode,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
status: session.status,
|
status: session.status,
|
||||||
@ -546,219 +587,6 @@ export class QqbotNapcatLoginService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startNewDeviceVerification(
|
|
||||||
session: QqbotLoginScanSession,
|
|
||||||
container: QqbotNapcatRuntime,
|
|
||||||
captchaResult: NapcatCaptchaLoginResult,
|
|
||||||
) {
|
|
||||||
session.status = 'pending';
|
|
||||||
session.captchaUrl = undefined;
|
|
||||||
session.qrcode = undefined;
|
|
||||||
session.deviceVerifyUrl = this.toolsService.toTrimmedString(
|
|
||||||
captchaResult.jumpUrl,
|
|
||||||
);
|
|
||||||
session.newDevicePullQrCodeSig = this.toolsService.toTrimmedString(
|
|
||||||
captchaResult.newDevicePullQrCodeSig,
|
|
||||||
);
|
|
||||||
session.newDeviceStatus = 'qr-pending';
|
|
||||||
session.errorMessage = '需要新设备验证二维码';
|
|
||||||
this.sessions.set(session.id, session);
|
|
||||||
this.publishScanResultEvent(
|
|
||||||
session,
|
|
||||||
'new-device-required',
|
|
||||||
'processing',
|
|
||||||
'需要新设备验证二维码',
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = new NapcatLoginApiClient({
|
|
||||||
post: (path, body) => this.postNapcat(container, path, body),
|
|
||||||
});
|
|
||||||
const qrcode = await client.getNewDeviceQRCode(session.id);
|
|
||||||
session.newDeviceQrcode = qrcode.qrcodeUrl;
|
|
||||||
session.newDevicePullQrCodeSig =
|
|
||||||
qrcode.pullQrCodeSig || session.newDevicePullQrCodeSig;
|
|
||||||
session.newDeviceStatus = qrcode.status;
|
|
||||||
session.errorMessage = '新设备二维码待扫码';
|
|
||||||
this.sessions.set(session.id, session);
|
|
||||||
this.publishScanResultEvent(
|
|
||||||
session,
|
|
||||||
'new-device-qrcode-ready',
|
|
||||||
'processing',
|
|
||||||
'新设备二维码待扫码',
|
|
||||||
);
|
|
||||||
return this.toResult(session);
|
|
||||||
} catch (err) {
|
|
||||||
return this.keepSessionPending(
|
|
||||||
session,
|
|
||||||
this.toolsService.getErrorMessage(err) || '新设备二维码生成失败',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async pollNewDeviceVerification(
|
|
||||||
session: QqbotLoginScanSession,
|
|
||||||
container: QqbotNapcatRuntime,
|
|
||||||
) {
|
|
||||||
const client = new NapcatLoginApiClient({
|
|
||||||
post: (path, body) => this.postNapcat(container, path, body),
|
|
||||||
});
|
|
||||||
const poll = await client.pollNewDeviceQR(session.id);
|
|
||||||
if (poll.status === 'scanned') {
|
|
||||||
return this.keepNewDevicePending(
|
|
||||||
session,
|
|
||||||
'scanned',
|
|
||||||
poll.message || '新设备二维码已扫码',
|
|
||||||
'new-device-scanned',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (poll.status === 'confirming') {
|
|
||||||
this.keepNewDevicePending(
|
|
||||||
session,
|
|
||||||
'confirming',
|
|
||||||
poll.message || '新设备确认中',
|
|
||||||
'new-device-confirming',
|
|
||||||
);
|
|
||||||
const loginResult = await client.newDeviceLogin(session.id);
|
|
||||||
if (!loginResult.success) {
|
|
||||||
return this.failNewDeviceVerification(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
loginResult.message || '新设备验证失败',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
session.newDeviceQrcode = undefined;
|
|
||||||
session.newDeviceStatus = 'verified';
|
|
||||||
session.errorMessage = '新设备验证成功,继续登录';
|
|
||||||
this.sessions.set(session.id, session);
|
|
||||||
this.publishScanResultEvent(
|
|
||||||
session,
|
|
||||||
'new-device-verified',
|
|
||||||
'success',
|
|
||||||
'新设备验证成功,继续登录',
|
|
||||||
);
|
|
||||||
return this.completePasswordLoginAfterChallenge(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
'新设备验证登录成功',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (poll.status === 'expired') {
|
|
||||||
return this.failNewDeviceVerification(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
poll.message || '新设备二维码已过期',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (poll.status === 'failed') {
|
|
||||||
return this.failNewDeviceVerification(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
poll.message || '新设备验证失败',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.keepNewDevicePending(
|
|
||||||
session,
|
|
||||||
'qr-pending',
|
|
||||||
poll.message || '新设备二维码待扫码',
|
|
||||||
'new-device-qrcode-ready',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private keepNewDevicePending(
|
|
||||||
session: QqbotLoginScanSession,
|
|
||||||
status: NonNullable<QqbotLoginScanSession['newDeviceStatus']>,
|
|
||||||
message: string,
|
|
||||||
step: string,
|
|
||||||
) {
|
|
||||||
const shouldPublish =
|
|
||||||
session.newDeviceStatus !== status || session.errorMessage !== message;
|
|
||||||
session.status = 'pending';
|
|
||||||
session.captchaUrl = undefined;
|
|
||||||
session.qrcode = undefined;
|
|
||||||
session.newDeviceStatus = status;
|
|
||||||
session.errorMessage = message;
|
|
||||||
session.expiresAt = Date.now() + this.getSessionTtlMs();
|
|
||||||
this.sessions.set(session.id, session);
|
|
||||||
if (shouldPublish) {
|
|
||||||
this.publishScanResultEvent(session, step, 'processing', message);
|
|
||||||
}
|
|
||||||
return this.toResult(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async failNewDeviceVerification(
|
|
||||||
session: QqbotLoginScanSession,
|
|
||||||
container: QqbotNapcatRuntime,
|
|
||||||
message: string,
|
|
||||||
) {
|
|
||||||
session.newDeviceQrcode = undefined;
|
|
||||||
session.newDeviceStatus = 'failed';
|
|
||||||
session.errorMessage = message;
|
|
||||||
this.sessions.set(session.id, session);
|
|
||||||
return this.failCaptchaLogin(session, container, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async completePasswordLoginAfterChallenge(
|
|
||||||
session: QqbotLoginScanSession,
|
|
||||||
container: QqbotNapcatRuntime,
|
|
||||||
successMessage: string,
|
|
||||||
) {
|
|
||||||
const loginStatus = await this.waitForPasswordLoginStatus(container);
|
|
||||||
if (!loginStatus.isLogin) {
|
|
||||||
const captchaUrl = this.getCaptchaUrlFromStatus(loginStatus);
|
|
||||||
if (captchaUrl) {
|
|
||||||
return this.keepPasswordCaptchaPending(
|
|
||||||
session,
|
|
||||||
captchaUrl,
|
|
||||||
loginStatus.loginError,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.failCaptchaLogin(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
`验证码登录未完成:${loginStatus.loginError || 'NapCat 未返回登录成功'}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginInfo = await this.getLoginInfo(container);
|
|
||||||
const selfId = this.toolsService.pickNapcatSelfId(loginInfo);
|
|
||||||
if (loginInfo.online === false || !selfId) {
|
|
||||||
return this.failCaptchaLogin(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
loginInfo.online === false
|
|
||||||
? 'NapCat 当前账号已离线'
|
|
||||||
: 'NapCat 未返回 QQ 号',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (session.expectedSelfId && session.expectedSelfId !== selfId) {
|
|
||||||
await this.clearRuntimeLoginPasswordAfterFailedPassword(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
selfId,
|
|
||||||
);
|
|
||||||
return this.failSession(
|
|
||||||
session,
|
|
||||||
`当前密码登录账号 ${selfId} 与目标账号 ${session.expectedSelfId} 不一致`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.clearRuntimeLoginPasswordAfterSuccess(
|
|
||||||
session,
|
|
||||||
container,
|
|
||||||
selfId,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return this.toResult(session);
|
|
||||||
}
|
|
||||||
return this.completeLogin(session, container, {
|
|
||||||
loginInfo,
|
|
||||||
successMessage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private publishScanEvent(
|
private publishScanEvent(
|
||||||
session: QqbotLoginScanSession,
|
session: QqbotLoginScanSession,
|
||||||
input: Omit<QqbotLoginScanEvent, 'createdAt'>,
|
input: Omit<QqbotLoginScanEvent, 'createdAt'>,
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import {
|
|||||||
isQqbotFf14RegionName,
|
isQqbotFf14RegionName,
|
||||||
isQqbotFf14WorldName,
|
isQqbotFf14WorldName,
|
||||||
splitQqbotFf14WorldPath,
|
splitQqbotFf14WorldPath,
|
||||||
} from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-worlds';
|
} from '../plugins/ff14Market/qqbot-ff14-worlds';
|
||||||
import type { QqbotFf14MarketCatalog } from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-market.types';
|
import type { QqbotFf14MarketCatalog } from '../plugins/ff14Market/qqbot-ff14-market.types';
|
||||||
import type { QqbotCommandMatchResult } from '../qqbot.types';
|
import type { QqbotCommandMatchResult } from '../qqbot.types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@ -7,11 +7,6 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { throwVbenError, ToolsService } from '@/common';
|
import { throwVbenError, ToolsService } from '@/common';
|
||||||
import {
|
|
||||||
NapcatDeviceIdentityService,
|
|
||||||
toNapcatDockerDeviceOptions,
|
|
||||||
type NapcatDockerDeviceOptions,
|
|
||||||
} from '@/modules/qqbot/napcat';
|
|
||||||
import { QqbotAccount } from '../account/qqbot-account.entity';
|
import { QqbotAccount } from '../account/qqbot-account.entity';
|
||||||
import { QqbotAccountNapcat } from './qqbot-account-napcat.entity';
|
import { QqbotAccountNapcat } from './qqbot-account-napcat.entity';
|
||||||
import { QqbotNapcatContainer } from './qqbot-napcat-container.entity';
|
import { QqbotNapcatContainer } from './qqbot-napcat-container.entity';
|
||||||
@ -57,7 +52,6 @@ export class QqbotNapcatContainerService {
|
|||||||
@InjectRepository(QqbotAccountNapcat)
|
@InjectRepository(QqbotAccountNapcat)
|
||||||
private readonly bindingRepository: Repository<QqbotAccountNapcat>,
|
private readonly bindingRepository: Repository<QqbotAccountNapcat>,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
private readonly deviceIdentityService?: NapcatDeviceIdentityService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async prepareCreateContainer() {
|
async prepareCreateContainer() {
|
||||||
@ -79,11 +73,7 @@ export class QqbotNapcatContainerService {
|
|||||||
return { ...existing, hasExistingPrimaryBinding: true };
|
return { ...existing, hasExistingPrimaryBinding: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await this.createManagedContainer(
|
const created = await this.createManagedContainer(account.selfId);
|
||||||
account.selfId,
|
|
||||||
undefined,
|
|
||||||
account.id,
|
|
||||||
);
|
|
||||||
return { ...created, hasExistingPrimaryBinding: false };
|
return { ...created, hasExistingPrimaryBinding: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -859,7 +849,6 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
private async createManagedContainer(
|
private async createManagedContainer(
|
||||||
selfId?: string,
|
selfId?: string,
|
||||||
loginPassword?: string,
|
loginPassword?: string,
|
||||||
accountId?: string,
|
|
||||||
) {
|
) {
|
||||||
const mode = this.getManagedMode();
|
const mode = this.getManagedMode();
|
||||||
if (mode !== 'ssh') {
|
if (mode !== 'ssh') {
|
||||||
@ -873,16 +862,7 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
const port = await this.allocatePort();
|
const port = await this.allocatePort();
|
||||||
const name = this.buildContainerName(selfId);
|
const name = this.buildContainerName(selfId);
|
||||||
const token = randomBytes(24).toString('hex');
|
const token = randomBytes(24).toString('hex');
|
||||||
let dataDir = `${this.getRootDir()}/${name}`;
|
const dataDir = `${this.getRootDir()}/${name}`;
|
||||||
let deviceIdentity: NapcatDockerDeviceOptions | undefined;
|
|
||||||
if (accountId && this.deviceIdentityService) {
|
|
||||||
const identity = await this.deviceIdentityService.resolveForAccount({
|
|
||||||
accountId,
|
|
||||||
selfId,
|
|
||||||
});
|
|
||||||
dataDir = identity.dataDir || dataDir;
|
|
||||||
deviceIdentity = toNapcatDockerDeviceOptions(identity);
|
|
||||||
}
|
|
||||||
const baseUrl = this.buildBaseUrl(port);
|
const baseUrl = this.buildBaseUrl(port);
|
||||||
const reverseWsUrl = this.buildReverseWsUrl();
|
const reverseWsUrl = this.buildReverseWsUrl();
|
||||||
|
|
||||||
@ -906,7 +886,6 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
await this.createRemoteDockerContainer({
|
await this.createRemoteDockerContainer({
|
||||||
account: selfId,
|
account: selfId,
|
||||||
dataDir,
|
dataDir,
|
||||||
deviceIdentity,
|
|
||||||
image,
|
image,
|
||||||
loginPassword,
|
loginPassword,
|
||||||
name,
|
name,
|
||||||
@ -922,16 +901,8 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
status: 'running',
|
status: 'running',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (accountId && this.deviceIdentityService) {
|
|
||||||
await this.deviceIdentityService.resolveForAccount({
|
|
||||||
accountId,
|
|
||||||
containerId: container.id,
|
|
||||||
selfId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
dataDir,
|
|
||||||
id: container.id,
|
id: container.id,
|
||||||
name,
|
name,
|
||||||
webuiPort: port,
|
webuiPort: port,
|
||||||
@ -953,7 +924,6 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
private async createRemoteDockerContainer(input: {
|
private async createRemoteDockerContainer(input: {
|
||||||
account?: string;
|
account?: string;
|
||||||
dataDir: string;
|
dataDir: string;
|
||||||
deviceIdentity?: NapcatDockerDeviceOptions;
|
|
||||||
image: string;
|
image: string;
|
||||||
loginPassword?: string;
|
loginPassword?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -969,7 +939,6 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
private buildRemoteCreateScript(input: {
|
private buildRemoteCreateScript(input: {
|
||||||
account?: string;
|
account?: string;
|
||||||
dataDir: string;
|
dataDir: string;
|
||||||
deviceIdentity?: NapcatDockerDeviceOptions;
|
|
||||||
image: string;
|
image: string;
|
||||||
loginPassword?: string;
|
loginPassword?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -994,29 +963,6 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
|||||||
? ' -e NAPCAT_QUICK_PASSWORD="$NAPCAT_QUICK_PASSWORD" \\\n'
|
? ' -e NAPCAT_QUICK_PASSWORD="$NAPCAT_QUICK_PASSWORD" \\\n'
|
||||||
: '';
|
: '';
|
||||||
const pullCmd = input.skipPull ? '' : 'docker pull "$IMAGE" >/dev/null\n';
|
const pullCmd = input.skipPull ? '' : 'docker pull "$IMAGE" >/dev/null\n';
|
||||||
const deviceHeader = input.deviceIdentity
|
|
||||||
? [
|
|
||||||
`NAPCAT_HOSTNAME=${this.sh(input.deviceIdentity.hostname)}`,
|
|
||||||
`NAPCAT_MAC_ADDRESS=${this.sh(input.deviceIdentity.macAddress)}`,
|
|
||||||
`MACHINE_ID_PATH=${this.sh(input.deviceIdentity.machineIdPath)}`,
|
|
||||||
`DEVICE_ENV_PATH=${this.sh(input.deviceIdentity.deviceEnvPath)}`,
|
|
||||||
].join('\n') + '\n'
|
|
||||||
: '';
|
|
||||||
const devicePrepareScript = input.deviceIdentity
|
|
||||||
? `
|
|
||||||
cat > "$DEVICE_ENV_PATH" <<EOF
|
|
||||||
NAPCAT_HOSTNAME=$NAPCAT_HOSTNAME
|
|
||||||
NAPCAT_MAC_ADDRESS=$NAPCAT_MAC_ADDRESS
|
|
||||||
MACHINE_ID_PATH=$MACHINE_ID_PATH
|
|
||||||
EOF
|
|
||||||
if [ ! -s "$MACHINE_ID_PATH" ]; then
|
|
||||||
printf '%s' "$NAME" | sha256sum | cut -c 1-32 > "$MACHINE_ID_PATH"
|
|
||||||
fi
|
|
||||||
`
|
|
||||||
: '';
|
|
||||||
const deviceRunFlags = input.deviceIdentity
|
|
||||||
? ' --hostname "$NAPCAT_HOSTNAME" \\\n --mac-address "$NAPCAT_MAC_ADDRESS" \\\n -v "$MACHINE_ID_PATH:/etc/machine-id:ro" \\\n'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
set -eu
|
set -eu
|
||||||
@ -1028,10 +974,8 @@ REVERSE_WS_URL=${reverseWsUrl}
|
|||||||
WEBUI_TOKEN=${token}
|
WEBUI_TOKEN=${token}
|
||||||
${accountHeader}
|
${accountHeader}
|
||||||
${passwordHeader}
|
${passwordHeader}
|
||||||
${deviceHeader}
|
|
||||||
mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs"
|
mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs"
|
||||||
chmod 700 "$DATA_DIR"
|
chmod 700 "$DATA_DIR"
|
||||||
${devicePrepareScript}
|
|
||||||
|
|
||||||
cat > "$DATA_DIR/config/webui.json" <<EOF
|
cat > "$DATA_DIR/config/webui.json" <<EOF
|
||||||
{
|
{
|
||||||
@ -1075,7 +1019,7 @@ docker run -d \\
|
|||||||
-e NAPCAT_UID=0 \\
|
-e NAPCAT_UID=0 \\
|
||||||
-e NAPCAT_GID=0 \\
|
-e NAPCAT_GID=0 \\
|
||||||
-e WEBUI_TOKEN="$WEBUI_TOKEN" \\
|
-e WEBUI_TOKEN="$WEBUI_TOKEN" \\
|
||||||
${accountRunFlag}${passwordRunFlag}${deviceRunFlags} -p "$PORT:6099" \\
|
${accountRunFlag}${passwordRunFlag} -p "$PORT:6099" \\
|
||||||
-v "$DATA_DIR/QQ:/app/.config/QQ" \\
|
-v "$DATA_DIR/QQ:/app/.config/QQ" \\
|
||||||
-v "$DATA_DIR/config:/app/napcat/config" \\
|
-v "$DATA_DIR/config:/app/napcat/config" \\
|
||||||
-v "$DATA_DIR/plugins:/app/napcat/plugins" \\
|
-v "$DATA_DIR/plugins:/app/napcat/plugins" \\
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { formatKtDateTime, throwVbenError } from '@/common';
|
import { formatKtDateTime, throwVbenError } from '@/common';
|
||||||
import { QqbotAccountService } from '../account/qqbot-account.service';
|
import { QqbotAccountService } from '../account/qqbot-account.service';
|
||||||
import { QqbotRepeaterPluginService } from '@/modules/qqbot/plugins/repeater/qqbot-repeater.plugin';
|
import { QqbotRepeaterPluginService } from '../plugins/repeater/qqbot-repeater.plugin';
|
||||||
import type {
|
import type {
|
||||||
QqbotEventPluginDefinition,
|
QqbotEventPluginDefinition,
|
||||||
QqbotPluginHealth,
|
QqbotPluginHealth,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { formatKtDateTime, throwVbenError } from '@/common';
|
import { formatKtDateTime, throwVbenError } from '@/common';
|
||||||
import { QqbotBangDreamPluginService } from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.plugin';
|
import { QqbotBangDreamPluginService } from '../plugins/bangDream/qqbot-bangdream.plugin';
|
||||||
import { QqbotFf14MarketPluginService } from '@/modules/qqbot/plugins/ff14Market/qqbot-ff14-market.plugin';
|
import { QqbotFf14MarketPluginService } from '../plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||||
import { QqbotFflogsPluginService } from '@/modules/qqbot/plugins/fflogs/qqbot-fflogs.plugin';
|
import { QqbotFflogsPluginService } from '../plugins/fflogs/qqbot-fflogs.plugin';
|
||||||
import type {
|
import type {
|
||||||
QqbotIntegrationPlugin,
|
QqbotIntegrationPlugin,
|
||||||
QqbotPluginHealth,
|
QqbotPluginHealth,
|
||||||
@ -13,7 +13,6 @@ import type {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotPluginRegistryService implements OnModuleInit {
|
export class QqbotPluginRegistryService implements OnModuleInit {
|
||||||
private readonly pluginAliases = new Map<string, string>();
|
|
||||||
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
private readonly plugins = new Map<string, QqbotIntegrationPlugin>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -33,13 +32,6 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
throwVbenError('QQBot 插件必须包含 key 和 operation');
|
throwVbenError('QQBot 插件必须包含 key 和 operation');
|
||||||
}
|
}
|
||||||
this.plugins.set(plugin.key, plugin);
|
this.plugins.set(plugin.key, plugin);
|
||||||
for (const legacyKey of plugin.legacyKeys || []) {
|
|
||||||
if (!legacyKey || legacyKey === plugin.key) continue;
|
|
||||||
if (this.plugins.has(legacyKey) || this.pluginAliases.has(legacyKey)) {
|
|
||||||
throwVbenError(`QQBot 插件别名重复:${legacyKey}`);
|
|
||||||
}
|
|
||||||
this.pluginAliases.set(legacyKey, plugin.key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listPlugins(): QqbotPluginSummary[] {
|
listPlugins(): QqbotPluginSummary[] {
|
||||||
@ -110,7 +102,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getOperation(pluginKey: string, operationKey: string) {
|
private getOperation(pluginKey: string, operationKey: string) {
|
||||||
const plugin = this.getPluginByKey(pluginKey);
|
const plugin = this.plugins.get(pluginKey);
|
||||||
if (!plugin) throwVbenError(`QQBot 插件不存在:${pluginKey}`);
|
if (!plugin) throwVbenError(`QQBot 插件不存在:${pluginKey}`);
|
||||||
|
|
||||||
const operation = plugin.operations.find(
|
const operation = plugin.operations.find(
|
||||||
@ -124,14 +116,7 @@ export class QqbotPluginRegistryService implements OnModuleInit {
|
|||||||
|
|
||||||
private getPlugins(pluginKey?: string) {
|
private getPlugins(pluginKey?: string) {
|
||||||
if (!pluginKey) return [...this.plugins.values()];
|
if (!pluginKey) return [...this.plugins.values()];
|
||||||
const plugin = this.getPluginByKey(pluginKey);
|
const plugin = this.plugins.get(pluginKey);
|
||||||
return plugin ? [plugin] : [];
|
return plugin ? [plugin] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPluginByKey(pluginKey: string) {
|
|
||||||
return (
|
|
||||||
this.plugins.get(pluginKey) ||
|
|
||||||
this.plugins.get(this.pluginAliases.get(pluginKey) || '')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,15 +4,15 @@ import type {
|
|||||||
QqbotBangDreamCommandInput,
|
QqbotBangDreamCommandInput,
|
||||||
QqbotBangDreamCommandOutput,
|
QqbotBangDreamCommandOutput,
|
||||||
QqbotBangDreamOperationKey,
|
QqbotBangDreamOperationKey,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
} from '@/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
||||||
import { waitForMainDataReady } from '@/modules/qqbot/plugins/bangDream/shared/main-data-store';
|
import { waitForMainDataReady } from '@/qqbot/plugins/bangDream/shared/main-data-store';
|
||||||
import {
|
import {
|
||||||
createTsuguLogHook,
|
createTsuguLogHook,
|
||||||
TsuguHookRegistry,
|
TsuguHookRegistry,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/hook/hook-registry';
|
} from '@/qqbot/plugins/bangDream/hook/hook-registry';
|
||||||
import { getBangDreamOperationDefinition } from '@/modules/qqbot/plugins/bangDream/registry/operation-registry';
|
import { getBangDreamOperationDefinition } from '@/qqbot/plugins/bangDream/registry/operation-registry';
|
||||||
import { TsuguOperationPipeline } from '@/modules/qqbot/plugins/bangDream/application/operation-pipeline';
|
import { TsuguOperationPipeline } from '@/qqbot/plugins/bangDream/application/operation-pipeline';
|
||||||
import { QqbotBangDreamRendererService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-renderer.facade';
|
import { QqbotBangDreamRendererService } from '@/qqbot/plugins/bangDream/application/bangdream-renderer.facade';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TsuguApplicationService implements OnApplicationBootstrap {
|
export class TsuguApplicationService implements OnApplicationBootstrap {
|
||||||
@ -4,8 +4,8 @@ import type {
|
|||||||
QqbotBangDreamOperationKey,
|
QqbotBangDreamOperationKey,
|
||||||
QqbotBangDreamSongSearchInput,
|
QqbotBangDreamSongSearchInput,
|
||||||
QqbotBangDreamSongSummary,
|
QqbotBangDreamSongSummary,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
} from '@/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
||||||
import { TsuguApplicationService } from '@/modules/qqbot/plugins/bangDream/application/bangdream-application.service';
|
import { TsuguApplicationService } from '@/qqbot/plugins/bangDream/application/bangdream-application.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotBangDreamClientService {
|
export class QqbotBangDreamClientService {
|
||||||
@ -1,59 +1,54 @@
|
|||||||
import { Injectable, Optional } from '@nestjs/common';
|
import { Injectable, Optional } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DictService } from '@/admin/dict/dict.service';
|
import { DictService } from '@/admin/dict/dict.service';
|
||||||
import { Card } from '@/modules/qqbot/plugins/bangDream/card/card.model';
|
import { Card } from '@/qqbot/plugins/bangDream/card/card.model';
|
||||||
import { getPresentEvent } from '@/modules/qqbot/plugins/bangDream/event/event.model';
|
import { getPresentEvent } from '@/qqbot/plugins/bangDream/event/event.model';
|
||||||
import {
|
import { Gacha, getPresentGachaList } from '@/qqbot/plugins/bangDream/gacha/gacha.model';
|
||||||
Gacha,
|
|
||||||
getPresentGachaList,
|
|
||||||
} from '@/modules/qqbot/plugins/bangDream/gacha/gacha.model';
|
|
||||||
import {
|
import {
|
||||||
BANGDREAM_GACHA_DEFAULT_SPIN_COUNT,
|
BANGDREAM_GACHA_DEFAULT_SPIN_COUNT,
|
||||||
isBirthdayGachaType,
|
isBirthdayGachaType,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/policy/gacha.policy';
|
} from '@/qqbot/plugins/bangDream/policy/gacha.policy';
|
||||||
import { Server } from '@/modules/qqbot/plugins/bangDream/catalog/server.model';
|
import { Server } from '@/qqbot/plugins/bangDream/catalog/server.model';
|
||||||
import { Song } from '@/modules/qqbot/plugins/bangDream/song/song.model';
|
import { Song } from '@/qqbot/plugins/bangDream/song/song.model';
|
||||||
import mainAPI, {
|
import mainAPI, { waitForMainDataReady } from '@/qqbot/plugins/bangDream/shared/main-data-store';
|
||||||
waitForMainDataReady,
|
|
||||||
} from '@/modules/qqbot/plugins/bangDream/shared/main-data-store';
|
|
||||||
import {
|
import {
|
||||||
fuzzySearch,
|
fuzzySearch,
|
||||||
type FuzzySearchResult,
|
type FuzzySearchResult,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/search/fuzzy-search';
|
} from '@/qqbot/plugins/bangDream/search/fuzzy-search';
|
||||||
import { drawCardDetail } from '@/modules/qqbot/plugins/bangDream/card/card-detail.renderer';
|
import { drawCardDetail } from '@/qqbot/plugins/bangDream/card/card-detail.renderer';
|
||||||
import { drawCardList } from '@/modules/qqbot/plugins/bangDream/card/card-search.renderer';
|
import { drawCardList } from '@/qqbot/plugins/bangDream/card/card-search.renderer';
|
||||||
import { drawCharacterDetail } from '@/modules/qqbot/plugins/bangDream/character/character-detail.renderer';
|
import { drawCharacterDetail } from '@/qqbot/plugins/bangDream/character/character-detail.renderer';
|
||||||
import { drawCharacterList } from '@/modules/qqbot/plugins/bangDream/character/character-search.renderer';
|
import { drawCharacterList } from '@/qqbot/plugins/bangDream/character/character-search.renderer';
|
||||||
import { drawCutoffAll } from '@/modules/qqbot/plugins/bangDream/cutoff/cutoff-all.renderer';
|
import { drawCutoffAll } from '@/qqbot/plugins/bangDream/cutoff/cutoff-all.renderer';
|
||||||
import { drawCutoffDetail } from '@/modules/qqbot/plugins/bangDream/cutoff/cutoff-detail.renderer';
|
import { drawCutoffDetail } from '@/qqbot/plugins/bangDream/cutoff/cutoff-detail.renderer';
|
||||||
import { drawCutoffEventTop } from '@/modules/qqbot/plugins/bangDream/cutoff/cutoff-event-top.renderer';
|
import { drawCutoffEventTop } from '@/qqbot/plugins/bangDream/cutoff/cutoff-event-top.renderer';
|
||||||
import { drawCutoffListOfRecentEvent } from '@/modules/qqbot/plugins/bangDream/cutoff/cutoff-recent.renderer';
|
import { drawCutoffListOfRecentEvent } from '@/qqbot/plugins/bangDream/cutoff/cutoff-recent.renderer';
|
||||||
import { drawEventDetail } from '@/modules/qqbot/plugins/bangDream/event/event-detail.renderer';
|
import { drawEventDetail } from '@/qqbot/plugins/bangDream/event/event-detail.renderer';
|
||||||
import { drawEventList } from '@/modules/qqbot/plugins/bangDream/event/event-search.renderer';
|
import { drawEventList } from '@/qqbot/plugins/bangDream/event/event-search.renderer';
|
||||||
import { drawEventStage } from '@/modules/qqbot/plugins/bangDream/event/event-stage.renderer';
|
import { drawEventStage } from '@/qqbot/plugins/bangDream/event/event-stage.renderer';
|
||||||
import { drawGachaDetail } from '@/modules/qqbot/plugins/bangDream/gacha/gacha-detail.renderer';
|
import { drawGachaDetail } from '@/qqbot/plugins/bangDream/gacha/gacha-detail.renderer';
|
||||||
import { drawRandomGacha } from '@/modules/qqbot/plugins/bangDream/gacha/gacha-simulate.renderer';
|
import { drawRandomGacha } from '@/qqbot/plugins/bangDream/gacha/gacha-simulate.renderer';
|
||||||
import { drawPlayerDetail } from '@/modules/qqbot/plugins/bangDream/player/player-detail.renderer';
|
import { drawPlayerDetail } from '@/qqbot/plugins/bangDream/player/player-detail.renderer';
|
||||||
import { drawSongChart } from '@/modules/qqbot/plugins/bangDream/song/song-chart.renderer';
|
import { drawSongChart } from '@/qqbot/plugins/bangDream/song/song-chart.renderer';
|
||||||
import { drawSongDetail } from '@/modules/qqbot/plugins/bangDream/song/song-detail.renderer';
|
import { drawSongDetail } from '@/qqbot/plugins/bangDream/song/song-detail.renderer';
|
||||||
import { drawSongList } from '@/modules/qqbot/plugins/bangDream/song/song-search.renderer';
|
import { drawSongList } from '@/qqbot/plugins/bangDream/song/song-search.renderer';
|
||||||
import { drawSongMetaList } from '@/modules/qqbot/plugins/bangDream/song/song-meta.renderer';
|
import { drawSongMetaList } from '@/qqbot/plugins/bangDream/song/song-meta.renderer';
|
||||||
import { drawSongRandom } from '@/modules/qqbot/plugins/bangDream/song/song-random.renderer';
|
import { drawSongRandom } from '@/qqbot/plugins/bangDream/song/song-random.renderer';
|
||||||
import {
|
import {
|
||||||
BangDreamDictionaryLoader,
|
BangDreamDictionaryLoader,
|
||||||
type BangDreamDictionaryItem,
|
type BangDreamDictionaryItem,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/dictionary/dictionary-loader';
|
} from '@/qqbot/plugins/bangDream/dictionary/dictionary-loader';
|
||||||
import type { QqbotBangDreamOperationHandlerName } from '@/modules/qqbot/plugins/bangDream/registry/operation-registry';
|
import type { QqbotBangDreamOperationHandlerName } from '@/qqbot/plugins/bangDream/registry/operation-registry';
|
||||||
import {
|
import {
|
||||||
BANGDREAM_TSUGU_ENV_KEYS,
|
BANGDREAM_TSUGU_ENV_KEYS,
|
||||||
normalizeBangDreamBoolean,
|
normalizeBangDreamBoolean,
|
||||||
splitBangDreamOptionList,
|
splitBangDreamOptionList,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/config/runtime-options';
|
} from '@/qqbot/plugins/bangDream/config/runtime-options';
|
||||||
import type {
|
import type {
|
||||||
QqbotBangDreamCommandInput,
|
QqbotBangDreamCommandInput,
|
||||||
QqbotBangDreamCommandOutput,
|
QqbotBangDreamCommandOutput,
|
||||||
QqbotBangDreamOperationKey,
|
QqbotBangDreamOperationKey,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
} from '@/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
||||||
|
|
||||||
const SOURCE_NAME = 'Tsugu BangDream Bot 内置源码';
|
const SOURCE_NAME = 'Tsugu BangDream Bot 内置源码';
|
||||||
|
|
||||||
@ -2,15 +2,15 @@ import type {
|
|||||||
QqbotBangDreamCommandInput,
|
QqbotBangDreamCommandInput,
|
||||||
QqbotBangDreamCommandOutput,
|
QqbotBangDreamCommandOutput,
|
||||||
QqbotBangDreamOperationKey,
|
QqbotBangDreamOperationKey,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
} from '@/qqbot/plugins/bangDream/qqbot-bangdream.types';
|
||||||
import type {
|
import type {
|
||||||
QqbotBangDreamOperationHandlerName,
|
QqbotBangDreamOperationHandlerName,
|
||||||
TsuguOperationDefinition,
|
TsuguOperationDefinition,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/registry/operation-registry';
|
} from '@/qqbot/plugins/bangDream/registry/operation-registry';
|
||||||
import {
|
import {
|
||||||
createTsuguHookContext,
|
createTsuguHookContext,
|
||||||
type TsuguHookRegistry,
|
type TsuguHookRegistry,
|
||||||
} from '@/modules/qqbot/plugins/bangDream/hook/hook-registry';
|
} from '@/qqbot/plugins/bangDream/hook/hook-registry';
|
||||||
|
|
||||||
export type TsuguOperationPipelineOptions = {
|
export type TsuguOperationPipelineOptions = {
|
||||||
executeHandler: (
|
executeHandler: (
|
||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 445 KiB After Width: | Height: | Size: 445 KiB |
|
Before Width: | Height: | Size: 594 KiB After Width: | Height: | Size: 594 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 524 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 223 B After Width: | Height: | Size: 223 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |