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;
|
||||
} | null>(null);
|
||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [notification, setNotification] = useState<{
|
||||
message: string;
|
||||
@@ -37,7 +37,7 @@ function App() {
|
||||
const showNotification = (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration = 3000
|
||||
duration = 3000,
|
||||
) => {
|
||||
// 清除之前的定时器
|
||||
if (timeoutRef.current) {
|
||||
@@ -182,7 +182,7 @@ function App() {
|
||||
showNotification(
|
||||
`切换成功!请重启 ${appName} 终端以生效`,
|
||||
"success",
|
||||
2000
|
||||
2000,
|
||||
);
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
|
||||
@@ -10,8 +10,12 @@ import {
|
||||
} from "../utils/providerConfigUtils";
|
||||
import { providerPresets } from "../config/providerPresets";
|
||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||
import JsonEditor from "./JsonEditor";
|
||||
import { X, AlertCircle, Save, Zap } from "lucide-react";
|
||||
import PresetSelector from "./ProviderForm/PresetSelector";
|
||||
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 {
|
||||
appType?: AppType;
|
||||
@@ -49,7 +53,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const [codexApiKey, setCodexApiKey] = useState("");
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||
showPresets && isCodex ? -1 : null
|
||||
showPresets && isCodex ? -1 : null,
|
||||
);
|
||||
|
||||
// 初始化 Codex 配置
|
||||
@@ -70,20 +74,42 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}
|
||||
}
|
||||
}, [isCodex, initialData]);
|
||||
|
||||
const [error, setError] = useState("");
|
||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||
showPresets ? -1 : null
|
||||
showPresets ? -1 : null,
|
||||
);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
|
||||
// Kimi 模型选择状态
|
||||
const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
|
||||
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
|
||||
useState("");
|
||||
|
||||
// 初始化时检查禁用签名状态
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||
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]);
|
||||
|
||||
@@ -155,7 +181,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
@@ -188,7 +214,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 更新JSON配置
|
||||
const updatedConfig = updateCoAuthoredSetting(
|
||||
formData.settingsConfig,
|
||||
checked
|
||||
checked,
|
||||
);
|
||||
setFormData({
|
||||
...formData,
|
||||
@@ -214,6 +240,24 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 同步选择框状态
|
||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||
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("");
|
||||
setDisableCoAuthored(false);
|
||||
setKimiAnthropicModel("");
|
||||
setKimiAnthropicSmallFastModel("");
|
||||
};
|
||||
|
||||
// Codex: 应用预设
|
||||
const applyCodexPreset = (
|
||||
preset: (typeof codexProviderPresets)[0],
|
||||
index: number
|
||||
index: number,
|
||||
) => {
|
||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||
setCodexAuth(authString);
|
||||
setCodexConfig(preset.config || "");
|
||||
|
||||
setFormData({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
name: preset.name,
|
||||
websiteUrl: preset.websiteUrl,
|
||||
settingsConfig: formData.settingsConfig,
|
||||
});
|
||||
}));
|
||||
|
||||
setSelectedCodexPreset(index);
|
||||
|
||||
@@ -269,7 +315,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const configString = setApiKeyInConfig(
|
||||
formData.settingsConfig,
|
||||
key.trim(),
|
||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 }
|
||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
||||
);
|
||||
|
||||
// 更新表单配置
|
||||
@@ -307,6 +353,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
selectedPreset >= 0 &&
|
||||
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 与官方标记
|
||||
const getCodexAuthApiKey = (authString: string): string => {
|
||||
try {
|
||||
@@ -316,20 +379,49 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义模式(-1)不显示独立的 API Key 输入框
|
||||
const showCodexApiKey =
|
||||
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
||||
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
||||
|
||||
const isCodexOfficialPreset =
|
||||
selectedCodexPreset !== null &&
|
||||
selectedCodexPreset >= 0 &&
|
||||
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(编辑模式)
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
const parsedKey = getApiKeyFromConfig(
|
||||
JSON.stringify(initialData.settingsConfig)
|
||||
JSON.stringify(initialData.settingsConfig),
|
||||
);
|
||||
if (parsedKey) setApiKey(parsedKey);
|
||||
}
|
||||
@@ -390,107 +482,25 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
)}
|
||||
|
||||
{showPresets && !isCodex && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||
选择配置类型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
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>
|
||||
<PresetSelector
|
||||
presets={providerPresets}
|
||||
selectedIndex={selectedPreset}
|
||||
onSelectPreset={(index) =>
|
||||
applyPreset(providerPresets[index], index)
|
||||
}
|
||||
onCustomClick={handleCustomClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPresets && isCodex && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--color-text-primary)] mb-3">
|
||||
选择配置类型
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
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>
|
||||
<PresetSelector
|
||||
presets={codexProviderPresets}
|
||||
selectedIndex={selectedCodexPreset}
|
||||
onSelectPreset={(index) =>
|
||||
applyCodexPreset(codexProviderPresets[index], index)
|
||||
}
|
||||
onCustomClick={handleCodexCustomClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -514,47 +524,36 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
</div>
|
||||
|
||||
{!isCodex && showApiKey && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="apiKey"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
API Key *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiKey"
|
||||
<ApiKeyInput
|
||||
value={apiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
onChange={handleApiKeyChange}
|
||||
placeholder={
|
||||
isOfficialPreset
|
||||
? "官方登录无需填写 API Key,直接保存即可"
|
||||
: shouldShowKimiSelector
|
||||
? "sk-xxx-api-key-here (填写后可获取模型列表)"
|
||||
: "只需要填这里,下方配置会自动填充"
|
||||
}
|
||||
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 && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="codexApiKey"
|
||||
className="block text-sm font-medium text-[var(--color-text-primary)]"
|
||||
>
|
||||
API Key *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
<ApiKeyInput
|
||||
id="codexApiKey"
|
||||
label="API Key"
|
||||
value={codexApiKey}
|
||||
onChange={(e) => handleCodexApiKeyChange(e.target.value)}
|
||||
onChange={handleCodexApiKeyChange}
|
||||
placeholder={
|
||||
isCodexOfficialPreset
|
||||
? "官方无需填写 API Key,直接保存即可"
|
||||
@@ -566,14 +565,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
selectedCodexPreset >= 0 &&
|
||||
!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">
|
||||
@@ -597,23 +589,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
|
||||
{/* Claude 或 Codex 的配置部分 */}
|
||||
{isCodex ? (
|
||||
// Codex: 双编辑器
|
||||
<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={codexAuth}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setCodexAuth(value);
|
||||
<CodexConfigEditor
|
||||
authValue={codexAuth}
|
||||
configValue={codexConfig}
|
||||
onAuthChange={setCodexAuth}
|
||||
onConfigChange={setCodexConfig}
|
||||
onAuthBlur={() => {
|
||||
try {
|
||||
const auth = JSON.parse(value || "{}");
|
||||
const auth = JSON.parse(codexAuth || "{}");
|
||||
const key =
|
||||
typeof auth.OPENAI_API_KEY === "string"
|
||||
? auth.OPENAI_API_KEY
|
||||
@@ -623,77 +606,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 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: 原有的单编辑器
|
||||
<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
|
||||
<ClaudeConfigEditor
|
||||
value={formData.settingsConfig}
|
||||
onChange={(value) =>
|
||||
handleChange({
|
||||
target: { name: "settingsConfig", value },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
}
|
||||
placeholder={`{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
||||
}
|
||||
}`}
|
||||
rows={12}
|
||||
disableCoAuthored={disableCoAuthored}
|
||||
onCoAuthoredToggle={handleCoAuthoredToggle}
|
||||
/>
|
||||
<p className="text-xs text-[var(--color-text-secondary)]">
|
||||
完整的 Claude Code settings.json 配置内容
|
||||
</p>
|
||||
</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: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "deepseek-chat",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
||||
},
|
||||
@@ -35,7 +35,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
settingsConfig: {
|
||||
env: {
|
||||
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: {
|
||||
ANTHROPIC_BASE_URL:
|
||||
"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: {
|
||||
env: {
|
||||
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_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
||||
},
|
||||
@@ -66,11 +66,11 @@ export const providerPresets: ProviderPreset[] = [
|
||||
name: "魔搭",
|
||||
websiteUrl: "https://modelscope.cn",
|
||||
settingsConfig: {
|
||||
"env": {
|
||||
ANTHROPIC_AUTH_TOKEN: "ms-your-api-key-here",
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: "ms-your-api-key",
|
||||
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||
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: {
|
||||
env: {
|
||||
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 (
|
||||
provider: Provider,
|
||||
app?: AppType
|
||||
app?: AppType,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("update_provider", { provider, app_type: app, app });
|
||||
@@ -76,7 +76,7 @@ export const tauriAPI = {
|
||||
// 切换供应商
|
||||
switchProvider: async (
|
||||
providerId: string,
|
||||
app?: AppType
|
||||
app?: AppType,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("switch_provider", {
|
||||
@@ -92,7 +92,7 @@ export const tauriAPI = {
|
||||
|
||||
// 导入当前配置为默认供应商
|
||||
importCurrentConfigAsDefault: async (
|
||||
app?: AppType
|
||||
app?: AppType,
|
||||
): Promise<ImportResult> => {
|
||||
try {
|
||||
const success = await invoke<boolean>("import_default_config", {
|
||||
@@ -180,7 +180,7 @@ export const tauriAPI = {
|
||||
|
||||
// 监听供应商切换事件
|
||||
onProviderSwitched: async (
|
||||
callback: (data: { appType: string; providerId: string }) => void
|
||||
callback: (data: { appType: string; providerId: string }) => void,
|
||||
): Promise<UnlistenFn> => {
|
||||
return await listen("provider-switched", (event) => {
|
||||
callback(event.payload as { appType: string; providerId: string });
|
||||
|
||||
@@ -33,7 +33,6 @@ export const checkCoAuthoredSetting = (jsonString: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 读取配置中的 API Key(env.ANTHROPIC_AUTH_TOKEN)
|
||||
export const getApiKeyFromConfig = (jsonString: string): string => {
|
||||
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>;
|
||||
updateTrayMenu: () => Promise<boolean>;
|
||||
onProviderSwitched: (
|
||||
callback: (data: { appType: string; providerId: string }) => void
|
||||
callback: (data: { appType: string; providerId: string }) => void,
|
||||
) => Promise<UnlistenFn>;
|
||||
};
|
||||
platform: {
|
||||
|
||||
Reference in New Issue
Block a user