diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2eb89df..6c711cf 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -330,6 +330,17 @@ "title": "Auth Files Management", "title_section": "Auth Files", "description": "Manage all CLI Proxy JSON auth files here (e.g. Qwen, Gemini, Vertex). Uploading a credential immediately enables the corresponding AI integration.", + "migrate_button": "Import from external?", + "migrate_title": "External Migration", + "migrate_subtitle": "gcli2api", + "migrate_email_label": "Email", + "migrate_email_placeholder": "Enter email to generate the credential", + "migrate_email_hint": "Used for the email field and the file name", + "migrate_email_required": "Enter an email first", + "migrate_upload_button": "Antigrvity Credential", + "migrate_parse_error": "Unable to parse credential file", + "migrate_missing_fields": "Credential missing required fields: {{fields}}", + "migrate_success": "Migration complete. Credential uploaded.", "upload_button": "Upload File", "delete_all_button": "Delete All", "empty_title": "No Auth Files", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 7848504..2f20b26 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -330,6 +330,17 @@ "title": "认证文件管理", "title_section": "认证文件", "description": "这里集中管理 CLI Proxy 支持的所有 JSON 认证文件(如 Qwen、Gemini、Vertex 等),上传后即可在运行时启用相应的 AI 服务。", + "migrate_button": "从外部迁移?", + "migrate_title": "外部迁移", + "migrate_subtitle": "gcli2api", + "migrate_email_label": "邮箱", + "migrate_email_placeholder": "输入邮箱用于生成认证文件", + "migrate_email_hint": "该邮箱将写入凭证并生成文件名", + "migrate_email_required": "请先填写邮箱", + "migrate_upload_button": "Antigrvity凭证", + "migrate_parse_error": "无法解析凭证文件", + "migrate_missing_fields": "凭证缺少必要字段: {{fields}}", + "migrate_success": "迁移完成,凭证已上传", "upload_button": "上传文件", "delete_all_button": "删除全部", "empty_title": "暂无认证文件", diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss index 8a48dc3..3eaf844 100644 --- a/src/pages/AuthFilesPage.module.scss +++ b/src/pages/AuthFilesPage.module.scss @@ -32,6 +32,23 @@ flex-wrap: wrap; } +.migrationContent { + display: flex; + flex-direction: column; + gap: $spacing-md; + + :global(.form-group) { + margin: 0; + } +} + +.migrationSubtitle { + font-size: 12px; + font-weight: 600; + color: var(--text-tertiary); + letter-spacing: 0.08em; +} + .titleWrapper { display: flex; align-items: center; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index adb3341..e21b15c 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -83,6 +83,7 @@ const OAUTH_PROVIDER_EXCLUDES = new Set(['all', 'unknown', 'empty']); const MIN_CARD_PAGE_SIZE = 3; const MAX_CARD_PAGE_SIZE = 30; const MAX_AUTH_FILE_SIZE = 50 * 1024; +const MIGRATION_EXPIRES_IN = 3599; const clampCardPageSize = (value: number) => Math.min(MAX_CARD_PAGE_SIZE, Math.max(MIN_CARD_PAGE_SIZE, Math.round(value))); @@ -178,6 +179,10 @@ export function AuthFilesPage() { const [deletingAll, setDeletingAll] = useState(false); const [keyStats, setKeyStats] = useState({ bySource: {}, byAuthIndex: {} }); const [usageDetails, setUsageDetails] = useState([]); + + const [migrationModalOpen, setMigrationModalOpen] = useState(false); + const [migrationEmail, setMigrationEmail] = useState(''); + const [migrationUploading, setMigrationUploading] = useState(false); // 详情弹窗相关 const [detailModalOpen, setDetailModalOpen] = useState(false); @@ -209,6 +214,7 @@ export function AuthFilesPage() { const [savingMappings, setSavingMappings] = useState(false); const fileInputRef = useRef(null); + const migrationFileInputRef = useRef(null); const loadingKeyStatsRef = useRef(false); const excludedUnsupportedRef = useRef(false); const mappingsUnsupportedRef = useRef(false); @@ -425,9 +431,13 @@ export function AuthFilesPage() { const handleUploadClick = () => { fileInputRef.current?.click(); }; + + const handleMigrationFilePick = () => { + migrationFileInputRef.current?.click(); + }; // 处理文件上传(支持多选) - const handleFileChange = async (event: React.ChangeEvent) => { + const handleFileChange = async (event: React.ChangeEvent) => { const fileList = event.target.files; if (!fileList || fileList.length === 0) return; @@ -490,8 +500,102 @@ export function AuthFilesPage() { } setUploading(false); - event.target.value = ''; - }; + event.target.value = ''; + }; + + const handleMigrationFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const trimmedEmail = migrationEmail.trim(); + if (!trimmedEmail) { + showNotification(t('auth_files.migrate_email_required'), 'warning'); + event.target.value = ''; + return; + } + + if (!file.name.endsWith('.json')) { + showNotification(t('auth_files.upload_error_json'), 'error'); + event.target.value = ''; + return; + } + + if (file.size > MAX_AUTH_FILE_SIZE) { + showNotification( + t('auth_files.upload_error_size', { maxSize: formatFileSize(MAX_AUTH_FILE_SIZE) }), + 'error' + ); + event.target.value = ''; + return; + } + + setMigrationUploading(true); + try { + const raw = await file.text(); + let payloadSource: Record | null = null; + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + payloadSource = parsed as Record; + } + } catch { + payloadSource = null; + } + + if (!payloadSource) { + showNotification(t('auth_files.migrate_parse_error'), 'error'); + return; + } + + const token = typeof payloadSource.token === 'string' ? payloadSource.token : ''; + const refreshToken = + typeof payloadSource.refresh_token === 'string' ? payloadSource.refresh_token : ''; + const projectId = typeof payloadSource.project_id === 'string' ? payloadSource.project_id : ''; + const expiry = typeof payloadSource.expiry === 'string' ? payloadSource.expiry : ''; + + const missingFields: string[] = []; + if (!token) missingFields.push('token'); + if (!expiry) missingFields.push('expiry'); + if (!refreshToken) missingFields.push('refresh_token'); + if (!projectId) missingFields.push('project_id'); + + if (missingFields.length > 0) { + showNotification( + t('auth_files.migrate_missing_fields', { fields: missingFields.join(', ') }), + 'error' + ); + return; + } + + const migratedPayload = { + access_token: token, + email: trimmedEmail, + expired: expiry, + expires_in: MIGRATION_EXPIRES_IN, + project_id: projectId, + refresh_token: refreshToken, + timestamp: Date.now(), + type: 'antigravity' + }; + + const fileName = `antigravity-${trimmedEmail.replace(/@/g, '_')}.json`; + const migratedFile = new File([JSON.stringify(migratedPayload)], fileName, { + type: 'application/json' + }); + + await authFilesApi.upload(migratedFile); + showNotification(t('auth_files.migrate_success'), 'success'); + await loadFiles(); + await loadKeyStats(); + setMigrationModalOpen(false); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.upload_failed')}: ${errorMessage}`, 'error'); + } finally { + setMigrationUploading(false); + event.target.value = ''; + } + }; // 删除单个文件 const handleDelete = async (name: string) => { @@ -1015,6 +1119,14 @@ export function AuthFilesPage() { title={titleNode} extra={
+ -
- )} + )} + setMigrationModalOpen(false)} + title={t('auth_files.migrate_title')} + footer={ + + } + > +
+
{t('auth_files.migrate_subtitle')}
+ setMigrationEmail(e.target.value)} + disabled={migrationUploading} + /> + + +
+
+ {/* OAuth 排除列表卡片 */}