mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat(ui): add model checklist for oauth exclusions
This commit is contained in:
@@ -488,8 +488,10 @@
|
|||||||
"provider_placeholder": "e.g. gemini-cli",
|
"provider_placeholder": "e.g. gemini-cli",
|
||||||
"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.",
|
||||||
"models_label": "Models to exclude",
|
"models_label": "Models to exclude",
|
||||||
"models_placeholder": "gpt-4.1-mini\n*-preview",
|
"models_loading": "Loading models...",
|
||||||
"models_hint": "Separate by commas or new lines; saving an empty list removes that provider. * wildcards are supported.",
|
"models_unsupported": "Current CPA version does not support fetching model lists.",
|
||||||
|
"models_loaded": "{{count}} models loaded. Check the models to exclude.",
|
||||||
|
"no_models_available": "No models available for this provider.",
|
||||||
"save": "Save/Update",
|
"save": "Save/Update",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"save_success": "Excluded models updated",
|
"save_success": "Excluded models updated",
|
||||||
|
|||||||
@@ -488,8 +488,10 @@
|
|||||||
"provider_placeholder": "例如 gemini-cli / openai",
|
"provider_placeholder": "例如 gemini-cli / openai",
|
||||||
"provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。",
|
"provider_hint": "默认选中当前筛选的提供商,也可直接输入或选择其他名称。",
|
||||||
"models_label": "排除的模型",
|
"models_label": "排除的模型",
|
||||||
"models_placeholder": "gpt-4.1-mini\n*-preview",
|
"models_loading": "正在加载模型列表...",
|
||||||
"models_hint": "逗号或换行分隔;留空保存将删除该提供商记录;支持 * 通配符。",
|
"models_unsupported": "当前 CPA 版本不支持获取模型列表。",
|
||||||
|
"models_loaded": "已加载 {{count}} 个模型,勾选要排除的模型。",
|
||||||
|
"no_models_available": "该提供商暂无可用模型列表。",
|
||||||
"save": "保存/更新",
|
"save": "保存/更新",
|
||||||
"saving": "正在保存...",
|
"saving": "正在保存...",
|
||||||
"save_success": "排除列表已更新",
|
"save_success": "排除列表已更新",
|
||||||
|
|||||||
@@ -995,3 +995,53 @@
|
|||||||
border: 1px solid var(--danger-color);
|
border: 1px solid var(--danger-color);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 排除模型勾选列表
|
||||||
|
.excludedCheckList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: $radius-md;
|
||||||
|
padding: $spacing-sm;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.excludedCheckItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
padding: $spacing-xs $spacing-sm;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color $transition-fast;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.excludedCheckLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
color: var(--text-primary);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excludedCheckDisplayName {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const clampCardPageSize = (value: number) =>
|
|||||||
|
|
||||||
interface ExcludedFormState {
|
interface ExcludedFormState {
|
||||||
provider: string;
|
provider: string;
|
||||||
modelsText: string;
|
selectedModels: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string };
|
type OAuthModelMappingFormEntry = OAuthModelAliasEntry & { id: string };
|
||||||
@@ -232,8 +232,11 @@ export function AuthFilesPage() {
|
|||||||
const [excludedModalOpen, setExcludedModalOpen] = useState(false);
|
const [excludedModalOpen, setExcludedModalOpen] = useState(false);
|
||||||
const [excludedForm, setExcludedForm] = useState<ExcludedFormState>({
|
const [excludedForm, setExcludedForm] = useState<ExcludedFormState>({
|
||||||
provider: '',
|
provider: '',
|
||||||
modelsText: '',
|
selectedModels: new Set(),
|
||||||
});
|
});
|
||||||
|
const [excludedModelsList, setExcludedModelsList] = useState<AuthFileModelItem[]>([]);
|
||||||
|
const [excludedModelsLoading, setExcludedModelsLoading] = useState(false);
|
||||||
|
const [excludedModelsError, setExcludedModelsError] = useState<'unsupported' | null>(null);
|
||||||
const [savingExcluded, setSavingExcluded] = useState(false);
|
const [savingExcluded, setSavingExcluded] = useState(false);
|
||||||
|
|
||||||
// OAuth 模型映射相关
|
// OAuth 模型映射相关
|
||||||
@@ -321,6 +324,61 @@ export function AuthFilesPage() {
|
|||||||
};
|
};
|
||||||
}, [mappingModalOpen, mappingForm.provider, showNotification, t]);
|
}, [mappingModalOpen, mappingForm.provider, showNotification, t]);
|
||||||
|
|
||||||
|
// 排除列表弹窗:根据 provider 加载模型定义
|
||||||
|
useEffect(() => {
|
||||||
|
if (!excludedModalOpen) return;
|
||||||
|
|
||||||
|
const channel = normalizeProviderKey(excludedForm.provider);
|
||||||
|
if (!channel) {
|
||||||
|
setExcludedModelsList([]);
|
||||||
|
setExcludedModelsError(null);
|
||||||
|
setExcludedModelsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = modelDefinitionsCacheRef.current.get(channel);
|
||||||
|
if (cached) {
|
||||||
|
setExcludedModelsList(cached);
|
||||||
|
setExcludedModelsError(null);
|
||||||
|
setExcludedModelsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setExcludedModelsLoading(true);
|
||||||
|
setExcludedModelsError(null);
|
||||||
|
|
||||||
|
authFilesApi
|
||||||
|
.getModelDefinitions(channel)
|
||||||
|
.then((models) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
modelDefinitionsCacheRef.current.set(channel, models);
|
||||||
|
setExcludedModelsList(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')
|
||||||
|
) {
|
||||||
|
setExcludedModelsList([]);
|
||||||
|
setExcludedModelsError('unsupported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showNotification(`${t('notification.load_failed')}: ${errorMessage}`, 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setExcludedModelsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [excludedModalOpen, excludedForm.provider, 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 };
|
||||||
@@ -1008,11 +1066,13 @@ export function AuthFilesPage() {
|
|||||||
const fallbackProvider =
|
const fallbackProvider =
|
||||||
normalizedProvider || (filter !== 'all' ? normalizeProviderKey(String(filter)) : '');
|
normalizedProvider || (filter !== 'all' ? normalizeProviderKey(String(filter)) : '');
|
||||||
const lookupKey = fallbackProvider ? excludedProviderLookup.get(fallbackProvider) : undefined;
|
const lookupKey = fallbackProvider ? excludedProviderLookup.get(fallbackProvider) : undefined;
|
||||||
const models = lookupKey ? excluded[lookupKey] : [];
|
const existingModels = lookupKey ? excluded[lookupKey] : [];
|
||||||
setExcludedForm({
|
setExcludedForm({
|
||||||
provider: lookupKey || fallbackProvider,
|
provider: lookupKey || fallbackProvider,
|
||||||
modelsText: Array.isArray(models) ? models.join('\n') : '',
|
selectedModels: new Set(existingModels),
|
||||||
});
|
});
|
||||||
|
setExcludedModelsList([]);
|
||||||
|
setExcludedModelsError(null);
|
||||||
setExcludedModalOpen(true);
|
setExcludedModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1022,10 +1082,7 @@ export function AuthFilesPage() {
|
|||||||
showNotification(t('oauth_excluded.provider_required'), 'error');
|
showNotification(t('oauth_excluded.provider_required'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const models = excludedForm.modelsText
|
const models = [...excludedForm.selectedModels];
|
||||||
.split(/[\n,]+/)
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
setSavingExcluded(true);
|
setSavingExcluded(true);
|
||||||
try {
|
try {
|
||||||
if (models.length) {
|
if (models.length) {
|
||||||
@@ -1886,16 +1943,55 @@ export function AuthFilesPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* 模型勾选列表 */}
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t('oauth_excluded.models_label')}</label>
|
<label>{t('oauth_excluded.models_label')}</label>
|
||||||
<textarea
|
{excludedModelsLoading ? (
|
||||||
className={styles.textarea}
|
<div className={styles.hint}>{t('common.loading')}</div>
|
||||||
rows={4}
|
) : excludedModelsList.length > 0 ? (
|
||||||
placeholder={t('oauth_excluded.models_placeholder')}
|
<>
|
||||||
value={excludedForm.modelsText}
|
<div className={styles.excludedCheckList}>
|
||||||
onChange={(e) => setExcludedForm((prev) => ({ ...prev, modelsText: e.target.value }))}
|
{excludedModelsList.map((model) => {
|
||||||
/>
|
const isChecked = excludedForm.selectedModels.has(model.id);
|
||||||
<div className={styles.hint}>{t('oauth_excluded.models_hint')}</div>
|
return (
|
||||||
|
<label key={model.id} className={styles.excludedCheckItem}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChecked}
|
||||||
|
disabled={savingExcluded}
|
||||||
|
onChange={(e) => {
|
||||||
|
setExcludedForm((prev) => {
|
||||||
|
const next = new Set(prev.selectedModels);
|
||||||
|
if (e.target.checked) {
|
||||||
|
next.add(model.id);
|
||||||
|
} else {
|
||||||
|
next.delete(model.id);
|
||||||
|
}
|
||||||
|
return { ...prev, selectedModels: next };
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className={styles.excludedCheckLabel}>
|
||||||
|
{model.id}
|
||||||
|
{model.display_name && model.display_name !== model.id && (
|
||||||
|
<span className={styles.excludedCheckDisplayName}>{model.display_name}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{excludedForm.provider.trim() && (
|
||||||
|
<div className={styles.hint}>
|
||||||
|
{excludedModelsError === 'unsupported'
|
||||||
|
? t('oauth_excluded.models_unsupported')
|
||||||
|
: t('oauth_excluded.models_loaded', { count: excludedModelsList.length })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : excludedForm.provider.trim() && !excludedModelsLoading ? (
|
||||||
|
<div className={styles.hint}>{t('oauth_excluded.no_models_available')}</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user