diff --git a/src/assets/icons/gitlab.svg b/src/assets/icons/gitlab.svg new file mode 100644 index 00000000..8ac35ca9 --- /dev/null +++ b/src/assets/icons/gitlab.svg @@ -0,0 +1,10 @@ + diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 87379e65..926cffcc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -794,6 +794,45 @@ "codex_oauth_status_error": "Authentication failed:", "codex_oauth_start_error": "Failed to start Codex OAuth:", "codex_oauth_polling_error": "Failed to check authentication status:", + "gitlab_oauth_title": "GitLab Duo OAuth", + "gitlab_oauth_button": "Start GitLab Duo Login", + "gitlab_oauth_hint": "Connect GitLab Duo with an OAuth application and save the auth file for CLI Proxy API.", + "gitlab_oauth_url_label": "Authorization URL:", + "gitlab_open_link": "Open Link", + "gitlab_copy_link": "Copy Link", + "gitlab_oauth_status_waiting": "Waiting for authentication...", + "gitlab_oauth_status_success": "Authentication successful!", + "gitlab_oauth_status_error": "Authentication failed:", + "gitlab_oauth_start_error": "Failed to start GitLab Duo OAuth:", + "gitlab_oauth_polling_error": "Failed to check authentication status:", + "gitlab_oauth_base_url_label": "GitLab Base URL:", + "gitlab_oauth_base_url_hint": "Use https://gitlab.com for GitLab.com or enter your self-managed GitLab URL.", + "gitlab_oauth_base_url_placeholder": "https://gitlab.com", + "gitlab_oauth_client_id_label": "OAuth Client ID:", + "gitlab_oauth_client_id_hint": "Create a GitLab OAuth application and paste its application ID here.", + "gitlab_oauth_client_id_placeholder": "Paste the GitLab OAuth application ID", + "gitlab_oauth_client_secret_label": "OAuth Client Secret (optional):", + "gitlab_oauth_client_secret_hint": "Leave blank for a public PKCE app. Fill it only if your GitLab app requires a client secret.", + "gitlab_oauth_client_secret_placeholder": "Paste the GitLab OAuth application secret", + "gitlab_oauth_client_id_required": "Please provide the GitLab OAuth client ID first.", + "gitlab_pat_title": "GitLab Duo Personal Access Token", + "gitlab_pat_hint": "Use a GitLab personal access token to connect faster when Duo is already enabled on the account.", + "gitlab_pat_button": "Submit PAT Login", + "gitlab_pat_base_url_label": "GitLab Base URL:", + "gitlab_pat_base_url_hint": "Use https://gitlab.com for GitLab.com or enter your self-managed GitLab URL.", + "gitlab_pat_base_url_placeholder": "https://gitlab.com", + "gitlab_pat_token_label": "Personal Access Token:", + "gitlab_pat_token_hint": "The token should have access to the current user API and GitLab Duo direct access endpoints.", + "gitlab_pat_token_placeholder": "Paste a GitLab personal access token", + "gitlab_pat_required": "Please provide a GitLab personal access token first.", + "gitlab_pat_status_success": "GitLab Duo PAT login succeeded and credentials were saved.", + "gitlab_pat_status_error": "GitLab Duo PAT login failed:", + "gitlab_pat_start_error": "Failed to submit GitLab Duo PAT login:", + "gitlab_pat_result_title": "GitLab Duo PAT Result", + "gitlab_pat_result_username": "Username", + "gitlab_pat_result_email": "Email", + "gitlab_pat_result_path": "Saved Path", + "gitlab_pat_result_model": "Managed Model", "anthropic_oauth_title": "Anthropic OAuth", "anthropic_oauth_button": "Start Anthropic Login", "anthropic_oauth_hint": "Login to Anthropic (Claude) service through OAuth flow, automatically obtain and save authentication files.", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 261b12c1..eb782f68 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -797,6 +797,45 @@ "codex_oauth_status_error": "Ошибка аутентификации:", "codex_oauth_start_error": "Не удалось запустить Codex OAuth:", "codex_oauth_polling_error": "Не удалось проверить статус аутентификации:", + "gitlab_oauth_title": "GitLab Duo OAuth", + "gitlab_oauth_button": "Начать вход GitLab Duo", + "gitlab_oauth_hint": "Подключите GitLab Duo через OAuth-приложение GitLab и сохраните auth-файл для CLI Proxy API.", + "gitlab_oauth_url_label": "URL авторизации:", + "gitlab_open_link": "Открыть ссылку", + "gitlab_copy_link": "Скопировать ссылку", + "gitlab_oauth_status_waiting": "Ожидание аутентификации...", + "gitlab_oauth_status_success": "Аутентификация успешна!", + "gitlab_oauth_status_error": "Ошибка аутентификации:", + "gitlab_oauth_start_error": "Не удалось запустить GitLab Duo OAuth:", + "gitlab_oauth_polling_error": "Не удалось проверить статус аутентификации:", + "gitlab_oauth_base_url_label": "GitLab Base URL:", + "gitlab_oauth_base_url_hint": "Используйте https://gitlab.com для GitLab.com или укажите URL self-managed GitLab.", + "gitlab_oauth_base_url_placeholder": "https://gitlab.com", + "gitlab_oauth_client_id_label": "OAuth Client ID:", + "gitlab_oauth_client_id_hint": "Создайте GitLab OAuth application и вставьте его application ID сюда.", + "gitlab_oauth_client_id_placeholder": "Вставьте GitLab OAuth application ID", + "gitlab_oauth_client_secret_label": "OAuth Client Secret (необязательно):", + "gitlab_oauth_client_secret_hint": "Оставьте пустым для public PKCE app. Заполняйте только если ваше GitLab-приложение требует client secret.", + "gitlab_oauth_client_secret_placeholder": "Вставьте GitLab OAuth application secret", + "gitlab_oauth_client_id_required": "Сначала укажите GitLab OAuth client ID.", + "gitlab_pat_title": "GitLab Duo Personal Access Token", + "gitlab_pat_hint": "Используйте personal access token GitLab для быстрого подключения, если Duo уже доступен для этого аккаунта.", + "gitlab_pat_button": "Отправить вход по PAT", + "gitlab_pat_base_url_label": "GitLab Base URL:", + "gitlab_pat_base_url_hint": "Используйте https://gitlab.com для GitLab.com или укажите URL self-managed GitLab.", + "gitlab_pat_base_url_placeholder": "https://gitlab.com", + "gitlab_pat_token_label": "Personal Access Token:", + "gitlab_pat_token_hint": "Токен должен иметь доступ к API текущего пользователя и endpoint GitLab Duo direct access.", + "gitlab_pat_token_placeholder": "Вставьте GitLab personal access token", + "gitlab_pat_required": "Сначала укажите GitLab personal access token.", + "gitlab_pat_status_success": "Вход в GitLab Duo по PAT выполнен, учётные данные сохранены.", + "gitlab_pat_status_error": "Ошибка входа в GitLab Duo по PAT:", + "gitlab_pat_start_error": "Не удалось отправить вход в GitLab Duo по PAT:", + "gitlab_pat_result_title": "Результат входа GitLab Duo по PAT", + "gitlab_pat_result_username": "Имя пользователя", + "gitlab_pat_result_email": "Email", + "gitlab_pat_result_path": "Путь сохранения", + "gitlab_pat_result_model": "Управляемая модель", "anthropic_oauth_title": "Anthropic OAuth", "anthropic_oauth_button": "Начать вход Anthropic", "anthropic_oauth_hint": "Выполните вход в сервис Anthropic (Claude) через OAuth и автоматически получите/сохраните файлы авторизации.", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index ca186657..a0a51f93 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -794,6 +794,45 @@ "codex_oauth_status_error": "认证失败:", "codex_oauth_start_error": "启动 Codex OAuth 失败:", "codex_oauth_polling_error": "检查认证状态失败:", + "gitlab_oauth_title": "GitLab Duo OAuth", + "gitlab_oauth_button": "开始 GitLab Duo 登录", + "gitlab_oauth_hint": "通过 GitLab OAuth 应用连接 GitLab Duo,并为 CLI Proxy API 保存认证文件。", + "gitlab_oauth_url_label": "授权链接:", + "gitlab_open_link": "打开链接", + "gitlab_copy_link": "复制链接", + "gitlab_oauth_status_waiting": "等待认证中...", + "gitlab_oauth_status_success": "认证成功!", + "gitlab_oauth_status_error": "认证失败:", + "gitlab_oauth_start_error": "启动 GitLab Duo OAuth 失败:", + "gitlab_oauth_polling_error": "检查认证状态失败:", + "gitlab_oauth_base_url_label": "GitLab Base URL:", + "gitlab_oauth_base_url_hint": "GitLab.com 请填写 https://gitlab.com,自托管实例请填写对应地址。", + "gitlab_oauth_base_url_placeholder": "https://gitlab.com", + "gitlab_oauth_client_id_label": "OAuth Client ID:", + "gitlab_oauth_client_id_hint": "先在 GitLab 中创建 OAuth application,然后把 application ID 填到这里。", + "gitlab_oauth_client_id_placeholder": "填写 GitLab OAuth application ID", + "gitlab_oauth_client_secret_label": "OAuth Client Secret(可选):", + "gitlab_oauth_client_secret_hint": "如果是 public PKCE app 可留空,只有应用要求 client secret 时才填写。", + "gitlab_oauth_client_secret_placeholder": "填写 GitLab OAuth application secret", + "gitlab_oauth_client_id_required": "请先填写 GitLab OAuth client ID。", + "gitlab_pat_title": "GitLab Duo Personal Access Token", + "gitlab_pat_hint": "如果当前账号已经开通 Duo,可直接使用 GitLab personal access token 快速接入。", + "gitlab_pat_button": "提交 PAT 登录", + "gitlab_pat_base_url_label": "GitLab Base URL:", + "gitlab_pat_base_url_hint": "GitLab.com 请填写 https://gitlab.com,自托管实例请填写对应地址。", + "gitlab_pat_base_url_placeholder": "https://gitlab.com", + "gitlab_pat_token_label": "Personal Access Token:", + "gitlab_pat_token_hint": "该令牌需要能访问当前用户 API 和 GitLab Duo direct access 接口。", + "gitlab_pat_token_placeholder": "填写 GitLab personal access token", + "gitlab_pat_required": "请先填写 GitLab personal access token。", + "gitlab_pat_status_success": "GitLab Duo PAT 登录成功,凭据已保存。", + "gitlab_pat_status_error": "GitLab Duo PAT 登录失败:", + "gitlab_pat_start_error": "提交 GitLab Duo PAT 登录失败:", + "gitlab_pat_result_title": "GitLab Duo PAT 登录结果", + "gitlab_pat_result_username": "用户名", + "gitlab_pat_result_email": "邮箱", + "gitlab_pat_result_path": "保存路径", + "gitlab_pat_result_model": "托管模型", "anthropic_oauth_title": "Anthropic OAuth", "anthropic_oauth_button": "开始 Anthropic 登录", "anthropic_oauth_hint": "通过 OAuth 流程登录 Anthropic (Claude) 服务,自动获取并保存认证文件。", diff --git a/src/pages/OAuthPage.tsx b/src/pages/OAuthPage.tsx index 250ffedf..79ba0d20 100644 --- a/src/pages/OAuthPage.tsx +++ b/src/pages/OAuthPage.tsx @@ -4,7 +4,12 @@ import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { useNotificationStore, useThemeStore } from '@/stores'; -import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth'; +import { + oauthApi, + type GitLabPatAuthResponse, + type IFlowCookieAuthResponse, + type OAuthProvider +} from '@/services/api/oauth'; import { vertexApi, type VertexImportResponse } from '@/services/api/vertex'; import { copyToClipboard } from '@/utils/clipboard'; import styles from './OAuthPage.module.scss'; @@ -13,6 +18,7 @@ import iconCodexDark from '@/assets/icons/codex_drak.svg'; import iconClaude from '@/assets/icons/claude.svg'; import iconAntigravity from '@/assets/icons/antigravity.svg'; import iconGemini from '@/assets/icons/gemini.svg'; +import iconGitlab from '@/assets/icons/gitlab.svg'; import iconKimiLight from '@/assets/icons/kimi-light.svg'; import iconKimiDark from '@/assets/icons/kimi-dark.svg'; import iconQwen from '@/assets/icons/qwen.svg'; @@ -31,6 +37,9 @@ interface ProviderState { callbackSubmitting?: boolean; callbackStatus?: 'success' | 'error'; callbackError?: string; + baseUrl?: string; + clientId?: string; + clientSecret?: string; } interface IFlowCookieState { @@ -41,6 +50,14 @@ interface IFlowCookieState { errorType?: 'error' | 'warning'; } +interface GitLabPatState { + baseUrl: string; + personalAccessToken: string; + loading: boolean; + result?: GitLabPatAuthResponse; + error?: string; +} + interface VertexImportResult { projectId?: string; email?: string; @@ -74,6 +91,7 @@ function getErrorStatus(error: unknown): number | undefined { const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabelKey: string; icon: string | { light: string; dark: string } }[] = [ { id: 'codex', titleKey: 'auth_login.codex_oauth_title', hintKey: 'auth_login.codex_oauth_hint', urlLabelKey: 'auth_login.codex_oauth_url_label', icon: { light: iconCodexLight, dark: iconCodexDark } }, + { id: 'gitlab', titleKey: 'auth_login.gitlab_oauth_title', hintKey: 'auth_login.gitlab_oauth_hint', urlLabelKey: 'auth_login.gitlab_oauth_url_label', icon: iconGitlab }, { id: 'anthropic', titleKey: 'auth_login.anthropic_oauth_title', hintKey: 'auth_login.anthropic_oauth_hint', urlLabelKey: 'auth_login.anthropic_oauth_url_label', icon: iconClaude }, { id: 'antigravity', titleKey: 'auth_login.antigravity_oauth_title', hintKey: 'auth_login.antigravity_oauth_hint', urlLabelKey: 'auth_login.antigravity_oauth_url_label', icon: iconAntigravity }, { id: 'gemini-cli', titleKey: 'auth_login.gemini_cli_oauth_title', hintKey: 'auth_login.gemini_cli_oauth_hint', urlLabelKey: 'auth_login.gemini_cli_oauth_url_label', icon: iconGemini }, @@ -81,7 +99,7 @@ const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabe { id: 'qwen', titleKey: 'auth_login.qwen_oauth_title', hintKey: 'auth_login.qwen_oauth_hint', urlLabelKey: 'auth_login.qwen_oauth_url_label', icon: iconQwen } ]; -const CALLBACK_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli']; +const CALLBACK_SUPPORTED: OAuthProvider[] = ['codex', 'gitlab', 'anthropic', 'antigravity', 'gemini-cli']; const getProviderI18nPrefix = (provider: OAuthProvider) => provider.replace('-', '_'); const getAuthKey = (provider: OAuthProvider, suffix: string) => `auth_login.${getProviderI18nPrefix(provider)}_${suffix}`; @@ -96,6 +114,11 @@ export function OAuthPage() { const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const [states, setStates] = useState>({} as Record); const [iflowCookie, setIflowCookie] = useState({ cookie: '', loading: false }); + const [gitlabPat, setGitlabPat] = useState({ + baseUrl: 'https://gitlab.com', + personalAccessToken: '', + loading: false + }); const [vertexState, setVertexState] = useState({ fileName: '', location: '', @@ -160,10 +183,18 @@ export function OAuthPage() { ? 'ALL' : rawProjectId : undefined; + const gitlabState = provider === 'gitlab' ? states[provider] : undefined; + const gitlabClientId = provider === 'gitlab' ? (gitlabState?.clientId || '').trim() : ''; + const gitlabClientSecret = provider === 'gitlab' ? (gitlabState?.clientSecret || '').trim() : ''; + const gitlabBaseUrl = provider === 'gitlab' ? (gitlabState?.baseUrl || '').trim() : ''; // 项目 ID 可选:留空自动选择第一个可用项目;输入 ALL 获取全部项目 if (provider === 'gemini-cli') { updateProviderState(provider, { projectIdError: undefined }); } + if (provider === 'gitlab' && !gitlabClientId) { + showNotification(t('auth_login.gitlab_oauth_client_id_required'), 'warning'); + return; + } updateProviderState(provider, { status: 'waiting', polling: true, @@ -175,7 +206,15 @@ export function OAuthPage() { try { const res = await oauthApi.startAuth( provider, - provider === 'gemini-cli' ? { projectId: projectId || undefined } : undefined + provider === 'gemini-cli' + ? { projectId: projectId || undefined } + : provider === 'gitlab' + ? { + baseUrl: gitlabBaseUrl || undefined, + clientId: gitlabClientId, + clientSecret: gitlabClientSecret || undefined + } + : undefined ); updateProviderState(provider, { url: res.url, state: res.state, status: 'waiting', polling: true }); if (res.state) { @@ -191,6 +230,45 @@ export function OAuthPage() { } }; + const submitGitlabPat = async () => { + const personalAccessToken = gitlabPat.personalAccessToken.trim(); + if (!personalAccessToken) { + showNotification(t('auth_login.gitlab_pat_required'), 'warning'); + return; + } + + setGitlabPat((prev) => ({ + ...prev, + loading: true, + error: undefined, + result: undefined + })); + + try { + const res = await oauthApi.gitlabPatAuth({ + baseUrl: gitlabPat.baseUrl.trim() || undefined, + personalAccessToken + }); + setGitlabPat((prev) => ({ + ...prev, + loading: false, + result: res + })); + showNotification(t('auth_login.gitlab_pat_status_success'), 'success'); + } catch (err: unknown) { + const message = getErrorMessage(err); + setGitlabPat((prev) => ({ + ...prev, + loading: false, + error: message + })); + showNotification( + `${t('auth_login.gitlab_pat_start_error')}${message ? ` ${message}` : ''}`, + 'error' + ); + } + }; + const copyLink = async (url?: string) => { if (!url) return; const copied = await copyToClipboard(url); @@ -384,6 +462,48 @@ export function OAuthPage() { /> )} + {provider.id === 'gitlab' && ( + <> + + updateProviderState(provider.id, { + baseUrl: e.target.value + }) + } + placeholder={t('auth_login.gitlab_oauth_base_url_placeholder')} + /> + + updateProviderState(provider.id, { + clientId: e.target.value + }) + } + placeholder={t('auth_login.gitlab_oauth_client_id_placeholder')} + /> + + updateProviderState(provider.id, { + clientSecret: e.target.value + }) + } + placeholder={t('auth_login.gitlab_oauth_client_secret_placeholder')} + /> + + )} {state.url && (
{t(provider.urlLabelKey)}
@@ -454,6 +574,90 @@ export function OAuthPage() { ); })} + + + {t('auth_login.gitlab_pat_title')} + + } + extra={ + + } + > +
+
{t('auth_login.gitlab_pat_hint')}
+ + setGitlabPat((prev) => ({ + ...prev, + baseUrl: e.target.value + })) + } + placeholder={t('auth_login.gitlab_pat_base_url_placeholder')} + /> + + setGitlabPat((prev) => ({ + ...prev, + personalAccessToken: e.target.value + })) + } + placeholder={t('auth_login.gitlab_pat_token_placeholder')} + /> + {gitlabPat.error && ( +
+ {t('auth_login.gitlab_pat_status_error')} {gitlabPat.error} +
+ )} + {gitlabPat.result?.status === 'ok' && ( +
+
{t('auth_login.gitlab_pat_result_title')}
+
+ {gitlabPat.result.username && ( +
+ {t('auth_login.gitlab_pat_result_username')} + {gitlabPat.result.username} +
+ )} + {gitlabPat.result.email && ( +
+ {t('auth_login.gitlab_pat_result_email')} + {gitlabPat.result.email} +
+ )} + {gitlabPat.result.saved_path && ( +
+ {t('auth_login.gitlab_pat_result_path')} + {gitlabPat.result.saved_path} +
+ )} + {(gitlabPat.result.model_provider || gitlabPat.result.model_name) && ( +
+ {t('auth_login.gitlab_pat_result_model')} + + {[gitlabPat.result.model_provider, gitlabPat.result.model_name] + .filter(Boolean) + .join(' / ')} + +
+ )} +
+
+ )} +
+
+ {/* Vertex JSON 登录 */} > = { 'gemini-cli': 'gemini' }; export const oauthApi = { - startAuth: (provider: OAuthProvider, options?: { projectId?: string }) => { + startAuth: (provider: OAuthProvider, options?: OAuthStartOptions) => { const params: Record = {}; if (WEBUI_SUPPORTED.includes(provider)) { params.is_webui = true; @@ -44,13 +37,24 @@ export const oauthApi = { if (provider === 'gemini-cli' && options?.projectId) { params.project_id = options.projectId; } + if (provider === 'gitlab') { + if (options?.baseUrl) { + params.base_url = options.baseUrl; + } + if (options?.clientId) { + params.client_id = options.clientId; + } + if (options?.clientSecret) { + params.client_secret = options.clientSecret; + } + } return apiClient.get(`/${provider}-auth-url`, { params: Object.keys(params).length ? params : undefined }); }, getAuthStatus: (state: string) => - apiClient.get<{ status: 'ok' | 'wait' | 'error'; error?: string }>(`/get-auth-status`, { + apiClient.get(`/get-auth-status`, { params: { state } }), @@ -64,5 +68,11 @@ export const oauthApi = { /** iFlow cookie 认证 */ iflowCookieAuth: (cookie: string) => - apiClient.post('/iflow-auth-url', { cookie }) + apiClient.post('/iflow-auth-url', { cookie }), + + gitlabPatAuth: (payload: { baseUrl?: string; personalAccessToken: string }) => + apiClient.post('/gitlab-auth-url', { + base_url: payload.baseUrl, + personal_access_token: payload.personalAccessToken + }) }; diff --git a/src/types/oauth.ts b/src/types/oauth.ts index 524eb135..015e14b6 100644 --- a/src/types/oauth.ts +++ b/src/types/oauth.ts @@ -9,9 +9,31 @@ export type OAuthProvider = | 'anthropic' | 'antigravity' | 'gemini-cli' + | 'gitlab' | 'kimi' | 'qwen'; +export interface OAuthStartOptions { + projectId?: string; + baseUrl?: string; + clientId?: string; + clientSecret?: string; +} + +export interface OAuthStartResponse { + url: string; + state?: string; +} + +export interface OAuthStatusResponse { + status: 'ok' | 'wait' | 'error'; + error?: string; +} + +export interface OAuthCallbackResponse { + status: 'ok'; +} + // OAuth 流程状态 export interface OAuthFlow { provider: OAuthProvider; @@ -30,6 +52,26 @@ export interface OAuthConfig { redirectUri?: string; } +export interface IFlowCookieAuthResponse { + status: 'ok' | 'error'; + error?: string; + saved_path?: string; + email?: string; + expired?: string; + type?: string; +} + +export interface GitLabPatAuthResponse { + status: 'ok' | 'error'; + error?: string; + saved_path?: string; + username?: string; + email?: string; + token_label?: string; + model_provider?: string; + model_name?: string; +} + // OAuth 排除模型列表 export interface OAuthExcludedModels { models: string[];