Compare commits
2 Commits
aaeea9102b
...
14e2ec92fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 14e2ec92fd | |||
| 5926f50fa3 |
@ -63,6 +63,7 @@ QQBOT_PLUGIN_QUEUE_REDIS_DB=0
|
|||||||
QQBOT_PLUGIN_QUEUE_REDIS_PREFIX=kt:qqbot:plugin-worker
|
QQBOT_PLUGIN_QUEUE_REDIS_PREFIX=kt:qqbot:plugin-worker
|
||||||
QQBOT_PLUGIN_QUEUE_REMOVE_ON_FAIL=100
|
QQBOT_PLUGIN_QUEUE_REMOVE_ON_FAIL=100
|
||||||
QQBOT_PLUGIN_QUEUE_WAIT_BUFFER_MS=5000
|
QQBOT_PLUGIN_QUEUE_WAIT_BUFFER_MS=5000
|
||||||
|
QQBOT_PLUGIN_QUEUE_WAIT_TIMEOUT_MS=120000
|
||||||
QQBOT_COMMAND_MIN_COOLDOWN_MS=5000
|
QQBOT_COMMAND_MIN_COOLDOWN_MS=5000
|
||||||
QQBOT_RULE_MIN_COOLDOWN_MS=30000
|
QQBOT_RULE_MIN_COOLDOWN_MS=30000
|
||||||
QQBOT_REPEATER_THRESHOLD=4
|
QQBOT_REPEATER_THRESHOLD=4
|
||||||
|
|||||||
4
API.md
4
API.md
@ -85,7 +85,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
| 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_PLUGIN_QUEUE_REDIS_*`、`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_PLUGIN_QUEUE_REDIS_*`、`QQBOT_PLUGIN_QUEUE_WAIT_TIMEOUT_MS`、`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` |
|
||||||
@ -94,7 +94,7 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
|
|
||||||
真实密码、Token、OAuth secret 和生产 env 不提交到 Git。
|
真实密码、Token、OAuth secret 和生产 env 不提交到 Git。
|
||||||
|
|
||||||
QQBot 插件 worker 队列依赖 Redis。K8s 生产清单提供内部 Redis Service `kt-qqbot-plugin-redis:6379`,用于 `QQBOT_PLUGIN_QUEUE_REDIS_HOST` / `QQBOT_PLUGIN_QUEUE_REDIS_PORT`。
|
QQBot 插件 worker 队列依赖 Redis。K8s 生产清单提供内部 Redis Service `kt-qqbot-plugin-redis:6379`,用于 `QQBOT_PLUGIN_QUEUE_REDIS_HOST` / `QQBOT_PLUGIN_QUEUE_REDIS_PORT`。`QQBOT_PLUGIN_QUEUE_WAIT_TIMEOUT_MS` 控制串行队列的排队等待窗口,避免排队时间挤占插件操作本身的 `timeoutMs` 执行预算。
|
||||||
|
|
||||||
## Admin 与基础后台
|
## Admin 与基础后台
|
||||||
|
|
||||||
|
|||||||
@ -62,14 +62,14 @@ ci/ Jenkins Agent/Docker 辅助文件
|
|||||||
| 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_PLUGIN_QUEUE_REDIS_*`、`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_PLUGIN_QUEUE_REDIS_*`、`QQBOT_PLUGIN_QUEUE_WAIT_TIMEOUT_MS`、`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/迁移脚本。
|
||||||
|
|
||||||
QQBot 插件 worker 使用 BullMQ 队列串行执行同一插件安装实例的请求。K8s 生产清单包含内部服务 `kt-qqbot-plugin-redis`,生产 env 可将 `QQBOT_PLUGIN_QUEUE_REDIS_HOST` 配为该服务名。
|
QQBot 插件 worker 使用 BullMQ 队列串行执行同一插件安装实例的请求。K8s 生产清单包含内部服务 `kt-qqbot-plugin-redis`,生产 env 可将 `QQBOT_PLUGIN_QUEUE_REDIS_HOST` 配为该服务名。`QQBOT_PLUGIN_QUEUE_WAIT_TIMEOUT_MS` 控制排队等待窗口,插件 `operation.timeoutMs` 仍表示单次执行预算。
|
||||||
|
|
||||||
## 启动
|
## 启动
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,389 @@
|
|||||||
|
# QQBot 插件平台定时任务桥设计
|
||||||
|
|
||||||
|
日期:2026-06-16
|
||||||
|
|
||||||
|
状态:用户已确认方向,进入设计文档审阅。
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
QQBot 第三期重构已经让 Plugin Platform 接管插件 manifest、安装生命周期、worker runtime、operation、event、运行事件和插件管理接口。但插件还没有统一的定时任务能力:如果某个插件需要周期同步数据,只能把定时逻辑藏进插件自身 `activate()` 或业务代码里。这会导致启停不可控、运行记录不可见、cron 不能在线修改,也无法复用于后续插件。
|
||||||
|
|
||||||
|
本轮要把“插件暴露的定时任务能力”升级为平台能力,并先用 BangDream 落地:定期同步 Bestdori JSON 主数据到服务端缓存,降低 `查分数表`、`查卡` 等重命令冷启动成本。
|
||||||
|
|
||||||
|
相关当前事实:
|
||||||
|
|
||||||
|
- API 已使用 `@nestjs/bullmq` + `bullmq` 管理 QQBot 插件 worker 请求队列。
|
||||||
|
- Admin 当前技术栈是 `antdv-next`,不是传统 `ant-design-vue`。
|
||||||
|
- BangDream 当前通过 `waitForBangDreamCatalogReady()` 懒加载 catalog;`remote-resource.client` 已具备 cache 路径参数形态,但当前文件缓存还未真正落地为“服务端预同步主数据”。
|
||||||
|
- 现有插件 manifest 没有 `tasks` 能力,`QqbotPluginWorkerRuntime` 也没有 `executeTask` RPC。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
1. Plugin Platform 统一管理插件暴露的定时任务能力。
|
||||||
|
2. 插件通过 manifest 声明定时任务,平台解析、持久化、展示、调度和执行。
|
||||||
|
3. 支持 Admin 在线启停任务、修改 cron 表达式、手动运行一次、查看运行记录。
|
||||||
|
4. 调度层使用 BullMQ Redis-backed 调度,不手写内存定时器,也不让插件自己 `setInterval`。
|
||||||
|
5. Admin cron 编辑不手写 cron 解析/生成算法,不引入 `ant-design-vue`;使用 `@vue-js-cron/core`,以现有 `antdv-next` 控件做薄适配。
|
||||||
|
6. BangDream 第一阶段只同步 Bestdori JSON 主数据;图片资源继续按需缓存。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 本轮不做 Bestdori 全量图片镜像。
|
||||||
|
- 本轮不做所有插件的实际定时任务,只建立平台能力并完成 BangDream 参考实现。
|
||||||
|
- 本轮不做复杂任务依赖编排、分布式工作流 DAG、用户自定义脚本。
|
||||||
|
- 本轮不把 task 管理塞回现有“插件能力”页面,而是新增专门页面。
|
||||||
|
|
||||||
|
## 外部技术选择
|
||||||
|
|
||||||
|
### 后端调度
|
||||||
|
|
||||||
|
选择 BullMQ Job Scheduler / repeatable job 抽象作为平台调度实现。BullMQ v5 文档说明 Job Schedulers 是新版本对 repeatable jobs 的推荐能力,可按 fixed interval 或 cron 生成任务;当前 API 已引入 BullMQ,因此新增平台 task queue 不需要再引入另一套队列系统。
|
||||||
|
|
||||||
|
Nest 官方 `@nestjs/schedule` 支持动态 cron,但它是进程内调度。考虑线上 K8s、后续多副本和重启恢复,本轮不选 `@nestjs/schedule` 作为核心调度器。
|
||||||
|
|
||||||
|
### 前端 cron 编辑
|
||||||
|
|
||||||
|
选择 `@vue-js-cron/core`。它是 renderless Vue cron editor core,可以提供 cron 表达式生成/校验基础能力。Admin 侧新增 `CronEditorAntdvNext` 适配组件,只负责把现有 `antdv-next` 控件连接到 core 状态,不引入 `@vue-js-cron/ant`,因为该包依赖 `ant-design-vue`,会和当前 Admin 的 `antdv-next` 技术栈冲突。
|
||||||
|
|
||||||
|
参考来源:
|
||||||
|
|
||||||
|
- Nest task scheduling 文档:https://docs.nestjs.com/techniques/task-scheduling
|
||||||
|
- BullMQ Job Schedulers 文档:https://docs.bullmq.io/guide/job-schedulers
|
||||||
|
- Vue JS Cron 文档:https://abichinger.github.io/vue-js-cron/guide/getting-started-ant
|
||||||
|
- `@vue-js-cron/core` npm 信息:https://www.npmjs.com/package/@vue-js-cron/core
|
||||||
|
|
||||||
|
## Manifest 契约
|
||||||
|
|
||||||
|
`plugin.json` 新增 `tasks` 数组。旧插件不声明 `tasks` 时解析为 `[]`,保持兼容。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"key": "bangdream.bestdori.sync-main-data",
|
||||||
|
"name": "同步 Bestdori 主数据",
|
||||||
|
"handlerName": "syncBestdoriMainData",
|
||||||
|
"description": "同步 BangDream 重命令依赖的 Bestdori JSON 主数据。",
|
||||||
|
"defaultCron": "0 */6 * * *",
|
||||||
|
"enabled": true,
|
||||||
|
"timeoutMs": 120000,
|
||||||
|
"permissions": ["runtime.http", "plugin.storage.read", "plugin.storage.write"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段规则:
|
||||||
|
|
||||||
|
- `key`:平台能力 key,使用现有 operation/event capability key 规则。
|
||||||
|
- `handlerName`:插件 worker 内部处理器名。
|
||||||
|
- `defaultCron`:标准 5 段 cron 表达式,平台存储前校验并规范化。
|
||||||
|
- `enabled`:安装时默认启用状态。管理员后续启停以数据库配置为准。
|
||||||
|
- `timeoutMs`:单次任务执行预算。
|
||||||
|
- `permissions`:任务需要的 host 权限,沿用 manifest 权限白名单。
|
||||||
|
|
||||||
|
Manifest parser 新增校验:
|
||||||
|
|
||||||
|
- task key 不合法时报 `INVALID_CAPABILITY_KEY`。
|
||||||
|
- task key 重复时报 `DUPLICATE_TASK_KEY`。
|
||||||
|
- handlerName 缺失时报 `MISSING_TASK_HANDLER`。
|
||||||
|
- cron 缺失或不合法时报 `INVALID_TASK_CRON`。
|
||||||
|
- timeoutMs 缺失时报 `MISSING_TASK_TIMEOUT`。
|
||||||
|
- permission 不在白名单内时报 `UNKNOWN_PERMISSION`。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
新增两张平台表。表结构仍遵循第三期规则:Snowflake `BIGINT` 主键、lower snake case、可查询字段结构化、日志表追加写。
|
||||||
|
|
||||||
|
### `qqbot_plugin_task`
|
||||||
|
|
||||||
|
表示插件安装实例暴露的一条可管理定时任务。
|
||||||
|
|
||||||
|
核心字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `plugin_id`
|
||||||
|
- `installation_id`
|
||||||
|
- `task_key`
|
||||||
|
- `task_name`
|
||||||
|
- `handler_name`
|
||||||
|
- `description`
|
||||||
|
- `default_cron`
|
||||||
|
- `cron_expression`
|
||||||
|
- `enabled`
|
||||||
|
- `timeout_ms`
|
||||||
|
- `runtime_status`:`idle | scheduled | running | failed | disabled`
|
||||||
|
- `last_run_id`
|
||||||
|
- `last_run_at`
|
||||||
|
- `last_status`
|
||||||
|
- `last_error`
|
||||||
|
- `last_duration_ms`
|
||||||
|
- `next_run_at`
|
||||||
|
- `create_time`
|
||||||
|
- `update_time`
|
||||||
|
|
||||||
|
唯一约束:`installation_id + task_key`。
|
||||||
|
|
||||||
|
### `qqbot_plugin_task_run`
|
||||||
|
|
||||||
|
表示一次任务运行记录。
|
||||||
|
|
||||||
|
核心字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `task_id`
|
||||||
|
- `plugin_id`
|
||||||
|
- `installation_id`
|
||||||
|
- `task_key`
|
||||||
|
- `trigger_type`:`schedule | manual | bootstrap`
|
||||||
|
- `status`:`running | success | failed | skipped`
|
||||||
|
- `job_id`
|
||||||
|
- `started_at`
|
||||||
|
- `finished_at`
|
||||||
|
- `duration_ms`
|
||||||
|
- `safe_summary`
|
||||||
|
- `error_message`
|
||||||
|
- `create_time`
|
||||||
|
|
||||||
|
索引:
|
||||||
|
|
||||||
|
- `task_id + create_time`
|
||||||
|
- `plugin_id + create_time`
|
||||||
|
- `status + create_time`
|
||||||
|
|
||||||
|
## 后端架构
|
||||||
|
|
||||||
|
新增 `plugin-platform/application/task` 能力域,职责与现有 operation/event/lifecycle 分开。
|
||||||
|
|
||||||
|
主要组件:
|
||||||
|
|
||||||
|
- `QqbotPluginTaskService`:任务查询、启停、cron 更新、手动运行、运行记录分页。
|
||||||
|
- `QqbotPluginTaskSchedulerService`:把 DB 中 enabled task 同步到 BullMQ scheduler,处理 enable/disable/update cron 的重建。
|
||||||
|
- `QqbotPluginTaskWorkerProcessor`:消费调度 job,创建 task run 记录,调用 active worker 的 `executeTask`,写回状态。
|
||||||
|
- `QqbotPluginTaskManifestSynchronizer`:安装、启用、升级插件时,将 manifest tasks 同步到 `qqbot_plugin_task`。
|
||||||
|
- `QqbotPluginTaskCronValidator`:后端统一校验 5 段 cron,拒绝空值、秒级 6/7 段表达式和过高频表达式。
|
||||||
|
|
||||||
|
调度语义:
|
||||||
|
|
||||||
|
- 插件启用后,平台同步任务记录,并为 enabled task 注册 BullMQ scheduler。
|
||||||
|
- 插件禁用或卸载后,平台暂停或移除该安装实例下的 scheduler。
|
||||||
|
- Admin 修改 cron 后,数据库先更新,再重建 scheduler;失败时回滚数据库更新或返回明确错误。
|
||||||
|
- 任务运行通过 worker RPC `executeTask`,不直接调用插件内部函数。
|
||||||
|
- 同一 `installationId + taskKey` 禁止重叠运行:上一轮未结束时,新一轮标记 `skipped`,不并发进入插件。
|
||||||
|
- 手动运行允许管理员显式触发;即使 schedule disabled,也可手动运行,但插件安装实例必须 enabled。
|
||||||
|
- 首次启用且没有成功运行记录的 enabled task,可触发一次 `bootstrap` run,用于预热缓存,但不阻塞 API 启动。
|
||||||
|
|
||||||
|
运行事件:
|
||||||
|
|
||||||
|
- task start/success/failed/skipped 同步写 `qqbot_plugin_task_run`。
|
||||||
|
- 摘要事件也写入 `qqbot_plugin_runtime_event`,eventType 使用 `task-started`、`task-finished`、`task-failed`、`task-skipped`。
|
||||||
|
- `safe_summary` 只存 taskKey、triggerType、resourceCounts、durationMs、outputKeys 等安全字段,不存大 JSON、不存图片、不存外部响应全文。
|
||||||
|
|
||||||
|
## Worker RPC
|
||||||
|
|
||||||
|
`QqbotPluginWorkerRequestType` 新增 `executeTask`。
|
||||||
|
|
||||||
|
请求字段:
|
||||||
|
|
||||||
|
- `taskId`
|
||||||
|
- `taskKey`
|
||||||
|
- `taskHandlerName`
|
||||||
|
- `triggerType`
|
||||||
|
- `input`
|
||||||
|
- `timeoutMs`
|
||||||
|
- `safeInputSummary`
|
||||||
|
|
||||||
|
worker thread 处理流程:
|
||||||
|
|
||||||
|
1. `load` 阶段创建插件实例。
|
||||||
|
2. `activate` 阶段完成插件基础预热。
|
||||||
|
3. `executeTask` 时按 `handlerName` 找到插件暴露的 task handler。
|
||||||
|
4. handler 返回结构化结果,例如 `{ syncedKeys, updatedFiles, cacheRoot, source }`。
|
||||||
|
5. worker 将错误序列化返回,平台记录 run 和 runtime event。
|
||||||
|
|
||||||
|
插件入口约定:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type QqbotIntegrationPlugin = {
|
||||||
|
tasks?: Array<{
|
||||||
|
key: string;
|
||||||
|
handlerName: string;
|
||||||
|
execute(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
具体类型实现时应落到 plugin-platform contract,避免每个插件自己定义一套。
|
||||||
|
|
||||||
|
## BangDream 同步设计
|
||||||
|
|
||||||
|
新增 task:
|
||||||
|
|
||||||
|
- key:`bangdream.bestdori.sync-main-data`
|
||||||
|
- handlerName:`syncBestdoriMainData`
|
||||||
|
- defaultCron:`0 */6 * * *`
|
||||||
|
- timeoutMs:`120000`
|
||||||
|
- 默认 enabled:`true`
|
||||||
|
|
||||||
|
同步范围只包括 Bestdori JSON 主数据:
|
||||||
|
|
||||||
|
- `songs`
|
||||||
|
- `meta`
|
||||||
|
- `cards`
|
||||||
|
- `skills`
|
||||||
|
- `events`
|
||||||
|
- `gacha`
|
||||||
|
- `costumes`
|
||||||
|
- `bands`
|
||||||
|
- `characters`
|
||||||
|
- `areaItems`
|
||||||
|
- 后续如 operation catalog keys 明确需要,可追加 `items`、`degrees` 等 JSON 集合,但仍不含图片资源。
|
||||||
|
|
||||||
|
同步流程:
|
||||||
|
|
||||||
|
1. 读取 BangDream runtime config:`BANGDREAM_TSUGU_CACHE_ROOT`、Bestdori base url、请求超时和 retry。
|
||||||
|
2. 对目标 JSON key 逐个计算 Bestdori URL 和服务端缓存路径。
|
||||||
|
3. 使用 host HTTP 能力下载 JSON。
|
||||||
|
4. 写入插件服务端缓存目录,采用临时文件 + rename,避免半文件污染缓存。
|
||||||
|
5. 下载失败时保留上一份成功缓存,不删除旧文件。
|
||||||
|
6. 同步完成后刷新 worker 内存 catalog,让后续命令优先使用已同步数据。
|
||||||
|
7. 返回安全摘要:同步 key 数、成功数、失败数、缓存根路径是否存在、耗时。
|
||||||
|
|
||||||
|
缓存路径:
|
||||||
|
|
||||||
|
- 本地默认仍可使用 `.kt-workspace/cache/bangdream`。
|
||||||
|
- 生产应通过 env 配置到持久目录,例如 `BANGDREAM_TSUGU_CACHE_ROOT=/data/qqbot/plugins/bangdream/cache`。
|
||||||
|
- K8s 上需要为该目录配置持久卷或可复用 hostPath,否则重建 Pod 后缓存会丢失,定时同步只能降低运行期冷启动,不能跨发布保留。
|
||||||
|
|
||||||
|
命令 fallback:
|
||||||
|
|
||||||
|
- 若定时同步未完成或失败,BangDream 命令仍保留现有按需拉取逻辑。
|
||||||
|
- 命令读取缓存失败时可以继续请求 Bestdori,不因为预同步失败完全不可用。
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
新增接口均走 Vben 成功/错误响应包装,受 Admin auth 保护。
|
||||||
|
|
||||||
|
- `GET /qqbot/plugin-platform/tasks/page`
|
||||||
|
- 支持 `pageNo/pageSize/pluginId/pluginKey/taskKey/enabled/status`
|
||||||
|
- `GET /qqbot/plugin-platform/tasks/:id`
|
||||||
|
- `POST /qqbot/plugin-platform/tasks/:id/enable`
|
||||||
|
- `POST /qqbot/plugin-platform/tasks/:id/disable`
|
||||||
|
- `POST /qqbot/plugin-platform/tasks/:id/cron`
|
||||||
|
- body:`{ cronExpression: string }`
|
||||||
|
- `POST /qqbot/plugin-platform/tasks/:id/run`
|
||||||
|
- body:`{ input?: Record<string, unknown> }`
|
||||||
|
- `GET /qqbot/plugin-platform/tasks/:id/runs`
|
||||||
|
- 支持 `pageNo/pageSize/status/triggerType/startTime/endTime`
|
||||||
|
|
||||||
|
返回任务列表字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `pluginId`
|
||||||
|
- `pluginKey`
|
||||||
|
- `pluginName`
|
||||||
|
- `installationId`
|
||||||
|
- `taskKey`
|
||||||
|
- `taskName`
|
||||||
|
- `description`
|
||||||
|
- `cronExpression`
|
||||||
|
- `defaultCron`
|
||||||
|
- `enabled`
|
||||||
|
- `runtimeStatus`
|
||||||
|
- `lastStatus`
|
||||||
|
- `lastRunAt`
|
||||||
|
- `lastDurationMs`
|
||||||
|
- `lastError`
|
||||||
|
- `nextRunAt`
|
||||||
|
|
||||||
|
## Admin 页面
|
||||||
|
|
||||||
|
新增 QQBot 下的专门页面:插件定时任务。
|
||||||
|
|
||||||
|
建议路径:
|
||||||
|
|
||||||
|
- route path:`/qqbot/plugin-task`
|
||||||
|
- view root:`apps/web-antdv-next/src/views/qqbot/plugin-task`
|
||||||
|
- API caller:`apps/web-antdv-next/src/api/qqbot/plugin.ts` 扩展 task 方法,或拆 `plugin-task.ts` 并从 qqbot API index 暴露。
|
||||||
|
|
||||||
|
页面能力:
|
||||||
|
|
||||||
|
- KtTable 分页列表。
|
||||||
|
- 筛选:插件、任务 key、启用状态、运行状态。
|
||||||
|
- 操作:启用、停用、修改 cron、手动运行、查看运行记录。
|
||||||
|
- 状态列:enabled、runtimeStatus、lastStatus、lastRunAt、nextRunAt、duration。
|
||||||
|
- 运行记录 Drawer:展示最近运行状态、触发类型、耗时、安全摘要和错误。
|
||||||
|
- Cron 编辑 Modal:使用 `CronEditorAntdvNext`。
|
||||||
|
|
||||||
|
Cron 组件设计:
|
||||||
|
|
||||||
|
- 新增 `components/CronEditorAntdvNext.vue`。
|
||||||
|
- 内部使用 `@vue-js-cron/core` 管理 cron state/校验。
|
||||||
|
- UI 控件全部来自 `antdv-next`,例如 `RadioGroup`、`Select`、`Input`、`Segmented`、`Space`、`Alert`。
|
||||||
|
- 组件只输出标准 5 段 cron 表达式。
|
||||||
|
- Modal 保存前调用后端校验;前端校验只做即时反馈,后端仍是最终真相源。
|
||||||
|
|
||||||
|
依赖策略:
|
||||||
|
|
||||||
|
- 在 Admin workspace catalog 增加 `@vue-js-cron/core`。
|
||||||
|
- `apps/web-antdv-next` 依赖 `@vue-js-cron/core`。
|
||||||
|
- 不添加 `@vue-js-cron/ant`、`ant-design-vue` 或 `vue3-cron-antd`。
|
||||||
|
|
||||||
|
## 权限与菜单
|
||||||
|
|
||||||
|
新增 Admin 权限建议:
|
||||||
|
|
||||||
|
- `QqBot:PluginTask:List`
|
||||||
|
- `QqBot:PluginTask:UpdateCron`
|
||||||
|
- `QqBot:PluginTask:Enable`
|
||||||
|
- `QqBot:PluginTask:Disable`
|
||||||
|
- `QqBot:PluginTask:Run`
|
||||||
|
- `QqBot:PluginTask:RunLog`
|
||||||
|
|
||||||
|
菜单新增在 QQBot 管理分组下,标题“插件定时任务”。现有“插件平台”页继续负责安装、manifest、operation、runtime events 等;新页面只负责 scheduled task 管理。
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- cron 不合法:接口返回可读中文错误,不更新调度器。
|
||||||
|
- 插件未启用:手动运行返回“插件运行时未启用”。
|
||||||
|
- 任务禁用:scheduler 不触发;手动运行仍可触发,但要写 `triggerType=manual`。
|
||||||
|
- 上一轮未结束:新 schedule tick 写 `skipped`,不并发执行。
|
||||||
|
- worker 崩溃:任务 run 标记 failed,平台复用现有 worker recovery 机制;不删除上一次成功缓存。
|
||||||
|
- BullMQ/Redis 不可用:任务页展示 degraded,operation/event 命令仍按现有插件 runtime 能力运行,不因为调度器不可用整体停摆。
|
||||||
|
- BangDream 单个 JSON key 下载失败:记录失败 key,保留旧缓存,整体 run 可标记 failed 或 partial failed;第一版使用 failed,避免虚报成功。
|
||||||
|
|
||||||
|
## 验证计划
|
||||||
|
|
||||||
|
后端 TDD:
|
||||||
|
|
||||||
|
- Manifest parser:解析 `tasks`,拒绝重复 key、非法 cron、缺 handler、缺 timeout、未知权限。
|
||||||
|
- Persistence/schema:`qqbot_plugin_task`、`qqbot_plugin_task_run` 与 full schema SQL 对齐。
|
||||||
|
- Scheduler service:enable/disable/update cron 能同步 BullMQ scheduler,重复同步幂等。
|
||||||
|
- Worker runtime:`executeTask` RPC 进入 worker thread 并按 handlerName 分发。
|
||||||
|
- Platform service:安装/启用/升级插件时同步 task 能力,禁用插件时暂停 scheduler。
|
||||||
|
- BangDream:mock Bestdori JSON,验证 sync task 写入缓存、刷新 catalog、失败保留旧缓存。
|
||||||
|
|
||||||
|
Admin TDD:
|
||||||
|
|
||||||
|
- API caller:任务分页、启停、改 cron、手动运行、运行记录接口路径和参数正确。
|
||||||
|
- Cron component:不依赖 `ant-design-vue`,输出 5 段 cron,非法值展示错误。
|
||||||
|
- 页面状态:列表操作触发对应 caller,运行记录 Drawer 可打开。
|
||||||
|
|
||||||
|
集成验证:
|
||||||
|
|
||||||
|
- 本地 API 启动后真实请求:
|
||||||
|
- `GET /qqbot/plugin-platform/tasks/page`
|
||||||
|
- `POST /qqbot/plugin-platform/tasks/:id/cron`
|
||||||
|
- `POST /qqbot/plugin-platform/tasks/:id/run`
|
||||||
|
- `GET /qqbot/plugin-platform/tasks/:id/runs`
|
||||||
|
- 本地 Admin 页面 route smoke:`/qqbot/plugin-task` 能加载,cron modal 能打开并保存。
|
||||||
|
- BangDream 手动运行 task 后,缓存目录出现目标 JSON 文件,`/查分数表 cn` 或 `/查卡 472` 走热缓存不再每次重新拉全量主数据。
|
||||||
|
- 上线后验证 Jenkins/K8s、Redis/BullMQ、task 手动 run、BangDream 命令 smoke。
|
||||||
|
|
||||||
|
## 实施边界
|
||||||
|
|
||||||
|
实现计划应拆成后端平台能力、BangDream 任务、Admin 页面和上线闭环四段。每段都必须有独立 RED/GREEN 检查,不能先写 UI 再补接口,也不能让 BangDream 直接绕过平台调度。
|
||||||
|
|
||||||
|
本 spec 通过后,下一步进入 Superpowers `writing-plans`。
|
||||||
@ -25,6 +25,7 @@ export type QqbotBullmqWorkerRequestQueueOptions = {
|
|||||||
installationId: string;
|
installationId: string;
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
|
queueWaitTimeoutMs: number;
|
||||||
removeOnFailCount: number;
|
removeOnFailCount: number;
|
||||||
waitUntilFinishedBufferMs: number;
|
waitUntilFinishedBufferMs: number;
|
||||||
workerInstanceId: string;
|
workerInstanceId: string;
|
||||||
@ -48,6 +49,8 @@ type QqbotWorkerQueueResult =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerRequestQueue {
|
export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerRequestQueue {
|
||||||
|
readonly handlesRequestTimeout = true;
|
||||||
|
readonly queueWaitTimeoutMs: number;
|
||||||
private readonly queue: Queue<
|
private readonly queue: Queue<
|
||||||
QqbotWorkerQueueJobData,
|
QqbotWorkerQueueJobData,
|
||||||
QqbotWorkerQueueResult,
|
QqbotWorkerQueueResult,
|
||||||
@ -70,6 +73,7 @@ export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerReq
|
|||||||
private readonly driver: QqbotPluginWorkerDriver,
|
private readonly driver: QqbotPluginWorkerDriver,
|
||||||
private readonly options: QqbotBullmqWorkerRequestQueueOptions,
|
private readonly options: QqbotBullmqWorkerRequestQueueOptions,
|
||||||
) {
|
) {
|
||||||
|
this.queueWaitTimeoutMs = options.queueWaitTimeoutMs;
|
||||||
const queueName = buildWorkerQueueName(
|
const queueName = buildWorkerQueueName(
|
||||||
options.pluginKey,
|
options.pluginKey,
|
||||||
options.installationId,
|
options.installationId,
|
||||||
@ -113,7 +117,7 @@ export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerReq
|
|||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
result: await this.driver.request(job.data.message),
|
result: await this.requestDriverWithTimeout(job.data.message),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof QqbotPluginWorkerResponseError) {
|
if (error instanceof QqbotPluginWorkerResponseError) {
|
||||||
@ -122,6 +126,16 @@ export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerReq
|
|||||||
ok: false,
|
ok: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (error instanceof QqbotPluginWorkerExpiredRequestError) {
|
||||||
|
await this.reset();
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: error.message,
|
||||||
|
name: error.name,
|
||||||
|
},
|
||||||
|
ok: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
await this.reset();
|
await this.reset();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -159,6 +173,7 @@ export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerReq
|
|||||||
expiresAt:
|
expiresAt:
|
||||||
Date.now() +
|
Date.now() +
|
||||||
message.timeoutMs +
|
message.timeoutMs +
|
||||||
|
this.options.queueWaitTimeoutMs +
|
||||||
this.options.waitUntilFinishedBufferMs,
|
this.options.waitUntilFinishedBufferMs,
|
||||||
generation: this.generation,
|
generation: this.generation,
|
||||||
message,
|
message,
|
||||||
@ -173,7 +188,9 @@ export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerReq
|
|||||||
);
|
);
|
||||||
const result = await job.waitUntilFinished(
|
const result = await job.waitUntilFinished(
|
||||||
this.queueEvents,
|
this.queueEvents,
|
||||||
message.timeoutMs + this.options.waitUntilFinishedBufferMs,
|
message.timeoutMs +
|
||||||
|
this.options.queueWaitTimeoutMs +
|
||||||
|
this.options.waitUntilFinishedBufferMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result?.ok === false) {
|
if (result?.ok === false) {
|
||||||
@ -212,6 +229,31 @@ export class QqbotBullmqPluginWorkerRequestQueue implements QqbotPluginWorkerReq
|
|||||||
error.stack,
|
error.stack,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async requestDriverWithTimeout(
|
||||||
|
message: QqbotPluginWorkerRequest,
|
||||||
|
): Promise<unknown> {
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
const requestPromise = this.driver.request(message);
|
||||||
|
requestPromise.catch(() => undefined);
|
||||||
|
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
reject(
|
||||||
|
new QqbotPluginWorkerExpiredRequestError(
|
||||||
|
'worker-request-execution-timeout',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, message.timeoutMs);
|
||||||
|
timer.unref?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await Promise.race([requestPromise, timeoutPromise]);
|
||||||
|
} finally {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createQqbotBullmqWorkerQueueOptions(
|
export function createQqbotBullmqWorkerQueueOptions(
|
||||||
@ -224,6 +266,11 @@ export function createQqbotBullmqWorkerQueueOptions(
|
|||||||
installationId,
|
installationId,
|
||||||
pluginKey,
|
pluginKey,
|
||||||
prefix: resolveQqbotPluginQueuePrefix(configService),
|
prefix: resolveQqbotPluginQueuePrefix(configService),
|
||||||
|
queueWaitTimeoutMs: readNumberConfig(
|
||||||
|
configService,
|
||||||
|
['QQBOT_PLUGIN_QUEUE_WAIT_TIMEOUT_MS'],
|
||||||
|
120_000,
|
||||||
|
),
|
||||||
removeOnFailCount: readNumberConfig(
|
removeOnFailCount: readNumberConfig(
|
||||||
configService,
|
configService,
|
||||||
['QQBOT_PLUGIN_QUEUE_REMOVE_ON_FAIL'],
|
['QQBOT_PLUGIN_QUEUE_REMOVE_ON_FAIL'],
|
||||||
|
|||||||
@ -188,7 +188,11 @@ export class QqbotPluginWorkerRuntime {
|
|||||||
);
|
);
|
||||||
requestPromise.catch(() => undefined);
|
requestPromise.catch(() => undefined);
|
||||||
|
|
||||||
const timeout = this.createTimeoutPromise(type, message, timeoutMs);
|
const timeout = this.createTimeoutPromise(
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
this.getRequestTimeoutMs(timeoutMs),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await Promise.race([requestPromise, timeout.promise]);
|
return await Promise.race([requestPromise, timeout.promise]);
|
||||||
@ -287,6 +291,13 @@ export class QqbotPluginWorkerRuntime {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getRequestTimeoutMs(timeoutMs: number) {
|
||||||
|
if (!this.requestQueue.handlesRequestTimeout) return timeoutMs;
|
||||||
|
|
||||||
|
const queueWaitTimeoutMs = Number(this.requestQueue.queueWaitTimeoutMs || 0);
|
||||||
|
return timeoutMs + Math.max(0, queueWaitTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
private async recoverIfNeeded(triggerType: QqbotPluginWorkerRequestType) {
|
private async recoverIfNeeded(triggerType: QqbotPluginWorkerRequestType) {
|
||||||
if (this.status !== 'failed' || !this.manifestForRecovery) return;
|
if (this.status !== 'failed' || !this.manifestForRecovery) return;
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export type QqbotPluginWorkerDriver = {
|
|||||||
|
|
||||||
export type QqbotPluginWorkerRequestQueue = {
|
export type QqbotPluginWorkerRequestQueue = {
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
|
handlesRequestTimeout?: boolean;
|
||||||
|
queueWaitTimeoutMs?: number;
|
||||||
request(message: QqbotPluginWorkerRequest): Promise<unknown>;
|
request(message: QqbotPluginWorkerRequest): Promise<unknown>;
|
||||||
reset(): Promise<void>;
|
reset(): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -74,6 +74,7 @@ describe('QQBot BullMQ plugin worker request queue', () => {
|
|||||||
installationId: 'install:1',
|
installationId: 'install:1',
|
||||||
pluginKey: 'bang:dream',
|
pluginKey: 'bang:dream',
|
||||||
prefix: 'kt:qqbot:plugin-worker',
|
prefix: 'kt:qqbot:plugin-worker',
|
||||||
|
queueWaitTimeoutMs: 120_000,
|
||||||
removeOnFailCount: 100,
|
removeOnFailCount: 100,
|
||||||
waitUntilFinishedBufferMs: 5_000,
|
waitUntilFinishedBufferMs: 5_000,
|
||||||
workerInstanceId: 'worker-1',
|
workerInstanceId: 'worker-1',
|
||||||
@ -95,5 +96,7 @@ describe('QQBot BullMQ plugin worker request queue', () => {
|
|||||||
(creation) => creation.options.prefix === 'kt:qqbot:plugin-worker',
|
(creation) => creation.options.prefix === 'kt:qqbot:plugin-worker',
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(queue.handlesRequestTimeout).toBe(true);
|
||||||
|
expect(queue.queueWaitTimeoutMs).toBe(120_000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -106,6 +106,11 @@ class GenerationAwareRequestQueue implements QqbotPluginWorkerRequestQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TimeoutAwareRecordingRequestQueue extends RecordingRequestQueue {
|
||||||
|
readonly handlesRequestTimeout = true;
|
||||||
|
readonly queueWaitTimeoutMs = 50;
|
||||||
|
}
|
||||||
|
|
||||||
const createRuntime = (driver = new RecordingDriver()) => {
|
const createRuntime = (driver = new RecordingDriver()) => {
|
||||||
const runtime = new QqbotPluginWorkerRuntime(new RecordingRequestQueue(driver), {
|
const runtime = new QqbotPluginWorkerRuntime(new RecordingRequestQueue(driver), {
|
||||||
defaultTimeoutMs: 50,
|
defaultTimeoutMs: 50,
|
||||||
@ -179,6 +184,49 @@ describe('QQBot plugin worker runtime', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not count queue wait time against a queued operation execution timeout', async () => {
|
||||||
|
const requestOrder: string[] = [];
|
||||||
|
const driver: QqbotPluginWorkerDriver = {
|
||||||
|
dispose: jest.fn(async () => undefined),
|
||||||
|
request: jest.fn(async (message) => {
|
||||||
|
requestOrder.push(message.operationId || message.type);
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, message.operationId === 'op-1' ? 20 : 15),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
operationId: message.operationId,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const runtime = new QqbotPluginWorkerRuntime(
|
||||||
|
new TimeoutAwareRecordingRequestQueue(driver),
|
||||||
|
{
|
||||||
|
defaultTimeoutMs: 30,
|
||||||
|
installationId: 'install-queue-wait',
|
||||||
|
pluginKey: 'demo-plugin',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const first = runtime.executeOperation({
|
||||||
|
input: { text: 'first' },
|
||||||
|
operationId: 'op-1',
|
||||||
|
operationKey: 'demo-plugin.echo',
|
||||||
|
});
|
||||||
|
const second = runtime.executeOperation({
|
||||||
|
input: { text: 'second' },
|
||||||
|
operationId: 'op-2',
|
||||||
|
operationKey: 'demo-plugin.echo',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(Promise.all([first, second])).resolves.toEqual([
|
||||||
|
{ ok: true, operationId: 'op-1' },
|
||||||
|
{ ok: true, operationId: 'op-2' },
|
||||||
|
]);
|
||||||
|
expect(requestOrder).toEqual(['op-1', 'op-2']);
|
||||||
|
expect(driver.dispose).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('recovers a failed worker by reloading and activating it before the next request', async () => {
|
it('recovers a failed worker by reloading and activating it before the next request', async () => {
|
||||||
const requestTypes: string[] = [];
|
const requestTypes: string[] = [];
|
||||||
const driver: QqbotPluginWorkerDriver = {
|
const driver: QqbotPluginWorkerDriver = {
|
||||||
@ -596,6 +644,7 @@ describe('QQBot plugin worker queue config', () => {
|
|||||||
installationId: 'install-1',
|
installationId: 'install-1',
|
||||||
pluginKey: 'bangdream',
|
pluginKey: 'bangdream',
|
||||||
prefix: 'kt:qqbot:plugin-worker',
|
prefix: 'kt:qqbot:plugin-worker',
|
||||||
|
queueWaitTimeoutMs: 120_000,
|
||||||
removeOnFailCount: 100,
|
removeOnFailCount: 100,
|
||||||
waitUntilFinishedBufferMs: 5_000,
|
waitUntilFinishedBufferMs: 5_000,
|
||||||
workerInstanceId: expect.any(String),
|
workerInstanceId: expect.any(String),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user