mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-12 18:24:22 +00:00
fix(cliproxy): prevent blank page in remote mode
This commit is contained in:
@@ -30,6 +30,7 @@ interface RemoteAuthFile {
|
|||||||
export interface RemoteAccountInfo {
|
export interface RemoteAccountInfo {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
provider: CLIProxyProvider;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
status: 'active' | 'disabled' | 'unavailable';
|
status: 'active' | 'disabled' | 'unavailable';
|
||||||
}
|
}
|
||||||
@@ -124,6 +125,8 @@ function transformRemoteAuthFiles(files: RemoteAuthFile[]): RemoteAuthStatus[] {
|
|||||||
const accounts: RemoteAccountInfo[] = providerFiles.map((f, idx) => ({
|
const accounts: RemoteAccountInfo[] = providerFiles.map((f, idx) => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
email: f.email || f.name || 'Unknown',
|
email: f.email || f.name || 'Unknown',
|
||||||
|
// Keep provider on each account so UI account rendering can infer capabilities safely.
|
||||||
|
provider,
|
||||||
isDefault: idx === 0,
|
isDefault: idx === 0,
|
||||||
status: f.status,
|
status: f.status,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -184,13 +184,7 @@ router.get('/accounts', async (_req: Request, res: Response): Promise<void> => {
|
|||||||
const target = getProxyTarget();
|
const target = getProxyTarget();
|
||||||
if (target.isRemote) {
|
if (target.isRemote) {
|
||||||
const authStatus = await fetchRemoteAuthStatus(target);
|
const authStatus = await fetchRemoteAuthStatus(target);
|
||||||
// Transform RemoteAuthStatus[] to account summary format
|
const accounts = authStatus.flatMap((status) => status.accounts);
|
||||||
const accounts = authStatus.flatMap((status) =>
|
|
||||||
status.accounts.map((acc) => ({
|
|
||||||
provider: status.provider,
|
|
||||||
...acc,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
res.json({ accounts, source: 'remote' });
|
res.json({ accounts, source: 'remote' });
|
||||||
return;
|
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,
|
missingRequiredFields,
|
||||||
} = useProviderEditor(provider);
|
} = 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)
|
// Fetch effective API key for presets (uses configured value, not hardcoded)
|
||||||
const { data: authTokens } = useQuery<{ apiKey: { value: string } }>({
|
const { data: authTokens } = useQuery<{ apiKey: { value: string } }>({
|
||||||
|
|||||||
Reference in New Issue
Block a user