fix(cliproxy): prevent blank page in remote mode

This commit is contained in:
Tam Nhu Tran
2026-02-24 00:47:40 +07:00
parent a6e1455f38
commit 4eb2be79fc
4 changed files with 72 additions and 8 deletions
+3
View File
@@ -30,6 +30,7 @@ interface RemoteAuthFile {
export interface RemoteAccountInfo {
id: string;
email: string;
provider: CLIProxyProvider;
isDefault: boolean;
status: 'active' | 'disabled' | 'unavailable';
}
@@ -124,6 +125,8 @@ function transformRemoteAuthFiles(files: RemoteAuthFile[]): RemoteAuthStatus[] {
const accounts: RemoteAccountInfo[] = providerFiles.map((f, idx) => ({
id: f.id,
email: f.email || f.name || 'Unknown',
// Keep provider on each account so UI account rendering can infer capabilities safely.
provider,
isDefault: idx === 0,
status: f.status,
}));
@@ -184,13 +184,7 @@ router.get('/accounts', async (_req: Request, res: Response): Promise<void> => {
const target = getProxyTarget();
if (target.isRemote) {
const authStatus = await fetchRemoteAuthStatus(target);
// Transform RemoteAuthStatus[] to account summary format
const accounts = authStatus.flatMap((status) =>
status.accounts.map((acc) => ({
provider: status.provider,
...acc,
}))
);
const accounts = authStatus.flatMap((status) => status.accounts);
res.json({ accounts, source: 'remote' });
return;
}
@@ -0,0 +1,58 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { fetchRemoteAuthStatus } from '../../../src/cliproxy/remote-auth-fetcher';
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
describe('remote-auth-fetcher', () => {
let originalFetch: typeof fetch;
const remoteTarget: ProxyTarget = {
host: 'remote.example.com',
port: 8317,
protocol: 'https',
authToken: 'token',
isRemote: true,
};
beforeEach(() => {
originalFetch = global.fetch;
});
afterEach(() => {
global.fetch = originalFetch;
});
it('includes provider on each remote account', async () => {
global.fetch = mock((url: string) => {
expect(url).toBe('https://remote.example.com:8317/v0/management/auth-files');
return Promise.resolve(
new Response(
JSON.stringify({
files: [
{
id: 'acc-codex',
name: 'codex-main',
type: 'oauth',
provider: 'codex',
email: 'codex@example.com',
status: 'active',
source: 'file',
},
],
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
);
}) as typeof fetch;
const result = await fetchRemoteAuthStatus(remoteTarget);
expect(result).toHaveLength(1);
expect(result[0]?.provider).toBe('codex');
expect(result[0]?.accounts).toHaveLength(1);
expect(result[0]?.accounts[0]?.provider).toBe('codex');
expect(result[0]?.accounts[0]?.email).toBe('codex@example.com');
});
});
@@ -105,7 +105,16 @@ export function ProviderEditor({
missingRequiredFields,
} = useProviderEditor(provider);
const accounts = authStatus.accounts || [];
// Defensive normalization: remote/legacy payloads may omit account.provider.
// Fallback to current editor provider to avoid runtime crashes in account UI.
const accounts = useMemo(
() =>
(authStatus.accounts || []).map((account) => ({
...account,
provider: account.provider || baseProvider || provider,
})),
[authStatus.accounts, baseProvider, provider]
);
// Fetch effective API key for presets (uses configured value, not hardcoded)
const { data: authTokens } = useQuery<{ apiKey: { value: string } }>({