feat: refactor ProviderForm component with new subcomponents (#13)
* feat: refactor ProviderForm component with new subcomponents - Introduced PresetSelector, ApiKeyInput, ClaudeConfigEditor, and CodexConfigEditor for improved modularity and readability. - Simplified preset selection logic for both Claude and Codex configurations. - Enhanced API Key input handling with dedicated components for better user experience. - Removed redundant code and improved state management in the ProviderForm component. * feat: add Kimi model selection to ProviderForm component - Introduced KimiModelSelector for enhanced model configuration options. - Implemented state management for Kimi model selection, including initialization and updates based on preset selection. - Improved user experience by conditionally displaying the Kimi model selector based on the selected preset. - Refactored related logic to ensure proper handling of Kimi-specific settings in the ProviderForm. * feat: enhance API Key input and model selection in ProviderForm - Added toggle functionality to show/hide API Key in ApiKeyInput component for improved user experience. - Updated placeholder text in ProviderForm to provide clearer instructions based on the selected preset. - Enhanced KimiModelSelector to display a more informative message when API Key is not provided. - Refactored provider presets to remove hardcoded API Key values for better security practices. * fix(kimi): optimize debounce implementation in model selector - Fix initial state: use empty string instead of apiKey.trim() - Refactor fetchModels to fetchModelsWithKey with explicit key parameter - Ensure consistent behavior between auto-fetch and manual refresh - Eliminate mental overhead from optional parameter fallback logic * fix(api-key): remove custom masking logic, use native password input - Remove getDisplayValue function with custom star masking - Use native browser password input behavior for better UX consistency - Simplify component logic while maintaining show/hide toggle functionality * chore: format code with prettier - Apply consistent code formatting across all TypeScript files - Fix indentation and spacing according to project style guide --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -18,7 +18,7 @@ function App() {
|
|||||||
path: string;
|
path: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
const [notification, setNotification] = useState<{
|
const [notification, setNotification] = useState<{
|
||||||
message: string;
|
message: string;
|
||||||
@@ -37,7 +37,7 @@ function App() {
|
|||||||
const showNotification = (
|
const showNotification = (
|
||||||
message: string,
|
message: string,
|
||||||
type: "success" | "error",
|
type: "success" | "error",
|
||||||
duration = 3000
|
duration = 3000,
|
||||||
) => {
|
) => {
|
||||||
// 清除之前的定时器
|
// 清除之前的定时器
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
@@ -182,7 +182,7 @@ function App() {
|
|||||||
showNotification(
|
showNotification(
|
||||||
`切换成功!请重启 ${appName} 终端以生效`,
|
`切换成功!请重启 ${appName} 终端以生效`,
|
||||||
"success",
|
"success",
|
||||||
2000
|
2000,
|
||||||
);
|
);
|
||||||
// 更新托盘菜单
|
// 更新托盘菜单
|
||||||
await window.api.updateTrayMenu();
|
await window.api.updateTrayMenu();
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import {
|
|||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||||
import JsonEditor from "./JsonEditor";
|
import PresetSelector from "./ProviderForm/PresetSelector";
|
||||||
import { X, AlertCircle, Save, Zap } from "lucide-react";
|
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
||||||
|
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
||||||
|
import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
||||||
|
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
||||||
|
import { X, AlertCircle, Save } from "lucide-react";
|
||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
appType?: AppType;
|
appType?: AppType;
|
||||||
@@ -49,7 +53,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexApiKey, setCodexApiKey] = useState("");
|
const [codexApiKey, setCodexApiKey] = useState("");
|
||||||
// -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化 Codex 配置
|
// 初始化 Codex 配置
|
||||||
@@ -70,20 +74,42 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isCodex, initialData]);
|
}, [isCodex, initialData]);
|
||||||
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||||
showPresets ? -1 : null
|
showPresets ? -1 : null,
|
||||||
);
|
);
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
|
||||||
|
// Kimi 模型选择状态
|
||||||
|
const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
|
||||||
|
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
|
||||||
|
useState("");
|
||||||
|
|
||||||
// 初始化时检查禁用签名状态
|
// 初始化时检查禁用签名状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
|
||||||
|
// 初始化 Kimi 模型选择(编辑模式)
|
||||||
|
if (
|
||||||
|
initialData.settingsConfig &&
|
||||||
|
typeof initialData.settingsConfig === "object"
|
||||||
|
) {
|
||||||
|
const config = initialData.settingsConfig as {
|
||||||
|
env?: Record<string, any>;
|
||||||
|
};
|
||||||
|
if (config.env) {
|
||||||
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
|
setKimiAnthropicSmallFastModel(
|
||||||
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
@@ -155,7 +181,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
@@ -188,7 +214,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 更新JSON配置
|
// 更新JSON配置
|
||||||
const updatedConfig = updateCoAuthoredSetting(
|
const updatedConfig = updateCoAuthoredSetting(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
checked
|
checked,
|
||||||
);
|
);
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
@@ -214,6 +240,24 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 同步选择框状态
|
// 同步选择框状态
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
|
||||||
|
// 如果是 Kimi 预设,初始化模型选择
|
||||||
|
if (
|
||||||
|
preset.name?.includes("Kimi") &&
|
||||||
|
preset.settingsConfig &&
|
||||||
|
typeof preset.settingsConfig === "object"
|
||||||
|
) {
|
||||||
|
const config = preset.settingsConfig as { env?: Record<string, any> };
|
||||||
|
if (config.env) {
|
||||||
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
|
setKimiAnthropicSmallFastModel(
|
||||||
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setKimiAnthropicModel("");
|
||||||
|
setKimiAnthropicSmallFastModel("");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理点击自定义按钮
|
// 处理点击自定义按钮
|
||||||
@@ -226,22 +270,24 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
});
|
});
|
||||||
setApiKey("");
|
setApiKey("");
|
||||||
setDisableCoAuthored(false);
|
setDisableCoAuthored(false);
|
||||||
|
setKimiAnthropicModel("");
|
||||||
|
setKimiAnthropicSmallFastModel("");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Codex: 应用预设
|
// Codex: 应用预设
|
||||||
const applyCodexPreset = (
|
const applyCodexPreset = (
|
||||||
preset: (typeof codexProviderPresets)[0],
|
preset: (typeof codexProviderPresets)[0],
|
||||||
index: number
|
index: number,
|
||||||
) => {
|
) => {
|
||||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||||
setCodexAuth(authString);
|
setCodexAuth(authString);
|
||||||
setCodexConfig(preset.config || "");
|
setCodexConfig(preset.config || "");
|
||||||
|
|
||||||
setFormData({
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
websiteUrl: preset.websiteUrl,
|
websiteUrl: preset.websiteUrl,
|
||||||
settingsConfig: formData.settingsConfig,
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedCodexPreset(index);
|
setSelectedCodexPreset(index);
|
||||||
|
|
||||||
@@ -269,7 +315,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const configString = setApiKeyInConfig(
|
const configString = setApiKeyInConfig(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
key.trim(),
|
key.trim(),
|
||||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 }
|
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新表单配置
|
// 更新表单配置
|
||||||
@@ -307,6 +353,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
selectedPreset >= 0 &&
|
selectedPreset >= 0 &&
|
||||||
providerPresets[selectedPreset]?.isOfficial === true;
|
providerPresets[selectedPreset]?.isOfficial === true;
|
||||||
|
|
||||||
|
// 判断当前选中的预设是否是 Kimi
|
||||||
|
const isKimiPreset =
|
||||||
|
selectedPreset !== null &&
|
||||||
|
selectedPreset >= 0 &&
|
||||||
|
providerPresets[selectedPreset]?.name?.includes("Kimi");
|
||||||
|
|
||||||
|
// 判断当前编辑的是否是 Kimi 提供商(通过名称或配置判断)
|
||||||
|
const isEditingKimi =
|
||||||
|
initialData &&
|
||||||
|
(formData.name.includes("Kimi") ||
|
||||||
|
formData.name.includes("kimi") ||
|
||||||
|
(formData.settingsConfig.includes("api.moonshot.cn") &&
|
||||||
|
formData.settingsConfig.includes("ANTHROPIC_MODEL")));
|
||||||
|
|
||||||
|
// 综合判断是否应该显示 Kimi 模型选择器
|
||||||
|
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
||||||
|
|
||||||
// Codex: 控制显示 API Key 与官方标记
|
// Codex: 控制显示 API Key 与官方标记
|
||||||
const getCodexAuthApiKey = (authString: string): string => {
|
const getCodexAuthApiKey = (authString: string): string => {
|
||||||
try {
|
try {
|
||||||
@@ -316,20 +379,49 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义模式(-1)不显示独立的 API Key 输入框
|
// 自定义模式(-1)不显示独立的 API Key 输入框
|
||||||
const showCodexApiKey =
|
const showCodexApiKey =
|
||||||
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
||||||
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
||||||
|
|
||||||
const isCodexOfficialPreset =
|
const isCodexOfficialPreset =
|
||||||
selectedCodexPreset !== null &&
|
selectedCodexPreset !== null &&
|
||||||
selectedCodexPreset >= 0 &&
|
selectedCodexPreset >= 0 &&
|
||||||
codexProviderPresets[selectedCodexPreset]?.isOfficial === true;
|
codexProviderPresets[selectedCodexPreset]?.isOfficial === true;
|
||||||
|
|
||||||
|
// Kimi 模型选择处理函数
|
||||||
|
const handleKimiModelChange = (
|
||||||
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
if (field === "ANTHROPIC_MODEL") {
|
||||||
|
setKimiAnthropicModel(value);
|
||||||
|
} else {
|
||||||
|
setKimiAnthropicSmallFastModel(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置 JSON
|
||||||
|
try {
|
||||||
|
const currentConfig = JSON.parse(formData.settingsConfig || "{}");
|
||||||
|
if (!currentConfig.env) currentConfig.env = {};
|
||||||
|
currentConfig.env[field] = value;
|
||||||
|
|
||||||
|
const updatedConfigString = JSON.stringify(currentConfig, null, 2);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
settingsConfig: updatedConfigString,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("更新 Kimi 模型配置失败:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始时从配置中同步 API Key(编辑模式)
|
// 初始时从配置中同步 API Key(编辑模式)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const parsedKey = getApiKeyFromConfig(
|
const parsedKey = getApiKeyFromConfig(
|
||||||
JSON.stringify(initialData.settingsConfig)
|
JSON.stringify(initialData.settingsConfig),
|
||||||
);
|
);
|
||||||
if (parsedKey) setApiKey(parsedKey);
|
if (parsedKey) setApiKey(parsedKey);
|
||||||
}
|
}
|
||||||
@@ -390,107 +482,25 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showPresets && !isCodex && (
|
{showPresets && !isCodex && (
|
||||||
<div className="space-y-4">
|
<PresetSelector
|
||||||
<div>
|
presets={providerPresets}
|
||||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
selectedIndex={selectedPreset}
|
||||||
选择配置类型
|
onSelectPreset={(index) =>
|
||||||
</label>
|
applyPreset(providerPresets[index], index)
|
||||||
<div className="flex flex-wrap gap-2">
|
}
|
||||||
<button
|
onCustomClick={handleCustomClick}
|
||||||
type="button"
|
/>
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
selectedPreset === -1
|
|
||||||
? "bg-[var(--color-primary)] text-white"
|
|
||||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
|
||||||
}`}
|
|
||||||
onClick={handleCustomClick}
|
|
||||||
>
|
|
||||||
自定义
|
|
||||||
</button>
|
|
||||||
{providerPresets.map((preset, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
selectedPreset === index
|
|
||||||
? preset.isOfficial
|
|
||||||
? "bg-[var(--color-warning)] text-white"
|
|
||||||
: "bg-[var(--color-primary)] text-white"
|
|
||||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
|
||||||
}`}
|
|
||||||
onClick={() => applyPreset(preset, index)}
|
|
||||||
>
|
|
||||||
{preset.isOfficial && <Zap size={14} />}
|
|
||||||
{preset.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{selectedPreset === -1 && (
|
|
||||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
||||||
手动配置供应商,需要填写完整的配置信息
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{selectedPreset !== -1 && selectedPreset !== null && (
|
|
||||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
||||||
{isOfficialPreset
|
|
||||||
? "Claude 官方登录,不需要填写 API Key"
|
|
||||||
: "使用预设配置,只需填写 API Key"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showPresets && isCodex && (
|
{showPresets && isCodex && (
|
||||||
<div className="space-y-4">
|
<PresetSelector
|
||||||
<div>
|
presets={codexProviderPresets}
|
||||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
selectedIndex={selectedCodexPreset}
|
||||||
选择配置类型
|
onSelectPreset={(index) =>
|
||||||
</label>
|
applyCodexPreset(codexProviderPresets[index], index)
|
||||||
<div className="flex flex-wrap gap-2">
|
}
|
||||||
<button
|
onCustomClick={handleCodexCustomClick}
|
||||||
type="button"
|
/>
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
selectedCodexPreset === -1
|
|
||||||
? "bg-[var(--color-primary)] text-white"
|
|
||||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
|
||||||
}`}
|
|
||||||
onClick={handleCodexCustomClick}
|
|
||||||
>
|
|
||||||
自定义
|
|
||||||
</button>
|
|
||||||
{codexProviderPresets.map((preset, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
selectedCodexPreset === index
|
|
||||||
? preset.isOfficial
|
|
||||||
? "bg-[var(--color-warning)] text-white"
|
|
||||||
: "bg-[var(--color-primary)] text-white"
|
|
||||||
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]"
|
|
||||||
}`}
|
|
||||||
onClick={() => applyCodexPreset(preset, index)}
|
|
||||||
>
|
|
||||||
{preset.isOfficial && <Zap size={14} />}
|
|
||||||
{preset.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{selectedCodexPreset === -1 && (
|
|
||||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
||||||
手动配置供应商,需要填写完整的配置信息
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{selectedCodexPreset !== -1 && selectedCodexPreset !== null && (
|
|
||||||
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
||||||
{isCodexOfficialPreset
|
|
||||||
? "Codex 官方登录,不需要填写 API Key"
|
|
||||||
: "使用预设配置,只需填写 API Key"}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -514,47 +524,36 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isCodex && showApiKey && (
|
{!isCodex && showApiKey && (
|
||||||
<div className="space-y-2">
|
<ApiKeyInput
|
||||||
<label
|
|
||||||
htmlFor="apiKey"
|
|
||||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
|
||||||
>
|
|
||||||
API Key *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="apiKey"
|
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
onChange={handleApiKeyChange}
|
||||||
placeholder={
|
placeholder={
|
||||||
isOfficialPreset
|
isOfficialPreset
|
||||||
? "官方登录无需填写 API Key,直接保存即可"
|
? "官方登录无需填写 API Key,直接保存即可"
|
||||||
|
: shouldShowKimiSelector
|
||||||
|
? "sk-xxx-api-key-here (填写后可获取模型列表)"
|
||||||
: "只需要填这里,下方配置会自动填充"
|
: "只需要填这里,下方配置会自动填充"
|
||||||
}
|
}
|
||||||
disabled={isOfficialPreset}
|
disabled={isOfficialPreset}
|
||||||
autoComplete="off"
|
|
||||||
className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${
|
|
||||||
isOfficialPreset
|
|
||||||
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
|
||||||
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{!isCodex && shouldShowKimiSelector && apiKey.trim() && (
|
||||||
|
<KimiModelSelector
|
||||||
|
apiKey={apiKey}
|
||||||
|
anthropicModel={kimiAnthropicModel}
|
||||||
|
anthropicSmallFastModel={kimiAnthropicSmallFastModel}
|
||||||
|
onModelChange={handleKimiModelChange}
|
||||||
|
disabled={isOfficialPreset}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCodex && showCodexApiKey && (
|
{isCodex && showCodexApiKey && (
|
||||||
<div className="space-y-2">
|
<ApiKeyInput
|
||||||
<label
|
|
||||||
htmlFor="codexApiKey"
|
|
||||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
|
||||||
>
|
|
||||||
API Key *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="codexApiKey"
|
id="codexApiKey"
|
||||||
|
label="API Key"
|
||||||
value={codexApiKey}
|
value={codexApiKey}
|
||||||
onChange={(e) => handleCodexApiKeyChange(e.target.value)}
|
onChange={handleCodexApiKeyChange}
|
||||||
placeholder={
|
placeholder={
|
||||||
isCodexOfficialPreset
|
isCodexOfficialPreset
|
||||||
? "官方无需填写 API Key,直接保存即可"
|
? "官方无需填写 API Key,直接保存即可"
|
||||||
@@ -566,14 +565,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
selectedCodexPreset >= 0 &&
|
selectedCodexPreset >= 0 &&
|
||||||
!isCodexOfficialPreset
|
!isCodexOfficialPreset
|
||||||
}
|
}
|
||||||
autoComplete="off"
|
|
||||||
className={`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${
|
|
||||||
isCodexOfficialPreset
|
|
||||||
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
|
||||||
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -597,23 +589,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
{/* Claude 或 Codex 的配置部分 */}
|
{/* Claude 或 Codex 的配置部分 */}
|
||||||
{isCodex ? (
|
{isCodex ? (
|
||||||
// Codex: 双编辑器
|
<CodexConfigEditor
|
||||||
<div className="space-y-6">
|
authValue={codexAuth}
|
||||||
<div className="space-y-2">
|
configValue={codexConfig}
|
||||||
<label
|
onAuthChange={setCodexAuth}
|
||||||
htmlFor="codexAuth"
|
onConfigChange={setCodexConfig}
|
||||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
onAuthBlur={() => {
|
||||||
>
|
|
||||||
auth.json (JSON) *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="codexAuth"
|
|
||||||
value={codexAuth}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setCodexAuth(value);
|
|
||||||
try {
|
try {
|
||||||
const auth = JSON.parse(value || "{}");
|
const auth = JSON.parse(codexAuth || "{}");
|
||||||
const key =
|
const key =
|
||||||
typeof auth.OPENAI_API_KEY === "string"
|
typeof auth.OPENAI_API_KEY === "string"
|
||||||
? auth.OPENAI_API_KEY
|
? auth.OPENAI_API_KEY
|
||||||
@@ -623,77 +606,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={`{
|
|
||||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
|
||||||
}`}
|
|
||||||
rows={6}
|
|
||||||
required
|
|
||||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[8rem]"
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
|
||||||
Codex auth.json 配置内容
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label
|
|
||||||
htmlFor="codexConfig"
|
|
||||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
|
||||||
>
|
|
||||||
config.toml (TOML)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="codexConfig"
|
|
||||||
value={codexConfig}
|
|
||||||
onChange={(e) => setCodexConfig(e.target.value)}
|
|
||||||
placeholder=""
|
|
||||||
rows={8}
|
|
||||||
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[10rem]"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
|
||||||
Codex config.toml 配置内容
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
// Claude: 原有的单编辑器
|
<ClaudeConfigEditor
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label
|
|
||||||
htmlFor="settingsConfig"
|
|
||||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
|
||||||
>
|
|
||||||
Claude Code 配置 (JSON) *
|
|
||||||
</label>
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-[var(--color-text-secondary)] cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={disableCoAuthored}
|
|
||||||
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
|
||||||
className="w-4 h-4 text-[var(--color-primary)] bg-white border-[var(--color-border)] rounded focus:ring-[var(--color-primary)] focus:ring-2"
|
|
||||||
/>
|
|
||||||
禁止 Claude Code 签名
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<JsonEditor
|
|
||||||
value={formData.settingsConfig}
|
value={formData.settingsConfig}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleChange({
|
handleChange({
|
||||||
target: { name: "settingsConfig", value },
|
target: { name: "settingsConfig", value },
|
||||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||||
}
|
}
|
||||||
placeholder={`{
|
disableCoAuthored={disableCoAuthored}
|
||||||
"env": {
|
onCoAuthoredToggle={handleCoAuthoredToggle}
|
||||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
rows={12}
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
|
||||||
完整的 Claude Code settings.json 配置内容
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
70
src/components/ProviderForm/ApiKeyInput.tsx
Normal file
70
src/components/ProviderForm/ApiKeyInput.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
||||||
|
interface ApiKeyInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
label?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "请输入API Key",
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
label = "API Key",
|
||||||
|
id = "apiKey",
|
||||||
|
}) => {
|
||||||
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
|
||||||
|
const toggleShowKey = () => {
|
||||||
|
setShowKey(!showKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputClass = `w-full px-3 py-2 pr-10 border rounded-lg text-sm transition-colors ${
|
||||||
|
disabled
|
||||||
|
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||||
|
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
{label} {required && "*"}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showKey ? "text" : "password"}
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
autoComplete="off"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
{!disabled && value && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleShowKey}
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
|
||||||
|
aria-label={showKey ? "隐藏API Key" : "显示API Key"}
|
||||||
|
>
|
||||||
|
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiKeyInput;
|
||||||
54
src/components/ProviderForm/ClaudeConfigEditor.tsx
Normal file
54
src/components/ProviderForm/ClaudeConfigEditor.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import JsonEditor from "../JsonEditor";
|
||||||
|
|
||||||
|
interface ClaudeConfigEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
disableCoAuthored: boolean;
|
||||||
|
onCoAuthoredToggle: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disableCoAuthored,
|
||||||
|
onCoAuthoredToggle,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="settingsConfig"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
Claude Code 配置 (JSON) *
|
||||||
|
</label>
|
||||||
|
<label className="inline-flex items-center gap-2 text-sm text-[var(--color-text-secondary)] cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={disableCoAuthored}
|
||||||
|
onChange={(e) => onCoAuthoredToggle(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-[var(--color-primary)] bg-white border-[var(--color-border)] rounded focus:ring-[var(--color-primary)] focus:ring-2"
|
||||||
|
/>
|
||||||
|
禁止 Claude Code 签名
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<JsonEditor
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={`{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
rows={12}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||||
|
完整的 Claude Code settings.json 配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClaudeConfigEditor;
|
||||||
67
src/components/ProviderForm/CodexConfigEditor.tsx
Normal file
67
src/components/ProviderForm/CodexConfigEditor.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface CodexConfigEditorProps {
|
||||||
|
authValue: string;
|
||||||
|
configValue: string;
|
||||||
|
onAuthChange: (value: string) => void;
|
||||||
|
onConfigChange: (value: string) => void;
|
||||||
|
onAuthBlur?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||||
|
authValue,
|
||||||
|
configValue,
|
||||||
|
onAuthChange,
|
||||||
|
onConfigChange,
|
||||||
|
onAuthBlur,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="codexAuth"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
auth.json (JSON) *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="codexAuth"
|
||||||
|
value={authValue}
|
||||||
|
onChange={(e) => onAuthChange(e.target.value)}
|
||||||
|
onBlur={onAuthBlur}
|
||||||
|
placeholder={`{
|
||||||
|
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||||
|
}`}
|
||||||
|
rows={6}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[8rem]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||||
|
Codex auth.json 配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="codexConfig"
|
||||||
|
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||||
|
>
|
||||||
|
config.toml (TOML)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="codexConfig"
|
||||||
|
value={configValue}
|
||||||
|
onChange={(e) => onConfigChange(e.target.value)}
|
||||||
|
placeholder=""
|
||||||
|
rows={8}
|
||||||
|
className="w-full px-3 py-2 border border-[var(--color-border)] rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)] transition-colors resize-y min-h-[10rem]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||||
|
Codex config.toml 配置内容
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodexConfigEditor;
|
||||||
185
src/components/ProviderForm/KimiModelSelector.tsx
Normal file
185
src/components/ProviderForm/KimiModelSelector.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { ChevronDown, RefreshCw, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface KimiModel {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
owned_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KimiModelSelectorProps {
|
||||||
|
apiKey: string;
|
||||||
|
anthropicModel: string;
|
||||||
|
anthropicSmallFastModel: string;
|
||||||
|
onModelChange: (
|
||||||
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
|
value: string,
|
||||||
|
) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||||
|
apiKey,
|
||||||
|
anthropicModel,
|
||||||
|
anthropicSmallFastModel,
|
||||||
|
onModelChange,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const [models, setModels] = useState<KimiModel[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [debouncedKey, setDebouncedKey] = useState("");
|
||||||
|
|
||||||
|
// 获取模型列表
|
||||||
|
const fetchModelsWithKey = async (key: string) => {
|
||||||
|
if (!key) {
|
||||||
|
setError("请先填写 API Key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.moonshot.cn/v1/models", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.data && Array.isArray(data.data)) {
|
||||||
|
setModels(data.data);
|
||||||
|
} else {
|
||||||
|
throw new Error("返回数据格式错误");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取模型列表失败:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "获取模型列表失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 500ms 防抖 API Key
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedKey(apiKey.trim());
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [apiKey]);
|
||||||
|
|
||||||
|
// 当防抖后的 Key 改变时自动获取模型列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedKey) {
|
||||||
|
fetchModelsWithKey(debouncedKey);
|
||||||
|
} else {
|
||||||
|
setModels([]);
|
||||||
|
setError("");
|
||||||
|
}
|
||||||
|
}, [debouncedKey]);
|
||||||
|
|
||||||
|
const selectClass = `w-full px-3 py-2 border rounded-lg text-sm transition-colors appearance-none bg-white ${
|
||||||
|
disabled
|
||||||
|
? "bg-[var(--color-bg-tertiary)] border-[var(--color-border)] text-[var(--color-text-tertiary)] cursor-not-allowed"
|
||||||
|
: "border-[var(--color-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const ModelSelect: React.FC<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}> = ({ label, value, onChange }) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--color-text-primary)]">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
disabled={disabled || loading || models.length === 0}
|
||||||
|
className={selectClass}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{loading
|
||||||
|
? "加载中..."
|
||||||
|
: models.length === 0
|
||||||
|
? "暂无模型"
|
||||||
|
: "请选择模型"}
|
||||||
|
</option>
|
||||||
|
{models.map((model) => (
|
||||||
|
<option key={model.id} value={model.id}>
|
||||||
|
{model.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-[var(--color-text-secondary)] pointer-events-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium text-[var(--color-text-primary)]">
|
||||||
|
模型配置
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => debouncedKey && fetchModelsWithKey(debouncedKey)}
|
||||||
|
disabled={disabled || loading || !debouncedKey}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||||
|
刷新模型列表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="flex items-center gap-2 p-3 bg-[var(--color-error-light)] border border-[var(--color-error)]/20 rounded-lg">
|
||||||
|
<AlertCircle
|
||||||
|
size={16}
|
||||||
|
className="text-[var(--color-error)] flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<p className="text-[var(--color-error)] text-xs">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<ModelSelect
|
||||||
|
label="主模型 (ANTHROPIC_MODEL)"
|
||||||
|
value={anthropicModel}
|
||||||
|
onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)}
|
||||||
|
/>
|
||||||
|
<ModelSelect
|
||||||
|
label="快速模型 (ANTHROPIC_SMALL_FAST_MODEL)"
|
||||||
|
value={anthropicSmallFastModel}
|
||||||
|
onChange={(value) =>
|
||||||
|
onModelChange("ANTHROPIC_SMALL_FAST_MODEL", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!apiKey.trim() && (
|
||||||
|
<div className="p-3 bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded-lg">
|
||||||
|
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||||
|
📝 请先填写 API Key(格式:sk-xxx-api-key-here)以获取可用模型列表
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KimiModelSelector;
|
||||||
91
src/components/ProviderForm/PresetSelector.tsx
Normal file
91
src/components/ProviderForm/PresetSelector.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Zap } from "lucide-react";
|
||||||
|
|
||||||
|
interface Preset {
|
||||||
|
name: string;
|
||||||
|
isOfficial?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PresetSelectorProps {
|
||||||
|
title?: string;
|
||||||
|
presets: Preset[];
|
||||||
|
selectedIndex: number | null;
|
||||||
|
onSelectPreset: (index: number) => void;
|
||||||
|
onCustomClick: () => void;
|
||||||
|
customLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||||
|
title = "选择配置类型",
|
||||||
|
presets,
|
||||||
|
selectedIndex,
|
||||||
|
onSelectPreset,
|
||||||
|
onCustomClick,
|
||||||
|
customLabel = "自定义",
|
||||||
|
}) => {
|
||||||
|
const getButtonClass = (index: number, isOfficial?: boolean) => {
|
||||||
|
const isSelected = selectedIndex === index;
|
||||||
|
const baseClass =
|
||||||
|
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
return isOfficial
|
||||||
|
? `${baseClass} bg-[var(--color-warning)] text-white`
|
||||||
|
: `${baseClass} bg-[var(--color-primary)] text-white`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClass} bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-border)]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDescription = () => {
|
||||||
|
if (selectedIndex === -1) {
|
||||||
|
return "手动配置供应商,需要填写完整的配置信息";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIndex !== null && selectedIndex >= 0) {
|
||||||
|
const preset = presets[selectedIndex];
|
||||||
|
return preset?.isOfficial
|
||||||
|
? "Claude 官方登录,不需要填写 API Key"
|
||||||
|
: "使用预设配置,只需填写 API Key";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||||
|
{title}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={getButtonClass(-1)}
|
||||||
|
onClick={onCustomClick}
|
||||||
|
>
|
||||||
|
{customLabel}
|
||||||
|
</button>
|
||||||
|
{presets.map((preset, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
className={getButtonClass(index, preset.isOfficial)}
|
||||||
|
onClick={() => onSelectPreset(index)}
|
||||||
|
>
|
||||||
|
{preset.isOfficial && <Zap size={14} />}
|
||||||
|
{preset.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{getDescription() && (
|
||||||
|
<p className="text-sm text-[var(--color-text-secondary)]">
|
||||||
|
{getDescription()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PresetSelector;
|
||||||
@@ -23,7 +23,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "deepseek-chat",
|
ANTHROPIC_MODEL: "deepseek-chat",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -46,7 +46,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL:
|
ANTHROPIC_BASE_URL:
|
||||||
"https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
|
"https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -56,7 +56,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "kimi-k2-turbo-preview",
|
ANTHROPIC_MODEL: "kimi-k2-turbo-preview",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
||||||
},
|
},
|
||||||
@@ -66,11 +66,11 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
name: "魔搭",
|
name: "魔搭",
|
||||||
websiteUrl: "https://modelscope.cn",
|
websiteUrl: "https://modelscope.cn",
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
"env": {
|
env: {
|
||||||
ANTHROPIC_AUTH_TOKEN: "ms-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "ms-your-api-key",
|
||||||
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.5",
|
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5"
|
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -80,7 +80,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const tauriAPI = {
|
|||||||
// 更新供应商
|
// 更新供应商
|
||||||
updateProvider: async (
|
updateProvider: async (
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
app?: AppType
|
app?: AppType,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke("update_provider", { provider, app_type: app, app });
|
return await invoke("update_provider", { provider, app_type: app, app });
|
||||||
@@ -76,7 +76,7 @@ export const tauriAPI = {
|
|||||||
// 切换供应商
|
// 切换供应商
|
||||||
switchProvider: async (
|
switchProvider: async (
|
||||||
providerId: string,
|
providerId: string,
|
||||||
app?: AppType
|
app?: AppType,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
return await invoke("switch_provider", {
|
return await invoke("switch_provider", {
|
||||||
@@ -92,7 +92,7 @@ export const tauriAPI = {
|
|||||||
|
|
||||||
// 导入当前配置为默认供应商
|
// 导入当前配置为默认供应商
|
||||||
importCurrentConfigAsDefault: async (
|
importCurrentConfigAsDefault: async (
|
||||||
app?: AppType
|
app?: AppType,
|
||||||
): Promise<ImportResult> => {
|
): Promise<ImportResult> => {
|
||||||
try {
|
try {
|
||||||
const success = await invoke<boolean>("import_default_config", {
|
const success = await invoke<boolean>("import_default_config", {
|
||||||
@@ -180,7 +180,7 @@ export const tauriAPI = {
|
|||||||
|
|
||||||
// 监听供应商切换事件
|
// 监听供应商切换事件
|
||||||
onProviderSwitched: async (
|
onProviderSwitched: async (
|
||||||
callback: (data: { appType: string; providerId: string }) => void
|
callback: (data: { appType: string; providerId: string }) => void,
|
||||||
): Promise<UnlistenFn> => {
|
): Promise<UnlistenFn> => {
|
||||||
return await listen("provider-switched", (event) => {
|
return await listen("provider-switched", (event) => {
|
||||||
callback(event.payload as { appType: string; providerId: string });
|
callback(event.payload as { appType: string; providerId: string });
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const checkCoAuthoredSetting = (jsonString: string): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 读取配置中的 API Key(env.ANTHROPIC_AUTH_TOKEN)
|
// 读取配置中的 API Key(env.ANTHROPIC_AUTH_TOKEN)
|
||||||
export const getApiKeyFromConfig = (jsonString: string): string => {
|
export const getApiKeyFromConfig = (jsonString: string): string => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@@ -33,7 +33,7 @@ declare global {
|
|||||||
openExternal: (url: string) => Promise<void>;
|
openExternal: (url: string) => Promise<void>;
|
||||||
updateTrayMenu: () => Promise<boolean>;
|
updateTrayMenu: () => Promise<boolean>;
|
||||||
onProviderSwitched: (
|
onProviderSwitched: (
|
||||||
callback: (data: { appType: string; providerId: string }) => void
|
callback: (data: { appType: string; providerId: string }) => void,
|
||||||
) => Promise<UnlistenFn>;
|
) => Promise<UnlistenFn>;
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
|
|||||||
Reference in New Issue
Block a user