Compare commits

..

4 Commits

Author SHA1 Message Date
YoVinchen d8e8d5f095 Merge branch 'main' into fix/endpoint-auto-select-persist 2026-01-12 17:47:25 +08:00
Dex Miller 99c910e58e fix(provider): persist endpoint auto-select state (#611)
- Add endpointAutoSelect field to ProviderMeta for persistence
- Lift autoSelect state from EndpointSpeedTest to ProviderForm
- Save auto-select preference when provider is saved
- Restore preference when editing existing provider

Fixes https://github.com/farion1231/cc-switch/issues/589
2026-01-12 16:26:17 +08:00
Dex Miller 8f7423f011 Feat/deeplink multi endpoints (#597)
* feat(deeplink): support comma-separated multiple endpoints in URL

Allow importing multiple API endpoints via single endpoint parameter.
First URL becomes primary endpoint, rest are added as custom endpoints.

* feat(deeplink): add usage query fields to deeplink generator

Add form fields for usage query configuration in deeplink HTML generator:
- usageEnabled, usageBaseUrl, usageApiKey
- usageScript, usageAutoInterval
- usageAccessToken, usageUserId

* fix(deeplink): auto-infer homepage and improve multi-endpoint display

- Auto-infer homepage from primary endpoint when not provided
- Display multiple endpoints as list in import dialog (primary marked)
- Update deeplink parser in deplink.html to show multi-endpoint info
- Add test for homepage inference from endpoint
- Minor log format fix in live.rs

* fix(deeplink): use primary endpoint for usage script base_url

- Fix usage_script.base_url getting comma-separated string when multiple endpoints
- Add i18n support for primary endpoint label in DeepLinkImportDialog
2026-01-12 15:57:45 +08:00
YoVinchen e14e3ec141 fix(provider): persist endpoint auto-select state
- Add endpointAutoSelect field to ProviderMeta for persistence
- Lift autoSelect state from EndpointSpeedTest to ProviderForm
- Save auto-select preference when provider is saved
- Restore preference when editing existing provider

Fixes https://github.com/farion1231/cc-switch/issues/589
2026-01-12 15:34:32 +08:00
7 changed files with 47 additions and 2 deletions
+3
View File
@@ -147,6 +147,9 @@ pub struct ProviderMeta {
/// 用量查询脚本配置 /// 用量查询脚本配置
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub usage_script: Option<UsageScript>, pub usage_script: Option<UsageScript>,
/// 请求地址管理:测速后自动选择最佳端点
#[serde(rename = "endpointAutoSelect", skip_serializing_if = "Option::is_none")]
pub endpoint_auto_select: Option<bool>,
/// 合作伙伴标记(前端使用 isPartner,保持字段名一致) /// 合作伙伴标记(前端使用 isPartner,保持字段名一致)
#[serde(rename = "isPartner", skip_serializing_if = "Option::is_none")] #[serde(rename = "isPartner", skip_serializing_if = "Option::is_none")]
pub is_partner: Option<bool>, pub is_partner: Option<bool>,
@@ -36,6 +36,8 @@ interface ClaudeFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange?: (endpoints: string[]) => void; onCustomEndpointsChange?: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model Selector // Model Selector
shouldShowModelSelector: boolean; shouldShowModelSelector: boolean;
@@ -83,6 +85,8 @@ export function ClaudeFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelSelector, shouldShowModelSelector,
claudeModel, claudeModel,
reasoningModel, reasoningModel,
@@ -170,6 +174,8 @@ export function ClaudeFormFields({
initialEndpoints={speedTestEndpoints} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}
/> />
)} )}
@@ -25,6 +25,8 @@ interface CodexFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange?: (endpoints: string[]) => void; onCustomEndpointsChange?: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model Name // Model Name
shouldShowModelField?: boolean; shouldShowModelField?: boolean;
@@ -50,6 +52,8 @@ export function CodexFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelField = true, shouldShowModelField = true,
modelName = "", modelName = "",
onModelNameChange, onModelNameChange,
@@ -130,6 +134,8 @@ export function CodexFormFields({
initialEndpoints={speedTestEndpoints} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}
/> />
)} )}
@@ -30,6 +30,8 @@ interface EndpointSpeedTestProps {
initialEndpoints: EndpointCandidate[]; initialEndpoints: EndpointCandidate[];
visible?: boolean; visible?: boolean;
onClose: () => void; onClose: () => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// 新建模式:当自定义端点列表变化时回传(仅包含 isCustom 的条目) // 新建模式:当自定义端点列表变化时回传(仅包含 isCustom 的条目)
// 编辑模式:不使用此回调,端点直接保存到后端 // 编辑模式:不使用此回调,端点直接保存到后端
onCustomEndpointsChange?: (urls: string[]) => void; onCustomEndpointsChange?: (urls: string[]) => void;
@@ -85,6 +87,8 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
initialEndpoints, initialEndpoints,
visible = true, visible = true,
onClose, onClose,
autoSelect,
onAutoSelectChange,
onCustomEndpointsChange, onCustomEndpointsChange,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -93,7 +97,6 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
); );
const [customUrl, setCustomUrl] = useState(""); const [customUrl, setCustomUrl] = useState("");
const [addError, setAddError] = useState<string | null>(null); const [addError, setAddError] = useState<string | null>(null);
const [autoSelect, setAutoSelect] = useState(true);
const [isTesting, setIsTesting] = useState(false); const [isTesting, setIsTesting] = useState(false);
const [lastError, setLastError] = useState<string | null>(null); const [lastError, setLastError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@@ -488,7 +491,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
<input <input
type="checkbox" type="checkbox"
checked={autoSelect} checked={autoSelect}
onChange={(event) => setAutoSelect(event.target.checked)} onChange={(event) => {
onAutoSelectChange(event.target.checked);
}}
className="h-3.5 w-3.5 rounded border-border-default bg-background text-primary focus:ring-2 focus:ring-primary/20" className="h-3.5 w-3.5 rounded border-border-default bg-background text-primary focus:ring-2 focus:ring-primary/20"
/> />
{t("endpointTest.autoSelect")} {t("endpointTest.autoSelect")}
@@ -29,6 +29,8 @@ interface GeminiFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange: (endpoints: string[]) => void; onCustomEndpointsChange: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model // Model
shouldShowModelField: boolean; shouldShowModelField: boolean;
@@ -55,6 +57,8 @@ export function GeminiFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelField, shouldShowModelField,
model, model,
onModelChange, onModelChange,
@@ -142,6 +146,8 @@ export function GeminiFormFields({
initialEndpoints={speedTestEndpoints} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}
/> />
)} )}
@@ -124,6 +124,9 @@ export function ProviderForm({
return []; return [];
}, },
); );
const [endpointAutoSelect, setEndpointAutoSelect] = useState<boolean>(
() => initialData?.meta?.endpointAutoSelect ?? true,
);
// 使用 category hook // 使用 category hook
const { category } = useProviderCategory({ const { category } = useProviderCategory({
@@ -141,6 +144,7 @@ export function ProviderForm({
if (!initialData) { if (!initialData) {
setDraftCustomEndpoints([]); setDraftCustomEndpoints([]);
} }
setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true);
}, [appId, initialData]); }, [appId, initialData]);
const defaultValues: ProviderFormData = useMemo( const defaultValues: ProviderFormData = useMemo(
@@ -647,6 +651,13 @@ export function ProviderForm({
} }
} }
const baseMeta: ProviderMeta | undefined =
payload.meta ?? (initialData?.meta ? { ...initialData.meta } : undefined);
payload.meta = {
...(baseMeta ?? {}),
endpointAutoSelect,
};
onSubmit(payload); onSubmit(payload);
}; };
@@ -856,6 +867,8 @@ export function ProviderForm({
onCustomEndpointsChange={ onCustomEndpointsChange={
isEditMode ? undefined : setDraftCustomEndpoints isEditMode ? undefined : setDraftCustomEndpoints
} }
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
shouldShowModelSelector={category !== "official"} shouldShowModelSelector={category !== "official"}
claudeModel={claudeModel} claudeModel={claudeModel}
reasoningModel={reasoningModel} reasoningModel={reasoningModel}
@@ -889,6 +902,8 @@ export function ProviderForm({
onCustomEndpointsChange={ onCustomEndpointsChange={
isEditMode ? undefined : setDraftCustomEndpoints isEditMode ? undefined : setDraftCustomEndpoints
} }
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
shouldShowModelField={category !== "official"} shouldShowModelField={category !== "official"}
modelName={codexModelName} modelName={codexModelName}
onModelNameChange={handleCodexModelNameChange} onModelNameChange={handleCodexModelNameChange}
@@ -917,6 +932,8 @@ export function ProviderForm({
isEndpointModalOpen={isEndpointModalOpen} isEndpointModalOpen={isEndpointModalOpen}
onEndpointModalToggle={setIsEndpointModalOpen} onEndpointModalToggle={setIsEndpointModalOpen}
onCustomEndpointsChange={setDraftCustomEndpoints} onCustomEndpointsChange={setDraftCustomEndpoints}
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
shouldShowModelField={true} shouldShowModelField={true}
model={geminiModel} model={geminiModel}
onModelChange={handleGeminiModelChange} onModelChange={handleGeminiModelChange}
+2
View File
@@ -92,6 +92,8 @@ export interface ProviderMeta {
custom_endpoints?: Record<string, CustomEndpoint>; custom_endpoints?: Record<string, CustomEndpoint>;
// 用量查询脚本配置 // 用量查询脚本配置
usage_script?: UsageScript; usage_script?: UsageScript;
// 请求地址管理:测速后自动选择最佳端点
endpointAutoSelect?: boolean;
// 是否为官方合作伙伴 // 是否为官方合作伙伴
isPartner?: boolean; isPartner?: boolean;
// 合作伙伴促销 key(用于后端识别 PackyCode 等) // 合作伙伴促销 key(用于后端识别 PackyCode 等)