refactor(codex): remove configuration wizard and unify provider setup experience

- Remove CodexQuickWizardModal component (~300 lines)
- Add "Custom (Blank Template)" preset with annotated TOML template
- Unify configuration experience across Claude/Codex/Gemini
- Remove wizard-related i18n keys, keep apiUrlLabel for CodexFormFields
- Simplify component integration by removing wizard state management

This change reduces code complexity by ~250 lines while providing better
user education through commented configuration templates in Chinese.

Users can now:
1. Select "Custom (Blank Template)" preset
2. See annotated TOML template with inline documentation
3. Follow step-by-step comments to configure custom providers

BREAKING CHANGE: Configuration wizard UI removed, replaced with template-based approach
This commit is contained in:
Jason
2025-11-16 12:29:18 +08:00
parent 031ea3a58f
commit 6a6980c82c
7 changed files with 49 additions and 419 deletions

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections"; import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
import { CodexQuickWizardModal } from "./CodexQuickWizardModal";
import { CodexCommonConfigModal } from "./CodexCommonConfigModal"; import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
interface CodexConfigEditorProps { interface CodexConfigEditorProps {
@@ -27,14 +26,6 @@ interface CodexConfigEditorProps {
authError: string; authError: string;
configError: string; // config.toml 错误提示 configError: string; // config.toml 错误提示
onWebsiteUrlChange?: (url: string) => void; // 更新网址回调
isTemplateModalOpen?: boolean; // 模态框状态
setIsTemplateModalOpen?: (open: boolean) => void; // 设置模态框状态
onNameChange?: (name: string) => void; // 更新供应商名称回调
} }
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
@@ -50,21 +41,9 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
commonConfigError, commonConfigError,
authError, authError,
configError, configError,
onWebsiteUrlChange,
onNameChange,
isTemplateModalOpen: externalTemplateModalOpen,
setIsTemplateModalOpen: externalSetTemplateModalOpen,
}) => { }) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Use internal state or external state
const [internalTemplateModalOpen, setInternalTemplateModalOpen] =
useState(false);
const isTemplateModalOpen =
externalTemplateModalOpen ?? internalTemplateModalOpen;
const setIsTemplateModalOpen =
externalSetTemplateModalOpen ?? setInternalTemplateModalOpen;
// Auto-open common config modal if there's an error // Auto-open common config modal if there's an error
useEffect(() => { useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) { if (commonConfigError && !isCommonConfigModalOpen) {
@@ -72,23 +51,6 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
} }
}, [commonConfigError, isCommonConfigModalOpen]); }, [commonConfigError, isCommonConfigModalOpen]);
const handleQuickWizardApply = (
auth: string,
config: string,
extras: { websiteUrl?: string; displayName?: string },
) => {
onAuthChange(auth);
onConfigChange(config);
if (onWebsiteUrlChange && extras.websiteUrl) {
onWebsiteUrlChange(extras.websiteUrl);
}
if (onNameChange && extras.displayName) {
onNameChange(extras.displayName);
}
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Auth JSON Section */} {/* Auth JSON Section */}
@@ -110,13 +72,6 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
configError={configError} configError={configError}
/> />
{/* Quick Wizard Modal */}
<CodexQuickWizardModal
isOpen={isTemplateModalOpen}
onClose={() => setIsTemplateModalOpen(false)}
onApply={handleQuickWizardApply}
/>
{/* Common Config Modal */} {/* Common Config Modal */}
<CodexCommonConfigModal <CodexCommonConfigModal
isOpen={isCommonConfigModalOpen} isOpen={isCommonConfigModalOpen}

View File

