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
This commit is contained in:
Jason
2025-10-07 12:03:11 +08:00
parent 061aef1c2f
commit aefc5699a2
3 changed files with 116 additions and 93 deletions

View File

@@ -41,11 +41,11 @@ const collectTemplatePaths = (
source: unknown, source: unknown,
templateKeys: string[], templateKeys: string[],
currentPath: TemplatePath = [], currentPath: TemplatePath = [],
acc: TemplatePath[] = [], acc: TemplatePath[] = []
): TemplatePath[] => { ): TemplatePath[] => {
if (typeof source === "string") { if (typeof source === "string") {
const hasPlaceholder = templateKeys.some((key) => const hasPlaceholder = templateKeys.some((key) =>
source.includes(`\${${key}}`), source.includes(`\${${key}}`)
); );
if (hasPlaceholder) { if (hasPlaceholder) {
acc.push([...currentPath]); acc.push([...currentPath]);
@@ -55,14 +55,14 @@ const collectTemplatePaths = (
if (Array.isArray(source)) { if (Array.isArray(source)) {
source.forEach((item, index) => source.forEach((item, index) =>
collectTemplatePaths(item, templateKeys, [...currentPath, index], acc), collectTemplatePaths(item, templateKeys, [...currentPath, index], acc)
); );
return acc; return acc;
} }
if (source && typeof source === "object") { if (source && typeof source === "object") {
Object.entries(source).forEach(([key, value]) => 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 = ( const setValueAtPath = (
target: any, target: any,
path: TemplatePath, path: TemplatePath,
value: unknown, value: unknown
): any => { ): any => {
if (path.length === 0) { if (path.length === 0) {
return value; return value;
@@ -119,7 +119,7 @@ const setValueAtPath = (
const applyTemplateValuesToConfigString = ( const applyTemplateValuesToConfigString = (
presetConfig: any, presetConfig: any,
currentConfigString: string, currentConfigString: string,
values: TemplateValueMap, values: TemplateValueMap
) => { ) => {
const replacedConfig = applyTemplateValues(presetConfig, values); const replacedConfig = applyTemplateValues(presetConfig, values);
const templateKeys = Object.keys(values); const templateKeys = Object.keys(values);
@@ -209,8 +209,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState(""); const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态 const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
// 模板变量状态 // 模板变量状态
const [templateValues, setTemplateValues] = const [templateValues, setTemplateValues] = useState<
useState<Record<string, TemplateValueConfig>>({}); Record<string, TemplateValueConfig>
>({});
// Codex 特有的状态 // Codex 特有的状态
const [codexAuth, setCodexAuthState] = useState(""); const [codexAuth, setCodexAuthState] = useState("");
@@ -225,7 +226,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
); );
// 端点测速弹窗状态 // 端点测速弹窗状态
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
useState(false);
// -1 表示自定义null 表示未选择,>= 0 表示预设索引 // -1 表示自定义null 表示未选择,>= 0 表示预设索引
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>( const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
showPresets && isCodex ? -1 : null showPresets && isCodex ? -1 : null
@@ -238,7 +240,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const setCodexConfig = (value: string | ((prev: string) => string)) => { const setCodexConfig = (value: string | ((prev: string) => string)) => {
setCodexConfigState((prev) => 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<ProviderFormProps> = ({
} catch { } catch {
// ignore JSON parse errors // ignore JSON parse errors
} }
}, [ }, [isCodex, category, initialData, formData.settingsConfig, baseUrl]);
isCodex,
category,
initialData,
formData.settingsConfig,
baseUrl,
]);
// 与 TOML 配置保持基础 URL 同步Codex 第三方/自定义) // 与 TOML 配置保持基础 URL 同步Codex 第三方/自定义)
useEffect(() => { useEffect(() => {
@@ -774,7 +772,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
...config, ...config,
editorValue: config.editorValue editorValue: config.editorValue
? config.editorValue ? config.editorValue
: config.defaultValue ?? "", : (config.defaultValue ?? ""),
}, },
]) ])
); );
@@ -1112,9 +1110,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const templateValueEntries: Array<[string, TemplateValueConfig]> = const templateValueEntries: Array<[string, TemplateValueConfig]> =
selectedTemplatePreset?.templateValues selectedTemplatePreset?.templateValues
? (Object.entries( ? (Object.entries(selectedTemplatePreset.templateValues) as Array<
selectedTemplatePreset.templateValues [string, TemplateValueConfig]
) as Array<[string, TemplateValueConfig]>) >)
: []; : [];
// 判断当前选中的预设是否是官方 // 判断当前选中的预设是否是官方
@@ -1157,7 +1155,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
} }
if (initialData && typeof initialData.settingsConfig === "object") { 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") { if (typeof envUrl === "string") {
add(envUrl); add(envUrl);
} }
@@ -1173,6 +1172,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (typeof presetEnv === "string") { if (typeof presetEnv === "string") {
add(presetEnv); add(presetEnv);
} }
// 合并预设内置的请求地址候选
if (Array.isArray((preset as any).endpointCandidates)) {
((preset as any).endpointCandidates as string[]).forEach((u) => add(u));
}
} }
return Array.from(map.values()); return Array.from(map.values());
@@ -1206,20 +1209,19 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
selectedCodexPreset >= 0 && selectedCodexPreset >= 0 &&
selectedCodexPreset < codexProviderPresets.length selectedCodexPreset < codexProviderPresets.length
) { ) {
const presetConfig = codexProviderPresets[selectedCodexPreset]?.config; const preset = codexProviderPresets[selectedCodexPreset];
const presetBase = extractCodexBaseUrl(presetConfig); const presetBase = extractCodexBaseUrl(preset?.config || "");
if (presetBase) { if (presetBase) {
add(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()); return Array.from(map.values());
}, [ }, [isCodex, codexBaseUrl, initialData, selectedCodexPreset]);
isCodex,
codexBaseUrl,
initialData,
selectedCodexPreset,
]);
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示) // 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
const shouldShowApiKeyLink = const shouldShowApiKeyLink =
@@ -1536,7 +1538,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
</div> </div>
)} )}
{!isCodex && selectedTemplatePreset && templateValueEntries.length > 0 && ( {!isCodex &&
selectedTemplatePreset &&
templateValueEntries.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
- {selectedTemplatePreset.name.trim()} * - {selectedTemplatePreset.name.trim()} *
@@ -1574,7 +1578,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (selectedTemplatePreset) { if (selectedTemplatePreset) {
try { try {
const configString = applyTemplateValuesToConfigString( const configString =
applyTemplateValuesToConfigString(
selectedTemplatePreset.settingsConfig, selectedTemplatePreset.settingsConfig,
formData.settingsConfig, formData.settingsConfig,
nextValues nextValues

View File

@@ -13,6 +13,8 @@ export interface CodexProviderPreset {
isOfficial?: boolean; // 标识是否为官方预设 isOfficial?: boolean; // 标识是否为官方预设
category?: ProviderCategory; // 新增:分类 category?: ProviderCategory; // 新增:分类
isCustomTemplate?: boolean; // 标识是否为自定义模板 isCustomTemplate?: boolean; // 标识是否为自定义模板
// 新增:请求地址候选列表(用于地址管理/测速)
endpointCandidates?: string[];
} }
/** /**
@@ -71,5 +73,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [
"https://codex-api.packycode.com/v1", "https://codex-api.packycode.com/v1",
"gpt-5-codex" "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",
],
}, },
]; ];

View File

@@ -20,6 +20,8 @@ export interface ProviderPreset {
category?: ProviderCategory; // 新增:分类 category?: ProviderCategory; // 新增:分类
// 新增:模板变量定义,用于动态替换配置中的值 // 新增:模板变量定义,用于动态替换配置中的值
templateValues?: Record<string, TemplateValueConfig>; // editorValue 存储编辑器中的实时输入值 templateValues?: Record<string, TemplateValueConfig>; // editorValue 存储编辑器中的实时输入值
// 新增:请求地址候选列表(用于地址管理/测速)
endpointCandidates?: string[];
} }
export const providerPresets: ProviderPreset[] = [ export const providerPresets: ProviderPreset[] = [
@@ -108,6 +110,14 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_AUTH_TOKEN: "", 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", category: "third_party",
}, },
{ {