- i18n: complete remaining internationalization across the UI
- Locales: add and align keys (common.enterValidValue, apiKeyInput.*, jsonEditor.*, claudeConfig.*); fix zh common.unknown mapping - ProviderForm: localize labels/placeholders/hints/errors; unify JSON/auth validation to providerForm.*; add wizard CTA for Codex custom with i18n; cancel uses common.cancel - CodexConfigEditor: i18n for quick wizard, labels/placeholders/hints, common config modal (title/help/buttons) - ClaudeConfigEditor: i18n for main label, common-config toggle/button, modal title/help, footer buttons - EndpointSpeedTest: localize failed/noEndpoints/done and aria labels - ApiKeyInput: i18n for placeholder and show/hide aria - JsonEditor: i18n linter messages - PresetSelector: remove hardcoded defaults, use i18n keys - UpdateBadge: i18n close aria - Build/typecheck: pass; scan shows no visible hardcoded Chinese strings outside locales
This commit is contained in:
@@ -5,6 +5,7 @@ import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { placeholder } from "@codemirror/view";
|
||||
import { linter, Diagnostic } from "@codemirror/lint";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface JsonEditorProps {
|
||||
value: string;
|
||||
@@ -23,6 +24,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
rows = 12,
|
||||
showValidation = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
|
||||
@@ -46,12 +48,12 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
from: 0,
|
||||
to: doc.length,
|
||||
severity: "error",
|
||||
message: "配置必须是JSON对象,不能是数组或其他类型",
|
||||
message: t("jsonEditor.mustBeObject"),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 简单处理JSON解析错误
|
||||
const message = e instanceof SyntaxError ? e.message : "JSON格式错误";
|
||||
const message = e instanceof SyntaxError ? e.message : t("jsonEditor.invalidJson");
|
||||
diagnostics.push({
|
||||
from: 0,
|
||||
to: doc.length,
|
||||
@@ -62,7 +64,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
|
||||
return diagnostics;
|
||||
}),
|
||||
[showValidation],
|
||||
[showValidation, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Provider, ProviderCategory, CustomEndpoint } from "../types";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import {
|
||||
@@ -190,6 +191,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onSubmit,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// 对于 Codex,需要分离 auth 和 config
|
||||
const isCodex = appType === "codex";
|
||||
|
||||
@@ -331,21 +333,20 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
useState("");
|
||||
|
||||
const validateSettingsConfig = (value: string): string => {
|
||||
return validateJsonConfig(value, "配置内容");
|
||||
const err = validateJsonConfig(value, "配置内容");
|
||||
return err ? t("providerForm.configJsonError") : "";
|
||||
};
|
||||
|
||||
const validateCodexAuth = (value: string): string => {
|
||||
if (!value.trim()) {
|
||||
return "";
|
||||
}
|
||||
if (!value.trim()) return "";
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return "auth.json 必须是 JSON 对象";
|
||||
return t("providerForm.authJsonRequired");
|
||||
}
|
||||
return "";
|
||||
} catch {
|
||||
return "auth.json 格式错误,请检查JSON语法";
|
||||
return t("providerForm.authJsonError");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -520,7 +521,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
setError("");
|
||||
|
||||
if (!formData.name) {
|
||||
setError("请填写供应商名称");
|
||||
setError(t("providerForm.fillSupplierName"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -535,7 +536,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}
|
||||
// Codex: 仅要求 auth.json 必填;config.toml 可为空
|
||||
if (!codexAuth.trim()) {
|
||||
setError("请填写 auth.json 配置");
|
||||
setError(t("providerForm.fillAuthJson"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,7 +553,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
? authJson.OPENAI_API_KEY.trim()
|
||||
: "";
|
||||
if (!key) {
|
||||
setError("请填写 OPENAI_API_KEY");
|
||||
setError(t("providerForm.fillApiKey"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -563,7 +564,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
config: codexConfig ?? "",
|
||||
};
|
||||
} catch (err) {
|
||||
setError("auth.json 格式错误,请检查JSON语法");
|
||||
setError(t("providerForm.authJsonError"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -572,7 +573,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
);
|
||||
setSettingsConfigError(currentSettingsError);
|
||||
if (currentSettingsError) {
|
||||
setError(currentSettingsError);
|
||||
setError(t("providerForm.configJsonError"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -586,21 +587,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
""
|
||||
).trim();
|
||||
if (!resolvedValue) {
|
||||
setError(`请填写 ${config.label}`);
|
||||
setError(t("providerForm.fillParameter", { label: config.label }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Claude: 原有逻辑
|
||||
if (!formData.settingsConfig.trim()) {
|
||||
setError("请填写配置内容");
|
||||
setError(t("providerForm.fillConfigContent"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
settingsConfig = JSON.parse(formData.settingsConfig);
|
||||
} catch (err) {
|
||||
setError("配置JSON格式错误,请检查语法");
|
||||
setError(t("providerForm.configJsonError"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -669,7 +670,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
if (snippetError.includes("配置 JSON 解析失败")) {
|
||||
setSettingsConfigError("配置JSON格式错误,请检查语法");
|
||||
setSettingsConfigError(t("providerForm.configJsonError"));
|
||||
}
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
@@ -723,7 +724,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (removeResult.error) {
|
||||
setCommonConfigError(removeResult.error);
|
||||
if (removeResult.error.includes("配置 JSON 解析失败")) {
|
||||
setSettingsConfigError("配置JSON格式错误,请检查语法");
|
||||
setSettingsConfigError(t("providerForm.configJsonError"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -736,7 +737,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (addResult.error) {
|
||||
setCommonConfigError(addResult.error);
|
||||
if (addResult.error.includes("配置 JSON 解析失败")) {
|
||||
setSettingsConfigError("配置JSON格式错误,请检查语法");
|
||||
setSettingsConfigError(t("providerForm.configJsonError"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1456,13 +1457,13 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onCustomClick={handleCodexCustomClick}
|
||||
renderCustomDescription={() => (
|
||||
<>
|
||||
手动配置供应商,需要填写完整的配置信息,或者
|
||||
{t("providerForm.manualConfig")}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCodexTemplateModalOpen(true)}
|
||||
className="text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors ml-1"
|
||||
>
|
||||
使用配置向导
|
||||
{t("providerForm.useConfigWizard")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -1474,7 +1475,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
供应商名称 *
|
||||
{t("providerForm.supplierNameRequired")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -1482,7 +1483,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="例如:Anthropic 官方"
|
||||
placeholder={t("providerForm.supplierNamePlaceholder")}
|
||||
required
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||
@@ -1494,7 +1495,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
htmlFor="websiteUrl"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
官网地址
|
||||
{t("providerForm.websiteLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
@@ -1502,7 +1503,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
name="websiteUrl"
|
||||
value={formData.websiteUrl}
|
||||
onChange={handleChange}
|
||||
placeholder="https://example.com(可选)"
|
||||
placeholder={t("providerForm.websiteUrlPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||
/>
|
||||
@@ -1516,10 +1517,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
required={!isOfficialPreset}
|
||||
placeholder={
|
||||
isOfficialPreset
|
||||
? "官方登录无需填写 API Key,直接保存即可"
|
||||
? t("providerForm.officialNoApiKey")
|
||||
: shouldShowKimiSelector
|
||||
? "填写后可获取模型列表"
|
||||
: "只需要填这里,下方配置会自动填充"
|
||||
? t("providerForm.kimiApiKeyHint")
|
||||
: t("providerForm.apiKeyAutoFill")
|
||||
}
|
||||
disabled={isOfficialPreset}
|
||||
/>
|
||||
@@ -1531,7 +1532,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
获取 API Key
|
||||
{t("providerForm.getApiKey")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -1543,7 +1544,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
templateValueEntries.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
参数配置 - {selectedTemplatePreset.name.trim()} *
|
||||
{t("providerForm.parameterConfig", { name: selectedTemplatePreset.name.trim() })}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{templateValueEntries.map(([key, config]) => (
|
||||
@@ -1616,7 +1617,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
htmlFor="baseUrl"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
请求地址
|
||||
{t("providerForm.apiEndpoint")}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1624,7 +1625,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
管理与测速
|
||||
{t("providerForm.manageAndTest")}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -1632,13 +1633,13 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
id="baseUrl"
|
||||
value={baseUrl}
|
||||
onChange={(e) => handleBaseUrlChange(e.target.value)}
|
||||
placeholder="https://your-api-endpoint.com"
|
||||
placeholder={t("providerForm.apiEndpointPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||
/>
|
||||
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
💡 填写兼容 Claude API 的服务端点地址
|
||||
{t("providerForm.apiHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1677,8 +1678,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onChange={handleCodexApiKeyChange}
|
||||
placeholder={
|
||||
isCodexOfficialPreset
|
||||
? "官方无需填写 API Key,直接保存即可"
|
||||
: "只需要填这里,下方 auth.json 会自动填充"
|
||||
? t("codexConfig.codexOfficialNoApiKey")
|
||||
: t("codexConfig.codexApiKeyAutoFill")
|
||||
}
|
||||
disabled={isCodexOfficialPreset}
|
||||
required={
|
||||
@@ -1695,7 +1696,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
>
|
||||
获取 API Key
|
||||
{t("providerForm.getApiKey")}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -1709,7 +1710,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
htmlFor="codexBaseUrl"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
请求地址
|
||||
{t("codexConfig.apiUrlLabel")}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1717,7 +1718,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
管理与测速
|
||||
{t("providerForm.manageAndTest")}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -1725,7 +1726,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
id="codexBaseUrl"
|
||||
value={codexBaseUrl}
|
||||
onChange={(e) => handleCodexBaseUrlChange(e.target.value)}
|
||||
placeholder="https://your-api-endpoint.com/v1"
|
||||
placeholder={t("providerForm.codexApiEndpointPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||
/>
|
||||
@@ -1800,7 +1801,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
htmlFor="anthropicModel"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
主模型 (可选)
|
||||
{t("providerForm.mainModel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -1809,7 +1810,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onChange={(e) =>
|
||||
handleModelChange("ANTHROPIC_MODEL", e.target.value)
|
||||
}
|
||||
placeholder="例如: GLM-4.5"
|
||||
placeholder={t("providerForm.mainModelPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||
/>
|
||||
@@ -1820,7 +1821,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
htmlFor="anthropicSmallFastModel"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
快速模型 (可选)
|
||||
{t("providerForm.fastModel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -1832,7 +1833,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="例如: GLM-4.5-Air"
|
||||
placeholder={t("providerForm.fastModelPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
||||
/>
|
||||
@@ -1841,7 +1842,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
|
||||
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
💡 留空将使用供应商的默认模型
|
||||
{t("providerForm.modelHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1872,7 +1873,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ApiKeyInputProps {
|
||||
value: string;
|
||||
@@ -14,12 +15,13 @@ interface ApiKeyInputProps {
|
||||
const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "请输入API Key",
|
||||
placeholder,
|
||||
disabled = false,
|
||||
required = false,
|
||||
label = "API Key",
|
||||
id = "apiKey",
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
|
||||
const toggleShowKey = () => {
|
||||
@@ -46,7 +48,7 @@ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? t("apiKeyInput.placeholder")}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
autoComplete="off"
|
||||
@@ -57,7 +59,7 @@ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
|
||||
type="button"
|
||||
onClick={toggleShowKey}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||
aria-label={showKey ? "隐藏API Key" : "显示API Key"}
|
||||
aria-label={showKey ? t("apiKeyInput.hide") : t("apiKeyInput.show")}
|
||||
>
|
||||
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import JsonEditor from "../JsonEditor";
|
||||
import { X, Save } from "lucide-react";
|
||||
import { isLinux } from "../../lib/platform";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ClaudeConfigEditorProps {
|
||||
value: string;
|
||||
@@ -24,6 +25,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
commonConfigError,
|
||||
configError,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||
|
||||
@@ -82,7 +84,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
htmlFor="settingsConfig"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Claude Code 配置 (JSON) *
|
||||
{t("claudeConfig.configLabel")}
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
||||
<input
|
||||
@@ -91,7 +93,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
写入通用配置
|
||||
{t("claudeConfig.writeCommonConfig")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
@@ -100,7 +102,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
onClick={() => setIsCommonConfigModalOpen(true)}
|
||||
className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
编辑通用配置
|
||||
{t("claudeConfig.editCommonConfig")}
|
||||
</button>
|
||||
</div>
|
||||
{commonConfigError && !isCommonConfigModalOpen && (
|
||||
@@ -124,7 +126,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
完整的 Claude Code settings.json 配置内容
|
||||
{t("claudeConfig.fullSettingsHint")}
|
||||
</p>
|
||||
{isCommonConfigModalOpen && (
|
||||
<div
|
||||
@@ -145,13 +147,13 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
{/* Header - 统一标题栏样式 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
编辑通用配置片段
|
||||
{t("claudeConfig.editCommonConfigTitle")}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
aria-label="关闭"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -160,7 +162,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
{/* Content - 统一内容区域样式 */}
|
||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
该片段会在勾选"写入通用配置"时合并到 settings.json 中
|
||||
{t("claudeConfig.commonConfigHint")}
|
||||
</p>
|
||||
<JsonEditor
|
||||
value={commonConfigSnippet}
|
||||
@@ -182,7 +184,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
onClick={closeModal}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -190,7 +192,7 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
||||
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
保存
|
||||
{t("common.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
import { X, Save } from "lucide-react";
|
||||
|
||||
import { isLinux } from "../../lib/platform";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
generateThirdPartyAuth,
|
||||
@@ -74,6 +75,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
|
||||
setIsTemplateModalOpen: externalSetTemplateModalOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||
|
||||
// 使用内部状态或外部状态
|
||||
@@ -236,7 +238,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
htmlFor="codexAuth"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
auth.json (JSON) *
|
||||
{t("codexConfig.authJson")}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
@@ -244,9 +246,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
value={authValue}
|
||||
onChange={(e) => handleAuthChange(e.target.value)}
|
||||
onBlur={onAuthBlur}
|
||||
placeholder={`{
|
||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||
}`}
|
||||
placeholder={t("codexConfig.authJsonPlaceholder")}
|
||||
rows={6}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors resize-y min-h-[8rem]"
|
||||
@@ -266,7 +266,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Codex auth.json 配置内容
|
||||
{t("codexConfig.authJsonHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -276,7 +276,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
htmlFor="codexConfig"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
config.toml (TOML)
|
||||
{t("codexConfig.configToml")}
|
||||
</label>
|
||||
|
||||
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
||||
@@ -286,7 +286,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onChange={(e) => onCommonConfigToggle(e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
写入通用配置
|
||||
{t("codexConfig.writeCommonConfig")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -296,7 +296,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onClick={() => setIsCommonConfigModalOpen(true)}
|
||||
className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
编辑通用配置
|
||||
{t("codexConfig.editCommonConfig")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -325,7 +325,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
/>
|
||||
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Codex config.toml 配置内容
|
||||
{t("codexConfig.configTomlHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -348,14 +348,14 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
<div className="flex h-full min-h-0 flex-col" role="form">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
快速配置向导
|
||||
{t("codexConfig.quickWizard")}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeTemplateModal}
|
||||
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
|
||||
aria-label="关闭"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -364,15 +364,14 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6">
|
||||
<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">
|
||||
输入关键参数,系统将自动生成标准的 auth.json 和 config.toml
|
||||
配置。
|
||||
{t("codexConfig.wizardHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
API 密钥 *
|
||||
{t("codexConfig.apiKeyLabel")}
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -382,8 +381,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onChange={(e) => setTemplateApiKey(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title="请输入有效的内容"
|
||||
placeholder="sk-your-api-key-here"
|
||||
title={t("common.enterValidValue")}
|
||||
placeholder={t("codexConfig.apiKeyPlaceholder")}
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
@@ -391,7 +390,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
供应商名称 *
|
||||
{t("codexConfig.supplierNameLabel")}
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -405,21 +404,21 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
placeholder="例如:Codex 官方"
|
||||
placeholder={t("codexConfig.supplierNamePlaceholder")}
|
||||
required
|
||||
pattern=".*\S.*"
|
||||
title="请输入有效的内容"
|
||||
title={t("common.enterValidValue")}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
将显示在供应商列表中,可使用中文
|
||||
{t("codexConfig.supplierNameHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
供应商代号(英文)
|
||||
{t("codexConfig.supplierCodeLabel")}
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -427,18 +426,18 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
value={templateProviderName}
|
||||
onChange={(e) => setTemplateProviderName(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
placeholder="custom(可选)"
|
||||
placeholder={t("codexConfig.supplierCodePlaceholder")}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
将用作配置文件中的标识符,默认为 custom
|
||||
{t("codexConfig.supplierCodeHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
API 请求地址 *
|
||||
{t("codexConfig.apiUrlLabel")}
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -447,7 +446,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
ref={baseUrlInputRef}
|
||||
onChange={(e) => setTemplateBaseUrl(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
placeholder="https://your-api-endpoint.com/v1"
|
||||
placeholder={t("codexConfig.apiUrlPlaceholder")}
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
@@ -455,7 +454,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
官网地址
|
||||
{t("codexConfig.websiteLabel")}
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -463,18 +462,18 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
value={templateWebsiteUrl}
|
||||
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
placeholder="https://example.com"
|
||||
placeholder={t("codexConfig.websitePlaceholder")}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
官方网站地址(可选)
|
||||
{t("codexConfig.websiteHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
模型名称 *
|
||||
{t("codexConfig.modelNameLabel")}
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -484,8 +483,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onChange={(e) => setTemplateModelName(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title="请输入有效的内容"
|
||||
placeholder="gpt-5-codex"
|
||||
title={t("common.enterValidValue")}
|
||||
placeholder={t("codexConfig.modelNamePlaceholder")}
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
@@ -497,7 +496,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
templateBaseUrl) && (
|
||||
<div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<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">
|
||||
@@ -543,7 +542,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onClick={closeTemplateModal}
|
||||
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
|
||||
>
|
||||
取消
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -558,7 +557,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
className="flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
应用配置
|
||||
{t("codexConfig.applyConfig")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -588,14 +587,14 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
编辑 Codex 通用配置片段
|
||||
{t("codexConfig.editCommonConfigTitle")}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
aria-label="关闭"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -605,7 +604,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
|
||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
该片段会在勾选"写入通用配置"时追加到 config.toml 末尾
|
||||
{t("codexConfig.commonConfigHint")}
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
@@ -646,7 +645,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
onClick={closeModal}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -655,7 +654,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
保存
|
||||
{t("common.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Zap, Loader2, Plus, X, AlertCircle } from "lucide-react";
|
||||
import { isLinux } from "../../lib/platform";
|
||||
|
||||
@@ -74,6 +75,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
onClose,
|
||||
onCustomEndpointsChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [entries, setEntries] = useState<EndpointEntry[]>(() =>
|
||||
buildInitialEntries(initialEndpoints, value),
|
||||
);
|
||||
@@ -127,14 +129,14 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
return Array.from(map.values());
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载自定义端点失败:", error);
|
||||
console.error(t("endpointTest.loadEndpointsFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
if (visible) {
|
||||
loadCustomEndpoints();
|
||||
}
|
||||
}, [appType, visible, providerId]);
|
||||
}, [appType, visible, providerId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setEntries((prev) => {
|
||||
@@ -214,7 +216,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
let errorMsg: string | null = null;
|
||||
|
||||
if (!candidate) {
|
||||
errorMsg = "请输入有效的 URL";
|
||||
errorMsg = t("endpointTest.enterValidUrl");
|
||||
}
|
||||
|
||||
let parsed: URL | null = null;
|
||||
@@ -222,12 +224,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
try {
|
||||
parsed = new URL(candidate);
|
||||
} catch {
|
||||
errorMsg = "URL 格式不正确";
|
||||
errorMsg = t("endpointTest.invalidUrlFormat");
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) {
|
||||
errorMsg = "仅支持 HTTP/HTTPS";
|
||||
errorMsg = t("endpointTest.onlyHttps");
|
||||
}
|
||||
|
||||
let sanitized = "";
|
||||
@@ -236,7 +238,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError
|
||||
const isDuplicate = entries.some((entry) => entry.url === sanitized);
|
||||
if (isDuplicate) {
|
||||
errorMsg = "该地址已存在";
|
||||
errorMsg = t("endpointTest.urlExists");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,11 +279,11 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
setAddError(message || "保存失败,请重试");
|
||||
console.error("添加自定义端点失败:", error);
|
||||
setAddError(message || t("endpointTest.saveFailed"));
|
||||
console.error(t("endpointTest.addEndpointFailed"), error);
|
||||
}
|
||||
},
|
||||
[customUrl, entries, normalizedSelected, onChange, appType, providerId],
|
||||
[customUrl, entries, normalizedSelected, onChange, appType, providerId, t],
|
||||
);
|
||||
|
||||
const handleRemoveEndpoint = useCallback(
|
||||
@@ -291,7 +293,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
try {
|
||||
await window.api.removeCustomEndpoint(appType, providerId, entry.url);
|
||||
} catch (error) {
|
||||
console.error("删除自定义端点失败:", error);
|
||||
console.error(t("endpointTest.removeEndpointFailed"), error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -306,18 +308,18 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[normalizedSelected, onChange, appType, providerId],
|
||||
[normalizedSelected, onChange, appType, providerId, t],
|
||||
);
|
||||
|
||||
const runSpeedTest = useCallback(async () => {
|
||||
const urls = entries.map((entry) => entry.url);
|
||||
if (urls.length === 0) {
|
||||
setLastError("请先添加端点");
|
||||
setLastError(t("endpointTest.pleaseAddEndpoint"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === "undefined" || !window.api?.testApiEndpoints) {
|
||||
setLastError("测速功能不可用");
|
||||
setLastError(t("endpointTest.testUnavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,7 +352,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
...entry,
|
||||
latency: null,
|
||||
status: undefined,
|
||||
error: "未返回结果",
|
||||
error: t("endpointTest.noResult"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -374,12 +376,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : `测速失败: ${String(error)}`;
|
||||
error instanceof Error ? error.message : `${t("endpointTest.testFailed", { error: String(error) })}`;
|
||||
setLastError(message);
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
}, [entries, autoSelect, appType, normalizedSelected, onChange]);
|
||||
}, [entries, autoSelect, appType, normalizedSelected, onChange, t]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (url: string) => {
|
||||
@@ -431,13 +433,13 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||
请求地址管理
|
||||
{t("endpointTest.title")}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
aria-label="关闭"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
@@ -448,7 +450,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
{/* 测速控制栏 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{entries.length} 个端点
|
||||
{entries.length} {t("endpointTest.endpoints")}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
|
||||
@@ -458,7 +460,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
onChange={(event) => setAutoSelect(event.target.checked)}
|
||||
className="h-3.5 w-3.5 rounded border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
自动选择
|
||||
{t("endpointTest.autoSelect")}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@@ -469,12 +471,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
{isTesting ? (
|
||||
<>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
测速中
|
||||
{t("endpointTest.testing")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
测速
|
||||
{t("endpointTest.testSpeed")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -487,7 +489,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
<input
|
||||
type="url"
|
||||
value={customUrl}
|
||||
placeholder="https://api.example.com"
|
||||
placeholder={t("endpointTest.addEndpointPlaceholder")}
|
||||
onChange={(event) => setCustomUrl(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
@@ -567,7 +569,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
) : isTesting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-gray-400" />
|
||||
) : entry.error ? (
|
||||
<div className="text-xs text-gray-400">失败</div>
|
||||
<div className="text-xs text-gray-400">{t("endpointTest.failed")}</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-400">—</div>
|
||||
)}
|
||||
@@ -589,7 +591,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed border-gray-200 bg-gray-50 py-8 text-center text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400">
|
||||
暂无端点
|
||||
{t("endpointTest.noEndpoints")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -609,7 +611,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
完成
|
||||
{t("endpointTest.done")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, RefreshCw, AlertCircle } from "lucide-react";
|
||||
|
||||
interface KimiModel {
|
||||
@@ -26,6 +27,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
onModelChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [models, setModels] = useState<KimiModel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -34,7 +36,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
// 获取模型列表
|
||||
const fetchModelsWithKey = async (key: string) => {
|
||||
if (!key) {
|
||||
setError("请先填写 API Key");
|
||||
setError(t("kimiSelector.fillApiKeyFirst"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
|
||||
throw new Error(t("kimiSelector.requestFailed", { error: `${response.status} ${response.statusText}` }));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -58,11 +60,11 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
setModels(data.data);
|
||||
} else {
|
||||
throw new Error("返回数据格式错误");
|
||||
throw new Error(t("kimiSelector.invalidData"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("获取模型列表失败:", err);
|
||||
setError(err instanceof Error ? err.message : "获取模型列表失败");
|
||||
console.error(t("kimiSelector.fetchModelsFailed") + ":", err);
|
||||
setError(err instanceof Error ? err.message : t("kimiSelector.fetchModelsFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -110,10 +112,10 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
>
|
||||
<option value="">
|
||||
{loading
|
||||
? "加载中..."
|
||||
? t("common.loading")
|
||||
: models.length === 0
|
||||
? "暂无模型"
|
||||
: "请选择模型"}
|
||||
? t("kimiSelector.noModels")
|
||||
: t("kimiSelector.pleaseSelectModel")}
|
||||
</option>
|
||||
{models.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
@@ -133,7 +135,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
模型配置
|
||||
{t("kimiSelector.modelConfig")}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
@@ -142,7 +144,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||
刷新模型列表
|
||||
{t("kimiSelector.refreshModels")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -158,12 +160,12 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ModelSelect
|
||||
label="主模型"
|
||||
label={t("kimiSelector.mainModel")}
|
||||
value={anthropicModel}
|
||||
onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)}
|
||||
/>
|
||||
<ModelSelect
|
||||
label="快速模型"
|
||||
label={t("kimiSelector.fastModel")}
|
||||
value={anthropicSmallFastModel}
|
||||
onChange={(value) =>
|
||||
onModelChange("ANTHROPIC_SMALL_FAST_MODEL", value)
|
||||
@@ -174,7 +176,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
|
||||
{!apiKey.trim() && (
|
||||
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
💡 填写 API Key 后将自动获取可用模型列表
|
||||
{t("kimiSelector.apiKeyHint")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Zap } from "lucide-react";
|
||||
import { ProviderCategory } from "../../types";
|
||||
import { ClaudeIcon, CodexIcon } from "../BrandIcons";
|
||||
@@ -20,14 +21,16 @@ interface PresetSelectorProps {
|
||||
}
|
||||
|
||||
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
title = "选择配置类型",
|
||||
title,
|
||||
presets,
|
||||
selectedIndex,
|
||||
onSelectPreset,
|
||||
onCustomClick,
|
||||
customLabel = "自定义",
|
||||
customLabel,
|
||||
renderCustomDescription,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getButtonClass = (index: number, preset?: Preset) => {
|
||||
const isSelected = selectedIndex === index;
|
||||
const baseClass =
|
||||
@@ -54,14 +57,14 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
if (renderCustomDescription) {
|
||||
return renderCustomDescription();
|
||||
}
|
||||
return "手动配置供应商,需要填写完整的配置信息";
|
||||
return t("presetSelector.customDescription");
|
||||
}
|
||||
|
||||
if (selectedIndex !== null && selectedIndex >= 0) {
|
||||
const preset = presets[selectedIndex];
|
||||
return preset?.isOfficial || preset?.category === "official"
|
||||
? "官方登录,不需要填写 API Key"
|
||||
: "使用预设配置,只需填写 API Key";
|
||||
? t("presetSelector.officialDescription")
|
||||
: t("presetSelector.presetDescription");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -71,7 +74,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
{title}
|
||||
{title || t("presetSelector.title")}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -79,7 +82,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
className={`${getButtonClass(-1)} ${selectedIndex === -1 ? "" : ""}`}
|
||||
onClick={onCustomClick}
|
||||
>
|
||||
{customLabel}
|
||||
{customLabel || t("presetSelector.custom")}
|
||||
</button>
|
||||
{presets.map((preset, index) => (
|
||||
<button
|
||||
|
||||
@@ -71,12 +71,12 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
const applied = await window.api.isClaudePluginApplied();
|
||||
setClaudeApplied(applied);
|
||||
} catch (error) {
|
||||
console.error("检测 Claude 插件配置失败:", error);
|
||||
console.error(t("console.setupListenerFailed"), error);
|
||||
setClaudeApplied(false);
|
||||
}
|
||||
};
|
||||
checkClaude();
|
||||
}, [appType, currentProviderId, providers]);
|
||||
}, [appType, currentProviderId, providers, t]);
|
||||
|
||||
const handleApplyToClaudePlugin = async () => {
|
||||
try {
|
||||
@@ -182,7 +182,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
handleUrlClick(provider.websiteUrl!);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-blue-500 dark:text-blue-400 hover:opacity-90 transition-colors"
|
||||
title={`访问 ${provider.websiteUrl}`}
|
||||
title={t("providerForm.visitWebsite", { url: provider.websiteUrl })}
|
||||
>
|
||||
{provider.websiteUrl}
|
||||
</button>
|
||||
|
||||
@@ -369,7 +369,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
|
||||
alert(`${t("settings.configExported")}\n${result.filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("导出配置失败:", error);
|
||||
console.error(t("settings.exportFailedError"), error);
|
||||
alert(`${t("settings.exportFailed")}: ${error}`);
|
||||
}
|
||||
};
|
||||
@@ -384,7 +384,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
|
||||
setImportError('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择文件失败:', error);
|
||||
console.error(t("settings.selectFileFailed") + ":", error);
|
||||
alert(`${t("settings.selectFileFailed")}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { X, Download } from "lucide-react";
|
||||
import { useUpdate } from "../contexts/UpdateContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface UpdateBadgeProps {
|
||||
className?: string;
|
||||
@@ -8,6 +9,7 @@ interface UpdateBadgeProps {
|
||||
|
||||
export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
|
||||
const { hasUpdate, updateInfo, isDismissed, dismissUpdate } = useUpdate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 如果没有更新或已关闭,不显示
|
||||
if (!hasUpdate || isDismissed || !updateInfo) {
|
||||
@@ -52,7 +54,7 @@ export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
|
||||
transition-colors
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500/20
|
||||
"
|
||||
aria-label="关闭更新提醒"
|
||||
aria-label={t("common.close")}
|
||||
>
|
||||
<X className="w-3 h-3 text-gray-400 dark:text-gray-500" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user