fix: 修复 QQBot 更新登录链路
This commit is contained in:
parent
b0cf9323b8
commit
fbaaee8bd7
@ -55,6 +55,7 @@ export class QqbotNapcatLoginService {
|
|||||||
{
|
{
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
expectedSelfId: account.selfId,
|
expectedSelfId: account.selfId,
|
||||||
|
forceRelogin: true,
|
||||||
mode: 'refresh',
|
mode: 'refresh',
|
||||||
},
|
},
|
||||||
container,
|
container,
|
||||||
@ -83,12 +84,16 @@ export class QqbotNapcatLoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loginStatus.isOffline) {
|
if (loginStatus.isOffline) {
|
||||||
await this.restartNapcatForLogin(container, { waitForReady: false });
|
if (session.mode === 'refresh') {
|
||||||
|
await this.resetNapcatForLogin(container, { waitForReady: false });
|
||||||
|
} else {
|
||||||
|
await this.restartNapcatForLogin(container, { waitForReady: false });
|
||||||
|
}
|
||||||
session.lastRestartedAt = Date.now();
|
session.lastRestartedAt = Date.now();
|
||||||
return this.keepSessionPending(
|
return this.keepSessionPending(
|
||||||
session,
|
session,
|
||||||
loginStatus.loginError ||
|
loginStatus.loginError ||
|
||||||
'NapCat 账号已离线,已重启容器并重新生成二维码',
|
'NapCat 账号已离线,已重新生成二维码',
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -162,6 +167,7 @@ export class QqbotNapcatLoginService {
|
|||||||
options: {
|
options: {
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
expectedSelfId?: string;
|
expectedSelfId?: string;
|
||||||
|
forceRelogin?: boolean;
|
||||||
mode: QqbotLoginScanMode;
|
mode: QqbotLoginScanMode;
|
||||||
},
|
},
|
||||||
container: QqbotNapcatRuntime,
|
container: QqbotNapcatRuntime,
|
||||||
@ -169,9 +175,17 @@ export class QqbotNapcatLoginService {
|
|||||||
await this.cleanupSessions();
|
await this.cleanupSessions();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (options.forceRelogin) {
|
||||||
|
await this.resetNapcatForLogin(container);
|
||||||
|
}
|
||||||
|
|
||||||
const loginStatus = await this.getLoginStatus(container, true);
|
const loginStatus = await this.getLoginStatus(container, true);
|
||||||
if (loginStatus.isOffline) {
|
if (loginStatus.isOffline) {
|
||||||
await this.restartNapcatForLogin(container, { waitForReady: false });
|
if (options.forceRelogin) {
|
||||||
|
await this.resetNapcatForLogin(container, { waitForReady: false });
|
||||||
|
} else {
|
||||||
|
await this.restartNapcatForLogin(container, { waitForReady: false });
|
||||||
|
}
|
||||||
const session = this.createSession({
|
const session = this.createSession({
|
||||||
...options,
|
...options,
|
||||||
container,
|
container,
|
||||||
@ -180,7 +194,18 @@ export class QqbotNapcatLoginService {
|
|||||||
session.lastRestartedAt = Date.now();
|
session.lastRestartedAt = Date.now();
|
||||||
session.errorMessage =
|
session.errorMessage =
|
||||||
loginStatus.loginError ||
|
loginStatus.loginError ||
|
||||||
'NapCat 账号已离线,已重启容器并重新生成二维码';
|
'NapCat 账号已离线,已重新生成二维码';
|
||||||
|
this.sessions.set(session.id, session);
|
||||||
|
return this.toResult(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.forceRelogin && loginStatus.isLogin) {
|
||||||
|
const session = this.createSession({
|
||||||
|
...options,
|
||||||
|
container,
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
session.errorMessage = 'NapCat 正在重新生成二维码,请稍后刷新';
|
||||||
this.sessions.set(session.id, session);
|
this.sessions.set(session.id, session);
|
||||||
return this.toResult(session);
|
return this.toResult(session);
|
||||||
}
|
}
|
||||||
@ -588,6 +613,24 @@ export class QqbotNapcatLoginService {
|
|||||||
return this.requestNapcat<T>(container, path, body, credential);
|
return this.requestNapcat<T>(container, path, body, credential);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resetNapcatForLogin(
|
||||||
|
container: QqbotNapcatRuntime,
|
||||||
|
options: NapcatRestartOptions = {},
|
||||||
|
) {
|
||||||
|
const resetByContainer =
|
||||||
|
await this.containerService.resetRuntimeLoginState(container);
|
||||||
|
if (!resetByContainer) {
|
||||||
|
await this.restartNapcatForLogin(container, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.credentials.delete(this.getCredentialCacheKey(container));
|
||||||
|
if (options.waitForReady === false) return;
|
||||||
|
|
||||||
|
await this.toolsService.sleep(this.getRestartDelayMs());
|
||||||
|
await this.getLoginStatus(container, true);
|
||||||
|
}
|
||||||
|
|
||||||
private async restartNapcatForLogin(
|
private async restartNapcatForLogin(
|
||||||
container: QqbotNapcatRuntime,
|
container: QqbotNapcatRuntime,
|
||||||
options: NapcatRestartOptions = {},
|
options: NapcatRestartOptions = {},
|
||||||
|
|||||||
@ -166,6 +166,34 @@ export class QqbotNapcatContainerService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resetRuntimeLoginState(runtime: QqbotNapcatRuntime) {
|
||||||
|
if (this.getManagedMode() !== 'ssh' || !runtime.id || !runtime.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = await this.containerRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: runtime.id,
|
||||||
|
isDeleted: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!container) {
|
||||||
|
throwVbenError('NapCat 容器不存在或已删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = this.buildRemoteResetLoginStateScript(container);
|
||||||
|
await this.runProcess('ssh', [...this.getSshArgs(), 'sh -s'], script);
|
||||||
|
await this.containerRepository.update(
|
||||||
|
{ id: runtime.id },
|
||||||
|
{
|
||||||
|
lastError: null,
|
||||||
|
lastStartedAt: new Date(),
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async removeContainer(containerId: string) {
|
private async removeContainer(containerId: string) {
|
||||||
const container = await this.containerRepository.findOne({
|
const container = await this.containerRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -221,6 +249,39 @@ fi
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildRemoteResetLoginStateScript(container: QqbotNapcatContainer) {
|
||||||
|
const dataDir = this.sh(container.dataDir || '');
|
||||||
|
const name = this.sh(container.name);
|
||||||
|
const rootDir = this.sh(this.getRootDir());
|
||||||
|
|
||||||
|
return `
|
||||||
|
set -eu
|
||||||
|
NAME=${name}
|
||||||
|
DATA_DIR=${dataDir}
|
||||||
|
ROOT_DIR=${rootDir}
|
||||||
|
|
||||||
|
if [ -z "$DATA_DIR" ] || [ "$DATA_DIR" = "/" ]; then
|
||||||
|
echo "unsafe empty data dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$DATA_DIR" in
|
||||||
|
"$ROOT_DIR"/*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "skip unsafe data dir: $DATA_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
docker exec "$NAME" rm -f /app/napcat/cache/qrcode.png >/dev/null 2>&1 || true
|
||||||
|
docker stop "$NAME" >/dev/null 2>&1 || true
|
||||||
|
mkdir -p "$DATA_DIR/QQ"
|
||||||
|
find "$DATA_DIR/QQ" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
|
||||||
|
docker start "$NAME" >/dev/null
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private async getPrimaryRuntime(accountId: string) {
|
private async getPrimaryRuntime(accountId: string) {
|
||||||
const binding = await this.bindingRepository.findOne({
|
const binding = await this.bindingRepository.findOne({
|
||||||
order: {
|
order: {
|
||||||
|
|||||||
@ -90,12 +90,59 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
{
|
{
|
||||||
accountId: 'account-1',
|
accountId: 'account-1',
|
||||||
expectedSelfId: '10001',
|
expectedSelfId: '10001',
|
||||||
|
forceRelogin: true,
|
||||||
mode: 'refresh',
|
mode: 'refresh',
|
||||||
},
|
},
|
||||||
existingContainer,
|
existingContainer,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resets the existing container login state before refresh login scan', async () => {
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-current',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
resetRuntimeLoginState: jest.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
{} as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
jest
|
||||||
|
.spyOn((refreshService as any).toolsService, 'sleep')
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'cleanupSessions')
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'getLoginStatus')
|
||||||
|
.mockResolvedValueOnce({ isLogin: false })
|
||||||
|
.mockResolvedValueOnce({ isLogin: false });
|
||||||
|
jest
|
||||||
|
.spyOn(refreshService as any, 'refreshOrGetQrcode')
|
||||||
|
.mockResolvedValue('fresh-qrcode');
|
||||||
|
|
||||||
|
const result = await (refreshService as any).startScan(
|
||||||
|
{
|
||||||
|
accountId: 'account-1',
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
forceRelogin: true,
|
||||||
|
mode: 'refresh',
|
||||||
|
},
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.status).toBe('pending');
|
||||||
|
expect(result.qrcode).toBe('fresh-qrcode');
|
||||||
|
expect(containerService.resetRuntimeLoginState).toHaveBeenCalledWith(
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('requires a qrcode different from the current one when refreshing qrcode', async () => {
|
it('requires a qrcode different from the current one when refreshing qrcode', async () => {
|
||||||
(service as any).sessions.set('session-refresh-qrcode', {
|
(service as any).sessions.set('session-refresh-qrcode', {
|
||||||
containerId: 'container-3',
|
containerId: 'container-3',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user