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:
Jason
2025-10-16 17:40:25 +08:00
parent 2c1346a23d
commit 98c35c7c62
13 changed files with 2322 additions and 2 deletions

View File

@@ -0,0 +1,4 @@
export { useProviderCategory } from "./useProviderCategory";
export { useApiKeyState } from "./useApiKeyState";
export { useBaseUrlState } from "./useBaseUrlState";
export { useModelState } from "./useModelState";

View 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,
};
}

View 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);
// 从配置同步到 stateClaude
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]);
// 从配置同步到 stateCodex
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,
};
}

View 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,
};
}

View 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 };
}