From aefc5699a22cbcf1f585060360085c28d60f385b Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 7 Oct 2025 12:03:11 +0800 Subject: [PATCH] feat: add endpoint candidates support and code formatting improvements - Add endpointCandidates field to ProviderPreset and CodexProviderPreset interfaces - Integrate preset endpoint candidates into speed test endpoint selection - Add multiple endpoint options for PackyCode providers (Claude & Codex) - Apply consistent code formatting (trailing commas, line breaks) - Improve template value type safety and readability --- src/components/ProviderForm.tsx | 191 +++++++++++++++-------------- src/config/codexProviderPresets.ts | 8 ++ src/config/providerPresets.ts | 10 ++ 3 files changed, 116 insertions(+), 93 deletions(-) diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx index 3d701d7..b3fed86 100644 --- a/src/components/ProviderForm.tsx +++ b/src/components/ProviderForm.tsx @@ -41,11 +41,11 @@ const collectTemplatePaths = ( source: unknown, templateKeys: string[], currentPath: TemplatePath = [], - acc: TemplatePath[] = [], + acc: TemplatePath[] = [] ): TemplatePath[] => { if (typeof source === "string") { const hasPlaceholder = templateKeys.some((key) => - source.includes(`\${${key}}`), + source.includes(`\${${key}}`) ); if (hasPlaceholder) { acc.push([...currentPath]); @@ -55,14 +55,14 @@ const collectTemplatePaths = ( if (Array.isArray(source)) { source.forEach((item, index) => - collectTemplatePaths(item, templateKeys, [...currentPath, index], acc), + collectTemplatePaths(item, templateKeys, [...currentPath, index], acc) ); return acc; } if (source && typeof source === "object") { Object.entries(source).forEach(([key, value]) => - collectTemplatePaths(value, templateKeys, [...currentPath, key], acc), + collectTemplatePaths(value, templateKeys, [...currentPath, key], acc) ); } @@ -81,7 +81,7 @@ const getValueAtPath = (source: any, path: TemplatePath) => { const setValueAtPath = ( target: any, path: TemplatePath, - value: unknown, + value: unknown ): any => { if (path.length === 0) { return value; @@ -119,7 +119,7 @@ const setValueAtPath = ( const applyTemplateValuesToConfigString = ( presetConfig: any, currentConfigString: string, - values: TemplateValueMap, + values: TemplateValueMap ) => { const replacedConfig = applyTemplateValues(presetConfig, values); const templateKeys = Object.keys(values); @@ -209,8 +209,9 @@ const ProviderForm: React.FC = ({ const [claudeSmallFastModel, setClaudeSmallFastModel] = useState(""); const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态 // 模板变量状态 - const [templateValues, setTemplateValues] = - useState>({}); + const [templateValues, setTemplateValues] = useState< + Record + >({}); // Codex 特有的状态 const [codexAuth, setCodexAuthState] = useState(""); @@ -225,7 +226,8 @@ const ProviderForm: React.FC = ({ ); // 端点测速弹窗状态 const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); - const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); + const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = + useState(false); // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 const [selectedCodexPreset, setSelectedCodexPreset] = useState( showPresets && isCodex ? -1 : null @@ -238,7 +240,9 @@ const ProviderForm: React.FC = ({ const setCodexConfig = (value: string | ((prev: string) => string)) => { setCodexConfigState((prev) => - typeof value === "function" ? (value as (input: string) => string)(prev) : value, + typeof value === "function" + ? (value as (input: string) => string)(prev) + : value ); }; @@ -476,13 +480,7 @@ const ProviderForm: React.FC = ({ } catch { // ignore JSON parse errors } - }, [ - isCodex, - category, - initialData, - formData.settingsConfig, - baseUrl, - ]); + }, [isCodex, category, initialData, formData.settingsConfig, baseUrl]); // 与 TOML 配置保持基础 URL 同步(Codex 第三方/自定义) useEffect(() => { @@ -774,7 +772,7 @@ const ProviderForm: React.FC = ({ ...config, editorValue: config.editorValue ? config.editorValue - : config.defaultValue ?? "", + : (config.defaultValue ?? ""), }, ]) ); @@ -1112,9 +1110,9 @@ const ProviderForm: React.FC = ({ const templateValueEntries: Array<[string, TemplateValueConfig]> = selectedTemplatePreset?.templateValues - ? (Object.entries( - selectedTemplatePreset.templateValues - ) as Array<[string, TemplateValueConfig]>) + ? (Object.entries(selectedTemplatePreset.templateValues) as Array< + [string, TemplateValueConfig] + >) : []; // 判断当前选中的预设是否是官方 @@ -1157,7 +1155,8 @@ const ProviderForm: React.FC = ({ } if (initialData && typeof initialData.settingsConfig === "object") { - const envUrl = (initialData.settingsConfig as any)?.env?.ANTHROPIC_BASE_URL; + const envUrl = (initialData.settingsConfig as any)?.env + ?.ANTHROPIC_BASE_URL; if (typeof envUrl === "string") { add(envUrl); } @@ -1173,6 +1172,10 @@ const ProviderForm: React.FC = ({ if (typeof presetEnv === "string") { add(presetEnv); } + // 合并预设内置的请求地址候选 + if (Array.isArray((preset as any).endpointCandidates)) { + ((preset as any).endpointCandidates as string[]).forEach((u) => add(u)); + } } return Array.from(map.values()); @@ -1206,20 +1209,19 @@ const ProviderForm: React.FC = ({ selectedCodexPreset >= 0 && selectedCodexPreset < codexProviderPresets.length ) { - const presetConfig = codexProviderPresets[selectedCodexPreset]?.config; - const presetBase = extractCodexBaseUrl(presetConfig); + const preset = codexProviderPresets[selectedCodexPreset]; + const presetBase = extractCodexBaseUrl(preset?.config || ""); if (presetBase) { add(presetBase); } + // 合并预设内置的请求地址候选 + if (Array.isArray((preset as any)?.endpointCandidates)) { + ((preset as any).endpointCandidates as string[]).forEach((u) => add(u)); + } } return Array.from(map.values()); - }, [ - isCodex, - codexBaseUrl, - initialData, - selectedCodexPreset, - ]); + }, [isCodex, codexBaseUrl, initialData, selectedCodexPreset]); // 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示) const shouldShowApiKeyLink = @@ -1536,73 +1538,76 @@ const ProviderForm: React.FC = ({ )} - {!isCodex && selectedTemplatePreset && templateValueEntries.length > 0 && ( -
-

- 参数配置 - {selectedTemplatePreset.name.trim()} * -

-
- {templateValueEntries.map(([key, config]) => ( -
- - { - const newValue = e.target.value; - setTemplateValues((prev) => { - const prevEntry = prev[key]; - const nextEntry: TemplateValueConfig = { - ...config, - ...(prevEntry ?? {}), - editorValue: newValue, - }; - const nextValues: TemplateValueMap = { - ...prev, - [key]: nextEntry, - }; + {!isCodex && + selectedTemplatePreset && + templateValueEntries.length > 0 && ( +
+

+ 参数配置 - {selectedTemplatePreset.name.trim()} * +

+
+ {templateValueEntries.map(([key, config]) => ( +
+ + { + const newValue = e.target.value; + setTemplateValues((prev) => { + const prevEntry = prev[key]; + const nextEntry: TemplateValueConfig = { + ...config, + ...(prevEntry ?? {}), + editorValue: newValue, + }; + const nextValues: TemplateValueMap = { + ...prev, + [key]: nextEntry, + }; - if (selectedTemplatePreset) { - try { - const configString = applyTemplateValuesToConfigString( - selectedTemplatePreset.settingsConfig, - formData.settingsConfig, - nextValues - ); - setFormData((prevForm) => ({ - ...prevForm, - settingsConfig: configString, - })); - setSettingsConfigError( - validateSettingsConfig(configString) - ); - } catch (err) { - console.error("更新模板值失败:", err); + if (selectedTemplatePreset) { + try { + const configString = + applyTemplateValuesToConfigString( + selectedTemplatePreset.settingsConfig, + formData.settingsConfig, + nextValues + ); + setFormData((prevForm) => ({ + ...prevForm, + settingsConfig: configString, + })); + setSettingsConfigError( + validateSettingsConfig(configString) + ); + } catch (err) { + console.error("更新模板值失败:", err); + } } - } - return nextValues; - }); - }} - aria-label={config.label} - autoComplete="off" - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- ))} + return nextValues; + }); + }} + aria-label={config.label} + autoComplete="off" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ ))} +
-
- )} + )} {!isCodex && shouldShowSpeedTest && (
diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index b9ae042..fb633eb 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -13,6 +13,8 @@ export interface CodexProviderPreset { isOfficial?: boolean; // 标识是否为官方预设 category?: ProviderCategory; // 新增:分类 isCustomTemplate?: boolean; // 标识是否为自定义模板 + // 新增:请求地址候选列表(用于地址管理/测速) + endpointCandidates?: string[]; } /** @@ -71,5 +73,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [ "https://codex-api.packycode.com/v1", "gpt-5-codex" ), + // Codex 请求地址候选(用于地址管理/测速) + endpointCandidates: [ + "https://codex-api.packycode.com/v1", + "https://codex-api-hk-cn2.packycode.com/v1", + "https://codex-api-hk-cdn.packycode.com/v1", + ], }, ]; diff --git a/src/config/providerPresets.ts b/src/config/providerPresets.ts index fdb9d40..225584d 100644 --- a/src/config/providerPresets.ts +++ b/src/config/providerPresets.ts @@ -20,6 +20,8 @@ export interface ProviderPreset { category?: ProviderCategory; // 新增:分类 // 新增:模板变量定义,用于动态替换配置中的值 templateValues?: Record; // editorValue 存储编辑器中的实时输入值 + // 新增:请求地址候选列表(用于地址管理/测速) + endpointCandidates?: string[]; } export const providerPresets: ProviderPreset[] = [ @@ -108,6 +110,14 @@ export const providerPresets: ProviderPreset[] = [ ANTHROPIC_AUTH_TOKEN: "", }, }, + // 请求地址候选(用于地址管理/测速) + endpointCandidates: [ + "https://api.packycode.com", + "https://api-hk-cn2.packycode.com", + "https://api-hk-g.packycode.com", + "https://api-us-cn2.packycode.com", + "https://api-cf-pro.packycode.com", + ], category: "third_party", }, {