fix(qqbot): 修复运行态审查问题
This commit is contained in:
parent
d3c0affa33
commit
2d273994f6
@ -66,7 +66,7 @@ QQBOT_NAPCAT_SSH_PORT=
|
||||
QQBOT_NAPCAT_SSH_KEY_PATH=
|
||||
QQBOT_NAPCAT_SSH_TIMEOUT_MS=120000
|
||||
QQBOT_NAPCAT_ROOT=/vol1/docker/kt-qqbot/napcat-instances
|
||||
QQBOT_NAPCAT_IMAGE=mlikiowa/napcat-docker:latest
|
||||
QQBOT_NAPCAT_IMAGE=
|
||||
QQBOT_NAPCAT_CONTAINER_PREFIX=kt-qqbot-napcat
|
||||
QQBOT_NAPCAT_HOST=192.168.31.224
|
||||
QQBOT_NAPCAT_PORT_START=6100
|
||||
|
||||
2
API.md
2
API.md
@ -316,7 +316,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
扫码链路返回 `sessionId`,前端应使用 SSE 查看步骤进度,而不是等待长 HTTP 请求完成。
|
||||
|
||||
同一 QQ 账号只保留一个有效 NapCat 主容器。扫码后如果已有账号绑定到新容器,后端会释放旧绑定和未共享的旧容器,避免同账号多实例互相挤下线。NapCat 上报下线 notice 时,账号会被标记为离线,并把下线原因写入 `lastError`;后续无错误的普通断连只更新离线状态,不清空该原因。账号列表会按近期缓存检查绑定 NapCat 容器的最新登录状态日志,日志检测默认 5 秒超时;`isOnline:false` 属于离线信号;如果账号连接时间或心跳晚于容器检测时间,以账号在线态为准;如果最新日志状态是在线,会清空容器旧离线错误;如果最新状态是离线或被踢下线,会同步回写账号离线状态并生成 `qqbot.account.offline` 站内信。
|
||||
同一 QQ 账号只保留一个有效 NapCat 主容器。扫码后如果已有账号绑定到新容器,后端会释放旧绑定和未共享的旧容器,避免同账号多实例互相挤下线。OneBot notice 只有机器人下线、登录失效、`KickedOffLine` 等账号级信号才会把账号标记为离线,普通群成员 kick 不属于账号离线信号。下线原因写入 `lastError` 前按 `last_error` 500 字符列宽截断;后续无错误的普通断连只更新离线状态,不清空该原因。账号列表会按近期缓存检查绑定 NapCat 容器的最新登录状态日志,日志检测默认 5 秒超时;`isOnline:false` 属于离线信号;如果账号连接时间或心跳晚于容器检测时间,以账号在线态为准;如果最新日志状态是在线,会清空容器旧离线错误;如果最新状态是离线或被踢下线,会同步回写账号离线状态并生成 `qqbot.account.offline` 站内信。托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像。
|
||||
|
||||
### Command / Rule / Permission
|
||||
|
||||
|
||||
@ -132,7 +132,8 @@ pnpm exec jest --runInBand --runTestsByPath test/path/to/file.spec.ts
|
||||
- 系统日志由 pino 输出,Loki 查询统一通过后端 `/system/logs/*` 代理,前端不直连 Loki。
|
||||
- 日志级站内信只承接运行期事件:接口 5xx、QQBot 下线 notice、NapCat 容器最新离线日志会自动聚合通知 `super` 角色;服务端强制 `super` 访问,Admin 不再暴露人工新增/编辑入口;长路径接口错误会压缩 `dedupeKey/title` 到表字段长度内,避免通知入库失败。
|
||||
- QQBot 扫码登录通过 SSE `/qqbot/account/scan/events` 暴露进度,耗时链路不应阻塞普通 HTTP 响应。
|
||||
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,后续无错误的普通断连不能清空该原因;账号列表日志检测带近期缓存和短超时,账号连接时间或心跳晚于容器检测时间时以账号在线态为准,最新日志为在线时清空容器旧离线错误。
|
||||
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表日志检测带近期缓存和短超时,账号连接时间或心跳晚于容器检测时间时以账号在线态为准,最新日志为在线时清空容器旧离线错误。
|
||||
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。
|
||||
- BangDream 当前源码根目录是 `src/qqbot/plugins/bangDream`;不要恢复旧 `tsugu` 层级或旧大桶目录。
|
||||
- BangDream 在线命令以 `registry/operation-registry.ts` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 registry/command-SQL 测试。
|
||||
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
|
||||
|
||||
@ -260,6 +260,231 @@ CREATE TABLE IF NOT EXISTS `qqbot_dedupe` (
|
||||
UNIQUE KEY `uk_qqbot_dedupe_event_key` (`event_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_account` ADD COLUMN `connection_mode` varchar(32) NOT NULL DEFAULT ''reverse-ws''',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_account'
|
||||
AND column_name = 'connection_mode'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_account` ADD COLUMN `access_token` varchar(255) DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_account'
|
||||
AND column_name = 'access_token'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_account` ADD COLUMN `client_role` varchar(32) DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_account'
|
||||
AND column_name = 'client_role'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_account` ADD COLUMN `last_connected_at` datetime DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_account'
|
||||
AND column_name = 'last_connected_at'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_account` ADD COLUMN `last_heartbeat_at` datetime DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_account'
|
||||
AND column_name = 'last_heartbeat_at'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_account` ADD COLUMN `last_error` varchar(500) DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_account'
|
||||
AND column_name = 'last_error'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `webui_port` int DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'webui_port'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `webui_token` varchar(255) DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'webui_token'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `image` varchar(255) NOT NULL DEFAULT ''''',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'image'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `data_dir` varchar(500) NOT NULL DEFAULT ''''',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'data_dir'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `reverse_ws_url` varchar(500) NOT NULL DEFAULT ''''',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'reverse_ws_url'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `status` varchar(32) NOT NULL DEFAULT ''creating''',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'status'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `last_started_at` datetime DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'last_started_at'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `last_checked_at` datetime DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'last_checked_at'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE `qqbot_napcat_container` ADD COLUMN `last_error` varchar(500) DEFAULT NULL',
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'qqbot_napcat_container'
|
||||
AND column_name = 'last_error'
|
||||
);
|
||||
PREPARE qqbot_stmt FROM @qqbot_sql;
|
||||
EXECUTE qqbot_stmt;
|
||||
DEALLOCATE PREPARE qqbot_stmt;
|
||||
|
||||
SET @qqbot_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) > 0,
|
||||
|
||||
@ -346,7 +346,9 @@ export class QqbotAccountService {
|
||||
connectStatus: 'offline',
|
||||
};
|
||||
if (lastError !== undefined) {
|
||||
payload.lastError = lastError || null;
|
||||
payload.lastError = lastError
|
||||
? this.toolsService.toColumnText(lastError, 500)
|
||||
: null;
|
||||
}
|
||||
await this.accountRepository.update({ selfId }, payload);
|
||||
}
|
||||
@ -428,7 +430,11 @@ export class QqbotAccountService {
|
||||
|
||||
const cachedOfflineReason = this.getFreshCachedOfflineReason(container);
|
||||
if (cachedOfflineReason) {
|
||||
await this.applyNapcatOfflineState(account, container, cachedOfflineReason);
|
||||
await this.applyNapcatOfflineState(
|
||||
account,
|
||||
container,
|
||||
cachedOfflineReason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.isFreshRuntimeCheck(container.lastCheckedAt)) return;
|
||||
@ -457,7 +463,9 @@ export class QqbotAccountService {
|
||||
private getFreshCachedOfflineReason(container: QqbotNapcatContainer) {
|
||||
if (!this.isFreshRuntimeCheck(container.lastCheckedAt)) return null;
|
||||
const reason = this.toolsService.toTrimmedString(container.lastError);
|
||||
return this.toolsService.isNapcatOfflineLoginMessage(reason) ? reason : null;
|
||||
return this.toolsService.isNapcatOfflineLoginMessage(reason)
|
||||
? reason
|
||||
: null;
|
||||
}
|
||||
|
||||
private isAccountStateNewerThanRuntimeCheck(
|
||||
|
||||
@ -29,8 +29,8 @@ export function normalizeOneBotMessage(
|
||||
messageType === 'group'
|
||||
? groupId || ''
|
||||
: messageType === 'channel'
|
||||
? channelId || ''
|
||||
: userId;
|
||||
? channelId || ''
|
||||
: userId;
|
||||
const messageText = extractMessageText(payload);
|
||||
|
||||
return {
|
||||
@ -84,7 +84,15 @@ export function getOneBotOfflineReason(payload: QqbotOneBotEvent) {
|
||||
.join(' ')
|
||||
.trim();
|
||||
const probe = `${noticeType} ${subType} ${content}`;
|
||||
if (!/offline|kick|KickedOffLine|下线|离线|登录已失效/i.test(probe)) {
|
||||
const isBotOfflineNotice =
|
||||
['bot_offline', 'bot_self_offline', 'bot_login_expired'].includes(
|
||||
noticeType,
|
||||
) ||
|
||||
['kick_offline', 'kicked_offline', 'login_expired'].includes(subType) ||
|
||||
/KickedOffLine|下线通知|账号状态变更为离线|登录已失效|登录态失效|另一台终端/i.test(
|
||||
probe,
|
||||
);
|
||||
if (!isBotOfflineNotice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -231,7 +231,14 @@ export class QqbotNapcatContainerService {
|
||||
{
|
||||
lastCheckedAt: new Date(),
|
||||
...(loginState.state === 'offline'
|
||||
? { lastError: loginState.offlineReason }
|
||||
? {
|
||||
lastError: loginState.offlineReason
|
||||
? this.toolsService.toColumnText(
|
||||
loginState.offlineReason,
|
||||
500,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
: {}),
|
||||
...(loginState.state === 'online' ? { lastError: null } : {}),
|
||||
},
|
||||
@ -242,7 +249,10 @@ export class QqbotNapcatContainerService {
|
||||
{ id: container.id },
|
||||
{
|
||||
lastCheckedAt: new Date(),
|
||||
lastError: this.toolsService.getErrorMessage(err),
|
||||
lastError: this.toolsService.toColumnText(
|
||||
this.toolsService.getErrorMessage(err),
|
||||
500,
|
||||
),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
@ -463,13 +473,13 @@ docker logs --tail 300 "$NAME" 2>&1 || true
|
||||
throwVbenError('当前仅支持通过 SSH 创建 NapCat 容器');
|
||||
}
|
||||
|
||||
const image = this.getConfig('QQBOT_NAPCAT_IMAGE');
|
||||
if (!image) {
|
||||
throwVbenError('NapCat 镜像未配置,请先设置 QQBOT_NAPCAT_IMAGE');
|
||||
}
|
||||
const port = await this.allocatePort();
|
||||
const name = this.buildContainerName(selfId);
|
||||
const token = randomBytes(24).toString('hex');
|
||||
const image = this.getConfig(
|
||||
'QQBOT_NAPCAT_IMAGE',
|
||||
'mlikiowa/napcat-docker:latest',
|
||||
);
|
||||
const dataDir = `${this.getRootDir()}/${name}`;
|
||||
const baseUrl = this.buildBaseUrl(port);
|
||||
const reverseWsUrl = this.buildReverseWsUrl();
|
||||
@ -519,7 +529,7 @@ docker logs --tail 300 "$NAME" 2>&1 || true
|
||||
await this.containerRepository.update(
|
||||
{ id: container.id },
|
||||
{
|
||||
lastError: message,
|
||||
lastError: this.toolsService.toColumnText(message, 500),
|
||||
status: 'error',
|
||||
},
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ describe('QqbotAccountService', () => {
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
);
|
||||
|
||||
await service.markOffline('1914728559');
|
||||
@ -25,6 +25,30 @@ describe('QqbotAccountService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates offline reason before writing lastError column', async () => {
|
||||
const accountRepository = {
|
||||
update: jest.fn(),
|
||||
};
|
||||
const service = new QqbotAccountService(
|
||||
accountRepository as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
);
|
||||
|
||||
await service.markOffline('1914728559', '错误'.repeat(300));
|
||||
|
||||
expect(accountRepository.update).toHaveBeenCalledWith(
|
||||
{ selfId: '1914728559' },
|
||||
{
|
||||
connectStatus: 'offline',
|
||||
lastError: `${'错误'.repeat(248)}错...`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('syncs NapCat runtime offline logs back to account list status', async () => {
|
||||
const account = {
|
||||
connectStatus: 'online',
|
||||
|
||||
@ -27,7 +27,8 @@ describe('QqbotEventService', () => {
|
||||
);
|
||||
|
||||
const payload = {
|
||||
message: '[KickedOffLine] [下线通知] 你的帐号当前登录已失效,请重新登录。',
|
||||
message:
|
||||
'[KickedOffLine] [下线通知] 你的帐号当前登录已失效,请重新登录。',
|
||||
notice_type: 'bot_offline',
|
||||
post_type: 'notice',
|
||||
self_id: 1914728559,
|
||||
@ -42,7 +43,8 @@ describe('QqbotEventService', () => {
|
||||
);
|
||||
expect(systemNoticePublisher.publishSystemNotice).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: 'bot_offline/kick_offline:你的帐号当前登录已失效,请重新登录。',
|
||||
content:
|
||||
'bot_offline/kick_offline:你的帐号当前登录已失效,请重新登录。',
|
||||
dedupeKey: 'qqbot:offline:1914728559',
|
||||
eventType: 'qqbot.account.offline',
|
||||
metadata: expect.objectContaining({
|
||||
@ -52,9 +54,44 @@ describe('QqbotEventService', () => {
|
||||
notifyRoleCode: 'super',
|
||||
severity: 'error',
|
||||
source: 'qqbot',
|
||||
summary: 'bot_offline/kick_offline:你的帐号当前登录已失效,请重新登录。',
|
||||
summary:
|
||||
'bot_offline/kick_offline:你的帐号当前登录已失效,请重新登录。',
|
||||
title: 'QQBot 账号已下线:1914728559',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores normal group kick notices because they are not bot offline signals', async () => {
|
||||
const accountService = {
|
||||
markOffline: jest.fn(),
|
||||
};
|
||||
const systemNoticePublisher = {
|
||||
publishSystemNotice: jest.fn(),
|
||||
};
|
||||
const service = new (QqbotEventService as any)(
|
||||
{ publish: jest.fn() } as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{
|
||||
getErrorMessage: (error: unknown) =>
|
||||
error instanceof Error ? error.message : `${error}`,
|
||||
toStringId: (value: unknown) => `${value || ''}`,
|
||||
} as any,
|
||||
accountService as any,
|
||||
systemNoticePublisher,
|
||||
);
|
||||
|
||||
await service.handleIncoming({
|
||||
notice_type: 'group_decrease',
|
||||
operator_id: 12345,
|
||||
post_type: 'notice',
|
||||
self_id: 1914728559,
|
||||
sub_type: 'kick',
|
||||
user_id: 67890,
|
||||
});
|
||||
|
||||
expect(accountService.markOffline).not.toHaveBeenCalled();
|
||||
expect(systemNoticePublisher.publishSystemNotice).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -209,4 +209,101 @@ describe('QqbotNapcatContainerService', () => {
|
||||
5000,
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates runtime offline reason before writing container lastError', async () => {
|
||||
const containerRepository = {
|
||||
update: jest.fn(),
|
||||
};
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_CONTAINER_MODE: 'ssh',
|
||||
QQBOT_NAPCAT_SSH_TARGET: 'nas',
|
||||
};
|
||||
return values[key] || '';
|
||||
}),
|
||||
} as any,
|
||||
containerRepository as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
service.runProcess = jest.fn().mockResolvedValue({
|
||||
stderr: '',
|
||||
stdout: `06-11 08:39:19 [info] Mirror | [KickedOffLine] [下线通知] ${'错误'.repeat(300)}`,
|
||||
});
|
||||
|
||||
await service.detectRuntimeOffline({
|
||||
id: 'container-1',
|
||||
name: 'kt-qqbot-napcat-1914728559',
|
||||
});
|
||||
|
||||
expect(containerRepository.update).toHaveBeenCalledWith(
|
||||
{ id: 'container-1' },
|
||||
expect.objectContaining({
|
||||
lastError: `${'错误'.repeat(248)}错...`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates runtime check errors before writing container lastError', async () => {
|
||||
const containerRepository = {
|
||||
update: jest.fn(),
|
||||
};
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_CONTAINER_MODE: 'ssh',
|
||||
QQBOT_NAPCAT_SSH_TARGET: 'nas',
|
||||
};
|
||||
return values[key] || '';
|
||||
}),
|
||||
} as any,
|
||||
containerRepository as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
service.runProcess = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('错误'.repeat(300)));
|
||||
|
||||
await service.detectRuntimeOffline({
|
||||
id: 'container-1',
|
||||
name: 'kt-qqbot-napcat-1914728559',
|
||||
});
|
||||
|
||||
expect(containerRepository.update).toHaveBeenCalledWith(
|
||||
{ id: 'container-1' },
|
||||
expect.objectContaining({
|
||||
lastError: `${'错误'.repeat(248)}错...`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('requires an explicit NapCat image when creating a managed container', async () => {
|
||||
const containerRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
save: jest.fn(),
|
||||
};
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{
|
||||
get: jest.fn((key: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_CONTAINER_MODE: 'ssh',
|
||||
QQBOT_NAPCAT_SSH_TARGET: 'nas',
|
||||
};
|
||||
return values[key] || '';
|
||||
}),
|
||||
} as any,
|
||||
containerRepository as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
|
||||
await expect(service.createManagedContainer('1914728559')).rejects.toThrow(
|
||||
'NapCat 镜像未配置,请先设置 QQBOT_NAPCAT_IMAGE',
|
||||
);
|
||||
expect(containerRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user