修复AGC自定义服务器重启后仍需重新登录

- 启动恢复登录态前先读取落盘的服务器选择,用自定义 origin 调用 read_client_auth_state
- 删除与认证恢复竞态的独立服务器选择水合 effect,登录表单状态改由认证恢复链路统一设置
- 新增回归测试,断言自定义服务器会话恢复提交的 expectedApiBaseUrl 不再落到渠道默认地址
This commit is contained in:
2026-10-06 11:11:57 +08:00
parent ca53daaec4
commit 8f2006439d
2 changed files with 75 additions and 15 deletions
@@ -196,20 +196,6 @@ export function AuthenticatedClient({
setServerSelection(next);
setLoginStatus(`已选择 ${preset} 服务器`);
}
// 服务器选择是 Rust 客户端偏好:本地 debug 启动时读一次,打包产物始终用渠道默认值。
useEffect(() => {
if (!clientServerSelectionEnabled) return;
let disposed = false;
void hydrateClientServerSelection().then((selection) => {
if (disposed) return;
setServerSelection(selection);
setCustomServerUrl(selection.customBaseUrl);
});
return () => {
disposed = true;
};
}, []);
useEffect(() => {
const uninstallWebviewLogBridge = installWebviewLogBridge();
const handleError = (event: ErrorEvent) => {
@@ -238,8 +224,14 @@ export function AuthenticatedClient({
setAuthCheckError('');
setAuthCheckStage('refresh');
try {
// 服务器选择先于认证读取落盘值:Rust 按 `expectedApiBaseUrl` 判定凭据归属,竞态下用
// 渠道默认值会被当成「凭据属于另一个 origin」而误报未登录,自定义服务器每次启动都要重登。
const selection = await hydrateClientServerSelection();
if (!isActiveRun()) return;
setServerSelection(selection);
setCustomServerUrl(selection.customBaseUrl);
const state = await withAuthCheckTimeout(
readClientAuthState(getClientServerBaseUrl()),
readClientAuthState(getClientServerBaseUrl(selection)),
AUTH_CHECK_REQUEST_TIMEOUT_MS,
'检查登录状态超时,请检查服务器地址和网络后重试',
);
@@ -0,0 +1,68 @@
/** @vitest-environment jsdom */
/**
* 启动恢复登录态必须使用已落盘的服务器选择。
*
* 症状:本地 debug 选了 custom 服务器并登录成功后,重启客户端又被要求重新输密码。
* 凭据并没有丢:Rust 侧按 `expectedApiBaseUrl` 与凭据归属 origin 比对,不一致就只报告
* 未登录、不跨 origin 续期。因此渲染层必须在发起 `read_client_auth_state` 之前先读完
* `read_workspace_preferences`,否则预期地址会停在渠道默认值,自定义服务器的会话永远恢复不了。
*/
import { cleanup, render, screen } from '@testing-library/react';
import { createElement } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(async () => undefined),
}));
import { AuthenticatedClient } from '../src/app/AuthenticatedClient';
import { resetPlatformSessionForTests } from '../src/services/platformSession';
import { installTauriRuntime, resetTauriRuntime } from './tauriRuntimeFake';
const CUSTOM_SERVER = 'https://custom.example.com';
describe('启动时用已落盘的服务器选择恢复登录态', () => {
afterEach(() => {
cleanup();
resetPlatformSessionForTests();
resetTauriRuntime();
});
it('自定义服务器的会话恢复带自定义 origin,而不是渠道默认地址', async () => {
const authCalls: Array<{
command: string;
args?: Record<string, unknown>;
}> = [];
installTauriRuntime({
core: {
invoke: async (command, args) => {
authCalls.push({ command, args });
if (command === 'read_workspace_preferences') {
return {
clientServerSelection: {
preset: 'custom',
customBaseUrl: CUSTOM_SERVER,
},
};
}
if (command === 'read_client_auth_state') {
return { status: 'unauthenticated' };
}
return null;
},
},
});
render(
createElement(AuthenticatedClient, {
children: () => createElement('main', { 'aria-label': '已登录' }),
}),
);
await screen.findByRole('main', { name: '登录' });
const authCall = authCalls.find(
(call) => call.command === 'read_client_auth_state',
);
expect(authCall?.args).toEqual({ expectedApiBaseUrl: CUSTOM_SERVER });
});
});