fix: 修复NapCat已登录误判旧二维码复用
This commit is contained in:
parent
0497233798
commit
3647ec9a7c
@ -12,6 +12,8 @@ RUN set -eux; \
|
|||||||
fonts-wqy-microhei \
|
fonts-wqy-microhei \
|
||||||
locales \
|
locales \
|
||||||
tzdata \
|
tzdata \
|
||||||
|
unzip \
|
||||||
|
zip \
|
||||||
xdg-utils; \
|
xdg-utils; \
|
||||||
sed -i 's/^# *zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen; \
|
sed -i 's/^# *zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen; \
|
||||||
locale-gen zh_CN.UTF-8; \
|
locale-gen zh_CN.UTF-8; \
|
||||||
@ -20,6 +22,18 @@ RUN set -eux; \
|
|||||||
fc-cache -fv; \
|
fc-cache -fv; \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh /tmp/qq-login-real-online-guard.sh
|
||||||
|
RUN set -eux; \
|
||||||
|
sed -i 's/\r$//' /tmp/qq-login-real-online-guard.sh; \
|
||||||
|
rm -rf /tmp/NapCat.Shell; \
|
||||||
|
unzip -q /app/NapCat.Shell.zip -d /tmp/NapCat.Shell; \
|
||||||
|
NAPCAT_PATCH_ROOT=/tmp/NapCat.Shell sh /tmp/qq-login-real-online-guard.sh; \
|
||||||
|
cd /tmp/NapCat.Shell; \
|
||||||
|
zip -qr /app/NapCat.Shell.zip .; \
|
||||||
|
rm -rf /tmp/NapCat.Shell /tmp/qq-login-real-online-guard.sh
|
||||||
|
COPY ci/napcat-desktop-cn/verify.sh /ci/napcat-desktop-cn/verify.sh
|
||||||
|
RUN sed -i 's/\r$//' /ci/napcat-desktop-cn/verify.sh && chmod +x /ci/napcat-desktop-cn/verify.sh
|
||||||
|
|
||||||
ENV LANG=zh_CN.UTF-8 \
|
ENV LANG=zh_CN.UTF-8 \
|
||||||
LC_ALL=zh_CN.UTF-8 \
|
LC_ALL=zh_CN.UTF-8 \
|
||||||
LANGUAGE=zh_CN:zh \
|
LANGUAGE=zh_CN:zh \
|
||||||
|
|||||||
@ -7,14 +7,18 @@ $baseImage = docker image inspect mlikiowa/napcat-docker:latest --format '{{inde
|
|||||||
if (-not $baseImage) { throw 'NapCat upstream image digest not found; pull and inspect the image before building.' }
|
if (-not $baseImage) { throw 'NapCat upstream image digest not found; pull and inspect the image before building.' }
|
||||||
docker build `
|
docker build `
|
||||||
--build-arg NAPCAT_BASE_IMAGE=$baseImage `
|
--build-arg NAPCAT_BASE_IMAGE=$baseImage `
|
||||||
-t kt-napcat-desktop-cn:desktop-cn-v1 `
|
-t kt-napcat-desktop-cn:desktop-cn-v2 `
|
||||||
-f ci/napcat-desktop-cn/Dockerfile .
|
-f ci/napcat-desktop-cn/Dockerfile .
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify:
|
Verify:
|
||||||
|
|
||||||
```powershell
|
```bash
|
||||||
docker run --rm kt-napcat-desktop-cn:desktop-cn-v1 sh /ci/napcat-desktop-cn/verify.sh
|
name="kt-napcat-verify-$(date +%s)"
|
||||||
|
trap 'docker rm -f "$name" >/dev/null 2>&1 || true' EXIT
|
||||||
|
docker run -d --name "$name" kt-napcat-desktop-cn:desktop-cn-v2 >/dev/null
|
||||||
|
sleep 3
|
||||||
|
docker exec "$name" sh /ci/napcat-desktop-cn/verify.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Record the final digest in `QQBOT_NAPCAT_IMAGE`.
|
Record the final digest in `QQBOT_NAPCAT_IMAGE`.
|
||||||
|
|||||||
65
ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh
Normal file
65
ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="${NAPCAT_PATCH_ROOT:-/app/napcat}"
|
||||||
|
PATCHED_LIST="$(mktemp)"
|
||||||
|
PERL_PATCH="$(mktemp)"
|
||||||
|
trap 'rm -f "$PATCHED_LIST" "$PERL_PATCH"' EXIT
|
||||||
|
|
||||||
|
cat > "$PERL_PATCH" <<'PERL'
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
my ($file) = @ARGV;
|
||||||
|
open my $in, '<', $file or die "read $file failed: $!";
|
||||||
|
local $/;
|
||||||
|
my $source = <$in>;
|
||||||
|
close $in;
|
||||||
|
|
||||||
|
exit 0 if index($source, 'QQ Is Logined') < 0;
|
||||||
|
exit 0 if index($source, 'RefreshQRcode') < 0;
|
||||||
|
exit 0 if index($source, 'getQQLoginStatus') < 0;
|
||||||
|
|
||||||
|
my ($runtime) = $source =~ /([A-Za-z_\$][\w\$]*)\.getOneBotContext\(\)\?\.core\?\.selfInfo\?\.online,\s*[A-Za-z_\$][\w\$]*\s*=\s*\1\.getQQLoginStatus\(\)/;
|
||||||
|
die "Unable to identify NapCat WebUI runtime guard symbols in $file\n" unless $runtime;
|
||||||
|
|
||||||
|
my ($send_error) = $source =~ /return\s+([A-Za-z_\$][\w\$]*)\(e,\s*"QQ Is Logined"\)/;
|
||||||
|
die "Unable to identify NapCat sendError helper in $file\n" unless $send_error;
|
||||||
|
|
||||||
|
my $count = 0;
|
||||||
|
$count += $source =~ s/if \(\Q$runtime\E\.getQQLoginStatus\(\)\)\n(\s*)return \Q$send_error\E\(e, "QQ Is Logined"\);/
|
||||||
|
"if ($runtime.getQQLoginStatus() && $runtime.getOneBotContext()?.core?.selfInfo?.online !== false)\n"
|
||||||
|
. "$1return $send_error(e, \"QQ Is Logined\");\n"
|
||||||
|
. " if ($runtime.getQQLoginStatus() && $runtime.getOneBotContext()?.core?.selfInfo?.online === false)\n"
|
||||||
|
. "$1$runtime.setQQLoginStatus(false);"
|
||||||
|
/ge;
|
||||||
|
|
||||||
|
my $refresh_pattern = qr/\Q$runtime\E\.getQQLoginStatus\(\) \? \Q$send_error\E\(e, "QQ Is Logined"\) : \(await \Q$runtime\E\.refreshQRCode\(\),/;
|
||||||
|
my $refresh_replacement =
|
||||||
|
"$runtime.getQQLoginStatus() && $runtime.getOneBotContext()?.core?.selfInfo?.online !== false "
|
||||||
|
. "? $send_error(e, \"QQ Is Logined\") "
|
||||||
|
. ": ($runtime.getQQLoginStatus() && $runtime.getOneBotContext()?.core?.selfInfo?.online === false && $runtime.setQQLoginStatus(false), await $runtime.refreshQRCode(),";
|
||||||
|
$count += $source =~ s/$refresh_pattern/$refresh_replacement/g;
|
||||||
|
|
||||||
|
exit 0 if $count == 0;
|
||||||
|
open my $out, '>', $file or die "write $file failed: $!";
|
||||||
|
print {$out} $source;
|
||||||
|
close $out;
|
||||||
|
print "$file\n";
|
||||||
|
PERL
|
||||||
|
|
||||||
|
find "$ROOT" \
|
||||||
|
-type f \( -name '*.js' -o -name '*.mjs' \) \
|
||||||
|
! -path '*/node_modules/*' \
|
||||||
|
! -path '*/static/*' \
|
||||||
|
-size -20M \
|
||||||
|
-print | while IFS= read -r file; do
|
||||||
|
perl "$PERL_PATCH" "$file" >> "$PATCHED_LIST"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -s "$PATCHED_LIST" ]; then
|
||||||
|
echo 'No NapCat WebUI login guard file was patched' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Patched NapCat WebUI real-online login guard in %s file(s).\n' "$(wc -l < "$PATCHED_LIST" | tr -d ' ')"
|
||||||
@ -10,3 +10,5 @@ test "XDG_CACHE_HOME=${XDG_CACHE_HOME:-}" = "XDG_CACHE_HOME=/app/.cache"
|
|||||||
test "XDG_DATA_HOME=${XDG_DATA_HOME:-}" = "XDG_DATA_HOME=/app/.local/share"
|
test "XDG_DATA_HOME=${XDG_DATA_HOME:-}" = "XDG_DATA_HOME=/app/.local/share"
|
||||||
test ! -e /.dockerenv
|
test ! -e /.dockerenv
|
||||||
grep -q '^0::/$' /proc/1/cgroup
|
grep -q '^0::/$' /proc/1/cgroup
|
||||||
|
unzip -p /app/NapCat.Shell.zip napcat.mjs | grep -q 'selfInfo?.online !== false'
|
||||||
|
unzip -p /app/NapCat.Shell.zip napcat.mjs | grep -q 'setQQLoginStatus(false)'
|
||||||
|
|||||||
@ -33,7 +33,7 @@ spec:
|
|||||||
- name: DB_TIMEZONE
|
- name: DB_TIMEZONE
|
||||||
value: "+08:00"
|
value: "+08:00"
|
||||||
- name: QQBOT_NAPCAT_IMAGE
|
- name: QQBOT_NAPCAT_IMAGE
|
||||||
value: kt-napcat-desktop-cn:desktop-cn-v1
|
value: kt-napcat-desktop-cn:desktop-cn-v2
|
||||||
- name: QQBOT_NAPCAT_SSH_KEY_PATH
|
- name: QQBOT_NAPCAT_SSH_KEY_PATH
|
||||||
value: /app/secrets/napcat-ssh/id_rsa
|
value: /app/secrets/napcat-ssh/id_rsa
|
||||||
- name: QQBOT_PLUGIN_TASK_QUEUE_REDIS_PREFIX
|
- name: QQBOT_PLUGIN_TASK_QUEUE_REDIS_PREFIX
|
||||||
|
|||||||
@ -596,6 +596,7 @@ export class ToolsService {
|
|||||||
'NapCat 请求超时',
|
'NapCat 请求超时',
|
||||||
'NapCat 未返回登录二维码',
|
'NapCat 未返回登录二维码',
|
||||||
'NapCat 二维码仍未刷新',
|
'NapCat 二维码仍未刷新',
|
||||||
|
'NapCat WebUI 登录态仍阻止生成新二维码',
|
||||||
'QRCode Get Error',
|
'QRCode Get Error',
|
||||||
'socket hang up',
|
'socket hang up',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -2110,6 +2110,9 @@ export class QqbotNapcatLoginService {
|
|||||||
return this.toolsService.ensureFreshQrcode(qrcode, options);
|
return this.toolsService.ensureFreshQrcode(qrcode, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this.toolsService.isNapcatAlreadyLoggedInError(err)) {
|
if (this.toolsService.isNapcatAlreadyLoggedInError(err)) {
|
||||||
|
if (options.requireFresh) {
|
||||||
|
throw new Error('NapCat WebUI 登录态仍阻止生成新二维码');
|
||||||
|
}
|
||||||
const status = await this.getLoginStatus(container);
|
const status = await this.getLoginStatus(container);
|
||||||
return this.toolsService.ensureFreshQrcode(
|
return this.toolsService.ensureFreshQrcode(
|
||||||
status.qrcodeurl || '',
|
status.qrcodeurl || '',
|
||||||
@ -2405,6 +2408,13 @@ export class QqbotNapcatLoginService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (this.toolsService.isNapcatAlreadyLoggedInError(err)) {
|
||||||
|
return this.completeAlreadyLoggedInQuickRelogin(
|
||||||
|
session,
|
||||||
|
container,
|
||||||
|
hasPasswordFallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.publishQuickLoginFallback(
|
this.publishQuickLoginFallback(
|
||||||
session,
|
session,
|
||||||
this.toolsService.getErrorMessage(err),
|
this.toolsService.getErrorMessage(err),
|
||||||
@ -2420,6 +2430,81 @@ export class QqbotNapcatLoginService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 NapCat WebUI 拒绝重复 quick 登录时读取真实 QQ 在线态。
|
||||||
|
* @param session - 当前更新登录会话;成功时直接完成会话,失败时写入 fallback 进度。
|
||||||
|
* @param container - 当前 NapCat WebUI 容器;只读调用 CheckLoginStatus/GetQQLoginInfo。
|
||||||
|
* @param hasPasswordFallback - 是否还有密码登录分支;决定 fallback 文案里的下一步。
|
||||||
|
* @returns 真实 QQ 在线且账号匹配时返回 true,否则返回 false 继续后续登录分支。
|
||||||
|
*/
|
||||||
|
private async completeAlreadyLoggedInQuickRelogin(
|
||||||
|
session: QqbotLoginScanSession,
|
||||||
|
container: QqbotNapcatRuntime,
|
||||||
|
hasPasswordFallback: boolean,
|
||||||
|
) {
|
||||||
|
this.publishScanResultEvent(
|
||||||
|
session,
|
||||||
|
'quick-login-status-check',
|
||||||
|
'processing',
|
||||||
|
'NapCat 报告账号已登录,正在确认真实在线状态',
|
||||||
|
);
|
||||||
|
|
||||||
|
let loginInfo: NapcatLoginInfo;
|
||||||
|
try {
|
||||||
|
const loginStatus = await this.getLoginStatus(container, true);
|
||||||
|
if (!loginStatus.isLogin) {
|
||||||
|
await this.syncSessionQqLoginStatus(session, loginStatus);
|
||||||
|
this.publishQuickLoginFallback(
|
||||||
|
session,
|
||||||
|
loginStatus.loginError || 'NapCat 已登录标记残留但真实 QQ 已离线',
|
||||||
|
hasPasswordFallback,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginInfo = await this.getLoginInfo(container);
|
||||||
|
if (loginInfo.online === false) {
|
||||||
|
this.publishQuickLoginFallback(
|
||||||
|
session,
|
||||||
|
'NapCat 已登录标记残留但真实 QQ 已离线',
|
||||||
|
hasPasswordFallback,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfId = this.toolsService.pickNapcatSelfId(loginInfo);
|
||||||
|
if (!selfId) {
|
||||||
|
this.publishQuickLoginFallback(
|
||||||
|
session,
|
||||||
|
'NapCat 未返回 QQ 号',
|
||||||
|
hasPasswordFallback,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (session.expectedSelfId && session.expectedSelfId !== selfId) {
|
||||||
|
this.publishQuickLoginFallback(
|
||||||
|
session,
|
||||||
|
`当前已登录账号 ${selfId} 与目标账号 ${session.expectedSelfId} 不一致`,
|
||||||
|
hasPasswordFallback,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.publishQuickLoginFallback(
|
||||||
|
session,
|
||||||
|
this.toolsService.getErrorMessage(err),
|
||||||
|
hasPasswordFallback,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.completeLogin(session, container, {
|
||||||
|
loginInfo,
|
||||||
|
successMessage: 'NapCat 已登录,已确认真实在线状态',
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行 NapCat 登录运行态流程。
|
* 执行 NapCat 登录运行态流程。
|
||||||
* @param session - 更新登录会话;保存密码 MD5、验证码和新设备验证上下文。
|
* @param session - 更新登录会话;保存密码 MD5、验证码和新设备验证上下文。
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export class NapcatRuntimeProfileService {
|
|||||||
dataDir: input.dataDir,
|
dataDir: input.dataDir,
|
||||||
desktopProfileVersion: this.getString(
|
desktopProfileVersion: this.getString(
|
||||||
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION',
|
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION',
|
||||||
'desktop-cn-v1',
|
'desktop-cn-v2',
|
||||||
),
|
),
|
||||||
deviceIdentityId: input.deviceIdentityId,
|
deviceIdentityId: input.deviceIdentityId,
|
||||||
imageRef: this.getString('QQBOT_NAPCAT_IMAGE', ''),
|
imageRef: this.getString('QQBOT_NAPCAT_IMAGE', ''),
|
||||||
|
|||||||
@ -28,6 +28,8 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
|
|||||||
expect(dockerfile).toContain('fontconfig');
|
expect(dockerfile).toContain('fontconfig');
|
||||||
expect(dockerfile).toContain('fc-cache -fv');
|
expect(dockerfile).toContain('fc-cache -fv');
|
||||||
expect(dockerfile).toContain('dbus-x11');
|
expect(dockerfile).toContain('dbus-x11');
|
||||||
|
expect(dockerfile).toContain('unzip');
|
||||||
|
expect(dockerfile).toContain('zip');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('verifies locale, fontconfig, XDG, process user, and container hiding evidence', () => {
|
it('verifies locale, fontconfig, XDG, process user, and container hiding evidence', () => {
|
||||||
@ -39,5 +41,39 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
|
|||||||
expect(verify).toContain('/proc/1/cgroup');
|
expect(verify).toContain('/proc/1/cgroup');
|
||||||
expect(verify).toContain('XDG_CONFIG_HOME=/app/.config');
|
expect(verify).toContain('XDG_CONFIG_HOME=/app/.config');
|
||||||
expect(verify).toContain('Asia/Shanghai');
|
expect(verify).toContain('Asia/Shanghai');
|
||||||
|
expect(verify).toContain('selfInfo?.online !== false');
|
||||||
|
expect(verify).toContain('setQQLoginStatus(false)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('patches NapCat WebUI login guards to allow qrcode refresh after real QQ offline', () => {
|
||||||
|
const dockerfile = readSource('ci/napcat-desktop-cn/Dockerfile');
|
||||||
|
expect(dockerfile).toContain(
|
||||||
|
'ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh',
|
||||||
|
);
|
||||||
|
expect(dockerfile).toContain(
|
||||||
|
'sh /tmp/qq-login-real-online-guard.sh',
|
||||||
|
);
|
||||||
|
expect(dockerfile).toContain(
|
||||||
|
"sed -i 's/\\r$//' /tmp/qq-login-real-online-guard.sh",
|
||||||
|
);
|
||||||
|
expect(dockerfile).toContain('NAPCAT_PATCH_ROOT=/tmp/NapCat.Shell');
|
||||||
|
expect(dockerfile).toContain('zip -qr /app/NapCat.Shell.zip .');
|
||||||
|
expect(dockerfile).toContain(
|
||||||
|
'COPY ci/napcat-desktop-cn/verify.sh /ci/napcat-desktop-cn/verify.sh',
|
||||||
|
);
|
||||||
|
expect(dockerfile).toContain(
|
||||||
|
"sed -i 's/\\r$//' /ci/napcat-desktop-cn/verify.sh",
|
||||||
|
);
|
||||||
|
|
||||||
|
const patch = readSource(
|
||||||
|
'ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh',
|
||||||
|
);
|
||||||
|
expect(patch).toContain('QQ Is Logined');
|
||||||
|
expect(patch).toContain('getQQLoginStatus');
|
||||||
|
expect(patch).toContain('selfInfo?.online');
|
||||||
|
expect(patch).toContain('setQQLoginStatus(false)');
|
||||||
|
expect(patch).toContain('RefreshQRcode');
|
||||||
|
expect(patch).toContain('[A-Za-z_\\$]');
|
||||||
|
expect(patch).toContain('[\\w\\$]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -123,6 +123,7 @@ describe('NapCat runtime profile generation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(profile).toMatchObject({
|
expect(profile).toMatchObject({
|
||||||
|
desktopProfileVersion: 'desktop-cn-v2',
|
||||||
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||||
locale: 'zh_CN.UTF-8',
|
locale: 'zh_CN.UTF-8',
|
||||||
runtimeGid: 1101,
|
runtimeGid: 1101,
|
||||||
|
|||||||
@ -894,6 +894,138 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('confirms real login status when quick login reports the account is already logged in', async () => {
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-quick-already-online',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
};
|
||||||
|
const accountService = {
|
||||||
|
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
bindAccount: jest.fn().mockResolvedValue(undefined),
|
||||||
|
ensureRuntimeLoginEnv: jest.fn(),
|
||||||
|
resetRuntimeLoginState: jest.fn(),
|
||||||
|
restartRuntimeContainer: jest.fn(),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
accountService as unknown as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
const session = (refreshService as any).createSession({
|
||||||
|
accountId: 'account-1',
|
||||||
|
container,
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
mode: 'refresh',
|
||||||
|
preparingRelogin: true,
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
(refreshService as any).sessions.set(session.id, session);
|
||||||
|
mockWebuiLoginPost(refreshService, {
|
||||||
|
quickError: 'QQ Is Logined',
|
||||||
|
});
|
||||||
|
const getLoginStatus = jest
|
||||||
|
.spyOn(refreshService as any, 'getLoginStatus')
|
||||||
|
.mockResolvedValue({ isLogin: true });
|
||||||
|
jest.spyOn(refreshService as any, 'getLoginInfo').mockResolvedValue({
|
||||||
|
nickname: 'Kwi',
|
||||||
|
online: true,
|
||||||
|
uin: '10001',
|
||||||
|
});
|
||||||
|
const refreshQrcode = jest
|
||||||
|
.spyOn(refreshService as any, 'refreshOrGetQrcode')
|
||||||
|
.mockResolvedValue('fallback-qrcode');
|
||||||
|
|
||||||
|
await (refreshService as any).prepareReloginQrcode(session, container);
|
||||||
|
|
||||||
|
expect(getLoginStatus).toHaveBeenCalledWith(container, true);
|
||||||
|
expect(refreshQrcode).not.toHaveBeenCalled();
|
||||||
|
expect(accountService.ensureScannedAccount).toHaveBeenCalledWith({
|
||||||
|
accountId: 'account-1',
|
||||||
|
name: 'Kwi',
|
||||||
|
selfId: '10001',
|
||||||
|
});
|
||||||
|
expect(containerService.bindAccount).toHaveBeenCalledWith(
|
||||||
|
'account-1',
|
||||||
|
'container-quick-already-online',
|
||||||
|
'10001',
|
||||||
|
);
|
||||||
|
expect(session.status).toBe('success');
|
||||||
|
const events = (refreshService as any).sessionEventLogs.get(session.id);
|
||||||
|
expect(events).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ step: 'quick-login-start' }),
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'NapCat 已登录,已确认真实在线状态',
|
||||||
|
step: 'login-success',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps binding errors terminal after already-logged quick status is confirmed online', async () => {
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-quick-already-bind-error',
|
||||||
|
name: 'napcat-10001',
|
||||||
|
};
|
||||||
|
const accountService = {
|
||||||
|
ensureScannedAccount: jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(new Error('账号绑定写入失败')),
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
bindAccount: jest.fn().mockResolvedValue(undefined),
|
||||||
|
resetRuntimeLoginState: jest.fn(),
|
||||||
|
restartRuntimeContainer: jest.fn(),
|
||||||
|
};
|
||||||
|
const refreshService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
accountService as unknown as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
const session = (refreshService as any).createSession({
|
||||||
|
accountId: 'account-1',
|
||||||
|
container,
|
||||||
|
expectedSelfId: '10001',
|
||||||
|
mode: 'refresh',
|
||||||
|
preparingRelogin: true,
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
(refreshService as any).sessions.set(session.id, session);
|
||||||
|
mockWebuiLoginPost(refreshService, {
|
||||||
|
quickError: 'QQ Is Logined',
|
||||||
|
});
|
||||||
|
jest.spyOn(refreshService as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: true,
|
||||||
|
});
|
||||||
|
jest.spyOn(refreshService as any, 'getLoginInfo').mockResolvedValue({
|
||||||
|
nickname: 'Kwi',
|
||||||
|
online: true,
|
||||||
|
uin: '10001',
|
||||||
|
});
|
||||||
|
const refreshQrcode = jest
|
||||||
|
.spyOn(refreshService as any, 'refreshOrGetQrcode')
|
||||||
|
.mockResolvedValue('fallback-qrcode');
|
||||||
|
|
||||||
|
await (refreshService as any).prepareReloginQrcode(session, container);
|
||||||
|
|
||||||
|
expect(refreshQrcode).not.toHaveBeenCalled();
|
||||||
|
expect(session.status).toBe('error');
|
||||||
|
expect(session.qrcode).toBeUndefined();
|
||||||
|
expect(session.errorMessage).toBe('账号绑定写入失败');
|
||||||
|
const steps = (
|
||||||
|
(refreshService as any).sessionEventLogs.get(session.id) ?? []
|
||||||
|
).map((event: { step: string }) => event.step);
|
||||||
|
expect(steps).toContain('quick-login-status-check');
|
||||||
|
expect(steps).not.toContain('quick-login-fallback');
|
||||||
|
expect(steps).toContain('relogin-error');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not rebuild even when the refresh session already has a rebuild count', async () => {
|
it('does not rebuild even when the refresh session already has a rebuild count', async () => {
|
||||||
const container = {
|
const container = {
|
||||||
baseUrl: 'http://127.0.0.1:6103/',
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
@ -3456,6 +3588,23 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
expect((service as any).postNapcat).toHaveBeenCalledTimes(2);
|
expect((service as any).postNapcat).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects stale status qrcode when NapCat still blocks qrcode generation as already logged in', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(service as any, 'postNapcat')
|
||||||
|
.mockRejectedValue(new Error('QQ Is Logined'));
|
||||||
|
jest.spyOn(service as any, 'getLoginStatus').mockResolvedValue({
|
||||||
|
isLogin: false,
|
||||||
|
isOffline: true,
|
||||||
|
qrcodeurl: 'old-status-qrcode',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(service as any).getQrcode({ id: 'container-stale-login-guard' }, false, {
|
||||||
|
requireFresh: true,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('NapCat WebUI 登录态仍阻止生成新二维码');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not replace current qrcode with expired status qrcode', async () => {
|
it('does not replace current qrcode with expired status qrcode', async () => {
|
||||||
(service as any).sessions.set('session-2', {
|
(service as any).sessions.set('session-2', {
|
||||||
containerId: 'container-2',
|
containerId: 'container-2',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user