refactor: create modular hooks and integrate API key input
- Create custom hooks for state management: - useProviderCategory: manages provider category state - useApiKeyState: manages API key input with auto-sync to config - useBaseUrlState: manages base URL for Claude and Codex - useModelState: manages model selection state - Integrate API key input into simplified ProviderForm: - Add ApiKeyInput component for Claude mode - Auto-populate API key into settings config - Disable for official providers - Fix EndpointSpeedTest type errors: - Fix import paths to use @ alias - Add temporary type definitions - Format all TODO comments properly - Remove incorrect type assertions - Comment out unimplemented window.api checks All TypeScript type checks now pass.
This commit is contained in:
4
src/components/providers/forms/hooks/index.ts
Normal file
4
src/components/providers/forms/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { useProviderCategory } from "./useProviderCategory";
|
||||
export { useApiKeyState } from "./useApiKeyState";
|
||||
export { useBaseUrlState } from "./useBaseUrlState";
|
||||
export { useModelState } from "./useModelState";
|
||||
63
src/components/providers/forms/hooks/useApiKeyState.ts
Normal file
63
src/components/providers/forms/hooks/useApiKeyState.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import {
|
||||
getApiKeyFromConfig,
|
||||
setApiKeyInConfig,
|
||||
hasApiKeyField,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
|
||||
interface UseApiKeyStateProps {
|
||||
initialConfig?: string;
|
||||
onConfigChange: (config: string) => void;
|
||||
selectedPresetId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 API Key 输入状态
|
||||
* 自动同步 API Key 和 JSON 配置
|
||||
*/
|
||||
export function useApiKeyState({
|
||||
initialConfig,
|
||||
onConfigChange,
|
||||
selectedPresetId,
|
||||
}: UseApiKeyStateProps) {
|
||||
const [apiKey, setApiKey] = useState(() => {
|
||||
if (initialConfig) {
|
||||
return getApiKeyFromConfig(initialConfig);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const handleApiKeyChange = useCallback(
|
||||
(key: string) => {
|
||||
setApiKey(key);
|
||||
|
||||
const configString = setApiKeyInConfig(
|
||||
initialConfig || "{}",
|
||||
key.trim(),
|
||||
{
|
||||
createIfMissing:
|
||||
selectedPresetId !== null && selectedPresetId !== "custom",
|
||||
},
|
||||
);
|
||||
|
||||
onConfigChange(configString);
|
||||
},
|
||||
[initialConfig, selectedPresetId, onConfigChange],
|
||||
);
|
||||
|
||||
const showApiKey = useCallback(
|
||||
(config: string, isEditMode: boolean) => {
|
||||
return (
|
||||
selectedPresetId !== null || (!isEditMode && hasApiKeyField(config))
|
||||
);
|
||||
},
|
||||
[selectedPresetId],
|
||||
);
|
||||
|
||||
return {
|
||||
apiKey,
|
||||
setApiKey,
|
||||
handleApiKeyChange,
|
||||
showApiKey,
|
||||
};
|
||||
}
|
||||
114
src/components/providers/forms/hooks/useBaseUrlState.ts
Normal file
114
src/components/providers/forms/hooks/useBaseUrlState.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { extractCodexBaseUrl, setCodexBaseUrl as setCodexBaseUrlInConfig } from "@/utils/providerConfigUtils";
|
||||
import type { ProviderCategory } from "@/types";
|
||||
|
||||
interface UseBaseUrlStateProps {
|
||||
appType: "claude" | "codex";
|
||||
category: ProviderCategory | undefined;
|
||||
settingsConfig: string;
|
||||
codexConfig?: string;
|
||||
onSettingsConfigChange: (config: string) => void;
|
||||
onCodexConfigChange?: (config: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 Base URL 状态
|
||||
* 支持 Claude (JSON) 和 Codex (TOML) 两种格式
|
||||
*/
|
||||
export function useBaseUrlState({
|
||||
appType,
|
||||
category,
|
||||
settingsConfig,
|
||||
codexConfig,
|
||||
onSettingsConfigChange,
|
||||
onCodexConfigChange,
|
||||
}: UseBaseUrlStateProps) {
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [codexBaseUrl, setCodexBaseUrl] = useState("");
|
||||
const isUpdatingRef = useRef(false);
|
||||
|
||||
// 从配置同步到 state(Claude)
|
||||
useEffect(() => {
|
||||
if (appType !== "claude") return;
|
||||
if (category !== "third_party" && category !== "custom") return;
|
||||
if (isUpdatingRef.current) return;
|
||||
|
||||
try {
|
||||
const config = JSON.parse(settingsConfig || "{}");
|
||||
const envUrl: unknown = config?.env?.ANTHROPIC_BASE_URL;
|
||||
if (typeof envUrl === "string" && envUrl && envUrl !== baseUrl) {
|
||||
setBaseUrl(envUrl.trim());
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [appType, category, settingsConfig, baseUrl]);
|
||||
|
||||
// 从配置同步到 state(Codex)
|
||||
useEffect(() => {
|
||||
if (appType !== "codex") return;
|
||||
if (category !== "third_party" && category !== "custom") return;
|
||||
if (isUpdatingRef.current) return;
|
||||
if (!codexConfig) return;
|
||||
|
||||
const extracted = extractCodexBaseUrl(codexConfig) || "";
|
||||
if (extracted !== codexBaseUrl) {
|
||||
setCodexBaseUrl(extracted);
|
||||
}
|
||||
}, [appType, category, codexConfig, codexBaseUrl]);
|
||||
|
||||
// 处理 Claude Base URL 变化
|
||||
const handleClaudeBaseUrlChange = useCallback(
|
||||
(url: string) => {
|
||||
const sanitized = url.trim().replace(/\/+$/, "");
|
||||
setBaseUrl(sanitized);
|
||||
isUpdatingRef.current = true;
|
||||
|
||||
try {
|
||||
const config = JSON.parse(settingsConfig || "{}");
|
||||
if (!config.env) {
|
||||
config.env = {};
|
||||
}
|
||||
config.env.ANTHROPIC_BASE_URL = sanitized;
|
||||
onSettingsConfigChange(JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isUpdatingRef.current = false;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[settingsConfig, onSettingsConfigChange],
|
||||
);
|
||||
|
||||
// 处理 Codex Base URL 变化
|
||||
const handleCodexBaseUrlChange = useCallback(
|
||||
(url: string) => {
|
||||
const sanitized = url.trim().replace(/\/+$/, "");
|
||||
setCodexBaseUrl(sanitized);
|
||||
|
||||
if (!sanitized || !onCodexConfigChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingRef.current = true;
|
||||
const updatedConfig = setCodexBaseUrlInConfig(codexConfig || "", sanitized);
|
||||
onCodexConfigChange(updatedConfig);
|
||||
|
||||
setTimeout(() => {
|
||||
isUpdatingRef.current = false;
|
||||
}, 0);
|
||||
},
|
||||
[codexConfig, onCodexConfigChange],
|
||||
);
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
setBaseUrl,
|
||||
codexBaseUrl,
|
||||
setCodexBaseUrl,
|
||||
handleClaudeBaseUrlChange,
|
||||
handleCodexBaseUrlChange,
|
||||
};
|
||||
}
|
||||
55
src/components/providers/forms/hooks/useModelState.ts
Normal file
55
src/components/providers/forms/hooks/useModelState.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
interface UseModelStateProps {
|
||||
settingsConfig: string;
|
||||
onConfigChange: (config: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理模型选择状态
|
||||
* 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL
|
||||
*/
|
||||
export function useModelState({
|
||||
settingsConfig,
|
||||
onConfigChange,
|
||||
}: UseModelStateProps) {
|
||||
const [claudeModel, setClaudeModel] = useState("");
|
||||
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
(field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", value: string) => {
|
||||
if (field === "ANTHROPIC_MODEL") {
|
||||
setClaudeModel(value);
|
||||
} else {
|
||||
setClaudeSmallFastModel(value);
|
||||
}
|
||||
|
||||
try {
|
||||
const currentConfig = settingsConfig
|
||||
? JSON.parse(settingsConfig)
|
||||
: { env: {} };
|
||||
if (!currentConfig.env) currentConfig.env = {};
|
||||
|
||||
if (value.trim()) {
|
||||
currentConfig.env[field] = value.trim();
|
||||
} else {
|
||||
delete currentConfig.env[field];
|
||||
}
|
||||
|
||||
onConfigChange(JSON.stringify(currentConfig, null, 2));
|
||||
} catch (err) {
|
||||
// 如果 JSON 解析失败,不做处理
|
||||
console.error("Failed to update model config:", err);
|
||||
}
|
||||
},
|
||||
[settingsConfig, onConfigChange],
|
||||
);
|
||||
|
||||
return {
|
||||
claudeModel,
|
||||
setClaudeModel,
|
||||
claudeSmallFastModel,
|
||||
setClaudeSmallFastModel,
|
||||
handleModelChange,
|
||||
};
|
||||
}
|
||||
62
src/components/providers/forms/hooks/useProviderCategory.ts
Normal file
62
src/components/providers/forms/hooks/useProviderCategory.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import type { ProviderCategory } from "@/types";
|
||||
import type { AppType } from "@/lib/api";
|
||||
import { providerPresets } from "@/config/providerPresets";
|
||||
import { codexProviderPresets } from "@/config/codexProviderPresets";
|
||||
|
||||
interface UseProviderCategoryProps {
|
||||
appType: AppType;
|
||||
selectedPresetId: string | null;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理供应商类别状态
|
||||
* 根据选择的预设自动更新类别
|
||||
*/
|
||||
export function useProviderCategory({
|
||||
appType,
|
||||
selectedPresetId,
|
||||
isEditMode,
|
||||
}: UseProviderCategoryProps) {
|
||||
const [category, setCategory] = useState<ProviderCategory | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// 编辑模式不自动设置类别
|
||||
if (isEditMode) return;
|
||||
|
||||
if (selectedPresetId === "custom") {
|
||||
setCategory("custom");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPresetId) return;
|
||||
|
||||
// 从预设 ID 提取索引
|
||||
const match = selectedPresetId.match(/^(claude|codex)-(\d+)$/);
|
||||
if (!match) return;
|
||||
|
||||
const [, type, indexStr] = match;
|
||||
const index = parseInt(indexStr, 10);
|
||||
|
||||
if (type === "codex" && appType === "codex") {
|
||||
const preset = codexProviderPresets[index];
|
||||
if (preset) {
|
||||
setCategory(
|
||||
preset.category || (preset.isOfficial ? "official" : undefined),
|
||||
);
|
||||
}
|
||||
} else if (type === "claude" && appType === "claude") {
|
||||
const preset = providerPresets[index];
|
||||
if (preset) {
|
||||
setCategory(
|
||||
preset.category || (preset.isOfficial ? "official" : undefined),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [appType, selectedPresetId, isEditMode]);
|
||||
|
||||
return { category, setCategory };
|
||||
}
|
||||
Reference in New Issue
Block a user