feat: NapCat 启用 -q 快速登录与离线看门狗
- 容器为已知 selfId 创建/重建时注入 ACCOUNT 环境变量启用 NapCat -q 快速登录, 重启可从持久化会话免扫码自动重登;旧容器在「更新登录」原地重建补齐 ACCOUNT (docker inspect 已带则跳过、失败非阻断、保留 QQ 数据卷)。 - 新增 QqbotNapcatWatchdogService 定时巡检在线账号,复用既有 super 站内信离线告警, 仅检测告警不重建容器,避免与 NapCat 自身重连竞争造成设备登录抖动。 - 新增 QQBOT_NAPCAT_WATCHDOG_ENABLED/INTERVAL_MS 配置及 README/.env.example 说明。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4d8e8c3bbc
commit
bb6d0263e7
@ -80,6 +80,10 @@ QQBOT_NAPCAT_PORT_START=6100
|
||||
QQBOT_NAPCAT_PORT_END=6199
|
||||
QQBOT_NAPCAT_BASE_URL_TEMPLATE=http://192.168.31.224:{port}
|
||||
QQBOT_NAPCAT_REVERSE_WS_BASE=ws://192.168.31.224:48085/qqbot/onebot/reverse
|
||||
# 离线看门狗:定时巡检在线账号,掉线/被踢及时触发站内信告警(true/false,默认开启)
|
||||
QQBOT_NAPCAT_WATCHDOG_ENABLED=true
|
||||
# 看门狗巡检间隔毫秒,最小 30000,默认 120000
|
||||
QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS=120000
|
||||
|
||||
BANGDREAM_TSUGU_MAIN_SERVER=cn
|
||||
BANGDREAM_TSUGU_DISPLAYED_SERVERS=cn,jp
|
||||
|
||||
@ -137,6 +137,8 @@ pnpm exec jest --runInBand --runTestsByPath test/path/to/file.spec.ts
|
||||
- QQBot 复读机默认阈值为 4,同一会话默认 10 分钟只复读一次,默认只复读 120 字以内普通文本,避免群聊重复内容导致机器人过于频繁地模拟真人发言。
|
||||
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表日志检测带近期缓存和短超时,账号连接时间或心跳晚于容器检测时间时以账号在线态为准,最新日志为在线时清空容器旧离线错误。
|
||||
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。
|
||||
- NapCat 容器为已知 `selfId` 创建/重建时会注入 `ACCOUNT` 环境变量启用 `-q` 快速登录:容器重启(崩溃/重启策略/宿主重启)能从持久化会话免扫码自动重登;硬踢 `登录已失效` 会话作废仍需扫码。已绑定但缺少 `ACCOUNT` 的旧容器在下一次「更新登录」时原地重建一次补齐(保留 QQ 数据卷),`docker inspect` 已带 `ACCOUNT` 则跳过,重建失败不阻断登录。
|
||||
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000`,`QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,复用既有离线检测与 `super` 站内信告警,使掉线/被踢无需管理员打开列表页即可及时发现;看门狗只做检测告警、不重建容器,避免与 NapCat 自身重连竞争产生设备登录抖动。
|
||||
- BangDream 当前源码根目录是 `src/qqbot/plugins/bangDream`;不要恢复旧 `tsugu` 层级或旧大桶目录。
|
||||
- BangDream 在线命令以 `registry/operation-registry.ts` 为单一来源,新增命令必须同步 SQL/在线命令表并跑 registry/command-SQL 测试。
|
||||
- BangDream event stage 大图必须保持分页拆图行为,线上 smoke 关注 `imageCount=5`,避免大 canvas OOM 回归。
|
||||
|
||||
@ -341,6 +341,25 @@ export class QqbotAccountService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 看门狗:主动巡检在线的已绑定账号,复用既有离线检测 + 站内信告警逻辑,
|
||||
* 让掉线/被踢能被及时发现并通知超管,而不必等管理员打开账号列表页。
|
||||
* 仅做检测与告警,不做容器重建(避免与 NapCat 自身重连竞争、产生设备登录抖动)。
|
||||
*/
|
||||
async runOfflineWatchdog(): Promise<{ checked: number }> {
|
||||
const accounts = await this.accountRepository.find({
|
||||
where: {
|
||||
connectStatus: 'online',
|
||||
enabled: true,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (accounts.length <= 0) return { checked: 0 };
|
||||
|
||||
await this.appendNapcatRuntime(accounts);
|
||||
return { checked: accounts.length };
|
||||
}
|
||||
|
||||
async markOffline(selfId: string, lastError?: string) {
|
||||
const payload: Partial<QqbotAccount> = {
|
||||
connectStatus: 'offline',
|
||||
|
||||
82
src/qqbot/account/qqbot-napcat-watchdog.service.ts
Normal file
82
src/qqbot/account/qqbot-napcat-watchdog.service.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleDestroy,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { QqbotAccountService } from './qqbot-account.service';
|
||||
|
||||
const DEFAULT_INTERVAL_MS = 120_000;
|
||||
const MIN_INTERVAL_MS = 30_000;
|
||||
|
||||
/**
|
||||
* NapCat 离线看门狗:定时主动巡检在线账号,及时发现掉线/被踢并触发既有站内信告警。
|
||||
* 不依赖 @nestjs/schedule,采用 OnModuleInit + setInterval(与代码库既有定时器一致)。
|
||||
* 仅检测 + 告警,自动重登由容器 ACCOUNT 快速登录(-q)+ docker restart 策略负责。
|
||||
*/
|
||||
@Injectable()
|
||||
export class QqbotNapcatWatchdogService
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
private readonly logger = new Logger(QqbotNapcatWatchdogService.name);
|
||||
private timer?: ReturnType<typeof setInterval>;
|
||||
private running = false;
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly accountService: QqbotAccountService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
const intervalMs = this.getIntervalMs();
|
||||
this.timer = setInterval(() => void this.tick(), intervalMs);
|
||||
this.timer.unref?.();
|
||||
this.logger.log(`NapCat 离线看门狗已启用,巡检间隔 ${intervalMs}ms`);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async tick() {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
try {
|
||||
await this.accountService.runOfflineWatchdog();
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`NapCat 离线看门狗巡检失败:${
|
||||
err instanceof Error ? err.message : `${err}`
|
||||
}`,
|
||||
);
|
||||
} finally {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private isEnabled() {
|
||||
const value = `${
|
||||
this.configService.get<string>('QQBOT_NAPCAT_WATCHDOG_ENABLED') ?? 'true'
|
||||
}`
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return value !== 'false' && value !== '0' && value !== 'off';
|
||||
}
|
||||
|
||||
private getIntervalMs() {
|
||||
const value = Number(
|
||||
this.configService.get<string>('QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS') ||
|
||||
DEFAULT_INTERVAL_MS,
|
||||
);
|
||||
if (!Number.isFinite(value) || value < MIN_INTERVAL_MS) {
|
||||
return DEFAULT_INTERVAL_MS;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -42,11 +42,92 @@ export class QqbotNapcatContainerService {
|
||||
}
|
||||
|
||||
const existing = await this.getPrimaryRuntime(account.id);
|
||||
if (existing) return existing;
|
||||
if (existing) {
|
||||
await this.ensureRuntimeQuickLogin(existing, account.selfId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
return this.createManagedContainer(account.selfId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 让已绑定账号的容器带上 ACCOUNT 环境变量(NapCat 的 -q 快速登录)。
|
||||
* 仅在 ssh 托管模式下原地重建容器:保留 QQ 数据卷,因此随后的容器重启
|
||||
* 能从持久化会话免扫码自动重登。硬踢(登录已失效)时会话作废,仍需扫码。
|
||||
*/
|
||||
async ensureRuntimeQuickLogin(runtime: QqbotNapcatRuntime, selfId?: string) {
|
||||
const account = `${selfId || ''}`.trim();
|
||||
if (!account || this.getManagedMode() !== 'ssh' || !runtime.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const container = await this.containerRepository
|
||||
.createQueryBuilder('container')
|
||||
.addSelect('container.webuiToken')
|
||||
.where('container.id = :containerId', { containerId: runtime.id })
|
||||
.andWhere('container.isDeleted = :isDeleted', { isDeleted: false })
|
||||
.getOne();
|
||||
if (
|
||||
!container ||
|
||||
!container.name ||
|
||||
!container.webuiPort ||
|
||||
!container.webuiToken
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已带 ACCOUNT(或无法确认)则跳过,避免重复重建:只在首次缺失时原地重建一次。
|
||||
if (await this.runtimeHasAccountEnv(container.name)) return false;
|
||||
|
||||
try {
|
||||
await this.createRemoteDockerContainer({
|
||||
account,
|
||||
dataDir: container.dataDir || `${this.getRootDir()}/${container.name}`,
|
||||
image: container.image,
|
||||
name: container.name,
|
||||
port: container.webuiPort,
|
||||
reverseWsUrl: container.reverseWsUrl || this.buildReverseWsUrl(),
|
||||
skipPull: true,
|
||||
token: container.webuiToken,
|
||||
});
|
||||
await this.containerRepository.update(
|
||||
{ id: container.id },
|
||||
{
|
||||
lastError: null,
|
||||
lastStartedAt: new Date(),
|
||||
status: 'running',
|
||||
},
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
// 重建失败不阻断登录流程:保持原容器继续走扫码登录。
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async runtimeHasAccountEnv(name: string) {
|
||||
try {
|
||||
const result = await this.runProcess(
|
||||
'ssh',
|
||||
[
|
||||
...this.getSshArgs(),
|
||||
'docker',
|
||||
'inspect',
|
||||
'--format',
|
||||
'{{range .Config.Env}}{{println .}}{{end}}',
|
||||
name,
|
||||
],
|
||||
'',
|
||||
undefined,
|
||||
this.getRuntimeCheckTimeoutMs(),
|
||||
);
|
||||
return /^ACCOUNT=\S/m.test(result.stdout);
|
||||
} catch {
|
||||
// 无法确认时按“已存在”处理,宁可不重建也不误删运行中的容器。
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async findRuntimeById(containerId?: string) {
|
||||
if (!containerId) return this.getLegacyRuntime();
|
||||
|
||||
@ -502,6 +583,7 @@ docker logs --tail 300 "$NAME" 2>&1 || true
|
||||
|
||||
try {
|
||||
await this.createRemoteDockerContainer({
|
||||
account: selfId,
|
||||
dataDir,
|
||||
image,
|
||||
name,
|
||||
@ -538,11 +620,13 @@ docker logs --tail 300 "$NAME" 2>&1 || true
|
||||
}
|
||||
|
||||
private async createRemoteDockerContainer(input: {
|
||||
account?: string;
|
||||
dataDir: string;
|
||||
image: string;
|
||||
name: string;
|
||||
port: number;
|
||||
reverseWsUrl: string;
|
||||
skipPull?: boolean;
|
||||
token: string;
|
||||
}) {
|
||||
const script = this.buildRemoteCreateScript(input);
|
||||
@ -550,11 +634,13 @@ docker logs --tail 300 "$NAME" 2>&1 || true
|
||||
}
|
||||
|
||||
private buildRemoteCreateScript(input: {
|
||||
account?: string;
|
||||
dataDir: string;
|
||||
image: string;
|
||||
name: string;
|
||||
port: number;
|
||||
reverseWsUrl: string;
|
||||
skipPull?: boolean;
|
||||
token: string;
|
||||
}) {
|
||||
const dataDir = this.sh(input.dataDir);
|
||||
@ -562,6 +648,10 @@ docker logs --tail 300 "$NAME" 2>&1 || true
|
||||
const name = this.sh(input.name);
|
||||
const reverseWsUrl = this.sh(input.reverseWsUrl);
|
||||
const token = this.sh(input.token);
|
||||
const account = `${input.account || ''}`.trim();
|
||||
const accountHeader = account ? `ACCOUNT=${this.sh(account)}\n` : '';
|
||||
const accountRunFlag = account ? ' -e ACCOUNT="$ACCOUNT" \\\n' : '';
|
||||
const pullCmd = input.skipPull ? '' : 'docker pull "$IMAGE" >/dev/null\n';
|
||||
|
||||
return `
|
||||
set -eu
|
||||
@ -571,7 +661,7 @@ NAME=${name}
|
||||
PORT=${input.port}
|
||||
REVERSE_WS_URL=${reverseWsUrl}
|
||||
WEBUI_TOKEN=${token}
|
||||
|
||||
${accountHeader}
|
||||
mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs"
|
||||
chmod 700 "$DATA_DIR"
|
||||
|
||||
@ -610,15 +700,14 @@ cat > "$DATA_DIR/config/onebot11.json" <<EOF
|
||||
}
|
||||
EOF
|
||||
|
||||
docker pull "$IMAGE" >/dev/null
|
||||
docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||
${pullCmd}docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||
docker run -d \\
|
||||
--name "$NAME" \\
|
||||
--restart unless-stopped \\
|
||||
-e NAPCAT_UID=0 \\
|
||||
-e NAPCAT_GID=0 \\
|
||||
-e WEBUI_TOKEN="$WEBUI_TOKEN" \\
|
||||
-p "$PORT:6099" \\
|
||||
${accountRunFlag} -p "$PORT:6099" \\
|
||||
-v "$DATA_DIR/QQ:/app/.config/QQ" \\
|
||||
-v "$DATA_DIR/config:/app/napcat/config" \\
|
||||
-v "$DATA_DIR/plugins:/app/napcat/plugins" \\
|
||||
|
||||
@ -8,6 +8,7 @@ import { QqbotAccountAbility } from './account/qqbot-account-ability.entity';
|
||||
import { QqbotAccount } from './account/qqbot-account.entity';
|
||||
import { QqbotAccountService } from './account/qqbot-account.service';
|
||||
import { QqbotNapcatLoginService } from './account/qqbot-napcat-login.service';
|
||||
import { QqbotNapcatWatchdogService } from './account/qqbot-napcat-watchdog.service';
|
||||
import { QqbotCommandController } from './command/qqbot-command.controller';
|
||||
import { QqbotCommand } from './command/qqbot-command.entity';
|
||||
import { QqbotCommandEngineService } from './command/qqbot-command-engine.service';
|
||||
@ -108,6 +109,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
||||
QqbotFflogsPluginService,
|
||||
QqbotMessageService,
|
||||
QqbotNapcatLoginService,
|
||||
QqbotNapcatWatchdogService,
|
||||
QqbotNapcatContainerService,
|
||||
QqbotPermissionService,
|
||||
QqbotEventPluginRegistryService,
|
||||
|
||||
102
test/qqbot/account/qqbot-napcat-watchdog.service.spec.ts
Normal file
102
test/qqbot/account/qqbot-napcat-watchdog.service.spec.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { QqbotNapcatWatchdogService } from '@/qqbot/account/qqbot-napcat-watchdog.service';
|
||||
|
||||
function buildService(
|
||||
configValues: Record<string, string | undefined>,
|
||||
runOfflineWatchdog: jest.Mock,
|
||||
) {
|
||||
const configService = {
|
||||
get: jest.fn((key: string) => configValues[key]),
|
||||
} as unknown as ConfigService;
|
||||
const accountService = { runOfflineWatchdog } as any;
|
||||
return new QqbotNapcatWatchdogService(configService, accountService);
|
||||
}
|
||||
|
||||
// 刷新微任务队列,让定时器回调里的 async tick(含 finally 复位 running)执行完。
|
||||
async function flushMicrotasks() {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('QqbotNapcatWatchdogService', () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('periodically triggers the offline watchdog when enabled', async () => {
|
||||
jest.useFakeTimers();
|
||||
const runOfflineWatchdog = jest.fn().mockResolvedValue({ checked: 1 });
|
||||
const service = buildService(
|
||||
{ QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS: '30000' },
|
||||
runOfflineWatchdog,
|
||||
);
|
||||
|
||||
service.onModuleInit();
|
||||
expect(runOfflineWatchdog).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(30000);
|
||||
await flushMicrotasks();
|
||||
expect(runOfflineWatchdog).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(30000);
|
||||
await flushMicrotasks();
|
||||
expect(runOfflineWatchdog).toHaveBeenCalledTimes(2);
|
||||
|
||||
service.onModuleDestroy();
|
||||
jest.advanceTimersByTime(60000);
|
||||
await flushMicrotasks();
|
||||
expect(runOfflineWatchdog).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not start a timer when disabled', async () => {
|
||||
jest.useFakeTimers();
|
||||
const runOfflineWatchdog = jest.fn().mockResolvedValue({ checked: 0 });
|
||||
const service = buildService(
|
||||
{ QQBOT_NAPCAT_WATCHDOG_ENABLED: 'false' },
|
||||
runOfflineWatchdog,
|
||||
);
|
||||
|
||||
service.onModuleInit();
|
||||
jest.advanceTimersByTime(600000);
|
||||
await flushMicrotasks();
|
||||
expect(runOfflineWatchdog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clamps an unreasonably small interval to the safe default', () => {
|
||||
const service = buildService(
|
||||
{ QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS: '1000' },
|
||||
jest.fn(),
|
||||
) as any;
|
||||
expect(service.getIntervalMs()).toBe(120_000);
|
||||
});
|
||||
|
||||
it('skips overlapping ticks while a previous run is still pending', async () => {
|
||||
jest.useFakeTimers();
|
||||
let resolvePending: (() => void) | undefined;
|
||||
const runOfflineWatchdog = jest.fn(
|
||||
() =>
|
||||
new Promise<{ checked: number }>((resolve) => {
|
||||
resolvePending = () => resolve({ checked: 1 });
|
||||
}),
|
||||
);
|
||||
const service = buildService(
|
||||
{ QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS: '30000' },
|
||||
runOfflineWatchdog,
|
||||
);
|
||||
|
||||
service.onModuleInit();
|
||||
jest.advanceTimersByTime(30000); // first tick starts, stays pending
|
||||
await flushMicrotasks();
|
||||
jest.advanceTimersByTime(30000); // second tick should be skipped
|
||||
await flushMicrotasks();
|
||||
expect(runOfflineWatchdog).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolvePending?.();
|
||||
await flushMicrotasks();
|
||||
|
||||
jest.advanceTimersByTime(30000); // running reset, a new tick can run
|
||||
await flushMicrotasks();
|
||||
expect(runOfflineWatchdog).toHaveBeenCalledTimes(2);
|
||||
service.onModuleDestroy();
|
||||
});
|
||||
});
|
||||
@ -281,6 +281,101 @@ describe('QqbotNapcatContainerService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('injects ACCOUNT env (NapCat -q quick login) only when a selfId is provided', () => {
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{ get: jest.fn().mockReturnValue('') } as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
|
||||
const baseInput = {
|
||||
dataDir: '/vol1/docker/kt-qqbot/napcat-instances/kt-qqbot-napcat-test',
|
||||
image: 'mlikiowa/napcat-docker:latest',
|
||||
name: 'kt-qqbot-napcat-test',
|
||||
port: 6100,
|
||||
reverseWsUrl: 'ws://127.0.0.1:48085/qqbot/onebot/reverse',
|
||||
token: 'token-test',
|
||||
};
|
||||
|
||||
const withAccount = service.buildRemoteCreateScript({
|
||||
...baseInput,
|
||||
account: '2354598417',
|
||||
});
|
||||
expect(withAccount).toContain("ACCOUNT='2354598417'");
|
||||
expect(withAccount).toContain('-e ACCOUNT="$ACCOUNT"');
|
||||
|
||||
const withoutAccount = service.buildRemoteCreateScript(baseInput);
|
||||
expect(withoutAccount).not.toContain('-e ACCOUNT="$ACCOUNT"');
|
||||
expect(withoutAccount).not.toMatch(/^ACCOUNT=/m);
|
||||
});
|
||||
|
||||
it('skips docker pull when recreating in place for quick login', () => {
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{ get: jest.fn().mockReturnValue('') } as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
|
||||
const baseInput = {
|
||||
dataDir: '/vol1/docker/kt-qqbot/napcat-instances/kt-qqbot-napcat-test',
|
||||
image: 'mlikiowa/napcat-docker:latest',
|
||||
name: 'kt-qqbot-napcat-test',
|
||||
port: 6100,
|
||||
reverseWsUrl: 'ws://127.0.0.1:48085/qqbot/onebot/reverse',
|
||||
token: 'token-test',
|
||||
};
|
||||
|
||||
expect(service.buildRemoteCreateScript(baseInput)).toContain('docker pull');
|
||||
expect(
|
||||
service.buildRemoteCreateScript({ ...baseInput, skipPull: true }),
|
||||
).not.toContain('docker pull');
|
||||
});
|
||||
|
||||
it('skips quick-login recreate when the container already carries ACCOUNT', async () => {
|
||||
const containerRepository = {
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn().mockResolvedValue({
|
||||
dataDir: '/vol1/docker/kt-qqbot/napcat-instances/kt-qqbot-napcat-x',
|
||||
id: 'container-1',
|
||||
image: 'mlikiowa/napcat-docker:latest',
|
||||
name: 'kt-qqbot-napcat-x',
|
||||
reverseWsUrl: 'ws://127.0.0.1:48085/qqbot/onebot/reverse',
|
||||
webuiPort: 6100,
|
||||
webuiToken: 'token-x',
|
||||
}),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
})),
|
||||
update: jest.fn(),
|
||||
};
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{
|
||||
get: jest.fn((key: string) =>
|
||||
key === 'QQBOT_NAPCAT_CONTAINER_MODE' ? 'ssh' : '',
|
||||
),
|
||||
} as any,
|
||||
containerRepository as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
service.runProcess = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ stderr: '', stdout: 'ACCOUNT=2354598417\nPATH=/x' });
|
||||
|
||||
const recreated = await service.ensureRuntimeQuickLogin(
|
||||
{ id: 'container-1', name: 'kt-qqbot-napcat-x' },
|
||||
'2354598417',
|
||||
);
|
||||
|
||||
expect(recreated).toBe(false);
|
||||
// 仅做了 inspect 检查,没有触发任何重建(rm/run)。
|
||||
expect(service.runProcess).toHaveBeenCalledTimes(1);
|
||||
expect(containerRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('requires an explicit NapCat image when creating a managed container', async () => {
|
||||
const containerRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user