Skip to content

Commit d3eaf41

Browse files
Bowl42claude
andauthored
feat: add Claude provider frontend support (#292)
* feat: add Claude provider frontend support Add full frontend support for the Claude provider type, complementing the backend added in PR #291. Users can now create, view, and manage Claude providers through the UI with OAuth login and manual token import. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review comments on Claude provider frontend - Add 'claude' mapping to getProviderDisplayName in theme.ts - Replace invalid CSS `${CLAUDE_COLOR}15` with Tailwind `bg-provider-claude/15` class in claude-provider-view.tsx and claude-token-import.tsx - Add popup null check for window.open() in OAuth flow - Store setInterval in ref (oauthPollRef) for proper cleanup on unmount - Clear interval in cancel handler to prevent memory leaks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 393c28f commit d3eaf41

File tree

16 files changed

+1452
-7
lines changed

16 files changed

+1452
-7
lines changed

web/src/index.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
--provider-antigravity: oklch(0.7123 0.2345 345.6789); /* #EC4899 粉色 */
8484
--provider-kiro: oklch(0.689 0.1456 195.6789); /* #00BCD4 青色 */
8585
--provider-codex: oklch(0.6789 0.12 145.6789); /* #10A37F OpenAI 绿色 */
86+
--provider-claude: oklch(0.65 0.15 28); /* Anthropic 橙色/珊瑚色 */
8687

8788
/* Client 品牌色 (引用 Provider 颜色) */
8889
--client-claude: var(--provider-anthropic);
@@ -366,6 +367,7 @@
366367
--color-provider-antigravity: var(--provider-antigravity);
367368
--color-provider-kiro: var(--provider-kiro);
368369
--color-provider-codex: var(--provider-codex);
370+
--color-provider-claude: var(--provider-claude);
369371

370372
/* Client 颜色映射 (Tailwind 可用) */
371373
--color-client-claude: var(--client-claude);

web/src/lib/theme.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export type ProviderType =
1818
| 'custom'
1919
| 'antigravity'
2020
| 'kiro'
21-
| 'codex';
21+
| 'codex'
22+
| 'claude';
2223

2324
/**
2425
* Client 类型定义
@@ -401,6 +402,7 @@ export function getProviderDisplayName(type: string): string {
401402
aws: 'AWS Bedrock',
402403
cohere: 'Cohere',
403404
mistral: 'Mistral',
405+
claude: 'Claude',
404406
custom: 'Custom',
405407
};
406408
return names[type.toLowerCase()] || type;

web/src/lib/transport/http-transport.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {
3939
CodexTokenValidationResult,
4040
CodexUsageResponse,
4141
CodexQuotaData,
42+
ClaudeTokenValidationResult,
4243
AuthStatus,
4344
AuthVerifyResult,
4445
APIToken,
@@ -543,6 +544,38 @@ export class HttpTransport implements Transport {
543544
return data;
544545
}
545546

547+
// ===== Claude API =====
548+
549+
async validateClaudeToken(refreshToken: string): Promise<ClaudeTokenValidationResult> {
550+
const { data } = await axios.post<ClaudeTokenValidationResult>('/api/claude/validate-token', {
551+
refreshToken,
552+
});
553+
return data;
554+
}
555+
556+
async startClaudeOAuth(): Promise<{ authURL: string; state: string }> {
557+
const { data } = await axios.post<{ authURL: string; state: string }>('/api/claude/oauth/start');
558+
return data;
559+
}
560+
561+
async exchangeClaudeOAuthCallback(
562+
code: string,
563+
state: string,
564+
): Promise<import('./types').ClaudeOAuthResult> {
565+
const { data } = await axios.post<import('./types').ClaudeOAuthResult>(
566+
'/api/claude/oauth/exchange',
567+
{ code, state },
568+
);
569+
return data;
570+
}
571+
572+
async refreshClaudeProviderInfo(providerId: number): Promise<ClaudeTokenValidationResult> {
573+
const { data } = await axios.post<ClaudeTokenValidationResult>(
574+
`/api/claude/provider/${providerId}/refresh`,
575+
);
576+
return data;
577+
}
578+
546579
// ===== Cooldown API =====
547580

548581
async getCooldowns(): Promise<Cooldown[]> {

web/src/lib/transport/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export type {
5858
ProviderConfigCodex,
5959
CodexTokenValidationResult,
6060
CodexOAuthResult,
61+
ProviderConfigClaude,
62+
ClaudeTokenValidationResult,
63+
ClaudeOAuthResult,
6164
CodexUsageWindow,
6265
CodexRateLimitInfo,
6366
CodexUsageResponse,

web/src/lib/transport/interface.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import type {
3737
CodexUsageResponse,
3838
CodexQuotaData,
3939
CodexOAuthResult,
40+
ClaudeTokenValidationResult,
41+
ClaudeOAuthResult,
4042
AuthStatus,
4143
AuthVerifyResult,
4244
APIToken,
@@ -167,6 +169,12 @@ export interface Transport {
167169
refreshCodexQuotas(): Promise<{ success: boolean; refreshed: boolean }>;
168170
sortCodexRoutes(): Promise<{ success: boolean }>;
169171

172+
// ===== Claude API =====
173+
validateClaudeToken(refreshToken: string): Promise<ClaudeTokenValidationResult>;
174+
startClaudeOAuth(): Promise<{ authURL: string; state: string }>;
175+
exchangeClaudeOAuthCallback(code: string, state: string): Promise<ClaudeOAuthResult>;
176+
refreshClaudeProviderInfo(providerId: number): Promise<ClaudeTokenValidationResult>;
177+
170178
// ===== Cooldown API =====
171179
getCooldowns(): Promise<Cooldown[]>;
172180
clearCooldown(providerId: number): Promise<void>;

web/src/lib/transport/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,22 @@ export interface ProviderConfigCodex {
5757
useCLIProxyAPI?: boolean;
5858
}
5959

60+
export interface ProviderConfigClaude {
61+
email: string;
62+
refreshToken: string;
63+
accessToken?: string;
64+
expiresAt?: string; // RFC3339 format
65+
organizationId?: string;
66+
modelMapping?: Record<string, string>;
67+
}
68+
6069
export interface ProviderConfig {
6170
disableErrorCooldown?: boolean;
6271
custom?: ProviderConfigCustom;
6372
antigravity?: ProviderConfigAntigravity;
6473
kiro?: ProviderConfigKiro;
6574
codex?: ProviderConfigCodex;
75+
claude?: ProviderConfigClaude;
6676
}
6777

6878
export interface Provider {
@@ -314,6 +324,7 @@ export type WSMessageType =
314324
| 'log_message'
315325
| 'antigravity_oauth_result'
316326
| 'codex_oauth_result'
327+
| 'claude_oauth_result'
317328
| 'new_session_pending'
318329
| 'session_pending_cancelled'
319330
| 'cooldown_update'
@@ -557,6 +568,29 @@ export interface CodexBatchQuotaResult {
557568
quotas: Record<number, CodexQuotaData>; // providerId -> quota
558569
}
559570

571+
// ===== Claude 类型 =====
572+
573+
export interface ClaudeTokenValidationResult {
574+
valid: boolean;
575+
error?: string;
576+
email?: string;
577+
organizationId?: string;
578+
accessToken?: string;
579+
refreshToken?: string;
580+
expiresAt?: string; // RFC3339 format
581+
}
582+
583+
export interface ClaudeOAuthResult {
584+
state: string;
585+
success: boolean;
586+
accessToken?: string;
587+
refreshToken?: string;
588+
expiresAt?: string; // RFC3339 format
589+
email?: string;
590+
organizationId?: string;
591+
error?: string;
592+
}
593+
560594
// ===== 回调类型 =====
561595

562596
export type EventCallback<T = unknown> = (data: T) => void;

web/src/locales/en.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@
250250
"notFound": "Provider not found",
251251
"antigravityType": "Antigravity Provider",
252252
"codexType": "Codex Provider",
253+
"claudeType": "Claude Provider",
254+
"claudeAccountDetails": "Account Details",
255+
"organizationId": "Organization ID",
253256
"subscription": "Subscription",
254257
"planType": "Plan Type",
255258
"subscriptionPeriod": "Subscription Period",
@@ -400,6 +403,59 @@
400403
"validateFirst": "Please validate the token first"
401404
}
402405
},
406+
"claudeTokenImport": {
407+
"title": "Add Claude Account",
408+
"connectTitle": "Connect Claude Account",
409+
"connectDescription": "Sign in with your Anthropic account or import a refresh token manually.",
410+
"oauthLogin": "OAuth Login",
411+
"tokenImport": "Token Import",
412+
"anthropicOauth": "Anthropic OAuth",
413+
"anthropicOauthDesc": "Sign in with your Anthropic account",
414+
"signInWithAnthropic": "Sign in with Anthropic",
415+
"popupClosed": "Popup window closed",
416+
"waitingAuth": "Waiting for authorization...",
417+
"pasteCallbackHint": "You can paste the callback URL below to continue",
418+
"completeSignIn": "Complete the sign-in in the popup window",
419+
"copyAuthUrlHint": "Copy the auth URL to open in your browser, then paste the callback URL.",
420+
"popupNotWorkingHint": "Popup window not working? Copy the auth URL or paste the callback URL manually.",
421+
"copyAuthUrl": "Copy Auth URL",
422+
"pasteCallbackUrl": "Paste Callback URL",
423+
"callbackNote": "After signing in, copy the URL from your browser's address bar (it will show an error page, that's expected).",
424+
"exchanging": "Exchanging...",
425+
"submitCallbackUrl": "Submit Callback URL",
426+
"authorizationSuccessful": "Authorization Successful",
427+
"signedInAs": "Signed in as",
428+
"creatingProvider": "Creating Provider...",
429+
"completeSetup": "Complete Setup",
430+
"credentials": "Credentials",
431+
"credentialsDesc": "Enter your Anthropic refresh token",
432+
"emailAddress": "Email Address",
433+
"optional": "Optional",
434+
"emailPlaceholder": "e.g. user@example.com",
435+
"displayOnlyNote": "Used for display purposes only. Auto-detected if valid token provided.",
436+
"refreshToken": "Refresh Token",
437+
"refreshTokenPlaceholder": "Paste your Anthropic refresh token here...",
438+
"chars": "chars",
439+
"validatingToken": "Validating Token...",
440+
"revalidate": "Re-validate",
441+
"validateToken": "Validate Token",
442+
"tokenVerified": "Token Verified Successfully",
443+
"readyToConnectAs": "Ready to connect as",
444+
"error": "Error",
445+
"errors": {
446+
"oauthFailed": "OAuth authorization failed",
447+
"invalidCallbackUrl": "Invalid callback URL. Please paste the complete URL from the browser address bar.",
448+
"stateMismatch": "State mismatch. Please make sure you are using the callback URL from the current OAuth session.",
449+
"exchangeFailed": "Failed to exchange callback",
450+
"startOAuthFailed": "Failed to start OAuth flow",
451+
"invalidRefreshToken": "Please enter a valid refresh token",
452+
"tokenValidationFailed": "Token validation failed",
453+
"validationFailed": "Validation failed",
454+
"oauthResultMissing": "No valid OAuth result",
455+
"createFailed": "Failed to create provider",
456+
"validateFirst": "Please validate the token first"
457+
}
458+
},
403459
"kiroTokenImport": {
404460
"title": "Add Kiro Account",
405461
"importTitle": "Import Kiro Social Token",
@@ -1060,6 +1116,10 @@
10601116
"name": "Kiro (Q Developer)",
10611117
"description": "AWS CodeWhisperer / Q Developer"
10621118
},
1119+
"claude": {
1120+
"name": "Claude",
1121+
"description": "Anthropic Claude with OAuth authentication"
1122+
},
10631123
"custom": {
10641124
"name": "Custom Provider",
10651125
"description": "Configure your own API endpoint"

web/src/locales/zh.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@
250250
"notFound": "提供商未找到",
251251
"antigravityType": "Antigravity 提供商",
252252
"codexType": "Codex 提供商",
253+
"claudeType": "Claude 提供商",
254+
"claudeAccountDetails": "账号详情",
255+
"organizationId": "组织 ID",
253256
"subscription": "订阅信息",
254257
"planType": "订阅计划",
255258
"subscriptionPeriod": "订阅周期",
@@ -400,6 +403,59 @@
400403
"validateFirst": "请先验证 token"
401404
}
402405
},
406+
"claudeTokenImport": {
407+
"title": "添加 Claude 账号",
408+
"connectTitle": "连接 Claude 账号",
409+
"connectDescription": "使用 Anthropic 账号登录或手动导入 refresh token。",
410+
"oauthLogin": "OAuth 登录",
411+
"tokenImport": "Token 导入",
412+
"anthropicOauth": "Anthropic OAuth",
413+
"anthropicOauthDesc": "使用 Anthropic 账号登录",
414+
"signInWithAnthropic": "使用 Anthropic 登录",
415+
"popupClosed": "弹窗已关闭",
416+
"waitingAuth": "等待授权...",
417+
"pasteCallbackHint": "你可以在下方粘贴回调地址以继续",
418+
"completeSignIn": "请在弹窗中完成登录",
419+
"copyAuthUrlHint": "复制授权地址在浏览器打开,然后粘贴回调地址。",
420+
"popupNotWorkingHint": "弹窗不可用?复制授权地址或手动粘贴回调地址。",
421+
"copyAuthUrl": "复制授权地址",
422+
"pasteCallbackUrl": "粘贴回调地址",
423+
"callbackNote": "登录后请从浏览器地址栏复制回调 URL(会显示错误页,这是正常的)。",
424+
"exchanging": "正在交换...",
425+
"submitCallbackUrl": "提交回调地址",
426+
"authorizationSuccessful": "授权成功",
427+
"signedInAs": "已登录为",
428+
"creatingProvider": "正在创建提供商...",
429+
"completeSetup": "完成设置",
430+
"credentials": "凭据",
431+
"credentialsDesc": "输入 Anthropic refresh token",
432+
"emailAddress": "邮箱地址",
433+
"optional": "可选",
434+
"emailPlaceholder": "例如 user@example.com",
435+
"displayOnlyNote": "仅用于显示,如提供有效 token 将自动识别。",
436+
"refreshToken": "Refresh Token",
437+
"refreshTokenPlaceholder": "在此粘贴 Anthropic refresh token...",
438+
"chars": "字符",
439+
"validatingToken": "正在验证 Token...",
440+
"revalidate": "重新验证",
441+
"validateToken": "验证 Token",
442+
"tokenVerified": "Token 验证成功",
443+
"readyToConnectAs": "将以以下身份连接",
444+
"error": "错误",
445+
"errors": {
446+
"oauthFailed": "OAuth 授权失败",
447+
"invalidCallbackUrl": "回调地址无效,请粘贴完整的浏览器地址栏 URL。",
448+
"stateMismatch": "State 不匹配,请确保使用当前 OAuth 会话的回调地址。",
449+
"exchangeFailed": "交换回调失败",
450+
"startOAuthFailed": "启动 OAuth 流程失败",
451+
"invalidRefreshToken": "请输入有效的 refresh token",
452+
"tokenValidationFailed": "Token 验证失败",
453+
"validationFailed": "验证失败",
454+
"oauthResultMissing": "未获取到有效的 OAuth 结果",
455+
"createFailed": "创建失败",
456+
"validateFirst": "请先验证 token"
457+
}
458+
},
403459
"kiroTokenImport": {
404460
"title": "添加 Kiro 账号",
405461
"importTitle": "导入 Kiro Social Token",
@@ -1059,6 +1115,10 @@
10591115
"name": "Kiro (Q Developer)",
10601116
"description": "AWS CodeWhisperer / Q Developer"
10611117
},
1118+
"claude": {
1119+
"name": "Claude",
1120+
"description": "Anthropic Claude,支持 OAuth 认证"
1121+
},
10621122
"custom": {
10631123
"name": "自定义提供商",
10641124
"description": "配置您自己的 API 端点"

0 commit comments

Comments
 (0)