feat: implement Codex OAuth flow with loopback callback server and management UI

- Add localCallbackServer for handling OAuth callback requests.
- Implement service functions for managing OAuth sessions and accounts.
- Create CodexAdapter for platform-specific interactions.
- Develop OAuthManagement page for managing OAuth connections and providers.
- Add tests for localCallbackServer and OAuthManagement components.
This commit is contained in:
ciat-777
2026-03-17 11:07:25 +08:00
parent c156e0c164
commit 575a15232b
40 changed files with 3219 additions and 32 deletions
+34
View File
@@ -0,0 +1,34 @@
import { BasePlatformAdapter, type BalanceInfo, type CheckinResult, type UserInfo } from './base.js';
function normalizeBaseUrl(baseUrl: string): string {
return (baseUrl || '').replace(/\/+$/, '');
}
export class CodexAdapter extends BasePlatformAdapter {
readonly platformName = 'codex';
async detect(url: string): Promise<boolean> {
const normalized = normalizeBaseUrl(url).toLowerCase();
return normalized.includes('chatgpt.com/backend-api/codex');
}
override async login(_baseUrl: string, _username: string, _password: string) {
return { success: false as const, message: 'codex oauth login is managed via OAuth flow' };
}
override async getUserInfo(_baseUrl: string, _accessToken: string): Promise<UserInfo | null> {
return null;
}
async checkin(_baseUrl: string, _accessToken: string): Promise<CheckinResult> {
return { success: false, message: 'codex oauth connections do not support checkin' };
}
async getBalance(_baseUrl: string, _accessToken: string): Promise<BalanceInfo> {
return { balance: 0, used: 0, quota: 0 };
}
async getModels(_baseUrl: string, _token: string): Promise<string[]> {
return [];
}
}
@@ -58,6 +58,11 @@ describe('getAdapter platform aliases', () => {
expect(getAdapter('gemini')?.platformName).toBe('gemini');
});
it('supports dedicated codex adapter and aliases', () => {
expect(getAdapter('codex')?.platformName).toBe('codex');
expect(getAdapter('chatgpt-codex')?.platformName).toBe('codex');
});
it('detects anyrouter URL before generic new-api adapter', async () => {
const adapter = await detectPlatform('https://anyrouter.top');
expect(adapter?.platformName).toBe('anyrouter');
+6
View File
@@ -7,6 +7,7 @@ import { OneHubAdapter } from './oneHub.js';
import { DoneHubAdapter } from './doneHub.js';
import { Sub2ApiAdapter } from './sub2api.js';
import { OpenAiAdapter } from './openai.js';
import { CodexAdapter } from './codex.js';
import { ClaudeAdapter } from './claude.js';
import { GeminiAdapter } from './gemini.js';
import { CliProxyApiAdapter } from './cliproxyapi.js';
@@ -15,6 +16,7 @@ import { detectPlatformByTitle } from './titleHint.js';
const adapters: PlatformAdapter[] = [
// Specific forks before generic adapters for better auto-detection.
new OpenAiAdapter(),
new CodexAdapter(),
new ClaudeAdapter(),
new GeminiAdapter(),
new CliProxyApiAdapter(),
@@ -49,6 +51,9 @@ const platformAliases: Record<string, string> = {
sub2api: 'sub2api',
// Official upstream APIs
openai: 'openai',
codex: 'codex',
'chatgpt-codex': 'codex',
'chatgpt codex': 'codex',
anthropic: 'claude',
claude: 'claude',
gemini: 'gemini',
@@ -83,6 +88,7 @@ function detectPlatformByUrlHint(url: string): string | undefined {
// Official upstream endpoints.
if (normalized.includes('api.openai.com')) return 'openai';
if (normalized.includes('chatgpt.com/backend-api/codex')) return 'codex';
if (normalized.includes('api.anthropic.com') || normalized.includes('anthropic.com/v1')) return 'claude';
if (
normalized.includes('generativelanguage.googleapis.com')