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:
@@ -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,73 +1538,76 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isCodex && selectedTemplatePreset && templateValueEntries.length > 0 && (
|
{!isCodex &&
|
||||||
<div className="space-y-3">
|
selectedTemplatePreset &&
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
templateValueEntries.length > 0 && (
|
||||||
参数配置 - {selectedTemplatePreset.name.trim()} *
|
<div className="space-y-3">
|
||||||
</h3>
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<div className="space-y-4">
|
参数配置 - {selectedTemplatePreset.name.trim()} *
|
||||||
{templateValueEntries.map(([key, config]) => (
|
</h3>
|
||||||
<div key={key} className="space-y-2">
|
<div className="space-y-4">
|
||||||
<label className="sr-only" htmlFor={`template-${key}`}>
|
{templateValueEntries.map(([key, config]) => (
|
||||||
{config.label}
|
<div key={key} className="space-y-2">
|
||||||
</label>
|
<label className="sr-only" htmlFor={`template-${key}`}>
|
||||||
<input
|
{config.label}
|
||||||
id={`template-${key}`}
|
</label>
|
||||||
type="text"
|
<input
|
||||||
required
|
id={`template-${key}`}
|
||||||
placeholder={`${config.label} *`}
|
type="text"
|
||||||
value={
|
required
|
||||||
templateValues[key]?.editorValue ??
|
placeholder={`${config.label} *`}
|
||||||
config.editorValue ??
|
value={
|
||||||
config.defaultValue ??
|
templateValues[key]?.editorValue ??
|
||||||
""
|
config.editorValue ??
|
||||||
}
|
config.defaultValue ??
|
||||||
onChange={(e) => {
|
""
|
||||||
const newValue = e.target.value;
|
}
|
||||||
setTemplateValues((prev) => {
|
onChange={(e) => {
|
||||||
const prevEntry = prev[key];
|
const newValue = e.target.value;
|
||||||
const nextEntry: TemplateValueConfig = {
|
setTemplateValues((prev) => {
|
||||||
...config,
|
const prevEntry = prev[key];
|
||||||
...(prevEntry ?? {}),
|
const nextEntry: TemplateValueConfig = {
|
||||||
editorValue: newValue,
|
...config,
|
||||||
};
|
...(prevEntry ?? {}),
|
||||||
const nextValues: TemplateValueMap = {
|
editorValue: newValue,
|
||||||
...prev,
|
};
|
||||||
[key]: nextEntry,
|
const nextValues: TemplateValueMap = {
|
||||||
};
|
...prev,
|
||||||
|
[key]: nextEntry,
|
||||||
|
};
|
||||||
|
|
||||||
if (selectedTemplatePreset) {
|
if (selectedTemplatePreset) {
|
||||||
try {
|
try {
|
||||||
const configString = applyTemplateValuesToConfigString(
|
const configString =
|
||||||
selectedTemplatePreset.settingsConfig,
|
applyTemplateValuesToConfigString(
|
||||||
formData.settingsConfig,
|
selectedTemplatePreset.settingsConfig,
|
||||||
nextValues
|
formData.settingsConfig,
|
||||||
);
|
nextValues
|
||||||
setFormData((prevForm) => ({
|
);
|
||||||
...prevForm,
|
setFormData((prevForm) => ({
|
||||||
settingsConfig: configString,
|
...prevForm,
|
||||||
}));
|
settingsConfig: configString,
|
||||||
setSettingsConfigError(
|
}));
|
||||||
validateSettingsConfig(configString)
|
setSettingsConfigError(
|
||||||
);
|
validateSettingsConfig(configString)
|
||||||
} catch (err) {
|
);
|
||||||
console.error("更新模板值失败:", err);
|
} catch (err) {
|
||||||
|
console.error("更新模板值失败:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nextValues;
|
return nextValues;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
aria-label={config.label}
|
aria-label={config.label}
|
||||||
autoComplete="off"
|
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"
|
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>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{!isCodex && shouldShowSpeedTest && (
|
{!isCodex && shouldShowSpeedTest && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user