mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat(auth): load model lists via /model-definitions/{channel} instead of per-file
model sources.
This commit is contained in:
@@ -512,7 +512,7 @@
|
|||||||
"upgrade_required_title": "Please upgrade CLI Proxy API",
|
"upgrade_required_title": "Please upgrade CLI Proxy API",
|
||||||
"upgrade_required_desc": "The current server does not support the OAuth excluded models API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
"upgrade_required_desc": "The current server does not support the OAuth excluded models API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
||||||
},
|
},
|
||||||
"oauth_model_mappings": {
|
"oauth_model_alias": {
|
||||||
"title": "OAuth Model Aliases",
|
"title": "OAuth Model Aliases",
|
||||||
"add": "Add Alias",
|
"add": "Add Alias",
|
||||||
"add_title": "Add provider model aliases",
|
"add_title": "Add provider model aliases",
|
||||||
|
|||||||
@@ -512,7 +512,7 @@
|
|||||||
"upgrade_required_title": "需要升级 CPA 版本",
|
"upgrade_required_title": "需要升级 CPA 版本",
|
||||||
"upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
"upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
||||||
},
|
},
|
||||||
"oauth_model_mappings": {
|
"oauth_model_alias": {
|
||||||
"title": "OAuth 模型别名",
|
"title": "OAuth 模型别名",
|
||||||
"add": "新增别名",
|
"add": "新增别名",
|
||||||
"add_title": "新增提供商模型别名",
|
"add_title": "新增提供商模型别名",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores';
|
import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||||
import { authFilesApi, usageApi } from '@/services/api';
|
import { authFilesApi, usageApi } from '@/services/api';
|
||||||
import { apiClient } from '@/services/api/client';
|
import { apiClient } from '@/services/api/client';
|
||||||
import type { AuthFileItem, OAuthModelMappingEntry } from '@/types';
|
import type { AuthFileItem, OAuthModelAliasEntry } from '@/types';
|
||||||
import {
|
import {
|
||||||
calculateStatusBarData,
|
calculateStatusBarData,
|
||||||
collectUsageDetails,
|
collectUsageDetails,
|
||||||
@@ -107,9 +107,9 @@ interface ExcludedFormState {
|
|||||||
modelsText: string;
|
modelsText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthModelMappingFormEntry = OAuthModelMappingEntry & { id: string };
|
type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string };
|
||||||
|
|
||||||
interface ModelMappingsFormState {
|
interface ModelAliasFormState {
|
||||||
provider: string;
|
provider: string;
|
||||||
mappings: OAuthModelMappingFormEntry[];
|
mappings: OAuthModelMappingFormEntry[];
|
||||||
}
|
}
|
||||||
@@ -237,14 +237,13 @@ export function AuthFilesPage() {
|
|||||||
const [savingExcluded, setSavingExcluded] = useState(false);
|
const [savingExcluded, setSavingExcluded] = useState(false);
|
||||||
|
|
||||||
// OAuth 模型映射相关
|
// OAuth 模型映射相关
|
||||||
const [modelMappings, setModelMappings] = useState<Record<string, OAuthModelMappingEntry[]>>({});
|
const [modelAlias, setModelAlias] = useState<Record<string, OAuthModelAliasEntry[]>>({});
|
||||||
const [modelMappingsError, setModelMappingsError] = useState<'unsupported' | null>(null);
|
const [modelAliasError, setModelAliasError] = useState<'unsupported' | null>(null);
|
||||||
const [mappingModalOpen, setMappingModalOpen] = useState(false);
|
const [mappingModalOpen, setMappingModalOpen] = useState(false);
|
||||||
const [mappingForm, setMappingForm] = useState<ModelMappingsFormState>({
|
const [mappingForm, setMappingForm] = useState<ModelAliasFormState>({
|
||||||
provider: '',
|
provider: '',
|
||||||
mappings: [buildEmptyMappingEntry()],
|
mappings: [buildEmptyMappingEntry()],
|
||||||
});
|
});
|
||||||
const [mappingModelsFileName, setMappingModelsFileName] = useState('');
|
|
||||||
const [mappingModelsList, setMappingModelsList] = useState<AuthFileModelItem[]>([]);
|
const [mappingModelsList, setMappingModelsList] = useState<AuthFileModelItem[]>([]);
|
||||||
const [mappingModelsLoading, setMappingModelsLoading] = useState(false);
|
const [mappingModelsLoading, setMappingModelsLoading] = useState(false);
|
||||||
const [mappingModelsError, setMappingModelsError] = useState<'unsupported' | null>(null);
|
const [mappingModelsError, setMappingModelsError] = useState<'unsupported' | null>(null);
|
||||||
@@ -265,55 +264,21 @@ export function AuthFilesPage() {
|
|||||||
setPageSizeInput(String(pageSize));
|
setPageSizeInput(String(pageSize));
|
||||||
}, [pageSize]);
|
}, [pageSize]);
|
||||||
|
|
||||||
const modelSourceFileOptions = useMemo(() => {
|
// 模型定义缓存(按 channel 缓存)
|
||||||
const normalizedProvider = normalizeProviderKey(mappingForm.provider);
|
const modelDefinitionsCacheRef = useRef<Map<string, AuthFileModelItem[]>>(new Map());
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (!mappingModalOpen) return;
|
if (!mappingModalOpen) return;
|
||||||
|
|
||||||
const fileName = mappingModelsFileName.trim();
|
const channel = normalizeProviderKey(mappingForm.provider);
|
||||||
if (!fileName) {
|
if (!channel) {
|
||||||
setMappingModelsList([]);
|
setMappingModelsList([]);
|
||||||
setMappingModelsError(null);
|
setMappingModelsError(null);
|
||||||
setMappingModelsLoading(false);
|
setMappingModelsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = modelsCacheRef.current.get(fileName);
|
const cached = modelDefinitionsCacheRef.current.get(channel);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setMappingModelsList(cached);
|
setMappingModelsList(cached);
|
||||||
setMappingModelsError(null);
|
setMappingModelsError(null);
|
||||||
@@ -326,10 +291,10 @@ export function AuthFilesPage() {
|
|||||||
setMappingModelsError(null);
|
setMappingModelsError(null);
|
||||||
|
|
||||||
authFilesApi
|
authFilesApi
|
||||||
.getModelsForAuthFile(fileName)
|
.getModelDefinitions(channel)
|
||||||
.then((models) => {
|
.then((models) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
modelsCacheRef.current.set(fileName, models);
|
modelDefinitionsCacheRef.current.set(channel, models);
|
||||||
setMappingModelsList(models);
|
setMappingModelsList(models);
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
@@ -354,7 +319,7 @@ export function AuthFilesPage() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [mappingModalOpen, mappingModelsFileName, showNotification, t]);
|
}, [mappingModalOpen, mappingForm.provider, showNotification, t]);
|
||||||
|
|
||||||
const prefixProxyUpdatedText = useMemo(() => {
|
const prefixProxyUpdatedText = useMemo(() => {
|
||||||
if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? '';
|
if (!prefixProxyEditor?.json) return prefixProxyEditor?.rawText ?? '';
|
||||||
@@ -489,12 +454,12 @@ export function AuthFilesPage() {
|
|||||||
}, [showNotification, t]);
|
}, [showNotification, t]);
|
||||||
|
|
||||||
// 加载 OAuth 模型映射
|
// 加载 OAuth 模型映射
|
||||||
const loadModelMappings = useCallback(async () => {
|
const loadModelAlias = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authFilesApi.getOauthModelMappings();
|
const res = await authFilesApi.getOauthModelAlias();
|
||||||
mappingsUnsupportedRef.current = false;
|
mappingsUnsupportedRef.current = false;
|
||||||
setModelMappings(res || {});
|
setModelAlias(res || {});
|
||||||
setModelMappingsError(null);
|
setModelAliasError(null);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const status =
|
const status =
|
||||||
typeof err === 'object' && err !== null && 'status' in err
|
typeof err === 'object' && err !== null && 'status' in err
|
||||||
@@ -502,11 +467,11 @@ export function AuthFilesPage() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (status === 404) {
|
if (status === 404) {
|
||||||
setModelMappings({});
|
setModelAlias({});
|
||||||
setModelMappingsError('unsupported');
|
setModelAliasError('unsupported');
|
||||||
if (!mappingsUnsupportedRef.current) {
|
if (!mappingsUnsupportedRef.current) {
|
||||||
mappingsUnsupportedRef.current = true;
|
mappingsUnsupportedRef.current = true;
|
||||||
showNotification(t('oauth_model_mappings.upgrade_required'), 'warning');
|
showNotification(t('oauth_model_alias.upgrade_required'), 'warning');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -515,8 +480,8 @@ export function AuthFilesPage() {
|
|||||||
}, [showNotification, t]);
|
}, [showNotification, t]);
|
||||||
|
|
||||||
const handleHeaderRefresh = useCallback(async () => {
|
const handleHeaderRefresh = useCallback(async () => {
|
||||||
await Promise.all([loadFiles(), loadKeyStats(), loadExcluded(), loadModelMappings()]);
|
await Promise.all([loadFiles(), loadKeyStats(), loadExcluded(), loadModelAlias()]);
|
||||||
}, [loadFiles, loadKeyStats, loadExcluded, loadModelMappings]);
|
}, [loadFiles, loadKeyStats, loadExcluded, loadModelAlias]);
|
||||||
|
|
||||||
useHeaderRefresh(handleHeaderRefresh);
|
useHeaderRefresh(handleHeaderRefresh);
|
||||||
|
|
||||||
@@ -524,8 +489,8 @@ export function AuthFilesPage() {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
loadKeyStats();
|
loadKeyStats();
|
||||||
loadExcluded();
|
loadExcluded();
|
||||||
loadModelMappings();
|
loadModelAlias();
|
||||||
}, [loadFiles, loadKeyStats, loadExcluded, loadModelMappings]);
|
}, [loadFiles, loadKeyStats, loadExcluded, loadModelAlias]);
|
||||||
|
|
||||||
// 定时刷新状态数据(每240秒)
|
// 定时刷新状态数据(每240秒)
|
||||||
useInterval(loadKeyStats, 240_000);
|
useInterval(loadKeyStats, 240_000);
|
||||||
@@ -554,14 +519,14 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
const mappingProviderLookup = useMemo(() => {
|
const mappingProviderLookup = useMemo(() => {
|
||||||
const lookup = new Map<string, string>();
|
const lookup = new Map<string, string>();
|
||||||
Object.keys(modelMappings).forEach((provider) => {
|
Object.keys(modelAlias).forEach((provider) => {
|
||||||
const key = provider.trim().toLowerCase();
|
const key = provider.trim().toLowerCase();
|
||||||
if (key && !lookup.has(key)) {
|
if (key && !lookup.has(key)) {
|
||||||
lookup.set(key, provider);
|
lookup.set(key, provider);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return lookup;
|
return lookup;
|
||||||
}, [modelMappings]);
|
}, [modelAlias]);
|
||||||
|
|
||||||
const providerOptions = useMemo(() => {
|
const providerOptions = useMemo(() => {
|
||||||
const extraProviders = new Set<string>();
|
const extraProviders = new Set<string>();
|
||||||
@@ -569,7 +534,7 @@ export function AuthFilesPage() {
|
|||||||
Object.keys(excluded).forEach((provider) => {
|
Object.keys(excluded).forEach((provider) => {
|
||||||
extraProviders.add(provider);
|
extraProviders.add(provider);
|
||||||
});
|
});
|
||||||
Object.keys(modelMappings).forEach((provider) => {
|
Object.keys(modelAlias).forEach((provider) => {
|
||||||
extraProviders.add(provider);
|
extraProviders.add(provider);
|
||||||
});
|
});
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@@ -591,7 +556,7 @@ export function AuthFilesPage() {
|
|||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
return [...OAUTH_PROVIDER_PRESETS, ...extraList];
|
return [...OAUTH_PROVIDER_PRESETS, ...extraList];
|
||||||
}, [excluded, files, modelMappings]);
|
}, [excluded, files, modelAlias]);
|
||||||
|
|
||||||
// 过滤和搜索
|
// 过滤和搜索
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@@ -1123,7 +1088,7 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
// OAuth 模型映射相关方法
|
// OAuth 模型映射相关方法
|
||||||
const normalizeMappingEntries = (
|
const normalizeMappingEntries = (
|
||||||
entries?: OAuthModelMappingEntry[]
|
entries?: OAuthModelAliasEntry[]
|
||||||
): OAuthModelMappingFormEntry[] => {
|
): OAuthModelMappingFormEntry[] => {
|
||||||
if (!Array.isArray(entries) || entries.length === 0) {
|
if (!Array.isArray(entries) || entries.length === 0) {
|
||||||
return [buildEmptyMappingEntry()];
|
return [buildEmptyMappingEntry()];
|
||||||
@@ -1142,29 +1107,13 @@ export function AuthFilesPage() {
|
|||||||
const lookupKey = fallbackProvider
|
const lookupKey = fallbackProvider
|
||||||
? mappingProviderLookup.get(fallbackProvider.toLowerCase())
|
? mappingProviderLookup.get(fallbackProvider.toLowerCase())
|
||||||
: undefined;
|
: undefined;
|
||||||
const mappings = lookupKey ? modelMappings[lookupKey] : [];
|
const mappings = lookupKey ? modelAlias[lookupKey] : [];
|
||||||
const providerValue = lookupKey || fallbackProvider;
|
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: providerValue,
|
provider: providerValue,
|
||||||
mappings: normalizeMappingEntries(mappings),
|
mappings: normalizeMappingEntries(mappings),
|
||||||
});
|
});
|
||||||
setMappingModelsFileName(defaultModelsFileName || '');
|
|
||||||
setMappingModelsList([]);
|
setMappingModelsList([]);
|
||||||
setMappingModelsError(null);
|
setMappingModelsError(null);
|
||||||
setMappingModalOpen(true);
|
setMappingModalOpen(true);
|
||||||
@@ -1172,7 +1121,7 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
const updateMappingEntry = (
|
const updateMappingEntry = (
|
||||||
index: number,
|
index: number,
|
||||||
field: keyof OAuthModelMappingEntry,
|
field: keyof OAuthModelAliasEntry,
|
||||||
value: string | boolean
|
value: string | boolean
|
||||||
) => {
|
) => {
|
||||||
setMappingForm((prev) => ({
|
setMappingForm((prev) => ({
|
||||||
@@ -1200,10 +1149,10 @@ export function AuthFilesPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveModelMappings = async () => {
|
const saveModelAlias = async () => {
|
||||||
const provider = mappingForm.provider.trim();
|
const provider = mappingForm.provider.trim();
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
showNotification(t('oauth_model_mappings.provider_required'), 'error');
|
showNotification(t('oauth_model_alias.provider_required'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1218,40 +1167,40 @@ export function AuthFilesPage() {
|
|||||||
seen.add(key);
|
seen.add(key);
|
||||||
return entry.fork ? { name, alias, fork: true } : { name, alias };
|
return entry.fork ? { name, alias, fork: true } : { name, alias };
|
||||||
})
|
})
|
||||||
.filter(Boolean) as OAuthModelMappingEntry[];
|
.filter(Boolean) as OAuthModelAliasEntry[];
|
||||||
|
|
||||||
setSavingMappings(true);
|
setSavingMappings(true);
|
||||||
try {
|
try {
|
||||||
if (mappings.length) {
|
if (mappings.length) {
|
||||||
await authFilesApi.saveOauthModelMappings(provider, mappings);
|
await authFilesApi.saveOauthModelAlias(provider, mappings);
|
||||||
} else {
|
} else {
|
||||||
await authFilesApi.deleteOauthModelMappings(provider);
|
await authFilesApi.deleteOauthModelAlias(provider);
|
||||||
}
|
}
|
||||||
await loadModelMappings();
|
await loadModelAlias();
|
||||||
showNotification(t('oauth_model_mappings.save_success'), 'success');
|
showNotification(t('oauth_model_alias.save_success'), 'success');
|
||||||
setMappingModalOpen(false);
|
setMappingModalOpen(false);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errorMessage = err instanceof Error ? err.message : '';
|
const errorMessage = err instanceof Error ? err.message : '';
|
||||||
showNotification(`${t('oauth_model_mappings.save_failed')}: ${errorMessage}`, 'error');
|
showNotification(`${t('oauth_model_alias.save_failed')}: ${errorMessage}`, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setSavingMappings(false);
|
setSavingMappings(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteModelMappings = async (provider: string) => {
|
const deleteModelAlias = async (provider: string) => {
|
||||||
showConfirmation({
|
showConfirmation({
|
||||||
title: t('oauth_model_mappings.delete_title', { defaultValue: 'Delete Mappings' }),
|
title: t('oauth_model_alias.delete_title', { defaultValue: 'Delete Mappings' }),
|
||||||
message: t('oauth_model_mappings.delete_confirm', { provider }),
|
message: t('oauth_model_alias.delete_confirm', { provider }),
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
confirmText: t('common.confirm'),
|
confirmText: t('common.confirm'),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
await authFilesApi.deleteOauthModelMappings(provider);
|
await authFilesApi.deleteOauthModelAlias(provider);
|
||||||
await loadModelMappings();
|
await loadModelAlias();
|
||||||
showNotification(t('oauth_model_mappings.delete_success'), 'success');
|
showNotification(t('oauth_model_alias.delete_success'), 'success');
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errorMessage = err instanceof Error ? err.message : '';
|
const errorMessage = err instanceof Error ? err.message : '';
|
||||||
showNotification(`${t('oauth_model_mappings.delete_failed')}: ${errorMessage}`, 'error');
|
showNotification(`${t('oauth_model_alias.delete_failed')}: ${errorMessage}`, 'error');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1657,42 +1606,42 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
{/* OAuth 模型映射卡片 */}
|
{/* OAuth 模型映射卡片 */}
|
||||||
<Card
|
<Card
|
||||||
title={t('oauth_model_mappings.title')}
|
title={t('oauth_model_alias.title')}
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => openMappingsModal()}
|
onClick={() => openMappingsModal()}
|
||||||
disabled={disableControls || modelMappingsError === 'unsupported'}
|
disabled={disableControls || modelAliasError === 'unsupported'}
|
||||||
>
|
>
|
||||||
{t('oauth_model_mappings.add')}
|
{t('oauth_model_alias.add')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{modelMappingsError === 'unsupported' ? (
|
{modelAliasError === 'unsupported' ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={t('oauth_model_mappings.upgrade_required_title')}
|
title={t('oauth_model_alias.upgrade_required_title')}
|
||||||
description={t('oauth_model_mappings.upgrade_required_desc')}
|
description={t('oauth_model_alias.upgrade_required_desc')}
|
||||||
/>
|
/>
|
||||||
) : Object.keys(modelMappings).length === 0 ? (
|
) : Object.keys(modelAlias).length === 0 ? (
|
||||||
<EmptyState title={t('oauth_model_mappings.list_empty_all')} />
|
<EmptyState title={t('oauth_model_alias.list_empty_all')} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.excludedList}>
|
<div className={styles.excludedList}>
|
||||||
{Object.entries(modelMappings).map(([provider, mappings]) => (
|
{Object.entries(modelAlias).map(([provider, mappings]) => (
|
||||||
<div key={provider} className={styles.excludedItem}>
|
<div key={provider} className={styles.excludedItem}>
|
||||||
<div className={styles.excludedInfo}>
|
<div className={styles.excludedInfo}>
|
||||||
<div className={styles.excludedProvider}>{provider}</div>
|
<div className={styles.excludedProvider}>{provider}</div>
|
||||||
<div className={styles.excludedModels}>
|
<div className={styles.excludedModels}>
|
||||||
{mappings?.length
|
{mappings?.length
|
||||||
? t('oauth_model_mappings.model_count', { count: mappings.length })
|
? t('oauth_model_alias.model_count', { count: mappings.length })
|
||||||
: t('oauth_model_mappings.no_models')}
|
: t('oauth_model_alias.no_models')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.excludedActions}>
|
<div className={styles.excludedActions}>
|
||||||
<Button variant="secondary" size="sm" onClick={() => openMappingsModal(provider)}>
|
<Button variant="secondary" size="sm" onClick={() => openMappingsModal(provider)}>
|
||||||
{t('common.edit')}
|
{t('common.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" size="sm" onClick={() => deleteModelMappings(provider)}>
|
<Button variant="danger" size="sm" onClick={() => deleteModelAlias(provider)}>
|
||||||
{t('oauth_model_mappings.delete')}
|
{t('oauth_model_alias.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1954,7 +1903,7 @@ export function AuthFilesPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={mappingModalOpen}
|
open={mappingModalOpen}
|
||||||
onClose={() => setMappingModalOpen(false)}
|
onClose={() => setMappingModalOpen(false)}
|
||||||
title={t('oauth_model_mappings.add_title')}
|
title={t('oauth_model_alias.add_title')}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -1964,8 +1913,8 @@ export function AuthFilesPage() {
|
|||||||
>
|
>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={saveModelMappings} loading={savingMappings}>
|
<Button onClick={saveModelAlias} loading={savingMappings}>
|
||||||
{t('oauth_model_mappings.save')}
|
{t('oauth_model_alias.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -1973,9 +1922,9 @@ export function AuthFilesPage() {
|
|||||||
<div className={styles.providerField}>
|
<div className={styles.providerField}>
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
id="oauth-model-alias-provider"
|
id="oauth-model-alias-provider"
|
||||||
label={t('oauth_model_mappings.provider_label')}
|
label={t('oauth_model_alias.provider_label')}
|
||||||
hint={t('oauth_model_mappings.provider_hint')}
|
hint={t('oauth_model_alias.provider_hint')}
|
||||||
placeholder={t('oauth_model_mappings.provider_placeholder')}
|
placeholder={t('oauth_model_alias.provider_placeholder')}
|
||||||
value={mappingForm.provider}
|
value={mappingForm.provider}
|
||||||
onChange={(val) => setMappingForm((prev) => ({ ...prev, provider: val }))}
|
onChange={(val) => setMappingForm((prev) => ({ ...prev, provider: val }))}
|
||||||
options={providerOptions}
|
options={providerOptions}
|
||||||
@@ -2000,37 +1949,27 @@ export function AuthFilesPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.providerField}>
|
{/* 模型定义加载状态提示 */}
|
||||||
<AutocompleteInput
|
{mappingForm.provider.trim() && (
|
||||||
id="oauth-model-mapping-model-source"
|
<div className={styles.hint}>
|
||||||
label={t('oauth_model_mappings.model_source_label')}
|
{mappingModelsLoading
|
||||||
hint={
|
? t('oauth_model_alias.model_source_loading')
|
||||||
mappingModelsLoading
|
: mappingModelsError === 'unsupported'
|
||||||
? t('oauth_model_mappings.model_source_loading')
|
? t('oauth_model_alias.model_source_unsupported')
|
||||||
: mappingModelsError === 'unsupported'
|
: t('oauth_model_alias.model_source_loaded', {
|
||||||
? t('oauth_model_mappings.model_source_unsupported')
|
count: mappingModelsList.length,
|
||||||
: !mappingModelsFileName.trim()
|
})}
|
||||||
? t('oauth_model_mappings.model_source_hint')
|
</div>
|
||||||
: t('oauth_model_mappings.model_source_loaded', {
|
)}
|
||||||
count: mappingModelsList.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={t('oauth_model_mappings.model_source_placeholder')}
|
|
||||||
value={mappingModelsFileName}
|
|
||||||
onChange={(val) => setMappingModelsFileName(val)}
|
|
||||||
disabled={savingMappings}
|
|
||||||
options={modelSourceFileOptions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
<label>{t('oauth_model_alias.mappings_label')}</label>
|
||||||
<div className="header-input-list">
|
<div className="header-input-list">
|
||||||
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map(
|
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map(
|
||||||
(entry, index) => (
|
(entry, index) => (
|
||||||
<div key={entry.id} className={styles.mappingRow}>
|
<div key={entry.id} className={styles.mappingRow}>
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
wrapperStyle={{ flex: 1, marginBottom: 0 }}
|
wrapperStyle={{ flex: 1, marginBottom: 0 }}
|
||||||
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
placeholder={t('oauth_model_alias.mapping_name_placeholder')}
|
||||||
value={entry.name}
|
value={entry.name}
|
||||||
onChange={(val) => updateMappingEntry(index, 'name', val)}
|
onChange={(val) => updateMappingEntry(index, 'name', val)}
|
||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
@@ -2042,7 +1981,7 @@ export function AuthFilesPage() {
|
|||||||
<span className={styles.mappingSeparator}>→</span>
|
<span className={styles.mappingSeparator}>→</span>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
placeholder={t('oauth_model_mappings.mapping_alias_placeholder')}
|
placeholder={t('oauth_model_alias.mapping_alias_placeholder')}
|
||||||
value={entry.alias}
|
value={entry.alias}
|
||||||
onChange={(e) => updateMappingEntry(index, 'alias', e.target.value)}
|
onChange={(e) => updateMappingEntry(index, 'alias', e.target.value)}
|
||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
@@ -2050,7 +1989,7 @@ export function AuthFilesPage() {
|
|||||||
/>
|
/>
|
||||||
<div className={styles.mappingFork}>
|
<div className={styles.mappingFork}>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
label={t('oauth_model_mappings.mapping_fork_label')}
|
label={t('oauth_model_alias.mapping_fork_label')}
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
checked={Boolean(entry.fork)}
|
checked={Boolean(entry.fork)}
|
||||||
onChange={(value) => updateMappingEntry(index, 'fork', value)}
|
onChange={(value) => updateMappingEntry(index, 'fork', value)}
|
||||||
@@ -2077,10 +2016,10 @@ export function AuthFilesPage() {
|
|||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
className="align-start"
|
className="align-start"
|
||||||
>
|
>
|
||||||
{t('oauth_model_mappings.add_mapping')}
|
{t('oauth_model_alias.add_mapping')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div>
|
<div className={styles.hint}>{t('oauth_model_alias.mappings_hint')}</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { apiClient } from './client';
|
import { apiClient } from './client';
|
||||||
import type { AuthFilesResponse } from '@/types/authFile';
|
import type { AuthFilesResponse } from '@/types/authFile';
|
||||||
import type { OAuthModelMappingEntry } from '@/types';
|
import type { OAuthModelAliasEntry } from '@/types';
|
||||||
|
|
||||||
type StatusError = { status?: number };
|
type StatusError = { status?: number };
|
||||||
type AuthFileStatusResponse = { status: string; disabled: boolean };
|
type AuthFileStatusResponse = { status: string; disabled: boolean };
|
||||||
@@ -53,18 +53,17 @@ const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthModelMappingEntry[]> => {
|
const normalizeOauthModelAlias = (payload: unknown): Record<string, OAuthModelAliasEntry[]> => {
|
||||||
if (!payload || typeof payload !== 'object') return {};
|
if (!payload || typeof payload !== 'object') return {};
|
||||||
|
|
||||||
const record = payload as Record<string, unknown>;
|
const record = payload as Record<string, unknown>;
|
||||||
const source =
|
const source =
|
||||||
record['oauth-model-mappings'] ??
|
|
||||||
record['oauth-model-alias'] ??
|
record['oauth-model-alias'] ??
|
||||||
record.items ??
|
record.items ??
|
||||||
payload;
|
payload;
|
||||||
if (!source || typeof source !== 'object') return {};
|
if (!source || typeof source !== 'object') return {};
|
||||||
|
|
||||||
const result: Record<string, OAuthModelMappingEntry[]> = {};
|
const result: Record<string, OAuthModelAliasEntry[]> = {};
|
||||||
|
|
||||||
Object.entries(source as Record<string, unknown>).forEach(([channel, mappings]) => {
|
Object.entries(source as Record<string, unknown>).forEach(([channel, mappings]) => {
|
||||||
const key = String(channel ?? '')
|
const key = String(channel ?? '')
|
||||||
@@ -86,12 +85,12 @@ const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthMode
|
|||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((entry) => {
|
.filter((entry) => {
|
||||||
const mapping = entry as OAuthModelMappingEntry;
|
const aliasEntry = entry as OAuthModelAliasEntry;
|
||||||
const dedupeKey = `${mapping.name.toLowerCase()}::${mapping.alias.toLowerCase()}::${mapping.fork ? '1' : '0'}`;
|
const dedupeKey = `${aliasEntry.name.toLowerCase()}::${aliasEntry.alias.toLowerCase()}::${aliasEntry.fork ? '1' : '0'}`;
|
||||||
if (seen.has(dedupeKey)) return false;
|
if (seen.has(dedupeKey)) return false;
|
||||||
seen.add(dedupeKey);
|
seen.add(dedupeKey);
|
||||||
return true;
|
return true;
|
||||||
}) as OAuthModelMappingEntry[];
|
}) as OAuthModelAliasEntry[];
|
||||||
|
|
||||||
if (normalized.length) {
|
if (normalized.length) {
|
||||||
result[key] = normalized;
|
result[key] = normalized;
|
||||||
@@ -101,8 +100,7 @@ const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthMode
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OAUTH_MODEL_MAPPINGS_ENDPOINT = '/oauth-model-mappings';
|
const OAUTH_MODEL_ALIAS_ENDPOINT = '/oauth-model-alias';
|
||||||
const OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT = '/oauth-model-alias';
|
|
||||||
|
|
||||||
export const authFilesApi = {
|
export const authFilesApi = {
|
||||||
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
||||||
@@ -143,63 +141,31 @@ export const authFilesApi = {
|
|||||||
replaceOauthExcludedModels: (map: Record<string, string[]>) =>
|
replaceOauthExcludedModels: (map: Record<string, string[]>) =>
|
||||||
apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)),
|
apiClient.put('/oauth-excluded-models', normalizeOauthExcludedModels(map)),
|
||||||
|
|
||||||
// OAuth 模型映射
|
// OAuth 模型别名
|
||||||
async getOauthModelMappings(): Promise<Record<string, OAuthModelMappingEntry[]>> {
|
async getOauthModelAlias(): Promise<Record<string, OAuthModelAliasEntry[]>> {
|
||||||
try {
|
const data = await apiClient.get(OAUTH_MODEL_ALIAS_ENDPOINT);
|
||||||
const data = await apiClient.get(OAUTH_MODEL_MAPPINGS_ENDPOINT);
|
return normalizeOauthModelAlias(data);
|
||||||
return normalizeOauthModelMappings(data);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (getStatusCode(err) !== 404) throw err;
|
|
||||||
const data = await apiClient.get(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT);
|
|
||||||
return normalizeOauthModelMappings(data);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
saveOauthModelMappings: async (channel: string, mappings: OAuthModelMappingEntry[]) => {
|
saveOauthModelAlias: async (channel: string, aliases: OAuthModelAliasEntry[]) => {
|
||||||
const normalizedChannel = String(channel ?? '')
|
const normalizedChannel = String(channel ?? '')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
const normalizedMappings = normalizeOauthModelMappings({ [normalizedChannel]: mappings })[normalizedChannel] ?? [];
|
const normalizedAliases = normalizeOauthModelAlias({ [normalizedChannel]: aliases })[normalizedChannel] ?? [];
|
||||||
|
await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: normalizedAliases });
|
||||||
try {
|
|
||||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: normalizedMappings });
|
|
||||||
return;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (getStatusCode(err) !== 404) throw err;
|
|
||||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT, { channel: normalizedChannel, aliases: normalizedMappings });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteOauthModelMappings: async (channel: string) => {
|
deleteOauthModelAlias: async (channel: string) => {
|
||||||
const normalizedChannel = String(channel ?? '')
|
const normalizedChannel = String(channel ?? '')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
const deleteViaPatch = async () => {
|
|
||||||
try {
|
|
||||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_ENDPOINT, { channel: normalizedChannel, mappings: [] });
|
|
||||||
return true;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (getStatusCode(err) !== 404) throw err;
|
|
||||||
await apiClient.patch(OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT, { channel: normalizedChannel, aliases: [] });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteViaPatch();
|
await apiClient.patch(OAUTH_MODEL_ALIAS_ENDPOINT, { channel: normalizedChannel, aliases: [] });
|
||||||
return;
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const status = getStatusCode(err);
|
const status = getStatusCode(err);
|
||||||
if (status !== 405) throw err;
|
if (status !== 405) throw err;
|
||||||
}
|
await apiClient.delete(`${OAUTH_MODEL_ALIAS_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`);
|
||||||
|
|
||||||
try {
|
|
||||||
await apiClient.delete(`${OAUTH_MODEL_MAPPINGS_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`);
|
|
||||||
return;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (getStatusCode(err) !== 404) throw err;
|
|
||||||
await apiClient.delete(`${OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT}?channel=${encodeURIComponent(normalizedChannel)}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -207,5 +173,13 @@ export const authFilesApi = {
|
|||||||
async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
async getModelsForAuthFile(name: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
||||||
const data = await apiClient.get(`/auth-files/models?name=${encodeURIComponent(name)}`);
|
const data = await apiClient.get(`/auth-files/models?name=${encodeURIComponent(name)}`);
|
||||||
return (data && Array.isArray(data['models'])) ? data['models'] : [];
|
return (data && Array.isArray(data['models'])) ? data['models'] : [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取指定 channel 的模型定义
|
||||||
|
async getModelDefinitions(channel: string): Promise<{ id: string; display_name?: string; type?: string; owned_by?: string }[]> {
|
||||||
|
const normalizedChannel = String(channel ?? '').trim().toLowerCase();
|
||||||
|
if (!normalizedChannel) return [];
|
||||||
|
const data = await apiClient.get(`/model-definitions/${encodeURIComponent(normalizedChannel)}`);
|
||||||
|
return (data && Array.isArray(data['models'])) ? data['models'] : [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ export interface OAuthExcludedModels {
|
|||||||
models: string[];
|
models: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuth 模型映射
|
// OAuth 模型别名
|
||||||
export interface OAuthModelMappingEntry {
|
export interface OAuthModelAliasEntry {
|
||||||
name: string;
|
name: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
fork?: boolean;
|
fork?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OAuthModelMappings = Record<string, OAuthModelMappingEntry[]>;
|
export type OAuthModelAlias = Record<string, OAuthModelAliasEntry[]>;
|
||||||
|
|||||||
Reference in New Issue
Block a user