@@ -1,298 +0,0 @@
import React, { useState, useRef } from "react";
import { Save } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
generateThirdPartyAuth,
generateThirdPartyConfig,
} from "@/config/codexProviderPresets";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
interface CodexQuickWizardModalProps {
isOpen: boolean;
onClose: () => void;
onApply: (
auth: string,
config: string,
extras: {
websiteUrl?: string;
displayName?: string;
},
) => void;
}
/**
* CodexQuickWizardModal - Codex quick configuration wizard
* Helps users quickly generate auth.json and config.toml
*/
export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
isOpen,
onClose,
onApply,
}) => {
const { t } = useTranslation();
const [templateApiKey, setTemplateApiKey] = useState("");
const [templateProviderName, setTemplateProviderName] = useState("");
const [templateBaseUrl, setTemplateBaseUrl] = useState("");
const [templateWebsiteUrl, setTemplateWebsiteUrl] = useState("");
const [templateModelName, setTemplateModelName] = useState("gpt-5-codex");
const [templateDisplayName, setTemplateDisplayName] = useState("");
const apiKeyInputRef = useRef<HTMLInputElement>(null);
const baseUrlInputRef = useRef<HTMLInputElement>(null);
const modelNameInputRef = useRef<HTMLInputElement>(null);
const displayNameInputRef = useRef<HTMLInputElement>(null);
const resetForm = () => {
setTemplateApiKey("");
setTemplateProviderName("");
setTemplateBaseUrl("");
setTemplateWebsiteUrl("");
setTemplateModelName("gpt-5-codex");
setTemplateDisplayName("");
};
const handleClose = () => {
resetForm();
onClose();
};
const applyTemplate = () => {
const requiredInputs = [
displayNameInputRef.current,
apiKeyInputRef.current,
baseUrlInputRef.current,
modelNameInputRef.current,
];
for (const input of requiredInputs) {
if (input && !input.checkValidity()) {
input.reportValidity();
input.focus();
return;
}
}
const trimmedKey = templateApiKey.trim();
const trimmedBaseUrl = templateBaseUrl.trim();
const trimmedModel = templateModelName.trim();
const auth = generateThirdPartyAuth(trimmedKey);
const config = generateThirdPartyConfig(
templateProviderName || "custom",
trimmedBaseUrl,
trimmedModel,
);
onApply(JSON.stringify(auth, null, 2), config, {
websiteUrl: templateWebsiteUrl.trim(),
displayName: templateDisplayName.trim(),
});
resetForm();
onClose();
};
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
applyTemplate();
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent
zIndex="nested"
className="max-w-2xl max-h-[90vh] flex flex-col p-0"
>
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle>
</DialogHeader>
<div className="flex-1 min-h-0 space-y-4 overflow-auto px-6 py-4">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p className="text-sm text-blue-800 dark:text-blue-200">
{t("codexConfig.wizardHint")}
</p>
</div>
<div className="space-y-4">
{/* API Key */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.apiKeyLabel")}
</label>
<Input
type="text"
value={templateApiKey}
ref={apiKeyInputRef}
onChange={(e) => setTemplateApiKey(e.target.value)}
onKeyDown={handleInputKeyDown}
pattern=".*\S.*"
title={t("common.enterValidValue")}
placeholder={t("codexConfig.apiKeyPlaceholder")}
required
className="font-mono"
/>
</div>
{/* Display Name */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.supplierNameLabel")}
</label>
<Input
type="text"
value={templateDisplayName}
ref={displayNameInputRef}
onChange={(e) => setTemplateDisplayName(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.supplierNamePlaceholder")}
required
pattern=".*\S.*"
title={t("common.enterValidValue")}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.supplierNameHint")}
</p>
</div>
{/* Provider Name */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.supplierCodeLabel")}
</label>
<Input
type="text"
value={templateProviderName}
onChange={(e) => setTemplateProviderName(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.supplierCodePlaceholder")}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.supplierCodeHint")}
</p>
</div>
{/* Base URL */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.apiUrlLabel")}
</label>
<Input
type="url"
value={templateBaseUrl}
ref={baseUrlInputRef}
onChange={(e) => setTemplateBaseUrl(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.apiUrlPlaceholder")}
required
className="font-mono"
/>
</div>
{/* Website URL */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.websiteLabel")}
</label>
<Input
type="url"
value={templateWebsiteUrl}
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder={t("codexConfig.websitePlaceholder")}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("codexConfig.websiteHint")}
</p>
</div>
{/* Model Name */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.modelNameLabel")}
</label>
<Input
type="text"
value={templateModelName}
ref={modelNameInputRef}
onChange={(e) => setTemplateModelName(e.target.value)}
onKeyDown={handleInputKeyDown}
pattern=".*\S.*"
title={t("common.enterValidValue")}
placeholder={t("codexConfig.modelNamePlaceholder")}
required
/>
</div>
</div>
{/* Preview */}
{(templateApiKey || templateProviderName || templateBaseUrl) && (
<div className="space-y-2 border-t border-border-default pt-4 ">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{t("codexConfig.configPreview")}
</h3>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div>
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
auth.json
</label>
<pre className="overflow-x-auto rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
{JSON.stringify(
generateThirdPartyAuth(templateApiKey),
null,
2,
)}
</pre>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
config.toml
</label>
<pre className="whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
{templateProviderName && templateBaseUrl
? generateThirdPartyConfig(
templateProviderName,
templateBaseUrl,
templateModelName,
)
: ""}
</pre>
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
applyTemplate();
}}
className="gap-2"
>
<Save className="h-4 w-4" />
{t("codexConfig.applyConfig")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -220,9 +220,6 @@ export function ProviderForm({
[originalHandleCodexConfigChange, debouncedValidate], [originalHandleCodexConfigChange, debouncedValidate],
); );
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
useState(false);
useEffect(() => { useEffect(() => {
form.reset(defaultValues); form.reset(defaultValues);
}, [defaultValues, form]); }, [defaultValues, form]);
@@ -615,11 +612,6 @@ export function ProviderForm({
onPresetChange={handlePresetChange} onPresetChange={handlePresetChange}
category={category} category={category}
appId={appId} appId={appId}
onOpenWizard={
appId === "codex"
? () => setIsCodexTemplateModalOpen(true)
: undefined
}
/> />
)} )}
@@ -739,10 +731,6 @@ export function ProviderForm({
commonConfigError={codexCommonConfigError} commonConfigError={codexCommonConfigError}
authError={codexAuthError} authError={codexAuthError}
configError={codexConfigError} configError={codexConfigError}
onWebsiteUrlChange={(url) => form.setValue("websiteUrl", url)}
onNameChange={(name) => form.setValue("name", name)}
isTemplateModalOpen={isCodexTemplateModalOpen}
setIsTemplateModalOpen={setIsCodexTemplateModalOpen}
/> />
{/* 配置验证错误显示 */} {/* 配置验证错误显示 */}
<FormField <FormField

View File

@@ -19,9 +19,8 @@ interface ProviderPresetSelectorProps {
categoryKeys: string[]; categoryKeys: string[];
presetCategoryLabels: Record<string, string>; presetCategoryLabels: Record<string, string>;
onPresetChange: (value: string) => void; onPresetChange: (value: string) => void;
category?: ProviderCategory; // 新增:当前选中的分类 category?: ProviderCategory; // 当前选中的分类
appId?: AppId; appId?: AppId;
onOpenWizard?: () => void; // Codex 专用:打开配置向导
} }
export function ProviderPresetSelector({ export function ProviderPresetSelector({
@@ -32,7 +31,6 @@ export function ProviderPresetSelector({
onPresetChange, onPresetChange,
category, category,
appId, appId,
onOpenWizard,
}: ProviderPresetSelectorProps) { }: ProviderPresetSelectorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -56,23 +54,6 @@ export function ProviderPresetSelector({
defaultValue: "💡 第三方供应商需要填写 API Key 和请求地址", defaultValue: "💡 第三方供应商需要填写 API Key 和请求地址",
}); });
case "custom": case "custom":
// Codex 自定义:在此位置显示"手动配置…或者 使用配置向导"
if (appId === "codex" && onOpenWizard) {
return (
<>
{t("providerForm.manualConfig")}
<button
type="button"
onClick={onOpenWizard}
className="ml-1 text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 underline-offset-2 hover:underline"
aria-label={t("providerForm.openConfigWizard")}
>
{t("providerForm.useConfigWizard")}
</button>
</>
);
}
// 其他情况沿用原提示
return t("providerForm.customApiKeyHint", { return t("providerForm.customApiKeyHint", {
defaultValue: "💡 自定义配置需手动填写所有必要字段", defaultValue: "💡 自定义配置需手动填写所有必要字段",
}); });

View File

@@ -98,6 +98,52 @@ requires_openai_auth = true`,
textColor: "#FFFFFF", textColor: "#FFFFFF",
}, },
}, },
{
name: "Custom (Blank Template)",
websiteUrl: "https://docs.anthropic.com",
category: "third_party",
isCustomTemplate: true,
auth: generateThirdPartyAuth(""),
config: `# ========================================
# Codex 自定义供应商配置模板
# ========================================
# 快速上手:
# 1. 在上方 auth.json 中设置 API Key
# 2. 将下方 'custom' 替换为供应商名称(小写、无空格)
# 3. 替换 base_url 为实际的 API 端点
# 4. 根据需要调整模型名称
#
# 文档: https://docs.anthropic.com
# ========================================
# ========== 模型配置 ==========
model_provider = "custom" # 供应商唯一标识
model = "gpt-5-codex" # 模型名称
model_reasoning_effort = "high" # 推理强度low, medium, high
disable_response_storage = true # 隐私:不本地存储响应
# ========== 供应商设置 ==========
[model_providers.custom]
name = "custom" # 与上方 model_provider 保持一致
base_url = "https://api.example.com/v1" # 👈 替换为实际端点
wire_api = "responses" # API 响应格式
requires_openai_auth = true # 使用 auth.json 中的 OPENAI_API_KEY
# ========== 可选:自定义请求头 ==========
# 如果供应商需要自定义请求头,取消注释:
# [model_providers.custom.headers]
# X-Custom-Header = "value"
# ========== 可选:模型覆盖 ==========
# 如果需要覆盖特定模型,取消注释:
# [model_overrides]
# "gpt-5-codex" = { model_provider = "custom", model = "your-model-name" }`,
theme: {
icon: "generic",
backgroundColor: "#6B7280", // gray-500
textColor: "#FFFFFF",
},
},
{ {
name: "AiHubMix", name: "AiHubMix",
websiteUrl: "https://aihubmix.com", websiteUrl: "https://aihubmix.com",

View File

@@ -239,9 +239,6 @@
"codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1", "codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1",
"manageAndTest": "Manage & Test", "manageAndTest": "Manage & Test",
"configContent": "Config Content", "configContent": "Config Content",
"useConfigWizard": "Use Configuration Wizard",
"openConfigWizard": "Open configuration wizard",
"manualConfig": "Manually configure provider, requires complete configuration, or",
"officialNoApiKey": "Official login does not require API Key, save directly", "officialNoApiKey": "Official login does not require API Key, save directly",
"codexOfficialNoApiKey": "Official does not require API Key, save directly", "codexOfficialNoApiKey": "Official does not require API Key, save directly",
"codexApiKeyAutoFill": "Just fill in here, auth.json below will be auto-filled", "codexApiKeyAutoFill": "Just fill in here, auth.json below will be auto-filled",
@@ -314,7 +311,6 @@
"testFailed": "Speed test failed: {{error}}" "testFailed": "Speed test failed: {{error}}"
}, },
"codexConfig": { "codexConfig": {
"quickWizard": "Quick Configuration Wizard",
"authJson": "auth.json (JSON) *", "authJson": "auth.json (JSON) *",
"authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}",
"authJsonHint": "Codex auth.json configuration content", "authJsonHint": "Codex auth.json configuration content",
@@ -324,24 +320,7 @@
"editCommonConfig": "Edit Common Config", "editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Codex Common Config Snippet", "editCommonConfigTitle": "Edit Codex Common Config Snippet",
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked", "commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked",
"wizardHint": "Enter key parameters, the system will automatically generate standard auth.json and config.toml configuration.", "apiUrlLabel": "API Request URL"
"apiKeyLabel": "API Key *",
"apiKeyPlaceholder": "sk-your-api-key-here",
"supplierNameLabel": "Provider Name *",
"supplierNamePlaceholder": "e.g., Codex Official",
"supplierNameHint": "Will be displayed in the provider list, can use Chinese",
"supplierCodeLabel": "Provider Code (English)",
"supplierCodePlaceholder": "custom (optional)",
"supplierCodeHint": "Will be used as identifier in config file, defaults to custom",
"apiUrlLabel": "API Request URL *",
"apiUrlPlaceholder": "https://your-api-endpoint.com/v1",
"websiteLabel": "Website URL",
"websitePlaceholder": "https://example.com",
"websiteHint": "Official website address (optional)",
"modelNameLabel": "Model Name *",
"modelNamePlaceholder": "gpt-5-codex",
"configPreview": "Configuration Preview",
"applyConfig": "Apply Configuration"
}, },
"geminiConfig": { "geminiConfig": {
"envFile": "Environment Variables (.env)", "envFile": "Environment Variables (.env)",

View File

@@ -239,9 +239,6 @@
"codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1", "codexApiEndpointPlaceholder": "https://your-api-endpoint.com/v1",
"manageAndTest": "管理与测速", "manageAndTest": "管理与测速",
"configContent": "配置内容", "configContent": "配置内容",
"useConfigWizard": "使用配置向导",
"openConfigWizard": "打开配置向导",
"manualConfig": "手动配置供应商,需要填写完整的配置信息,或者",
"officialNoApiKey": "官方登录无需填写 API Key直接保存即可", "officialNoApiKey": "官方登录无需填写 API Key直接保存即可",
"codexOfficialNoApiKey": "官方无需填写 API Key直接保存即可", "codexOfficialNoApiKey": "官方无需填写 API Key直接保存即可",
"codexApiKeyAutoFill": "只需要填这里,下方 auth.json 会自动填充", "codexApiKeyAutoFill": "只需要填这里,下方 auth.json 会自动填充",
@@ -314,7 +311,6 @@
"testFailed": "测速失败: {{error}}" "testFailed": "测速失败: {{error}}"
}, },
"codexConfig": { "codexConfig": {
"quickWizard": "快速配置向导",
"authJson": "auth.json (JSON) *", "authJson": "auth.json (JSON) *",
"authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}", "authJsonPlaceholder": "{\n \"OPENAI_API_KEY\": \"sk-your-api-key-here\"\n}",
"authJsonHint": "Codex auth.json 配置内容", "authJsonHint": "Codex auth.json 配置内容",
@@ -324,24 +320,7 @@
"editCommonConfig": "编辑通用配置", "editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑 Codex 通用配置片段", "editCommonConfigTitle": "编辑 Codex 通用配置片段",
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾", "commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
"wizardHint": "输入关键参数,系统将自动生成标准的 auth.json 和 config.toml 配置。", "apiUrlLabel": "API 请求地址"
"apiKeyLabel": "API 密钥 *",
"apiKeyPlaceholder": "sk-your-api-key-here",
"supplierNameLabel": "供应商名称 *",
"supplierNamePlaceholder": "例如Codex 官方",
"supplierNameHint": "将显示在供应商列表中,可使用中文",
"supplierCodeLabel": "供应商代号(英文)",
"supplierCodePlaceholder": "custom可选",
"supplierCodeHint": "将用作配置文件中的标识符,默认为 custom",
"apiUrlLabel": "API 请求地址 *",
"apiUrlPlaceholder": "https://your-api-endpoint.com/v1",
"websiteLabel": "官网地址",
"websitePlaceholder": "https://example.com",
"websiteHint": "官方网站地址(可选)",
"modelNameLabel": "模型名称 *",
"modelNamePlaceholder": "gpt-5-codex",
"configPreview": "配置预览",
"applyConfig": "应用配置"
}, },
"geminiConfig": { "geminiConfig": {
"envFile": "环境变量 (.env)", "envFile": "环境变量 (.env)",