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,
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<ProviderFormProps> = ({
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
// 模板变量状态
const [templateValues, setTemplateValues] =
useState<Record<string, TemplateValueConfig>>({});
const [templateValues, setTemplateValues] = useState<
Record<string, TemplateValueConfig>
>({});
// Codex 特有的状态
const [codexAuth, setCodexAuthState] = useState("");
@@ -225,7 +226,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
);
// 端点测速弹窗状态
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false);
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
useState(false);
// -1 表示自定义null 表示未选择,>= 0 表示预设索引
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
showPresets && isCodex ? -1 : null
@@ -238,7 +240,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
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<ProviderFormProps> = ({
} 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<ProviderFormProps> = ({
...config,
editorValue: config.editorValue
? config.editorValue
: config.defaultValue ?? "",
: (config.defaultValue ?? ""),
},
])
);
@@ -1112,9 +1110,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
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<ProviderFormProps> = ({
}
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<ProviderFormProps> = ({
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<ProviderFormProps> = ({
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<ProviderFormProps> = ({
</div>
)}
{!isCodex && selectedTemplatePreset && templateValueEntries.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
- {selectedTemplatePreset.name.trim()} *
</h3>
<div className="space-y-4">
{templateValueEntries.map(([key, config]) => (
<div key={key} className="space-y-2">
<label className="sr-only" htmlFor={`template-${key}`}>
{config.label}
</label>
<input
id={`template-${key}`}
type="text"
required
placeholder={`${config.label} *`}
value={
templateValues[key]?.editorValue ??
config.editorValue ??
config.defaultValue ??
""
}
onChange={(e) => {
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 && (
<div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
- {selectedTemplatePreset.name.trim()} *
</h3>
<div className="space-y-4">
{templateValueEntries.map(([key, config]) => (
<div key={key} className="space-y-2">
<label className="sr-only" htmlFor={`template-${key}`}>
{config.label}
</label>
<input
id={`template-${key}`}
type="text"
required
placeholder={`${config.label} *`}
value={
templateValues[key]?.editorValue ??
config.editorValue ??
config.defaultValue ??
""
}
onChange={(e) => {
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"
/>
</div>
))}
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"
/>
</div>
))}
</div>
</div>
</div>
)}
)}
{!isCodex && shouldShowSpeedTest && (
<div className="space-y-2">

View File

@@ -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",
],
},
];

View File

@@ -20,6 +20,8 @@ export interface ProviderPreset {
category?: ProviderCategory; // 新增:分类
// 新增:模板变量定义,用于动态替换配置中的值
templateValues?: Record<string, TemplateValueConfig>; // 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",
},
{