fix: 修复NapCat验证码会话持久化
This commit is contained in:
parent
041052900a
commit
749df7cd7e
@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS `napcat_login_session` (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `napcat_login_challenge` (
|
CREATE TABLE IF NOT EXISTS `napcat_login_challenge` (
|
||||||
`id` bigint NOT NULL,
|
`id` bigint NOT NULL,
|
||||||
`session_id` bigint NOT NULL,
|
`session_id` varchar(64) NOT NULL,
|
||||||
`challenge_type` varchar(64) NOT NULL,
|
`challenge_type` varchar(64) NOT NULL,
|
||||||
`status` varchar(32) NOT NULL,
|
`status` varchar(32) NOT NULL,
|
||||||
`challenge_url` text,
|
`challenge_url` text,
|
||||||
@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS `napcat_login_challenge` (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `napcat_runtime_cleanup` (
|
CREATE TABLE IF NOT EXISTS `napcat_runtime_cleanup` (
|
||||||
`id` bigint NOT NULL,
|
`id` bigint NOT NULL,
|
||||||
`session_id` bigint NOT NULL,
|
`session_id` varchar(64) NOT NULL,
|
||||||
`cleanup_type` varchar(64) NOT NULL,
|
`cleanup_type` varchar(64) NOT NULL,
|
||||||
`status` varchar(32) NOT NULL,
|
`status` varchar(32) NOT NULL,
|
||||||
`error_message` text,
|
`error_message` text,
|
||||||
|
|||||||
@ -948,7 +948,7 @@ CREATE TABLE IF NOT EXISTS napcat_login_session (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS napcat_login_challenge (
|
CREATE TABLE IF NOT EXISTS napcat_login_challenge (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
session_id BIGINT NOT NULL,
|
session_id VARCHAR(64) NOT NULL,
|
||||||
challenge_type VARCHAR(64) NOT NULL,
|
challenge_type VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
challenge_url TEXT 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 (
|
CREATE TABLE IF NOT EXISTS napcat_runtime_cleanup (
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
session_id BIGINT NOT NULL,
|
session_id VARCHAR(64) NOT NULL,
|
||||||
cleanup_type VARCHAR(64) NOT NULL,
|
cleanup_type VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
error_message TEXT NULL,
|
error_message TEXT NULL,
|
||||||
|
|||||||
@ -108,3 +108,17 @@ FROM information_schema.statistics
|
|||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
AND table_name = 'napcat_runtime_cleanup'
|
AND table_name = 'napcat_runtime_cleanup'
|
||||||
AND index_name = 'idx_napcat_runtime_cleanup_session';
|
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)';
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export class NapcatLoginChallengeEntity {
|
|||||||
@PrimaryColumn({ type: 'bigint' })
|
@PrimaryColumn({ type: 'bigint' })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column({ name: 'session_id', type: 'bigint' })
|
@Column({ length: 64, name: 'session_id', type: 'varchar' })
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
||||||
@Column({ length: 64, name: 'challenge_type' })
|
@Column({ length: 64, name: 'challenge_type' })
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Optional } from '@nestjs/common';
|
import { Injectable, Logger, Optional } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import type { QqbotLoginScanSession } from '@/modules/qqbot/core/contract/qqbot.types';
|
import type { QqbotLoginScanSession } from '@/modules/qqbot/core/contract/qqbot.types';
|
||||||
@ -16,6 +16,7 @@ type NapcatLoginStoreCache = Record<string, QqbotLoginScanSession>;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NapcatLoginStateStoreService {
|
export class NapcatLoginStateStoreService {
|
||||||
|
private readonly logger = new Logger(NapcatLoginStateStoreService.name);
|
||||||
private readonly cache: NapcatLoginStoreCache = {};
|
private readonly cache: NapcatLoginStoreCache = {};
|
||||||
private readonly pendingSessionWrites: Record<string, Promise<void> | undefined> =
|
private readonly pendingSessionWrites: Record<string, Promise<void> | undefined> =
|
||||||
{};
|
{};
|
||||||
@ -91,7 +92,9 @@ export class NapcatLoginStateStoreService {
|
|||||||
challengeUrl: session.captchaUrl,
|
challengeUrl: session.captchaUrl,
|
||||||
session,
|
session,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
});
|
}).catch((err) =>
|
||||||
|
this.warnPersistenceError('登录验证码 challenge 持久化失败', err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
recordNewDeviceChallenge(session: QqbotLoginScanSession) {
|
recordNewDeviceChallenge(session: QqbotLoginScanSession) {
|
||||||
@ -106,7 +109,9 @@ export class NapcatLoginStateStoreService {
|
|||||||
challengeUrl: session.deviceVerifyUrl || session.newDeviceQrcode || null,
|
challengeUrl: session.deviceVerifyUrl || session.newDeviceQrcode || null,
|
||||||
session,
|
session,
|
||||||
status: session.newDeviceStatus,
|
status: session.newDeviceStatus,
|
||||||
});
|
}).catch((err) =>
|
||||||
|
this.warnPersistenceError('新设备 challenge 持久化失败', err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
recordRuntimeCleanup(
|
recordRuntimeCleanup(
|
||||||
@ -124,7 +129,11 @@ export class NapcatLoginStateStoreService {
|
|||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
});
|
});
|
||||||
void this.runtimeCleanupRepository.save(cleanup);
|
void this.runtimeCleanupRepository
|
||||||
|
.save(cleanup)
|
||||||
|
.catch((err) =>
|
||||||
|
this.warnPersistenceError('运行态清理记录持久化失败', err),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async flushSessionWrites(sessionId?: string) {
|
async flushSessionWrites(sessionId?: string) {
|
||||||
@ -295,6 +304,16 @@ export class NapcatLoginStateStoreService {
|
|||||||
await this.loginChallengeRepository.save(entity);
|
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) {
|
private isResolvedChallenge(status: string) {
|
||||||
return ['failed', 'expired', 'verified'].includes(status);
|
return ['failed', 'expired', 'verified'].includes(status);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export class NapcatRuntimeCleanup {
|
|||||||
@PrimaryColumn({ type: 'bigint' })
|
@PrimaryColumn({ type: 'bigint' })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column({ name: 'session_id', type: 'bigint' })
|
@Column({ length: 64, name: 'session_id', type: 'varchar' })
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
||||||
@Column({ length: 64, name: 'cleanup_type' })
|
@Column({ length: 64, name: 'cleanup_type' })
|
||||||
|
|||||||
@ -30,6 +30,18 @@ const getEntityColumnNames = (entity: EntityClass) => {
|
|||||||
.map((column) => `${column.options.name || column.propertyName}`);
|
.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 createRepository = <T extends Record<string, any>>() => {
|
||||||
const rows: T[] = [];
|
const rows: T[] = [];
|
||||||
return {
|
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', () => {
|
it('keeps NapCat runtime tables covered by the v3 verification SQL', () => {
|
||||||
const verifySql = readSource('sql/refactor-v3/99-verify.sql');
|
const verifySql = readSource('sql/refactor-v3/99-verify.sql');
|
||||||
const requiredSignals = [
|
const requiredSignals = [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user