mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 18:50:49 +08:00
feat(auth-files): add auth-file model suggestions for OAuth mappings
This commit is contained in:
@@ -30,10 +30,10 @@ export function PageTransition({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const currentLayerRef = useRef<HTMLDivElement>(null);
|
const currentLayerRef = useRef<HTMLDivElement>(null);
|
||||||
const exitingLayerRef = useRef<HTMLDivElement>(null);
|
const exitingLayerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const transitionDirectionRef = useRef<TransitionDirection>('forward');
|
||||||
const exitScrollOffsetRef = useRef(0);
|
const exitScrollOffsetRef = useRef(0);
|
||||||
|
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
const [transitionDirection, setTransitionDirection] = useState<TransitionDirection>('forward');
|
|
||||||
const [layers, setLayers] = useState<Layer[]>(() => [
|
const [layers, setLayers] = useState<Layer[]>(() => [
|
||||||
{
|
{
|
||||||
key: location.key,
|
key: location.key,
|
||||||
@@ -70,7 +70,7 @@ export function PageTransition({
|
|||||||
? 'forward'
|
? 'forward'
|
||||||
: 'backward';
|
: 'backward';
|
||||||
|
|
||||||
setTransitionDirection(nextDirection);
|
transitionDirectionRef.current = nextDirection;
|
||||||
setLayers((prev) => {
|
setLayers((prev) => {
|
||||||
const prevCurrent = prev[prev.length - 1];
|
const prevCurrent = prev[prev.length - 1];
|
||||||
return [
|
return [
|
||||||
@@ -96,12 +96,16 @@ export function PageTransition({
|
|||||||
|
|
||||||
if (!currentLayerRef.current) return;
|
if (!currentLayerRef.current) return;
|
||||||
|
|
||||||
|
const currentLayerEl = currentLayerRef.current;
|
||||||
|
const exitingLayerEl = exitingLayerRef.current;
|
||||||
|
|
||||||
const scrollContainer = resolveScrollContainer();
|
const scrollContainer = resolveScrollContainer();
|
||||||
const scrollOffset = exitScrollOffsetRef.current;
|
const scrollOffset = exitScrollOffsetRef.current;
|
||||||
if (scrollContainer && scrollOffset > 0) {
|
if (scrollContainer && scrollOffset > 0) {
|
||||||
scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transitionDirection = transitionDirectionRef.current;
|
||||||
const enterFromY = transitionDirection === 'forward' ? TRAVEL_DISTANCE : -TRAVEL_DISTANCE;
|
const enterFromY = transitionDirection === 'forward' ? TRAVEL_DISTANCE : -TRAVEL_DISTANCE;
|
||||||
const exitToY = transitionDirection === 'forward' ? -TRAVEL_DISTANCE : TRAVEL_DISTANCE;
|
const exitToY = transitionDirection === 'forward' ? -TRAVEL_DISTANCE : TRAVEL_DISTANCE;
|
||||||
const exitBaseY = scrollOffset ? -scrollOffset : 0;
|
const exitBaseY = scrollOffset ? -scrollOffset : 0;
|
||||||
@@ -114,10 +118,10 @@ export function PageTransition({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Exit animation: fade out with slight movement (runs simultaneously)
|
// Exit animation: fade out with slight movement (runs simultaneously)
|
||||||
if (exitingLayerRef.current) {
|
if (exitingLayerEl) {
|
||||||
gsap.set(exitingLayerRef.current, { y: exitBaseY });
|
gsap.set(exitingLayerEl, { y: exitBaseY });
|
||||||
tl.to(
|
tl.to(
|
||||||
exitingLayerRef.current,
|
exitingLayerEl,
|
||||||
{
|
{
|
||||||
y: exitBaseY + exitToY,
|
y: exitBaseY + exitToY,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@@ -131,7 +135,7 @@ export function PageTransition({
|
|||||||
|
|
||||||
// Enter animation: fade in with slight movement (runs simultaneously)
|
// Enter animation: fade in with slight movement (runs simultaneously)
|
||||||
tl.fromTo(
|
tl.fromTo(
|
||||||
currentLayerRef.current,
|
currentLayerEl,
|
||||||
{ y: enterFromY, opacity: 0 },
|
{ y: enterFromY, opacity: 0 },
|
||||||
{
|
{
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -140,8 +144,8 @@ export function PageTransition({
|
|||||||
ease: 'circ.out',
|
ease: 'circ.out',
|
||||||
force3D: true,
|
force3D: true,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
if (currentLayerRef.current) {
|
if (currentLayerEl) {
|
||||||
gsap.set(currentLayerRef.current, { clearProps: 'transform,opacity' });
|
gsap.set(currentLayerEl, { clearProps: 'transform,opacity' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -150,9 +154,9 @@ export function PageTransition({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
tl.kill();
|
tl.kill();
|
||||||
gsap.killTweensOf([currentLayerRef.current, exitingLayerRef.current]);
|
gsap.killTweensOf([currentLayerEl, exitingLayerEl]);
|
||||||
};
|
};
|
||||||
}, [isAnimating, transitionDirection, resolveScrollContainer]);
|
}, [isAnimating, resolveScrollContainer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`page-transition${isAnimating ? ' page-transition--animating' : ''}`}>
|
<div className={`page-transition${isAnimating ? ' page-transition--animating' : ''}`}>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const normalizeOpenAIBaseUrl = (baseUrl: string): string => {
|
|||||||
export const buildOpenAIModelsEndpoint = (baseUrl: string): string => {
|
export const buildOpenAIModelsEndpoint = (baseUrl: string): string => {
|
||||||
const trimmed = normalizeOpenAIBaseUrl(baseUrl);
|
const trimmed = normalizeOpenAIBaseUrl(baseUrl);
|
||||||
if (!trimmed) return '';
|
if (!trimmed) return '';
|
||||||
return trimmed.endsWith('/v1') ? `${trimmed}/models` : `${trimmed}/v1/models`;
|
return `${trimmed}/models`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => {
|
export const buildOpenAIChatCompletionsEndpoint = (baseUrl: string): string => {
|
||||||
|
|||||||
@@ -293,12 +293,12 @@
|
|||||||
"openai_model_name_placeholder": "Model name, e.g. moonshotai/kimi-k2:free",
|
"openai_model_name_placeholder": "Model name, e.g. moonshotai/kimi-k2:free",
|
||||||
"openai_model_alias_placeholder": "Model alias (optional)",
|
"openai_model_alias_placeholder": "Model alias (optional)",
|
||||||
"openai_models_add_btn": "Add Model",
|
"openai_models_add_btn": "Add Model",
|
||||||
"openai_models_fetch_button": "Fetch via /v1/models",
|
"openai_models_fetch_button": "Fetch via /models",
|
||||||
"openai_models_fetch_title": "Pick Models from /v1/models",
|
"openai_models_fetch_title": "Pick Models from /models",
|
||||||
"openai_models_fetch_hint": "Call the /v1/models endpoint using the Base URL above, sending the first API key as Bearer plus custom headers.",
|
"openai_models_fetch_hint": "Call the /models endpoint using the Base URL above, sending the first API key as Bearer plus custom headers.",
|
||||||
"openai_models_fetch_url_label": "Request URL",
|
"openai_models_fetch_url_label": "Request URL",
|
||||||
"openai_models_fetch_refresh": "Refresh",
|
"openai_models_fetch_refresh": "Refresh",
|
||||||
"openai_models_fetch_loading": "Fetching models from /v1/models...",
|
"openai_models_fetch_loading": "Fetching models from /models...",
|
||||||
"openai_models_fetch_empty": "No models returned. Please check the endpoint or auth.",
|
"openai_models_fetch_empty": "No models returned. Please check the endpoint or auth.",
|
||||||
"openai_models_fetch_error": "Failed to fetch models",
|
"openai_models_fetch_error": "Failed to fetch models",
|
||||||
"openai_models_fetch_back": "Back to edit",
|
"openai_models_fetch_back": "Back to edit",
|
||||||
@@ -519,6 +519,12 @@
|
|||||||
"provider_label": "Provider",
|
"provider_label": "Provider",
|
||||||
"provider_placeholder": "e.g. gemini-cli / vertex",
|
"provider_placeholder": "e.g. gemini-cli / vertex",
|
||||||
"provider_hint": "Defaults to the current filter; pick an existing provider or type a new name.",
|
"provider_hint": "Defaults to the current filter; pick an existing provider or type a new name.",
|
||||||
|
"model_source_label": "Auth file model source",
|
||||||
|
"model_source_placeholder": "Select an auth file (for model suggestions)",
|
||||||
|
"model_source_hint": "Pick an auth file to enable model suggestions for “Source model name”. You can still type custom values.",
|
||||||
|
"model_source_loading": "Loading models...",
|
||||||
|
"model_source_unsupported": "The current CPA version does not support fetching model lists (manual input still works).",
|
||||||
|
"model_source_loaded": "{{count}} models loaded. Use the dropdown in “Source model name”, or type custom values.",
|
||||||
"mappings_label": "Model mappings",
|
"mappings_label": "Model mappings",
|
||||||
"mapping_name_placeholder": "Source model name",
|
"mapping_name_placeholder": "Source model name",
|
||||||
"mapping_alias_placeholder": "Alias (required)",
|
"mapping_alias_placeholder": "Alias (required)",
|
||||||
@@ -799,9 +805,9 @@
|
|||||||
"not_loaded": "Not Loaded",
|
"not_loaded": "Not Loaded",
|
||||||
"seconds_ago": "seconds ago",
|
"seconds_ago": "seconds ago",
|
||||||
"models_title": "Available Models",
|
"models_title": "Available Models",
|
||||||
"models_desc": "Shows the /v1/models response and uses saved API keys for auth automatically.",
|
"models_desc": "Shows the /models response and uses saved API keys for auth automatically.",
|
||||||
"models_loading": "Loading available models...",
|
"models_loading": "Loading available models...",
|
||||||
"models_empty": "No models returned by /v1/models",
|
"models_empty": "No models returned by /models",
|
||||||
"models_error": "Failed to load model list",
|
"models_error": "Failed to load model list",
|
||||||
"models_count": "{{count}} available models",
|
"models_count": "{{count}} available models",
|
||||||
"version_check_title": "Update Check",
|
"version_check_title": "Update Check",
|
||||||
|
|||||||
@@ -293,12 +293,12 @@
|
|||||||
"openai_model_name_placeholder": "模型名称,如 moonshotai/kimi-k2:free",
|
"openai_model_name_placeholder": "模型名称,如 moonshotai/kimi-k2:free",
|
||||||
"openai_model_alias_placeholder": "模型别名 (可选)",
|
"openai_model_alias_placeholder": "模型别名 (可选)",
|
||||||
"openai_models_add_btn": "添加模型",
|
"openai_models_add_btn": "添加模型",
|
||||||
"openai_models_fetch_button": "从 /v1/models 获取",
|
"openai_models_fetch_button": "从 /models 获取",
|
||||||
"openai_models_fetch_title": "从 /v1/models 选择模型",
|
"openai_models_fetch_title": "从 /models 选择模型",
|
||||||
"openai_models_fetch_hint": "使用上方 Base URL 调用 /v1/models 端点,附带首个 API Key(Bearer)与自定义请求头。",
|
"openai_models_fetch_hint": "使用上方 Base URL 调用 /models 端点,附带首个 API Key(Bearer)与自定义请求头。",
|
||||||
"openai_models_fetch_url_label": "请求地址",
|
"openai_models_fetch_url_label": "请求地址",
|
||||||
"openai_models_fetch_refresh": "重新获取",
|
"openai_models_fetch_refresh": "重新获取",
|
||||||
"openai_models_fetch_loading": "正在从 /v1/models 获取模型列表...",
|
"openai_models_fetch_loading": "正在从 /models 获取模型列表...",
|
||||||
"openai_models_fetch_empty": "未获取到模型,请检查端点或鉴权信息。",
|
"openai_models_fetch_empty": "未获取到模型,请检查端点或鉴权信息。",
|
||||||
"openai_models_fetch_error": "获取模型失败",
|
"openai_models_fetch_error": "获取模型失败",
|
||||||
"openai_models_fetch_back": "返回编辑",
|
"openai_models_fetch_back": "返回编辑",
|
||||||
@@ -519,6 +519,12 @@
|
|||||||
"provider_label": "提供商",
|
"provider_label": "提供商",
|
||||||
"provider_placeholder": "例如 gemini-cli / vertex",
|
"provider_placeholder": "例如 gemini-cli / vertex",
|
||||||
"provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。",
|
"provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。",
|
||||||
|
"model_source_label": "模型来源认证文件",
|
||||||
|
"model_source_placeholder": "选择认证文件(用于原模型下拉建议)",
|
||||||
|
"model_source_hint": "选择一个认证文件后,“原模型名称”支持下拉选择;也可手动输入自定义模型。",
|
||||||
|
"model_source_loading": "正在加载模型列表...",
|
||||||
|
"model_source_unsupported": "当前 CPA 版本不支持获取模型列表(仍可手动输入)。",
|
||||||
|
"model_source_loaded": "已加载 {{count}} 个模型,可在“原模型名称”中下拉选择;也可手动输入。",
|
||||||
"mappings_label": "模型映射",
|
"mappings_label": "模型映射",
|
||||||
"mapping_name_placeholder": "原模型名称",
|
"mapping_name_placeholder": "原模型名称",
|
||||||
"mapping_alias_placeholder": "别名 (必填)",
|
"mapping_alias_placeholder": "别名 (必填)",
|
||||||
@@ -799,9 +805,9 @@
|
|||||||
"not_loaded": "未加载",
|
"not_loaded": "未加载",
|
||||||
"seconds_ago": "秒前",
|
"seconds_ago": "秒前",
|
||||||
"models_title": "可用模型列表",
|
"models_title": "可用模型列表",
|
||||||
"models_desc": "展示 /v1/models 返回的模型,并自动使用服务器保存的 API Key 进行鉴权。",
|
"models_desc": "展示 /models 返回的模型,并自动使用服务器保存的 API Key 进行鉴权。",
|
||||||
"models_loading": "正在加载可用模型...",
|
"models_loading": "正在加载可用模型...",
|
||||||
"models_empty": "未从 /v1/models 获取到模型数据",
|
"models_empty": "未从 /models 获取到模型数据",
|
||||||
"models_error": "获取模型列表失败",
|
"models_error": "获取模型列表失败",
|
||||||
"models_count": "可用模型 {{count}} 个",
|
"models_count": "可用模型 {{count}} 个",
|
||||||
"version_check_title": "版本检查",
|
"version_check_title": "版本检查",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import styles from './AuthFilesPage.module.scss';
|
|||||||
type ThemeColors = { bg: string; text: string; border?: string };
|
type ThemeColors = { bg: string; text: string; border?: string };
|
||||||
type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
|
type TypeColorSet = { light: ThemeColors; dark?: ThemeColors };
|
||||||
type ResolvedTheme = 'light' | 'dark';
|
type ResolvedTheme = 'light' | 'dark';
|
||||||
|
type AuthFileModelItem = { id: string; display_name?: string; type?: string; owned_by?: string };
|
||||||
|
|
||||||
// 标签类型颜色配置(对齐重构前 styles.css 的 file-type-badge 颜色)
|
// 标签类型颜色配置(对齐重构前 styles.css 的 file-type-badge 颜色)
|
||||||
const TYPE_COLORS: Record<string, TypeColorSet> = {
|
const TYPE_COLORS: Record<string, TypeColorSet> = {
|
||||||
@@ -203,6 +204,7 @@ export function AuthFilesPage() {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(9);
|
const [pageSize, setPageSize] = useState(9);
|
||||||
|
const [pageSizeInput, setPageSizeInput] = useState('9');
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [deleting, setDeleting] = useState<string | null>(null);
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||||||
const [deletingAll, setDeletingAll] = useState(false);
|
const [deletingAll, setDeletingAll] = useState(false);
|
||||||
@@ -217,12 +219,11 @@ export function AuthFilesPage() {
|
|||||||
// 模型列表弹窗相关
|
// 模型列表弹窗相关
|
||||||
const [modelsModalOpen, setModelsModalOpen] = useState(false);
|
const [modelsModalOpen, setModelsModalOpen] = useState(false);
|
||||||
const [modelsLoading, setModelsLoading] = useState(false);
|
const [modelsLoading, setModelsLoading] = useState(false);
|
||||||
const [modelsList, setModelsList] = useState<
|
const [modelsList, setModelsList] = useState<AuthFileModelItem[]>([]);
|
||||||
{ id: string; display_name?: string; type?: string }[]
|
|
||||||
>([]);
|
|
||||||
const [modelsFileName, setModelsFileName] = useState('');
|
const [modelsFileName, setModelsFileName] = useState('');
|
||||||
const [modelsFileType, setModelsFileType] = useState('');
|
const [modelsFileType, setModelsFileType] = useState('');
|
||||||
const [modelsError, setModelsError] = useState<'unsupported' | null>(null);
|
const [modelsError, setModelsError] = useState<'unsupported' | null>(null);
|
||||||
|
const modelsCacheRef = useRef<Map<string, AuthFileModelItem[]>>(new Map());
|
||||||
|
|
||||||
// OAuth 排除模型相关
|
// OAuth 排除模型相关
|
||||||
const [excluded, setExcluded] = useState<Record<string, string[]>>({});
|
const [excluded, setExcluded] = useState<Record<string, string[]>>({});
|
||||||
@@ -242,6 +243,10 @@ export function AuthFilesPage() {
|
|||||||
provider: '',
|
provider: '',
|
||||||
mappings: [buildEmptyMappingEntry()],
|
mappings: [buildEmptyMappingEntry()],
|
||||||
});
|
});
|
||||||
|
const [mappingModelsFileName, setMappingModelsFileName] = useState('');
|
||||||
|
const [mappingModelsList, setMappingModelsList] = useState<AuthFileModelItem[]>([]);
|
||||||
|
const [mappingModelsLoading, setMappingModelsLoading] = useState(false);
|
||||||
|
const [mappingModelsError, setMappingModelsError] = useState<'unsupported' | null>(null);
|
||||||
const [savingMappings, setSavingMappings] = useState(false);
|
const [savingMappings, setSavingMappings] = useState(false);
|
||||||
|
|
||||||
const [prefixProxyEditor, setPrefixProxyEditor] = useState<PrefixProxyEditorState | null>(null);
|
const [prefixProxyEditor, setPrefixProxyEditor] = useState<PrefixProxyEditorState | null>(null);
|
||||||
@@ -251,8 +256,105 @@ export function AuthFilesPage() {
|
|||||||
const excludedUnsupportedRef = useRef(false);
|
const excludedUnsupportedRef = useRef(false);
|
||||||
const mappingsUnsupportedRef = useRef(false);
|
const mappingsUnsupportedRef = useRef(false);
|
||||||
|
|
||||||
|
const normalizeProviderKey = (value: string) => value.trim().toLowerCase();
|
||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPageSizeInput(String(pageSize));
|
||||||
|
}, [pageSize]);
|
||||||
|
|
||||||
|
const modelSourceFileOptions = useMemo(() => {
|
||||||
|
const normalizedProvider = normalizeProviderKey(mappingForm.provider);
|
||||||
|
const matching: string[] = [];
|
||||||
|
const others: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||||
|
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
|
||||||
|
const canShowModels = !isRuntimeOnly || isAistudio;
|
||||||
|
if (!canShowModels) return;
|
||||||
|
|
||||||
|
const fileName = String(file.name || '').trim();
|
||||||
|
if (!fileName) return;
|
||||||
|
if (seen.has(fileName)) return;
|
||||||
|
seen.add(fileName);
|
||||||
|
|
||||||
|
if (!normalizedProvider) {
|
||||||
|
matching.push(fileName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeKey = normalizeProviderKey(String(file.type || ''));
|
||||||
|
const providerKey = normalizeProviderKey(String(file.provider || ''));
|
||||||
|
const isMatch = typeKey === normalizedProvider || providerKey === normalizedProvider;
|
||||||
|
if (isMatch) {
|
||||||
|
matching.push(fileName);
|
||||||
|
} else {
|
||||||
|
others.push(fileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
matching.sort((a, b) => a.localeCompare(b));
|
||||||
|
others.sort((a, b) => a.localeCompare(b));
|
||||||
|
return [...matching, ...others];
|
||||||
|
}, [files, mappingForm.provider]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mappingModalOpen) return;
|
||||||
|
|
||||||
|
const fileName = mappingModelsFileName.trim();
|
||||||
|
if (!fileName) {
|
||||||
|
setMappingModelsList([]);
|
||||||
|
setMappingModelsError(null);
|
||||||
|
setMappingModelsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = modelsCacheRef.current.get(fileName);
|
||||||
|
if (cached) {
|
||||||
|
setMappingModelsList(cached);
|
||||||
|
setMappingModelsError(null);
|
||||||
|
setMappingModelsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setMappingModelsLoading(true);
|
||||||
|
setMappingModelsError(null);
|
||||||
|
|
||||||
|
authFilesApi
|
||||||
|
.getModelsForAuthFile(fileName)
|
||||||
|
.then((models) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
modelsCacheRef.current.set(fileName, models);
|
||||||
|
setMappingModelsList(models);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const errorMessage = err instanceof Error ? err.message : '';
|
||||||
|
if (
|
||||||
|
errorMessage.includes('404') ||
|
||||||
|
errorMessage.includes('not found') ||
|
||||||
|
errorMessage.includes('Not Found')
|
||||||
|
) {
|
||||||
|
setMappingModelsList([]);
|
||||||
|
setMappingModelsError('unsupported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setMappingModelsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [mappingModalOpen, mappingModelsFileName, showNotification, t]);
|
||||||
|
|
||||||
const prefixProxyUpdatedText = useMemo(() => {
|
const prefixProxyUpdatedText = useMemo(() => {
|
||||||
if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? '';
|
if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? '';
|
||||||
const next: Record<string, unknown> = { ...prefixProxyEditor.json };
|
const next: Record<string, unknown> = { ...prefixProxyEditor.json };
|
||||||
@@ -276,12 +378,39 @@ export function AuthFilesPage() {
|
|||||||
return prefixProxyUpdatedText !== prefixProxyEditor.originalText;
|
return prefixProxyUpdatedText !== prefixProxyEditor.originalText;
|
||||||
}, [prefixProxyEditor?.json, prefixProxyEditor?.originalText, prefixProxyUpdatedText]);
|
}, [prefixProxyEditor?.json, prefixProxyEditor?.originalText, prefixProxyUpdatedText]);
|
||||||
|
|
||||||
const normalizeProviderKey = (value: string) => value.trim().toLowerCase();
|
const commitPageSizeInput = (rawValue: string) => {
|
||||||
|
const trimmed = rawValue.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
setPageSizeInput(String(pageSize));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Number(trimmed);
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
setPageSizeInput(String(pageSize));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = clampCardPageSize(value);
|
||||||
|
setPageSize(next);
|
||||||
|
setPageSizeInput(String(next));
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePageSizeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.currentTarget.valueAsNumber;
|
const rawValue = event.currentTarget.value;
|
||||||
if (!Number.isFinite(value)) return;
|
setPageSizeInput(rawValue);
|
||||||
setPageSize(clampCardPageSize(value));
|
|
||||||
|
const trimmed = rawValue.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
const parsed = Number(trimmed);
|
||||||
|
if (!Number.isFinite(parsed)) return;
|
||||||
|
|
||||||
|
const rounded = Math.round(parsed);
|
||||||
|
if (rounded < MIN_CARD_PAGE_SIZE || rounded > MAX_CARD_PAGE_SIZE) return;
|
||||||
|
|
||||||
|
setPageSize(rounded);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -835,9 +964,18 @@ export function AuthFilesPage() {
|
|||||||
setModelsList([]);
|
setModelsList([]);
|
||||||
setModelsError(null);
|
setModelsError(null);
|
||||||
setModelsModalOpen(true);
|
setModelsModalOpen(true);
|
||||||
|
|
||||||
|
const cached = modelsCacheRef.current.get(item.name);
|
||||||
|
if (cached) {
|
||||||
|
setModelsList(cached);
|
||||||
|
setModelsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setModelsLoading(true);
|
setModelsLoading(true);
|
||||||
try {
|
try {
|
||||||
const models = await authFilesApi.getModelsForAuthFile(item.name);
|
const models = await authFilesApi.getModelsForAuthFile(item.name);
|
||||||
|
modelsCacheRef.current.set(item.name, models);
|
||||||
setModelsList(models);
|
setModelsList(models);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 检测是否是 API 不支持的错误 (404 或特定错误消息)
|
// 检测是否是 API 不支持的错误 (404 或特定错误消息)
|
||||||
@@ -984,10 +1122,30 @@ export function AuthFilesPage() {
|
|||||||
? mappingProviderLookup.get(fallbackProvider.toLowerCase())
|
? mappingProviderLookup.get(fallbackProvider.toLowerCase())
|
||||||
: undefined;
|
: undefined;
|
||||||
const mappings = lookupKey ? modelMappings[lookupKey] : [];
|
const mappings = lookupKey ? modelMappings[lookupKey] : [];
|
||||||
|
const providerValue = lookupKey || fallbackProvider;
|
||||||
|
|
||||||
|
const normalizedProviderKey = normalizeProviderKey(providerValue);
|
||||||
|
const defaultModelsFileName = files
|
||||||
|
.filter((file) => {
|
||||||
|
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
|
||||||
|
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
|
||||||
|
const canShowModels = !isRuntimeOnly || isAistudio;
|
||||||
|
if (!canShowModels) return false;
|
||||||
|
if (!normalizedProviderKey) return false;
|
||||||
|
const typeKey = normalizeProviderKey(String(file.type || ''));
|
||||||
|
const providerKey = normalizeProviderKey(String(file.provider || ''));
|
||||||
|
return typeKey === normalizedProviderKey || providerKey === normalizedProviderKey;
|
||||||
|
})
|
||||||
|
.map((file) => file.name)
|
||||||
|
.sort((a, b) => a.localeCompare(b))[0];
|
||||||
|
|
||||||
setMappingForm({
|
setMappingForm({
|
||||||
provider: lookupKey || fallbackProvider,
|
provider: providerValue,
|
||||||
mappings: normalizeMappingEntries(mappings),
|
mappings: normalizeMappingEntries(mappings),
|
||||||
});
|
});
|
||||||
|
setMappingModelsFileName(defaultModelsFileName || '');
|
||||||
|
setMappingModelsList([]);
|
||||||
|
setMappingModelsError(null);
|
||||||
setMappingModalOpen(true);
|
setMappingModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1310,7 +1468,15 @@ export function AuthFilesPage() {
|
|||||||
{t('common.refresh')}
|
{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
size="sm"
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
disabled={disableControls || uploading}
|
||||||
|
loading={uploading}
|
||||||
|
>
|
||||||
|
{t('auth_files.upload_button')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDeleteAll}
|
onClick={handleDeleteAll}
|
||||||
disabled={disableControls || loading || deletingAll}
|
disabled={disableControls || loading || deletingAll}
|
||||||
@@ -1320,14 +1486,6 @@ export function AuthFilesPage() {
|
|||||||
? t('auth_files.delete_all_button')
|
? t('auth_files.delete_all_button')
|
||||||
: `${t('common.delete')} ${getTypeLabel(filter)}`}
|
: `${t('common.delete')} ${getTypeLabel(filter)}`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleUploadClick}
|
|
||||||
disabled={disableControls || uploading}
|
|
||||||
loading={uploading}
|
|
||||||
>
|
|
||||||
{t('auth_files.upload_button')}
|
|
||||||
</Button>
|
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -1365,8 +1523,14 @@ export function AuthFilesPage() {
|
|||||||
min={MIN_CARD_PAGE_SIZE}
|
min={MIN_CARD_PAGE_SIZE}
|
||||||
max={MAX_CARD_PAGE_SIZE}
|
max={MAX_CARD_PAGE_SIZE}
|
||||||
step={1}
|
step={1}
|
||||||
value={pageSize}
|
value={pageSizeInput}
|
||||||
onChange={handlePageSizeChange}
|
onChange={handlePageSizeChange}
|
||||||
|
onBlur={(e) => commitPageSizeInput(e.currentTarget.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1815,6 +1979,33 @@ export function AuthFilesPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.providerField}>
|
||||||
|
<Input
|
||||||
|
id="oauth-model-mapping-model-source"
|
||||||
|
list="oauth-model-mapping-model-source-options"
|
||||||
|
label={t('oauth_model_mappings.model_source_label')}
|
||||||
|
hint={
|
||||||
|
mappingModelsLoading
|
||||||
|
? t('oauth_model_mappings.model_source_loading')
|
||||||
|
: mappingModelsError === 'unsupported'
|
||||||
|
? t('oauth_model_mappings.model_source_unsupported')
|
||||||
|
: !mappingModelsFileName.trim()
|
||||||
|
? t('oauth_model_mappings.model_source_hint')
|
||||||
|
: t('oauth_model_mappings.model_source_loaded', {
|
||||||
|
count: mappingModelsList.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={t('oauth_model_mappings.model_source_placeholder')}
|
||||||
|
value={mappingModelsFileName}
|
||||||
|
onChange={(e) => setMappingModelsFileName(e.target.value)}
|
||||||
|
disabled={savingMappings}
|
||||||
|
/>
|
||||||
|
<datalist id="oauth-model-mapping-model-source-options">
|
||||||
|
{modelSourceFileOptions.map((fileName) => (
|
||||||
|
<option key={fileName} value={fileName} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
||||||
<div className="header-input-list">
|
<div className="header-input-list">
|
||||||
@@ -1824,6 +2015,7 @@ export function AuthFilesPage() {
|
|||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
||||||
|
list={mappingModelsList.length ? 'oauth-model-mapping-model-options' : undefined}
|
||||||
value={entry.name}
|
value={entry.name}
|
||||||
onChange={(e) => updateMappingEntry(index, 'name', e.target.value)}
|
onChange={(e) => updateMappingEntry(index, 'name', e.target.value)}
|
||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
@@ -1868,6 +2060,13 @@ export function AuthFilesPage() {
|
|||||||
{t('oauth_model_mappings.add_mapping')}
|
{t('oauth_model_mappings.add_mapping')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<datalist id="oauth-model-mapping-model-options">
|
||||||
|
{mappingModelsList.map((model) => (
|
||||||
|
<option key={model.id} value={model.id}>
|
||||||
|
{model.display_name && model.display_name !== model.id ? model.display_name : null}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div>
|
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const normalizeBaseUrl = (baseUrl: string): string => {
|
|||||||
const buildModelsEndpoint = (baseUrl: string): string => {
|
const buildModelsEndpoint = (baseUrl: string): string => {
|
||||||
const normalized = normalizeBaseUrl(baseUrl);
|
const normalized = normalizeBaseUrl(baseUrl);
|
||||||
if (!normalized) return '';
|
if (!normalized) return '';
|
||||||
return normalized.endsWith('/v1') ? `${normalized}/models` : `${normalized}/v1/models`;
|
return `${normalized}/models`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const modelsApi = {
|
export const modelsApi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user