fix: 修复NapCat验证码会话持久化

This commit is contained in:
sunlei 2026-06-16 19:24:01 +08:00
parent 041052900a
commit 749df7cd7e
7 changed files with 74 additions and 10 deletions

View File

@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS `napcat_login_session` (
CREATE TABLE IF NOT EXISTS `napcat_login_challenge` (
`id` bigint NOT NULL,
`session_id` bigint NOT NULL,
`session_id` varchar(64) NOT NULL,
`challenge_type` varchar(64) NOT NULL,
`status` varchar(32) NOT NULL,
`challenge_url` text,
@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS `napcat_login_challenge` (
CREATE TABLE IF NOT EXISTS `napcat_runtime_cleanup` (
`id` bigint NOT NULL,
`session_id` bigint NOT NULL,
`session_id` varchar(64) NOT NULL,
`cleanup_type` varchar(64) NOT NULL,
`status` varchar(32) NOT NULL,
`error_message` text,

View File

@ -948,7 +948,7 @@ CREATE TABLE IF NOT EXISTS napcat_login_session (
CREATE TABLE IF NOT EXISTS napcat_login_challenge (
id BIGINT NOT NULL PRIMARY KEY,
session_id BIGINT NOT NULL,
session_id VARCHAR(64) NOT NULL,
challenge_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
challenge_url TEXT NULL,
@ -961,7 +961,7 @@ CREATE TABLE IF NOT EXISTS napcat_login_challenge (
CREATE TABLE IF NOT EXISTS napcat_runtime_cleanup (
id BIGINT NOT NULL PRIMARY KEY,
session_id BIGINT NOT NULL,
session_id VARCHAR(64) NOT NULL,
cleanup_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
error_message TEXT NULL,

View File

@ -108,3 +108,17 @@ FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'napcat_runtime_cleanup'
AND index_name = 'idx_napcat_runtime_cleanup_session';
SELECT 'column_napcat_login_challenge_session_id_varchar' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'napcat_login_challenge'
AND column_name = 'session_id'
AND column_type = 'varchar(64)';
SELECT 'column_napcat_runtime_cleanup_session_id_varchar' AS check_name, COUNT(*) AS matched_rows
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'napcat_runtime_cleanup'
AND column_name = 'session_id'
AND column_type = 'varchar(64)';

View File

@ -15,7 +15,7 @@ export class NapcatLoginChallengeEntity {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'session_id', type: 'bigint' })
@Column({ length: 64, name: 'session_id', type: 'varchar' })
sessionId: string;
@Column({ length: 64, name: 'challenge_type' })

View File

@ -1,4 +1,4 @@
import { Injectable, Optional } from '@nestjs/common';
import { Injectable, Logger, Optional } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import type { QqbotLoginScanSession } from '@/modules/qqbot/core/contract/qqbot.types';
@ -16,6 +16,7 @@ type NapcatLoginStoreCache = Record<string, QqbotLoginScanSession>;
@Injectable()
export class NapcatLoginStateStoreService {
private readonly logger = new Logger(NapcatLoginStateStoreService.name);
private readonly cache: NapcatLoginStoreCache = {};
private readonly pendingSessionWrites: Record<string, Promise<void> | undefined> =
{};
@ -91,7 +92,9 @@ export class NapcatLoginStateStoreService {
challengeUrl: session.captchaUrl,
session,
status: 'pending',
});
}).catch((err) =>
this.warnPersistenceError('登录验证码 challenge 持久化失败', err),
);
}
recordNewDeviceChallenge(session: QqbotLoginScanSession) {
@ -106,7 +109,9 @@ export class NapcatLoginStateStoreService {
challengeUrl: session.deviceVerifyUrl || session.newDeviceQrcode || null,
session,
status: session.newDeviceStatus,
});
}).catch((err) =>
this.warnPersistenceError('新设备 challenge 持久化失败', err),
);
}
recordRuntimeCleanup(
@ -124,7 +129,11 @@ export class NapcatLoginStateStoreService {
sessionId: session.id,
status: input.status,
});
void this.runtimeCleanupRepository.save(cleanup);
void this.runtimeCleanupRepository
.save(cleanup)
.catch((err) =>
this.warnPersistenceError('运行态清理记录持久化失败', err),
);
}
async flushSessionWrites(sessionId?: string) {
@ -295,6 +304,16 @@ export class NapcatLoginStateStoreService {
await this.loginChallengeRepository.save(entity);
}
private warnPersistenceError(message: string, err: unknown) {
const detail =
err instanceof Error
? err.message
: typeof err === 'string'
? err
: JSON.stringify(err);
this.logger.warn(`${message}: ${detail || 'unknown error'}`);
}
private isResolvedChallenge(status: string) {
return ['failed', 'expired', 'verified'].includes(status);
}

View File

@ -14,7 +14,7 @@ export class NapcatRuntimeCleanup {
@PrimaryColumn({ type: 'bigint' })
id: string;
@Column({ name: 'session_id', type: 'bigint' })
@Column({ length: 64, name: 'session_id', type: 'varchar' })
sessionId: string;
@Column({ length: 64, name: 'cleanup_type' })

View File

@ -30,6 +30,18 @@ const getEntityColumnNames = (entity: EntityClass) => {
.map((column) => `${column.options.name || column.propertyName}`);
};
const getEntityColumnOptions = (entity: EntityClass, propertyName: string) => {
return getMetadataArgsStorage().columns.find(
(column) => column.target === entity && column.propertyName === propertyName,
)?.options;
};
const getSchemaColumnDefinition = (tableName: string, columnName: string) => {
return schema
.getTableColumns(tableName)
.find((column) => column.name === columnName)?.definition;
};
const createRepository = <T extends Record<string, any>>() => {
const rows: T[] = [];
return {
@ -126,6 +138,25 @@ describe('NapCat persistent login state contract', () => {
}
});
it('stores challenge and cleanup session ids as uuid-safe text', () => {
expect(
getEntityColumnOptions(NapcatLoginChallengeEntity, 'sessionId'),
).toMatchObject({ length: 64, type: 'varchar' });
expect(getEntityColumnOptions(NapcatRuntimeCleanup, 'sessionId')).toMatchObject(
{ length: 64, type: 'varchar' },
);
expect(
getSchemaColumnDefinition('napcat_login_challenge', 'session_id'),
).toMatch(/^session_id VARCHAR\(64\) NOT NULL/i);
expect(
getSchemaColumnDefinition('napcat_runtime_cleanup', 'session_id'),
).toMatch(/^session_id VARCHAR\(64\) NOT NULL/i);
const qqbotInitSql = readSource('sql/qqbot-init.sql');
expect(qqbotInitSql).toContain('`session_id` varchar(64) NOT NULL');
});
it('keeps NapCat runtime tables covered by the v3 verification SQL', () => {
const verifySql = readSource('sql/refactor-v3/99-verify.sql');
const requiredSignals = [