From e8fc09869bcf304d250a251eae37bdafdc78b65f Mon Sep 17 00:00:00 2001 From: nianzs Date: Wed, 29 Apr 2026 16:29:35 +0800 Subject: [PATCH] feat(frontend): add kiro api and model presets --- frontend/src/api/admin/index.ts | 3 + frontend/src/api/admin/kiro.ts | 89 ++++++++ .../__tests__/useModelWhitelist.spec.ts | 9 + frontend/src/composables/useKiroOAuth.ts | 191 ++++++++++++++++++ frontend/src/composables/useModelWhitelist.ts | 43 +++- frontend/src/types/index.ts | 38 +++- 6 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 frontend/src/api/admin/kiro.ts create mode 100644 frontend/src/composables/useKiroOAuth.ts diff --git a/frontend/src/api/admin/index.ts b/frontend/src/api/admin/index.ts index 80241794..5763f102 100644 --- a/frontend/src/api/admin/index.ts +++ b/frontend/src/api/admin/index.ts @@ -17,6 +17,7 @@ import subscriptionsAPI from './subscriptions' import usageAPI from './usage' import geminiAPI from './gemini' import antigravityAPI from './antigravity' +import kiroAPI from './kiro' import userAttributesAPI from './userAttributes' import opsAPI from './ops' import errorPassthroughAPI from './errorPassthrough' @@ -49,6 +50,7 @@ export const adminAPI = { usage: usageAPI, gemini: geminiAPI, antigravity: antigravityAPI, + kiro: kiroAPI, userAttributes: userAttributesAPI, ops: opsAPI, errorPassthrough: errorPassthroughAPI, @@ -79,6 +81,7 @@ export { usageAPI, geminiAPI, antigravityAPI, + kiroAPI, userAttributesAPI, opsAPI, errorPassthroughAPI, diff --git a/frontend/src/api/admin/kiro.ts b/frontend/src/api/admin/kiro.ts new file mode 100644 index 00000000..60751b3c --- /dev/null +++ b/frontend/src/api/admin/kiro.ts @@ -0,0 +1,89 @@ +import { apiClient } from '../client' + +export interface KiroAuthUrlResponse { + auth_url: string + session_id: string + state: string +} + +export interface KiroIDCAuthUrlResponse extends KiroAuthUrlResponse { + client_id?: string + region?: string + start_url?: string +} + +export interface KiroTokenInfo { + access_token?: string + refresh_token?: string + profile_arn?: string + expires_at?: string + auth_method?: string + provider?: string + client_id?: string + client_secret?: string + client_id_hash?: string + email?: string + start_url?: string + region?: string + [key: string]: unknown +} + +export async function generateAuthUrl(payload: { + proxy_id?: number + provider?: string +}): Promise { + const { data } = await apiClient.post('/admin/kiro/oauth/auth-url', payload) + return data +} + +export async function generateIDCAuthUrl(payload: { + proxy_id?: number + start_url?: string + region?: string +}): Promise { + const { data } = await apiClient.post('/admin/kiro/oauth/idc-auth-url', payload) + return data +} + +export async function exchangeCode(payload: { + session_id: string + state: string + code: string + callback_path?: string + login_option?: string + proxy_id?: number +}): Promise { + const { data } = await apiClient.post('/admin/kiro/oauth/exchange-code', payload) + return data +} + +export async function refreshToken(payload: { + refresh_token: string + auth_method?: string + provider?: string + client_id?: string + client_secret?: string + start_url?: string + region?: string + profile_arn?: string + proxy_id?: number +}): Promise { + const { data } = await apiClient.post('/admin/kiro/oauth/refresh-token', payload) + return data +} + +export async function importToken(payload: { + token_json: string + device_registration_json?: string +}): Promise { + const { data } = await apiClient.post('/admin/kiro/oauth/import-token', payload) + return data +} + +export default { + generateAuthUrl, + generateIDCAuthUrl, + exchangeCode, + refreshToken, + importToken +} diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index d35e3b12..4a17d858 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -50,6 +50,15 @@ describe('useModelWhitelist', () => { expect(models.indexOf('gemini-2.5-flash-image')).toBeLessThan(models.indexOf('gemini-2.5-flash-lite')) }) + it('kiro 模型列表不暴露旧的 -agentic / -chat 后缀', () => { + const models = getModelsByPlatform('kiro') + + expect(models).toContain('claude-sonnet-4-6') + expect(models).toContain('claude-sonnet-4-6-thinking') + expect(models).not.toContain('claude-sonnet-4-6-chat') + expect(models.every((model) => !model.endsWith('-agentic') && !model.endsWith('-chat'))).toBe(true) + }) + it('whitelist 模式会忽略通配符条目', () => { const mapping = buildModelMappingObject('whitelist', ['claude-*', 'gemini-3.1-flash-image'], []) expect(mapping).toEqual({ diff --git a/frontend/src/composables/useKiroOAuth.ts b/frontend/src/composables/useKiroOAuth.ts new file mode 100644 index 00000000..010f231d --- /dev/null +++ b/frontend/src/composables/useKiroOAuth.ts @@ -0,0 +1,191 @@ +import { ref } from 'vue' +import { useI18n } from 'vue-i18n' +import { useAppStore } from '@/stores/app' +import { adminAPI } from '@/api/admin' +import type { KiroTokenInfo } from '@/api/admin/kiro' + +export function useKiroOAuth() { + const appStore = useAppStore() + const { t } = useI18n() + + const authUrl = ref('') + const sessionId = ref('') + const state = ref('') + const loading = ref(false) + const error = ref('') + + const resetState = () => { + authUrl.value = '' + sessionId.value = '' + state.value = '' + loading.value = false + error.value = '' + } + + const generateAuthUrl = async ( + proxyId: number | null | undefined, + provider: 'Google' | 'Github' = 'Google' + ): Promise => { + loading.value = true + error.value = '' + authUrl.value = '' + sessionId.value = '' + state.value = '' + + try { + const response = await adminAPI.kiro.generateAuthUrl({ + proxy_id: proxyId || undefined, + provider + }) + authUrl.value = response.auth_url + sessionId.value = response.session_id + state.value = response.state + return true + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.authFailed') + appStore.showError(error.value) + return false + } finally { + loading.value = false + } + } + + const generateIDCAuthUrl = async ( + params: { proxyId?: number | null; startUrl?: string; region?: string } + ): Promise => { + loading.value = true + error.value = '' + authUrl.value = '' + sessionId.value = '' + state.value = '' + + try { + const response = await adminAPI.kiro.generateIDCAuthUrl({ + proxy_id: params.proxyId || undefined, + start_url: params.startUrl, + region: params.region + }) + authUrl.value = response.auth_url + sessionId.value = response.session_id + state.value = response.state + return true + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.authFailed') + appStore.showError(error.value) + return false + } finally { + loading.value = false + } + } + + const exchangeAuthCode = async (params: { + code: string + sessionId: string + state: string + callbackPath?: string + loginOption?: string + proxyId?: number | null + }): Promise => { + loading.value = true + error.value = '' + try { + return await adminAPI.kiro.exchangeCode({ + session_id: params.sessionId, + state: params.state, + code: params.code.trim(), + callback_path: params.callbackPath, + login_option: params.loginOption, + proxy_id: params.proxyId || undefined + }) + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.authFailed') + appStore.showError(error.value) + return null + } finally { + loading.value = false + } + } + + const validateRefreshToken = async (payload: { + refreshToken: string + authMethod?: string + provider?: string + clientId?: string + clientSecret?: string + startUrl?: string + region?: string + profileArn?: string + proxyId?: number | null + }): Promise => { + loading.value = true + error.value = '' + try { + return await adminAPI.kiro.refreshToken({ + refresh_token: payload.refreshToken.trim(), + auth_method: payload.authMethod, + provider: payload.provider, + client_id: payload.clientId, + client_secret: payload.clientSecret, + start_url: payload.startUrl, + region: payload.region, + profile_arn: payload.profileArn, + proxy_id: payload.proxyId || undefined + }) + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.authFailed') + return null + } finally { + loading.value = false + } + } + + const importToken = async ( + tokenJSON: string, + deviceRegistrationJSON?: string + ): Promise => { + loading.value = true + error.value = '' + try { + return await adminAPI.kiro.importToken({ + token_json: tokenJSON, + device_registration_json: deviceRegistrationJSON + }) + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.authFailed') + appStore.showError(error.value) + return null + } finally { + loading.value = false + } + } + + const buildCredentials = (tokenInfo: KiroTokenInfo): Record => ({ + access_token: tokenInfo.access_token, + refresh_token: tokenInfo.refresh_token, + profile_arn: tokenInfo.profile_arn, + expires_at: tokenInfo.expires_at, + auth_method: tokenInfo.auth_method, + provider: tokenInfo.provider, + client_id: tokenInfo.client_id, + client_secret: tokenInfo.client_secret, + client_id_hash: tokenInfo.client_id_hash, + email: tokenInfo.email, + start_url: tokenInfo.start_url, + region: tokenInfo.region + }) + + return { + authUrl, + sessionId, + state, + loading, + error, + resetState, + generateAuthUrl, + generateIDCAuthUrl, + exchangeAuthCode, + validateRefreshToken, + importToken, + buildCredentials + } +} diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index c511b451..4e953fdc 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -76,6 +76,19 @@ const antigravityModels = [ 'tab_flash_lite_preview' ] +const kiroModels = [ + 'claude-opus-4-6', + 'claude-opus-4-6-thinking', + 'claude-sonnet-4-6', + 'claude-sonnet-4-6-thinking', + 'claude-opus-4-5-20251101', + 'claude-opus-4-5-20251101-thinking', + 'claude-sonnet-4-5-20250929', + 'claude-sonnet-4-5-20250929-thinking', + 'claude-haiku-4-5-20251001', + 'claude-haiku-4-5-20251001-thinking' +] + // 智谱 GLM const zhipuModels = [ 'glm-4', 'glm-4v', 'glm-4-plus', 'glm-4-0520', @@ -298,6 +311,19 @@ const antigravityPresetMappings = [ { label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'claude-opus-4-7', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' } ] +const kiroPresetMappings = [ + { label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'claude-opus-4.6', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-300' }, + { label: 'Opus 4.6 Thinking', from: 'claude-opus-4-6-thinking', to: 'claude-opus-4.6', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-300' }, + { label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4.6', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-300' }, + { label: 'Sonnet 4.6 Thinking', from: 'claude-sonnet-4-6-thinking', to: 'claude-sonnet-4.6', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-300' }, + { label: 'Opus 4.5', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4.5', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-300' }, + { label: 'Opus 4.5 Thinking', from: 'claude-opus-4-5-20251101-thinking', to: 'claude-opus-4.5', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-300' }, + { label: 'Sonnet 4.5', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4.5', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-300' }, + { label: 'Sonnet 4.5 Thinking', from: 'claude-sonnet-4-5-20250929-thinking', to: 'claude-sonnet-4.5', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-300' }, + { label: 'Haiku 4.5', from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4.5', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-300' }, + { label: 'Haiku 4.5 Thinking', from: 'claude-haiku-4-5-20251001-thinking', to: 'claude-haiku-4.5', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-300' } +] + // Bedrock 预设映射(与后端 DefaultBedrockModelMapping 保持一致) const bedrockPresetMappings = [ { label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'us.anthropic.claude-opus-4-6-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, @@ -308,15 +334,18 @@ const bedrockPresetMappings = [ { label: 'Haiku 4.5', from: 'claude-haiku-4-5', to: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }, ] +const kiroDefaultMappings = kiroPresetMappings.map(({ from, to }) => ({ from, to })) + // Antigravity 默认映射(从后端 API 获取,与 constants.go 保持一致) // 使用 fetchAntigravityDefaultMappings() 异步获取 import { getAntigravityDefaultModelMapping } from '@/api/admin/accounts' let _antigravityDefaultMappingsCache: { from: string; to: string }[] | null = null +let _kiroDefaultMappingsCache: { from: string; to: string }[] | null = null export async function fetchAntigravityDefaultMappings(): Promise<{ from: string; to: string }[]> { if (_antigravityDefaultMappingsCache !== null) { - return _antigravityDefaultMappingsCache + return _antigravityDefaultMappingsCache.map(({ from, to }) => ({ from, to })) } try { const mapping = await getAntigravityDefaultModelMapping() @@ -325,7 +354,15 @@ export async function fetchAntigravityDefaultMappings(): Promise<{ from: string; console.warn('[fetchAntigravityDefaultMappings] API failed, using empty fallback', e) _antigravityDefaultMappingsCache = [] } - return _antigravityDefaultMappingsCache + return _antigravityDefaultMappingsCache.map(({ from, to }) => ({ from, to })) +} + +export async function fetchKiroDefaultMappings(): Promise<{ from: string; to: string }[]> { + if (_kiroDefaultMappingsCache !== null) { + return _kiroDefaultMappingsCache.map(({ from, to }) => ({ from, to })) + } + _kiroDefaultMappingsCache = kiroDefaultMappings.map(({ from, to }) => ({ from, to })) + return _kiroDefaultMappingsCache.map(({ from, to }) => ({ from, to })) } // ===================== @@ -354,6 +391,7 @@ export function getModelsByPlatform(platform: string): string[] { case 'claude': return claudeModels case 'gemini': return geminiModels case 'antigravity': return antigravityModels + case 'kiro': return kiroModels case 'zhipu': return zhipuModels case 'qwen': return qwenModels case 'deepseek': return deepseekModels @@ -378,6 +416,7 @@ export function getPresetMappingsByPlatform(platform: string) { if (platform === 'openai') return openaiPresetMappings if (platform === 'gemini') return geminiPresetMappings if (platform === 'antigravity') return antigravityPresetMappings + if (platform === 'kiro') return kiroPresetMappings if (platform === 'bedrock') return bedrockPresetMappings return anthropicPresetMappings } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 479a8d95..71da2282 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -468,7 +468,7 @@ export interface PaginationConfig { // ==================== API Key & Group Types ==================== -export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' +export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'kiro' export type SubscriptionType = 'standard' | 'subscription' @@ -642,7 +642,7 @@ export interface UpdateGroupRequest { // ==================== Account & Proxy Types ==================== -export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' +export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'kiro' export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream' | 'bedrock' | 'service_account' export type OAuthAddMethod = 'oauth' | 'setup-token' export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h' @@ -801,6 +801,12 @@ export interface Account { overload_until: string | null temp_unschedulable_until: string | null temp_unschedulable_reason: string | null + kiro_quota_state?: string | null + kiro_quota_reason?: string | null + kiro_quota_reset_at?: string | null + kiro_runtime_state?: string | null + kiro_runtime_reason?: string | null + kiro_runtime_reset_at?: string | null // Session window fields (5-hour window) session_window_start: string | null @@ -885,6 +891,21 @@ export interface AntigravityModelQuota { reset_time: string // 重置时间 ISO8601 } +export interface KiroCreditProgress { + current_usage: number + usage_limit: number + percentage_used: number + days_remaining?: number + expiry_date?: string | null +} + +export interface KiroOverageInfo { + current_overages: number + overage_charges: number + currency_code?: string + currency_symbol?: string +} + export interface AccountUsageInfo { source?: 'passive' | 'active' updated_at: string | null @@ -903,6 +924,19 @@ export interface AccountUsageInfo { amount?: number minimum_balance?: number }> | null + kiro_subscription_name?: string | null + kiro_subscription_type?: string | null + kiro_reset_at?: string | null + kiro_overages_enabled?: boolean + kiro_credit?: KiroCreditProgress | null + kiro_bonus?: KiroCreditProgress | null + kiro_overage?: KiroOverageInfo | null + kiro_quota_state?: string | null + kiro_quota_reason?: string | null + kiro_quota_reset_at?: string | null + kiro_runtime_state?: string | null + kiro_runtime_reason?: string | null + kiro_runtime_reset_at?: string | null // Antigravity 403 forbidden 状态 is_forbidden?: boolean forbidden_reason?: string