mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Compare commits
3 Commits
@@ -0,0 +1 @@
|
||||
<svg fill="#ffffff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
|
||||
|
After Width: | Height: | Size: 752 B |
@@ -28,6 +28,7 @@ import {
|
||||
getTypeColor,
|
||||
getTypeLabel,
|
||||
isRuntimeOnlyAuthFile,
|
||||
normalizeProviderKey,
|
||||
parsePriorityValue,
|
||||
type QuotaProviderType,
|
||||
type ResolvedTheme,
|
||||
@@ -88,11 +89,12 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
failure: normalizeUsageTotal(file.failed),
|
||||
};
|
||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
|
||||
const providerKey = normalizeProviderKey(String(file.type ?? file.provider ?? 'unknown'));
|
||||
const isAistudio = providerKey === 'aistudio';
|
||||
const showModelsButton = !isRuntimeOnly || isAistudio;
|
||||
const typeColor = getTypeColor(file.type || 'unknown', resolvedTheme);
|
||||
const typeLabel = getTypeLabel(t, file.type || 'unknown');
|
||||
const providerIcon = getAuthFileIcon(file.type || 'unknown', resolvedTheme);
|
||||
const typeColor = getTypeColor(providerKey, resolvedTheme);
|
||||
const typeLabel = getTypeLabel(t, providerKey);
|
||||
const providerIcon = getAuthFileIcon(providerKey, resolvedTheme);
|
||||
|
||||
const quotaType =
|
||||
quotaFilterType && resolveQuotaType(file) === quotaFilterType ? quotaFilterType : null;
|
||||
|
||||
@@ -3,6 +3,8 @@ import iconAntigravity from '@/assets/icons/antigravity.svg';
|
||||
import iconClaude from '@/assets/icons/claude.svg';
|
||||
import iconCodex from '@/assets/icons/codex.svg';
|
||||
import iconGemini from '@/assets/icons/gemini.svg';
|
||||
import iconGrok from '@/assets/icons/grok.svg';
|
||||
import iconGrokDark from '@/assets/icons/grok-dark.svg';
|
||||
import iconIflow from '@/assets/icons/iflow.svg';
|
||||
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
|
||||
import iconKimiLight from '@/assets/icons/kimi-light.svg';
|
||||
@@ -82,6 +84,11 @@ export const TYPE_COLORS: Record<string, TypeColorSet> = {
|
||||
light: { bg: '#e0f7fa', text: '#006064' },
|
||||
dark: { bg: '#004d40', text: '#80deea' },
|
||||
},
|
||||
// xAI / Grok: graphite brand treatment, distinct from blue and purple providers
|
||||
xai: {
|
||||
light: { bg: '#f3f4f6', text: '#111827', border: '1px solid #d1d5db' },
|
||||
dark: { bg: '#111827', text: '#f9fafb', border: '1px solid #374151' },
|
||||
},
|
||||
// iFlow logo: 品红紫渐变 #5C5CFF → #AE5CFF,偏品红以区别于 Qwen 的紫罗兰
|
||||
iflow: {
|
||||
light: { bg: '#f5e3fc', text: '#9025c8' },
|
||||
@@ -109,6 +116,7 @@ export const AUTH_FILE_ICONS: Record<string, AuthFileIconAsset> = {
|
||||
codex: iconCodex,
|
||||
gemini: iconGemini,
|
||||
'gemini-cli': iconGemini,
|
||||
xai: { light: iconGrok, dark: iconGrokDark },
|
||||
iflow: iconIflow,
|
||||
kimi: { light: iconKimiLight, dark: iconKimiDark },
|
||||
qwen: iconQwen,
|
||||
@@ -128,7 +136,11 @@ export const resolveQuotaErrorMessage = (
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const normalizeProviderKey = (value: string) => value.trim().toLowerCase();
|
||||
export const normalizeProviderKey = (value: string) => {
|
||||
const key = value.trim().toLowerCase().replace(/_/g, '-');
|
||||
if (key === 'x-ai' || key === 'grok') return 'xai';
|
||||
return key;
|
||||
};
|
||||
|
||||
export const getAuthFileStatusMessage = (file: AuthFileItem): string => {
|
||||
const raw = file['status_message'] ?? file.statusMessage;
|
||||
@@ -141,15 +153,16 @@ export const hasAuthFileStatusMessage = (file: AuthFileItem): boolean =>
|
||||
getAuthFileStatusMessage(file).length > 0;
|
||||
|
||||
export const getTypeLabel = (t: TFunction, type: string): string => {
|
||||
const key = `auth_files.filter_${type}`;
|
||||
const providerKey = normalizeProviderKey(type);
|
||||
const key = `auth_files.filter_${providerKey}`;
|
||||
const translated = t(key);
|
||||
if (translated !== key) return translated;
|
||||
if (type.toLowerCase() === 'iflow') return 'iFlow';
|
||||
if (providerKey === 'iflow') return 'iFlow';
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
};
|
||||
|
||||
export const getTypeColor = (type: string, resolvedTheme: ResolvedTheme): ThemeColors => {
|
||||
const set = TYPE_COLORS[type] || TYPE_COLORS.unknown;
|
||||
const set = TYPE_COLORS[normalizeProviderKey(type)] || TYPE_COLORS.unknown;
|
||||
return resolvedTheme === 'dark' && set.dark ? set.dark : set.light;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getTypeLabel,
|
||||
hasAuthFileStatusMessage,
|
||||
isRuntimeOnlyAuthFile,
|
||||
normalizeProviderKey,
|
||||
} from '@/features/authFiles/constants';
|
||||
|
||||
type DeleteAllOptions = {
|
||||
@@ -310,7 +311,12 @@ export function useAuthFilesData(): UseAuthFilesDataResult {
|
||||
} else {
|
||||
const filesToDelete = files.filter((file) => {
|
||||
if (isRuntimeOnlyAuthFile(file)) return false;
|
||||
if (isFiltered && file.type !== filter) return false;
|
||||
if (
|
||||
isFiltered &&
|
||||
normalizeProviderKey(String(file.type ?? file.provider ?? '')) !== filter
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (isProblemOnly && !hasAuthFileStatusMessage(file)) return false;
|
||||
if (isDisabledOnly && file.disabled !== true) return false;
|
||||
return true;
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
"filter_claude": "Claude",
|
||||
"filter_codex": "Codex",
|
||||
"filter_antigravity": "Antigravity",
|
||||
"filter_xai": "xAI",
|
||||
"filter_iflow": "iFlow",
|
||||
"filter_vertex": "Vertex",
|
||||
"filter_empty": "Empty",
|
||||
@@ -552,6 +553,7 @@
|
||||
"type_claude": "Claude",
|
||||
"type_codex": "Codex",
|
||||
"type_antigravity": "Antigravity",
|
||||
"type_xai": "xAI",
|
||||
"type_iflow": "iFlow",
|
||||
"type_vertex": "Vertex",
|
||||
"type_empty": "Empty",
|
||||
@@ -901,6 +903,22 @@
|
||||
"kimi_oauth_status_error": "Authentication failed:",
|
||||
"kimi_oauth_start_error": "Failed to start Kimi OAuth:",
|
||||
"kimi_oauth_polling_error": "Failed to check authentication status:",
|
||||
"xai_oauth_title": "xAI OAuth",
|
||||
"xai_oauth_button": "Start xAI Login",
|
||||
"xai_oauth_hint": "Login to xAI Grok through OAuth flow, automatically obtain and save authentication files.",
|
||||
"xai_oauth_url_label": "Authorization URL:",
|
||||
"xai_open_link": "Open Link",
|
||||
"xai_copy_link": "Copy Link",
|
||||
"xai_oauth_status_waiting": "Waiting for authentication...",
|
||||
"xai_oauth_status_success": "Authentication successful!",
|
||||
"xai_oauth_status_error": "Authentication failed:",
|
||||
"xai_oauth_start_error": "Failed to start xAI OAuth:",
|
||||
"xai_oauth_polling_error": "Failed to check authentication status:",
|
||||
"xai_callback_label": "Callback URL or code",
|
||||
"xai_callback_placeholder": "Paste the code shown by Grok, or the full callback URL",
|
||||
"xai_callback_hint": "Grok may only show a code on the page. Paste that code directly and the app will submit it as http://127.0.0.1:56121/callback?... automatically.",
|
||||
"xai_callback_required": "Please paste the code shown by Grok or the full callback URL first.",
|
||||
"xai_callback_state_missing": "Missing the state for this xAI login. Start xAI login again, then submit the code.",
|
||||
"oauth_callback_label": "Callback URL",
|
||||
"oauth_callback_placeholder": "http://localhost:1455/auth/callback?code=...&state=...",
|
||||
"oauth_callback_hint": "Remote browser mode: after the provider redirects to http://localhost:..., copy the full URL and submit it here.",
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
"filter_claude": "Claude",
|
||||
"filter_codex": "Codex",
|
||||
"filter_antigravity": "Antigravity",
|
||||
"filter_xai": "xAI",
|
||||
"filter_iflow": "iFlow",
|
||||
"filter_vertex": "Vertex",
|
||||
"filter_empty": "Пусто",
|
||||
@@ -552,6 +553,7 @@
|
||||
"type_claude": "Claude",
|
||||
"type_codex": "Codex",
|
||||
"type_antigravity": "Antigravity",
|
||||
"type_xai": "xAI",
|
||||
"type_iflow": "iFlow",
|
||||
"type_vertex": "Vertex",
|
||||
"type_empty": "Пусто",
|
||||
@@ -898,6 +900,22 @@
|
||||
"kimi_oauth_status_error": "Ошибка аутентификации:",
|
||||
"kimi_oauth_start_error": "Не удалось запустить Kimi OAuth:",
|
||||
"kimi_oauth_polling_error": "Не удалось проверить статус аутентификации:",
|
||||
"xai_oauth_title": "xAI OAuth",
|
||||
"xai_oauth_button": "Начать вход xAI",
|
||||
"xai_oauth_hint": "Выполните вход в xAI Grok через OAuth и автоматически получите/сохраните файлы авторизации.",
|
||||
"xai_oauth_url_label": "URL авторизации:",
|
||||
"xai_open_link": "Открыть ссылку",
|
||||
"xai_copy_link": "Скопировать ссылку",
|
||||
"xai_oauth_status_waiting": "Ожидание аутентификации...",
|
||||
"xai_oauth_status_success": "Аутентификация успешна!",
|
||||
"xai_oauth_status_error": "Ошибка аутентификации:",
|
||||
"xai_oauth_start_error": "Не удалось запустить xAI OAuth:",
|
||||
"xai_oauth_polling_error": "Не удалось проверить статус аутентификации:",
|
||||
"xai_callback_label": "Callback URL или код",
|
||||
"xai_callback_placeholder": "Вставьте код со страницы Grok или полный callback URL",
|
||||
"xai_callback_hint": "Grok иногда показывает только код на странице. Вставьте этот код напрямую, приложение автоматически отправит его как http://127.0.0.1:56121/callback?...",
|
||||
"xai_callback_required": "Сначала вставьте код со страницы Grok или полный callback URL.",
|
||||
"xai_callback_state_missing": "Отсутствует state для этого входа xAI. Запустите вход xAI заново и затем отправьте код.",
|
||||
"oauth_callback_label": "Callback URL",
|
||||
"oauth_callback_placeholder": "http://localhost:1455/auth/callback?code=...&state=...",
|
||||
"oauth_callback_hint": "Режим удалённого браузера: после перенаправления провайдера на http://localhost:... скопируйте полный URL и отправьте его здесь.",
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
"filter_claude": "Claude",
|
||||
"filter_codex": "Codex",
|
||||
"filter_antigravity": "Antigravity",
|
||||
"filter_xai": "xAI",
|
||||
"filter_iflow": "iFlow",
|
||||
"filter_vertex": "Vertex",
|
||||
"filter_empty": "空文件",
|
||||
@@ -552,6 +553,7 @@
|
||||
"type_claude": "Claude",
|
||||
"type_codex": "Codex",
|
||||
"type_antigravity": "Antigravity",
|
||||
"type_xai": "xAI",
|
||||
"type_iflow": "iFlow",
|
||||
"type_vertex": "Vertex",
|
||||
"type_empty": "空文件",
|
||||
@@ -901,6 +903,22 @@
|
||||
"kimi_oauth_status_error": "认证失败:",
|
||||
"kimi_oauth_start_error": "启动 Kimi OAuth 失败:",
|
||||
"kimi_oauth_polling_error": "检查认证状态失败:",
|
||||
"xai_oauth_title": "xAI OAuth",
|
||||
"xai_oauth_button": "开始 xAI 登录",
|
||||
"xai_oauth_hint": "通过 OAuth 流程登录 xAI Grok 服务,自动获取并保存认证文件。",
|
||||
"xai_oauth_url_label": "授权链接:",
|
||||
"xai_open_link": "打开链接",
|
||||
"xai_copy_link": "复制链接",
|
||||
"xai_oauth_status_waiting": "等待认证中...",
|
||||
"xai_oauth_status_success": "认证成功!",
|
||||
"xai_oauth_status_error": "认证失败:",
|
||||
"xai_oauth_start_error": "启动 xAI OAuth 失败:",
|
||||
"xai_oauth_polling_error": "检查认证状态失败:",
|
||||
"xai_callback_label": "回调 URL 或授权码",
|
||||
"xai_callback_placeholder": "粘贴页面显示的 code,或完整 callback URL",
|
||||
"xai_callback_hint": "Grok 有时只在页面显示 code。可直接粘贴 code,系统会自动拼接为 http://127.0.0.1:56121/callback?... 后提交。",
|
||||
"xai_callback_required": "请先粘贴 Grok 页面显示的 code 或完整回调 URL。",
|
||||
"xai_callback_state_missing": "缺少本次 xAI 登录的 state,请重新开始 xAI 登录后再提交 code。",
|
||||
"oauth_callback_label": "回调 URL",
|
||||
"oauth_callback_placeholder": "http://localhost:1455/auth/callback?code=...&state=...",
|
||||
"oauth_callback_hint": "远程浏览器模式:当授权跳转到 http://localhost:... 后,复制完整 URL 并提交到这里。",
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
"filter_claude": "Claude",
|
||||
"filter_codex": "Codex",
|
||||
"filter_antigravity": "Antigravity",
|
||||
"filter_xai": "xAI",
|
||||
"filter_iflow": "iFlow",
|
||||
"filter_vertex": "Vertex",
|
||||
"filter_empty": "空檔案",
|
||||
@@ -552,6 +553,7 @@
|
||||
"type_claude": "Claude",
|
||||
"type_codex": "Codex",
|
||||
"type_antigravity": "Antigravity",
|
||||
"type_xai": "xAI",
|
||||
"type_iflow": "iFlow",
|
||||
"type_vertex": "Vertex",
|
||||
"type_empty": "空檔案",
|
||||
@@ -901,6 +903,22 @@
|
||||
"kimi_oauth_status_error": "驗證失敗:",
|
||||
"kimi_oauth_start_error": "啟動 Kimi OAuth 失敗:",
|
||||
"kimi_oauth_polling_error": "檢查驗證狀態失敗:",
|
||||
"xai_oauth_title": "xAI OAuth",
|
||||
"xai_oauth_button": "開始 xAI 登入",
|
||||
"xai_oauth_hint": "透過 OAuth 流程登入 xAI Grok 服務,自動取得並儲存驗證檔案。",
|
||||
"xai_oauth_url_label": "授權連結:",
|
||||
"xai_open_link": "開啟連結",
|
||||
"xai_copy_link": "複製連結",
|
||||
"xai_oauth_status_waiting": "等待驗證中...",
|
||||
"xai_oauth_status_success": "驗證成功!",
|
||||
"xai_oauth_status_error": "驗證失敗:",
|
||||
"xai_oauth_start_error": "啟動 xAI OAuth 失敗:",
|
||||
"xai_oauth_polling_error": "檢查驗證狀態失敗:",
|
||||
"xai_callback_label": "回調 URL 或授權碼",
|
||||
"xai_callback_placeholder": "貼上頁面顯示的 code,或完整 callback URL",
|
||||
"xai_callback_hint": "Grok 有時只在頁面顯示 code。可直接貼上 code,系統會自動拼接為 http://127.0.0.1:56121/callback?... 後提交。",
|
||||
"xai_callback_required": "請先貼上 Grok 頁面顯示的 code 或完整回調 URL。",
|
||||
"xai_callback_state_missing": "缺少本次 xAI 登入的 state,請重新開始 xAI 登入後再提交 code。",
|
||||
"qwen_oauth_title": "Qwen OAuth",
|
||||
"qwen_oauth_button": "開始 Qwen 登入",
|
||||
"qwen_oauth_hint": "透過裝置授權流程登入 Qwen 服務,自動取得並儲存驗證檔案。",
|
||||
|
||||
+16
-13
@@ -194,7 +194,7 @@ export function AuthFilesPage() {
|
||||
const persisted = readAuthFilesUiState();
|
||||
if (persisted) {
|
||||
if (typeof persisted.filter === 'string' && persisted.filter.trim()) {
|
||||
setFilter(persisted.filter);
|
||||
setFilter(normalizeProviderKey(persisted.filter));
|
||||
}
|
||||
if (typeof persisted.problemOnly === 'boolean') {
|
||||
setProblemOnly(persisted.problemOnly);
|
||||
@@ -349,9 +349,8 @@ export function AuthFilesPage() {
|
||||
const existingTypes = useMemo(() => {
|
||||
const types = new Set<string>(['all']);
|
||||
files.forEach((file) => {
|
||||
if (file.type) {
|
||||
types.add(file.type);
|
||||
}
|
||||
const type = normalizeProviderKey(String(file.type ?? file.provider ?? ''));
|
||||
if (type) types.add(type);
|
||||
});
|
||||
return Array.from(types);
|
||||
}, [files]);
|
||||
@@ -378,8 +377,9 @@ export function AuthFilesPage() {
|
||||
const typeCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = { all: filesMatchingStatusFilters.length };
|
||||
filesMatchingStatusFilters.forEach((file) => {
|
||||
if (!file.type) return;
|
||||
counts[file.type] = (counts[file.type] || 0) + 1;
|
||||
const type = normalizeProviderKey(String(file.type ?? file.provider ?? ''));
|
||||
if (!type) return;
|
||||
counts[type] = (counts[type] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
}, [filesMatchingStatusFilters]);
|
||||
@@ -391,7 +391,8 @@ export function AuthFilesPage() {
|
||||
const normalizedTerm = normalizedSearch.toLowerCase();
|
||||
|
||||
return filesMatchingStatusFilters.filter((item) => {
|
||||
const matchType = filter === 'all' || item.type === filter;
|
||||
const type = normalizeProviderKey(String(item.type ?? item.provider ?? ''));
|
||||
const matchType = normalizedFilter === 'all' || type === normalizedFilter;
|
||||
const matchSearch =
|
||||
!normalizedSearch ||
|
||||
[item.name, item.type, item.provider].some((value) => {
|
||||
@@ -402,7 +403,7 @@ export function AuthFilesPage() {
|
||||
});
|
||||
return matchType && matchSearch;
|
||||
});
|
||||
}, [filesMatchingStatusFilters, filter, normalizedSearch, wildcardSearch]);
|
||||
}, [filesMatchingStatusFilters, normalizedFilter, normalizedSearch, wildcardSearch]);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const copy = [...filtered];
|
||||
@@ -586,7 +587,7 @@ export function AuthFilesPage() {
|
||||
<div className={styles.filterRail}>
|
||||
<div className={styles.filterTags}>
|
||||
{existingTypes.map((type) => {
|
||||
const isActive = filter === type;
|
||||
const isActive = normalizedFilter === type;
|
||||
const iconSrc = getAuthFileIcon(type, resolvedTheme);
|
||||
const color =
|
||||
type === 'all'
|
||||
@@ -646,13 +647,15 @@ export function AuthFilesPage() {
|
||||
return t('auth_files.delete_filtered_result_button');
|
||||
}
|
||||
if (problemOnly) {
|
||||
return filter === 'all'
|
||||
return normalizedFilter === 'all'
|
||||
? t('auth_files.delete_problem_button')
|
||||
: t('auth_files.delete_problem_button_with_type', { type: getTypeLabel(t, filter) });
|
||||
: t('auth_files.delete_problem_button_with_type', {
|
||||
type: getTypeLabel(t, normalizedFilter),
|
||||
});
|
||||
}
|
||||
return filter === 'all'
|
||||
return normalizedFilter === 'all'
|
||||
? t('auth_files.delete_all_button')
|
||||
: `${t('common.delete')} ${getTypeLabel(t, filter)}`;
|
||||
: `${t('common.delete')} ${getTypeLabel(t, normalizedFilter)}`;
|
||||
})();
|
||||
|
||||
return (
|
||||
|
||||
+108
-7
@@ -16,6 +16,8 @@ import iconGemini from '@/assets/icons/gemini.svg';
|
||||
import iconKimiLight from '@/assets/icons/kimi-light.svg';
|
||||
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
|
||||
import iconVertex from '@/assets/icons/vertex.svg';
|
||||
import iconGrok from '@/assets/icons/grok.svg';
|
||||
import iconGrokDark from '@/assets/icons/grok-dark.svg';
|
||||
|
||||
interface ProviderState {
|
||||
url?: string;
|
||||
@@ -67,10 +69,18 @@ const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabe
|
||||
{ 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 },
|
||||
{ id: 'kimi', titleKey: 'auth_login.kimi_oauth_title', hintKey: 'auth_login.kimi_oauth_hint', urlLabelKey: 'auth_login.kimi_oauth_url_label', icon: { light: iconKimiLight, dark: iconKimiDark } }
|
||||
{ id: 'kimi', titleKey: 'auth_login.kimi_oauth_title', hintKey: 'auth_login.kimi_oauth_hint', urlLabelKey: 'auth_login.kimi_oauth_url_label', icon: { light: iconKimiLight, dark: iconKimiDark } },
|
||||
{ id: 'xai', titleKey: 'auth_login.xai_oauth_title', hintKey: 'auth_login.xai_oauth_hint', urlLabelKey: 'auth_login.xai_oauth_url_label', icon: { light: iconGrok, dark: iconGrokDark } }
|
||||
];
|
||||
|
||||
const CALLBACK_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli'];
|
||||
const CALLBACK_SUPPORTED: OAuthProvider[] = [
|
||||
'codex',
|
||||
'anthropic',
|
||||
'antigravity',
|
||||
'gemini-cli',
|
||||
'xai'
|
||||
];
|
||||
const XAI_CALLBACK_URL = 'http://127.0.0.1:56121/callback';
|
||||
const SUCCESS_RESET_DELAY_MS = 5000;
|
||||
const getProviderI18nPrefix = (provider: OAuthProvider) => provider.replace('-', '_');
|
||||
const getAuthKey = (provider: OAuthProvider, suffix: string) =>
|
||||
@@ -80,6 +90,77 @@ const getIcon = (icon: string | { light: string; dark: string }, theme: 'light'
|
||||
return typeof icon === 'string' ? icon : icon[theme];
|
||||
};
|
||||
|
||||
const isAbsoluteUrl = (value: string): boolean => {
|
||||
try {
|
||||
new URL(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const readQueryLikeCallbackInput = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const queryStart = trimmed.indexOf('?');
|
||||
const hashStart = trimmed.indexOf('#');
|
||||
const rawParams =
|
||||
queryStart >= 0
|
||||
? trimmed.slice(queryStart + 1)
|
||||
: hashStart >= 0
|
||||
? trimmed.slice(hashStart + 1)
|
||||
: trimmed;
|
||||
|
||||
if (!/(^|[&#?])(code|state|error)=/i.test(rawParams)) return null;
|
||||
return new URLSearchParams(rawParams.replace(/^[?#]/, ''));
|
||||
};
|
||||
|
||||
const extractDisplayedXaiCode = (value: string): string => {
|
||||
const trimmed = value.trim();
|
||||
const codeMatch = trimmed.match(/\bcode\s*[:=]\s*([^\s&]+)/i);
|
||||
return (codeMatch?.[1] ?? trimmed).trim();
|
||||
};
|
||||
|
||||
const buildXaiCallbackUrl = (input: string, state?: string): string | null => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return null;
|
||||
if (isAbsoluteUrl(trimmed)) return trimmed;
|
||||
|
||||
const params = readQueryLikeCallbackInput(trimmed);
|
||||
if (params) {
|
||||
const code = params.get('code')?.trim();
|
||||
const error = params.get('error')?.trim();
|
||||
const errorDescription = params.get('error_description')?.trim();
|
||||
const callbackState = params.get('state')?.trim() || state?.trim();
|
||||
if (!callbackState) return null;
|
||||
|
||||
const callbackUrl = new URL(XAI_CALLBACK_URL);
|
||||
callbackUrl.searchParams.set('state', callbackState);
|
||||
if (code) callbackUrl.searchParams.set('code', code);
|
||||
if (error) callbackUrl.searchParams.set('error', error);
|
||||
if (errorDescription) callbackUrl.searchParams.set('error_description', errorDescription);
|
||||
return callbackUrl.toString();
|
||||
}
|
||||
|
||||
const code = extractDisplayedXaiCode(trimmed);
|
||||
const callbackState = state?.trim();
|
||||
if (!code || !callbackState) return null;
|
||||
|
||||
const callbackUrl = new URL(XAI_CALLBACK_URL);
|
||||
callbackUrl.searchParams.set('code', code);
|
||||
callbackUrl.searchParams.set('state', callbackState);
|
||||
return callbackUrl.toString();
|
||||
};
|
||||
|
||||
const resolveCallbackUrl = (
|
||||
provider: OAuthProvider,
|
||||
input: string,
|
||||
state?: string
|
||||
): string | null => {
|
||||
if (provider !== 'xai') return input.trim();
|
||||
return buildXaiCallbackUrl(input, state);
|
||||
};
|
||||
|
||||
export function OAuthPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -262,9 +343,17 @@ export function OAuthPage() {
|
||||
};
|
||||
|
||||
const submitCallback = async (provider: OAuthProvider) => {
|
||||
const redirectUrl = (states[provider]?.callbackUrl || '').trim();
|
||||
const callbackInput = (states[provider]?.callbackUrl || '').trim();
|
||||
if (!callbackInput) {
|
||||
showNotification(
|
||||
t(provider === 'xai' ? 'auth_login.xai_callback_required' : 'auth_login.oauth_callback_required'),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const redirectUrl = resolveCallbackUrl(provider, callbackInput, states[provider]?.state);
|
||||
if (!redirectUrl) {
|
||||
showNotification(t('auth_login.oauth_callback_required'), 'warning');
|
||||
showNotification(t(provider === 'xai' ? 'auth_login.xai_callback_state_missing' : 'auth_login.missing_state'), 'warning');
|
||||
return;
|
||||
}
|
||||
updateProviderState(provider, {
|
||||
@@ -434,8 +523,16 @@ export function OAuthPage() {
|
||||
{canSubmitCallback && (
|
||||
<div className={styles.callbackSection}>
|
||||
<Input
|
||||
label={t('auth_login.oauth_callback_label')}
|
||||
hint={t('auth_login.oauth_callback_hint')}
|
||||
label={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_label'
|
||||
: 'auth_login.oauth_callback_label'
|
||||
)}
|
||||
hint={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_hint'
|
||||
: 'auth_login.oauth_callback_hint'
|
||||
)}
|
||||
value={state.callbackUrl || ''}
|
||||
onChange={(e) =>
|
||||
updateProviderState(provider.id, {
|
||||
@@ -444,7 +541,11 @@ export function OAuthPage() {
|
||||
callbackError: undefined
|
||||
})
|
||||
}
|
||||
placeholder={t('auth_login.oauth_callback_placeholder')}
|
||||
placeholder={t(
|
||||
provider.id === 'xai'
|
||||
? 'auth_login.xai_callback_placeholder'
|
||||
: 'auth_login.oauth_callback_placeholder'
|
||||
)}
|
||||
/>
|
||||
<div className={styles.callbackActions}>
|
||||
<Button
|
||||
|
||||
@@ -591,6 +591,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.premiumPlanValue {
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 18% 24%,
|
||||
rgba(255, 255, 255, 0.88) 0%,
|
||||
rgba(255, 255, 255, 0.58) 18%,
|
||||
rgba(255, 255, 255, 0) 42%
|
||||
),
|
||||
linear-gradient(135deg, #fff9e3 0%, #ffe07f 52%, #e0aa14 100%);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(133, 92, 0, 0.16),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.22) inset,
|
||||
0 0 10px rgba(255, 214, 98, 0.22);
|
||||
|
||||
&::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .premiumPlanValue {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 18% 24%,
|
||||
rgba(255, 229, 138, 0.28) 0%,
|
||||
rgba(255, 214, 98, 0.14) 18%,
|
||||
rgba(255, 214, 98, 0) 44%
|
||||
),
|
||||
linear-gradient(135deg, #4f3d0b 0%, #6e5510 48%, #8f6d10 100%);
|
||||
box-shadow:
|
||||
0 1px 6px rgba(0, 0, 0, 0.28),
|
||||
0 0 0 1px rgba(255, 220, 120, 0.16) inset,
|
||||
0 0 10px rgba(255, 196, 44, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.fileCard {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
@@ -26,6 +26,7 @@ import iconKimiLight from '@/assets/icons/kimi-light.svg';
|
||||
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
|
||||
import iconGlm from '@/assets/icons/glm.svg';
|
||||
import iconGrok from '@/assets/icons/grok.svg';
|
||||
import iconGrokDark from '@/assets/icons/grok-dark.svg';
|
||||
import iconDeepseek from '@/assets/icons/deepseek.svg';
|
||||
import iconMinimax from '@/assets/icons/minimax.svg';
|
||||
import styles from './SystemPage.module.scss';
|
||||
@@ -37,7 +38,7 @@ const MODEL_CATEGORY_ICONS: Record<string, string | { light: string; dark: strin
|
||||
qwen: iconQwen,
|
||||
kimi: { light: iconKimiLight, dark: iconKimiDark },
|
||||
glm: iconGlm,
|
||||
grok: iconGrok,
|
||||
grok: { light: iconGrok, dark: iconGrokDark },
|
||||
deepseek: iconDeepseek,
|
||||
minimax: iconMinimax,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ export type OAuthProvider =
|
||||
| 'anthropic'
|
||||
| 'antigravity'
|
||||
| 'gemini-cli'
|
||||
| 'kimi';
|
||||
| 'kimi'
|
||||
| 'xai';
|
||||
|
||||
export interface OAuthStartResponse {
|
||||
url: string;
|
||||
@@ -20,7 +21,13 @@ export interface OAuthCallbackResponse {
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
const WEBUI_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli'];
|
||||
const WEBUI_SUPPORTED: OAuthProvider[] = [
|
||||
'codex',
|
||||
'anthropic',
|
||||
'antigravity',
|
||||
'gemini-cli',
|
||||
'xai'
|
||||
];
|
||||
const CALLBACK_PROVIDER_MAP: Partial<Record<OAuthProvider, string>> = {
|
||||
'gemini-cli': 'gemini'
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export type AuthFileType =
|
||||
| 'claude'
|
||||
| 'codex'
|
||||
| 'antigravity'
|
||||
| 'xai'
|
||||
| 'iflow'
|
||||
| 'vertex'
|
||||
| 'empty'
|
||||
|
||||
@@ -58,14 +58,16 @@ export const OAUTH_CARD_IDS = [
|
||||
'anthropic-oauth-card',
|
||||
'antigravity-oauth-card',
|
||||
'gemini-cli-oauth-card',
|
||||
'kimi-oauth-card'
|
||||
'kimi-oauth-card',
|
||||
'xai-oauth-card'
|
||||
];
|
||||
export const OAUTH_PROVIDERS = {
|
||||
CODEX: 'codex',
|
||||
ANTHROPIC: 'anthropic',
|
||||
ANTIGRAVITY: 'antigravity',
|
||||
GEMINI_CLI: 'gemini-cli',
|
||||
KIMI: 'kimi'
|
||||
KIMI: 'kimi',
|
||||
XAI: 'xai'
|
||||
} as const;
|
||||
|
||||
// API 端点
|
||||
|
||||
Reference in New Issue
Block a user