mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
Merge branch 'main' into feat/gemini-proxy-integration
# Conflicts: # src-tauri/src/proxy/providers/claude.rs # src/i18n/locales/en.json # src/i18n/locales/ja.json # src/i18n/locales/zh.json
This commit is contained in:
+174
-9
@@ -9,6 +9,10 @@ import {
|
||||
Plus,
|
||||
Settings,
|
||||
ArrowLeft,
|
||||
Minus,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
X,
|
||||
Book,
|
||||
Wrench,
|
||||
RefreshCw,
|
||||
@@ -22,6 +26,7 @@ import {
|
||||
Shield,
|
||||
Cpu,
|
||||
} from "lucide-react";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { Provider, VisibleApps } from "@/types";
|
||||
import type { EnvConflict } from "@/types/env";
|
||||
import { useProvidersQuery, useSettingsQuery } from "@/lib/query";
|
||||
@@ -99,9 +104,8 @@ interface WebDavSyncStatusUpdatedPayload {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||
const DEFAULT_DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||
const HEADER_HEIGHT = 64; // px
|
||||
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
||||
|
||||
const STORAGE_KEY = "cc-switch-last-app";
|
||||
const VALID_APPS: AppId[] = [
|
||||
@@ -153,12 +157,17 @@ function App() {
|
||||
const [currentView, setCurrentView] = useState<View>(getInitialView);
|
||||
const [settingsDefaultTab, setSettingsDefaultTab] = useState("general");
|
||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
const [isWindowMaximized, setIsWindowMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
||||
}, [currentView]);
|
||||
|
||||
const { data: settingsData } = useSettingsQuery();
|
||||
const useAppWindowControls =
|
||||
isLinux() && (settingsData?.useAppWindowControls ?? false);
|
||||
const dragBarHeight = useAppWindowControls ? 32 : DEFAULT_DRAG_BAR_HEIGHT;
|
||||
const contentTopOffset = dragBarHeight + HEADER_HEIGHT;
|
||||
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
||||
claude: true,
|
||||
codex: true,
|
||||
@@ -261,7 +270,11 @@ function App() {
|
||||
deleteProvider,
|
||||
saveUsageScript,
|
||||
setAsDefaultModel,
|
||||
} = useProviderActions(activeApp, isProxyRunning);
|
||||
} = useProviderActions(
|
||||
activeApp,
|
||||
isProxyRunning,
|
||||
isProxyRunning && isCurrentAppTakeoverActive,
|
||||
);
|
||||
|
||||
const disableOmoMutation = useDisableCurrentOmo();
|
||||
const handleDisableOmo = () => {
|
||||
@@ -392,6 +405,77 @@ function App() {
|
||||
};
|
||||
}, [queryClient, t]);
|
||||
|
||||
// Listen for proxy-official-warning: warn when takeover is enabled with an official provider
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
const setup = async () => {
|
||||
unsubscribe = await listen("proxy-official-warning", (event) => {
|
||||
const { providerName } = event.payload as {
|
||||
appType: string;
|
||||
providerName: string;
|
||||
};
|
||||
toast.warning(
|
||||
t("notifications.proxyOfficialWarning", {
|
||||
name: providerName,
|
||||
defaultValue: `当前供应商 ${providerName} 是官方供应商,建议切换到第三方供应商后再使用代理接管`,
|
||||
}),
|
||||
{ duration: 8000 },
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
void setup();
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let unlistenResize: (() => void) | undefined;
|
||||
|
||||
const setupWindowStateSync = async () => {
|
||||
try {
|
||||
const currentWindow = getCurrentWindow();
|
||||
const syncWindowMaximizedState = async () => {
|
||||
const maximized = await currentWindow.isMaximized();
|
||||
if (active) {
|
||||
setIsWindowMaximized(maximized);
|
||||
}
|
||||
};
|
||||
|
||||
await syncWindowMaximizedState();
|
||||
unlistenResize = await currentWindow.onResized(() => {
|
||||
void syncWindowMaximizedState();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to sync window maximized state", error);
|
||||
}
|
||||
};
|
||||
|
||||
void setupWindowStateSync();
|
||||
return () => {
|
||||
active = false;
|
||||
unlistenResize?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// settingsData 未加载时跳过,避免用 fallback false 覆盖 Rust 侧已设好的装饰状态
|
||||
if (!settingsData) return;
|
||||
|
||||
const syncWindowDecorations = async () => {
|
||||
try {
|
||||
await getCurrentWindow().setDecorations(!useAppWindowControls);
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to update window decorations", error);
|
||||
}
|
||||
};
|
||||
|
||||
void syncWindowDecorations();
|
||||
}, [useAppWindowControls, settingsData]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkEnvOnStartup = async () => {
|
||||
try {
|
||||
@@ -734,6 +818,44 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const notifyWindowControlError = (error: unknown) => {
|
||||
toast.error(
|
||||
t("notifications.windowControlFailed", {
|
||||
defaultValue: "窗口控制失败:{{error}}",
|
||||
error: extractErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleWindowMinimize = async () => {
|
||||
try {
|
||||
await getCurrentWindow().minimize();
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to minimize window", error);
|
||||
notifyWindowControlError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWindowToggleMaximize = async () => {
|
||||
try {
|
||||
const currentWindow = getCurrentWindow();
|
||||
await currentWindow.toggleMaximize();
|
||||
setIsWindowMaximized(await currentWindow.isMaximized());
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to toggle maximize", error);
|
||||
notifyWindowControlError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWindowClose = async () => {
|
||||
try {
|
||||
await getCurrentWindow().close();
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to close window", error);
|
||||
notifyWindowControlError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
const content = (() => {
|
||||
switch (currentView) {
|
||||
@@ -880,14 +1002,57 @@ function App() {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
||||
style={{ overflowX: "hidden", paddingTop: CONTENT_TOP_OFFSET }}
|
||||
style={{ overflowX: "hidden", paddingTop: contentTopOffset }}
|
||||
>
|
||||
{DRAG_BAR_HEIGHT > 0 && (
|
||||
{(dragBarHeight > 0 || useAppWindowControls) && (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-[60]"
|
||||
className="fixed top-0 left-0 right-0 z-[70] flex items-center justify-end px-2"
|
||||
data-tauri-drag-region
|
||||
style={{ WebkitAppRegion: "drag", height: DRAG_BAR_HEIGHT } as any}
|
||||
/>
|
||||
style={{ WebkitAppRegion: "drag", height: dragBarHeight } as any}
|
||||
>
|
||||
{useAppWindowControls && (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
style={{ WebkitAppRegion: "no-drag" } as any}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => void handleWindowMinimize()}
|
||||
title={t("header.windowMinimize")}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => void handleWindowToggleMaximize()}
|
||||
title={
|
||||
isWindowMaximized
|
||||
? t("header.windowRestore")
|
||||
: t("header.windowMaximize")
|
||||
}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
{isWindowMaximized ? (
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
) : (
|
||||
<Maximize2 className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => void handleWindowClose()}
|
||||
title={t("header.windowClose")}
|
||||
className="h-7 w-7 hover:bg-red-500/15 hover:text-red-500"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showEnvBanner && envConflicts.length > 0 && (
|
||||
<EnvWarningBanner
|
||||
@@ -920,7 +1085,7 @@ function App() {
|
||||
style={
|
||||
{
|
||||
...DRAG_REGION_STYLE,
|
||||
top: DRAG_BAR_HEIGHT,
|
||||
top: dragBarHeight,
|
||||
height: HEADER_HEIGHT,
|
||||
} as any
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export function EditProviderDialog({
|
||||
}, [
|
||||
open, // 修复:编辑保存后再次打开显示旧数据,依赖 open 确保每次打开时重新读取最新 provider 数据
|
||||
provider?.id, // 只依赖 ID,provider 对象更新不会触发重新计算
|
||||
provider?.meta, // 需要依赖 meta 以便正确初始化 testConfig 和 proxyConfig
|
||||
provider?.meta, // 需要依赖 meta 以便正确初始化 testConfig
|
||||
initialSettingsConfig,
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Minus,
|
||||
Play,
|
||||
Plus,
|
||||
ShieldAlert,
|
||||
Terminal,
|
||||
TestTube2,
|
||||
Trash2,
|
||||
@@ -36,6 +37,7 @@ interface ProviderActionsProps {
|
||||
isAutoFailoverEnabled?: boolean;
|
||||
isInFailoverQueue?: boolean;
|
||||
onToggleFailover?: (enabled: boolean) => void;
|
||||
isOfficialBlockedByProxy?: boolean;
|
||||
// OpenClaw: default model
|
||||
isDefaultModel?: boolean;
|
||||
onSetAsDefault?: () => void;
|
||||
@@ -60,6 +62,7 @@ export function ProviderActions({
|
||||
isAutoFailoverEnabled = false,
|
||||
isInFailoverQueue = false,
|
||||
onToggleFailover,
|
||||
isOfficialBlockedByProxy = false,
|
||||
// OpenClaw: default model
|
||||
isDefaultModel = false,
|
||||
onSetAsDefault,
|
||||
@@ -166,6 +169,16 @@ export function ProviderActions({
|
||||
};
|
||||
}
|
||||
|
||||
if (isOfficialBlockedByProxy) {
|
||||
return {
|
||||
disabled: true,
|
||||
variant: "secondary" as const,
|
||||
className: "opacity-40 cursor-not-allowed",
|
||||
icon: <ShieldAlert className="h-4 w-4" />,
|
||||
text: t("provider.blockedByProxy", { defaultValue: "已拦截" }),
|
||||
};
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
return {
|
||||
disabled: true,
|
||||
|
||||
@@ -175,6 +175,8 @@ export function ProviderCard({
|
||||
|
||||
const usageEnabled = provider.meta?.usage_script?.enabled ?? false;
|
||||
const isOfficial = isOfficialProvider(provider, appId);
|
||||
const isOfficialBlockedByProxy =
|
||||
isProxyTakeover && (provider.category === "official" || isOfficial);
|
||||
const isCopilot =
|
||||
provider.meta?.providerType === PROVIDER_TYPES.GITHUB_COPILOT ||
|
||||
provider.meta?.usage_script?.templateType === "github_copilot";
|
||||
@@ -424,6 +426,7 @@ export function ProviderCard({
|
||||
isInConfig={isInConfig}
|
||||
isTesting={isTesting}
|
||||
isProxyTakeover={isProxyTakeover}
|
||||
isOfficialBlockedByProxy={isOfficialBlockedByProxy}
|
||||
isOmo={isAnyOmo}
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
onEdit={() => onEdit(provider)}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FlaskConical,
|
||||
Globe,
|
||||
Coins,
|
||||
Eye,
|
||||
EyeOff,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, FlaskConical, Coins } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -22,7 +12,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ProviderTestConfig, ProviderProxyConfig } from "@/types";
|
||||
import type { ProviderTestConfig } from "@/types";
|
||||
|
||||
export type PricingModelSourceOption = "inherit" | "request" | "response";
|
||||
|
||||
@@ -34,136 +24,31 @@ interface ProviderPricingConfig {
|
||||
|
||||
interface ProviderAdvancedConfigProps {
|
||||
testConfig: ProviderTestConfig;
|
||||
proxyConfig: ProviderProxyConfig;
|
||||
pricingConfig: ProviderPricingConfig;
|
||||
onTestConfigChange: (config: ProviderTestConfig) => void;
|
||||
onProxyConfigChange: (config: ProviderProxyConfig) => void;
|
||||
onPricingConfigChange: (config: ProviderPricingConfig) => void;
|
||||
}
|
||||
|
||||
/** 从 ProviderProxyConfig 构建完整 URL */
|
||||
function buildProxyUrl(config: ProviderProxyConfig): string {
|
||||
if (!config.proxyHost) return "";
|
||||
|
||||
const protocol = config.proxyType || "http";
|
||||
const host = config.proxyHost;
|
||||
const port = config.proxyPort || (protocol === "socks5" ? 1080 : 7890);
|
||||
|
||||
return `${protocol}://${host}:${port}`;
|
||||
}
|
||||
|
||||
/** 从完整 URL 解析为 ProviderProxyConfig */
|
||||
function parseProxyUrl(url: string): Partial<ProviderProxyConfig> {
|
||||
if (!url.trim()) {
|
||||
return { proxyHost: undefined, proxyPort: undefined, proxyType: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const protocol = parsed.protocol.replace(":", "") as
|
||||
| "http"
|
||||
| "https"
|
||||
| "socks5";
|
||||
const host = parsed.hostname;
|
||||
const port = parsed.port ? parseInt(parsed.port, 10) : undefined;
|
||||
|
||||
return {
|
||||
proxyType: protocol,
|
||||
proxyHost: host || undefined,
|
||||
proxyPort: port,
|
||||
};
|
||||
} catch {
|
||||
// 尝试简单解析(不是标准 URL 格式)
|
||||
const match = url.match(/^(?:(\w+):\/\/)?([^:]+)(?::(\d+))?$/);
|
||||
if (match) {
|
||||
return {
|
||||
proxyType: (match[1] as "http" | "https" | "socks5") || "http",
|
||||
proxyHost: match[2] || undefined,
|
||||
proxyPort: match[3] ? parseInt(match[3], 10) : undefined,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function ProviderAdvancedConfig({
|
||||
testConfig,
|
||||
proxyConfig,
|
||||
pricingConfig,
|
||||
onTestConfigChange,
|
||||
onProxyConfigChange,
|
||||
onPricingConfigChange,
|
||||
}: ProviderAdvancedConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isTestConfigOpen, setIsTestConfigOpen] = useState(testConfig.enabled);
|
||||
const [isProxyConfigOpen, setIsProxyConfigOpen] = useState(
|
||||
proxyConfig.enabled,
|
||||
);
|
||||
const [isPricingConfigOpen, setIsPricingConfigOpen] = useState(
|
||||
pricingConfig.enabled,
|
||||
);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// 代理 URL 输入状态(仅在初始化时从 proxyConfig 构建)
|
||||
const [proxyUrl, setProxyUrl] = useState(() => buildProxyUrl(proxyConfig));
|
||||
|
||||
// 标记是否为用户主动输入(用于区分外部更新和用户输入)
|
||||
const [isUserTyping, setIsUserTyping] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTestConfigOpen(testConfig.enabled);
|
||||
}, [testConfig.enabled]);
|
||||
|
||||
// 同步外部 proxyConfig.enabled 变化到展开状态
|
||||
useEffect(() => {
|
||||
setIsProxyConfigOpen(proxyConfig.enabled);
|
||||
}, [proxyConfig.enabled]);
|
||||
|
||||
// 同步外部 pricingConfig.enabled 变化到展开状态
|
||||
useEffect(() => {
|
||||
setIsPricingConfigOpen(pricingConfig.enabled);
|
||||
}, [pricingConfig.enabled]);
|
||||
|
||||
// 仅在外部 proxyConfig 变化且非用户输入时同步(如:重置表单、加载数据)
|
||||
useEffect(() => {
|
||||
if (!isUserTyping) {
|
||||
const newUrl = buildProxyUrl(proxyConfig);
|
||||
if (newUrl !== proxyUrl) {
|
||||
setProxyUrl(newUrl);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [proxyConfig.proxyType, proxyConfig.proxyHost, proxyConfig.proxyPort]);
|
||||
|
||||
// 处理代理 URL 变化(用户输入时不触发 URL 重建)
|
||||
const handleProxyUrlChange = (value: string) => {
|
||||
setIsUserTyping(true);
|
||||
setProxyUrl(value);
|
||||
const parsed = parseProxyUrl(value);
|
||||
onProxyConfigChange({
|
||||
...proxyConfig,
|
||||
...parsed,
|
||||
});
|
||||
};
|
||||
|
||||
// 输入框失焦时结束用户输入状态
|
||||
const handleProxyUrlBlur = () => {
|
||||
setIsUserTyping(false);
|
||||
};
|
||||
|
||||
// 清除代理配置
|
||||
const handleClearProxy = () => {
|
||||
setProxyUrl("");
|
||||
onProxyConfigChange({
|
||||
...proxyConfig,
|
||||
proxyType: undefined,
|
||||
proxyHost: undefined,
|
||||
proxyPort: undefined,
|
||||
proxyUsername: undefined,
|
||||
proxyPassword: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-border/50 bg-muted/20">
|
||||
@@ -342,141 +227,6 @@ export function ProviderAdvancedConfig({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 代理配置 */}
|
||||
<div className="rounded-lg border border-border/50 bg-muted/20">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between p-4 hover:bg-muted/30 transition-colors"
|
||||
onClick={() => setIsProxyConfigOpen(!isProxyConfigOpen)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">
|
||||
{t("providerAdvanced.proxyConfig", {
|
||||
defaultValue: "代理配置",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Label
|
||||
htmlFor="proxy-config-enabled"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t("providerAdvanced.useCustomProxy", {
|
||||
defaultValue: "使用单独代理",
|
||||
})}
|
||||
</Label>
|
||||
<Switch
|
||||
id="proxy-config-enabled"
|
||||
checked={proxyConfig.enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
onProxyConfigChange({ ...proxyConfig, enabled: checked });
|
||||
if (checked) setIsProxyConfigOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isProxyConfigOpen ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-200",
|
||||
isProxyConfigOpen
|
||||
? "max-h-[500px] opacity-100"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="border-t border-border/50 p-4 space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("providerAdvanced.proxyConfigDesc", {
|
||||
defaultValue:
|
||||
"为此供应商配置单独的网络代理,不启用时使用系统代理或全局设置。",
|
||||
})}
|
||||
</p>
|
||||
|
||||
{/* 代理地址输入框(仿照全局代理样式) */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="http://127.0.0.1:7890 / socks5://127.0.0.1:1080"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => handleProxyUrlChange(e.target.value)}
|
||||
onBlur={handleProxyUrlBlur}
|
||||
className="font-mono text-sm flex-1"
|
||||
disabled={!proxyConfig.enabled}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!proxyConfig.enabled || !proxyUrl}
|
||||
onClick={handleClearProxy}
|
||||
title={t("common.clear", { defaultValue: "清除" })}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 认证信息:用户名 + 密码(可选) */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t("providerAdvanced.proxyUsername", {
|
||||
defaultValue: "用户名(可选)",
|
||||
})}
|
||||
value={proxyConfig.proxyUsername || ""}
|
||||
onChange={(e) =>
|
||||
onProxyConfigChange({
|
||||
...proxyConfig,
|
||||
proxyUsername: e.target.value || undefined,
|
||||
})
|
||||
}
|
||||
className="font-mono text-sm flex-1"
|
||||
disabled={!proxyConfig.enabled}
|
||||
/>
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder={t("providerAdvanced.proxyPassword", {
|
||||
defaultValue: "密码(可选)",
|
||||
})}
|
||||
value={proxyConfig.proxyPassword || ""}
|
||||
onChange={(e) =>
|
||||
onProxyConfigChange({
|
||||
...proxyConfig,
|
||||
proxyPassword: e.target.value || undefined,
|
||||
})
|
||||
}
|
||||
className="font-mono text-sm pr-10"
|
||||
disabled={!proxyConfig.enabled}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
disabled={!proxyConfig.enabled}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 计费配置 */}
|
||||
<div className="rounded-lg border border-border/50 bg-muted/20">
|
||||
<button
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
ProviderCategory,
|
||||
ProviderMeta,
|
||||
ProviderTestConfig,
|
||||
ProviderProxyConfig,
|
||||
ClaudeApiFormat,
|
||||
ClaudeApiKeyField,
|
||||
} from "@/types";
|
||||
@@ -192,9 +191,6 @@ export function ProviderForm({
|
||||
const [testConfig, setTestConfig] = useState<ProviderTestConfig>(
|
||||
() => initialData?.meta?.testConfig ?? { enabled: false },
|
||||
);
|
||||
const [proxyConfig, setProxyConfig] = useState<ProviderProxyConfig>(
|
||||
() => initialData?.meta?.proxyConfig ?? { enabled: false },
|
||||
);
|
||||
const [pricingConfig, setPricingConfig] = useState<{
|
||||
enabled: boolean;
|
||||
costMultiplier?: string;
|
||||
@@ -231,7 +227,6 @@ export function ProviderForm({
|
||||
supportsFullUrl ? (initialData?.meta?.isFullUrl ?? false) : false,
|
||||
);
|
||||
setTestConfig(initialData?.meta?.testConfig ?? { enabled: false });
|
||||
setProxyConfig(initialData?.meta?.proxyConfig ?? { enabled: false });
|
||||
setPricingConfig({
|
||||
enabled:
|
||||
initialData?.meta?.costMultiplier !== undefined ||
|
||||
@@ -1075,7 +1070,6 @@ export function ProviderForm({
|
||||
? selectedGitHubAccountId
|
||||
: undefined,
|
||||
testConfig: testConfig.enabled ? testConfig : undefined,
|
||||
proxyConfig: proxyConfig.enabled ? proxyConfig : undefined,
|
||||
costMultiplier: pricingConfig.enabled
|
||||
? pricingConfig.costMultiplier
|
||||
: undefined,
|
||||
@@ -1847,10 +1841,8 @@ export function ProviderForm({
|
||||
appId !== "openclaw" && (
|
||||
<ProviderAdvancedConfig
|
||||
testConfig={testConfig}
|
||||
proxyConfig={proxyConfig}
|
||||
pricingConfig={pricingConfig}
|
||||
onTestConfigChange={setTestConfig}
|
||||
onProxyConfigChange={setProxyConfig}
|
||||
onPricingConfigChange={setPricingConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function SessionItem({
|
||||
{getProviderLabel(session.providerId, t)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="text-sm font-medium truncate flex-1">
|
||||
<span className="text-sm font-medium line-clamp-2 flex-1">
|
||||
{searchQuery ? highlightText(title, searchQuery) : title}
|
||||
</span>
|
||||
<ChevronRight
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSessionSearch } from "@/hooks/useSessionSearch";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { toast } from "sonner";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -69,8 +70,7 @@ export function SessionManagerPage({ appId }: { appId: string }) {
|
||||
const { data, isLoading, refetch } = useSessionsQuery();
|
||||
const sessions = data ?? [];
|
||||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
||||
const messageRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [activeMessageIndex, setActiveMessageIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
@@ -134,6 +134,20 @@ export function SessionManagerPage({ appId }: { appId: string }) {
|
||||
const deleteSessionMutation = useDeleteSessionMutation();
|
||||
const isDeleting = deleteSessionMutation.isPending || isBatchDeleting;
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
getScrollElement: () => scrollContainerRef.current,
|
||||
estimateSize: () => 120,
|
||||
overscan: 5,
|
||||
gap: 12,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [selectedKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const validKeys = new Set(
|
||||
sessions.map((session) => getSessionKey(session)),
|
||||
@@ -166,37 +180,36 @@ export function SessionManagerPage({ appId }: { appId: string }) {
|
||||
}, [messages]);
|
||||
|
||||
const scrollToMessage = (index: number) => {
|
||||
const el = messageRefs.current.get(index);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
setActiveMessageIndex(index);
|
||||
setTocDialogOpen(false); // 关闭弹窗
|
||||
// 清除高亮状态
|
||||
setTimeout(() => setActiveMessageIndex(null), 2000);
|
||||
}
|
||||
virtualizer.scrollToIndex(index, { align: "center", behavior: "smooth" });
|
||||
setActiveMessageIndex(index);
|
||||
setTocDialogOpen(false);
|
||||
setTimeout(() => setActiveMessageIndex(null), 2000);
|
||||
};
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 这里的 setTimeout 其实无法直接清理,因为它在函数闭包里。
|
||||
// 如果要严格清理,需要用 useRef 存 timer id。
|
||||
// 但对于 2秒的高亮清除,通常不清理也没大问题。
|
||||
// 为了代码规范,我们在组件卸载时将 activeMessageIndex 重置 (虽然 React 会处理)
|
||||
};
|
||||
}, []);
|
||||
const handleCopy = useCallback(
|
||||
async (text: string, successMessage: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success(successMessage);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
extractErrorMessage(error) ||
|
||||
t("common.error", { defaultValue: "Copy failed" }),
|
||||
);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const handleCopy = async (text: string, successMessage: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success(successMessage);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
extractErrorMessage(error) ||
|
||||
t("common.error", { defaultValue: "Copy failed" }),
|
||||
const handleMessageCopy = useCallback(
|
||||
(content: string) => {
|
||||
void handleCopy(
|
||||
content,
|
||||
t("sessionManager.messageCopied", { defaultValue: "已复制消息内容" }),
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
[handleCopy, t],
|
||||
);
|
||||
|
||||
const handleResume = async () => {
|
||||
if (!selectedSession?.resumeCommand) return;
|
||||
@@ -973,9 +986,9 @@ export function SessionManagerPage({ appId }: { appId: string }) {
|
||||
<CardContent className="flex-1 min-h-0 p-0">
|
||||
<div className="flex h-full min-w-0">
|
||||
{/* 消息列表 */}
|
||||
<ScrollArea className="flex-1 min-w-0">
|
||||
<div className="p-4 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<div className="px-4 pt-4 pb-2 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
{t("sessionManager.conversationHistory", {
|
||||
@@ -986,7 +999,11 @@ export function SessionManagerPage({ appId }: { appId: string }) {
|
||||
{messages.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-y-auto px-4 pb-4 min-w-0"
|
||||
>
|
||||
{isLoadingMessages ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="size-5 animate-spin text-muted-foreground" />
|
||||
@@ -999,32 +1016,41 @@ export function SessionManagerPage({ appId }: { appId: string }) {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{messages.map((message, index) => (
|
||||
<SessionMessageItem
|
||||
key={`${message.role}-${index}`}
|
||||
message={message}
|
||||
index={index}
|
||||
isActive={activeMessageIndex === index}
|
||||
searchQuery={search}
|
||||
setRef={(el) => {
|
||||
if (el) messageRefs.current.set(index, el);
|
||||
}}
|
||||
onCopy={(content) =>
|
||||
handleCopy(
|
||||
content,
|
||||
t("sessionManager.messageCopied", {
|
||||
defaultValue: "已复制消息内容",
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{virtualizer
|
||||
.getVirtualItems()
|
||||
.map((virtualRow) => (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<SessionMessageItem
|
||||
message={messages[virtualRow.index]}
|
||||
isActive={
|
||||
activeMessageIndex === virtualRow.index
|
||||
}
|
||||
searchQuery={search}
|
||||
onCopy={handleMessageCopy}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* 右侧目录 - 类似少数派 (大屏幕) */}
|
||||
<SessionTocSidebar
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Copy } from "lucide-react";
|
||||
import { memo, useState } from "react";
|
||||
import { ChevronDown, ChevronUp, Copy } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -16,29 +17,40 @@ import {
|
||||
highlightText,
|
||||
} from "./utils";
|
||||
|
||||
const COLLAPSE_THRESHOLD = 3000;
|
||||
const COLLAPSED_LENGTH = 1500;
|
||||
|
||||
interface SessionMessageItemProps {
|
||||
message: SessionMessage;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
searchQuery?: string;
|
||||
setRef: (el: HTMLDivElement | null) => void;
|
||||
onCopy: (content: string) => void;
|
||||
}
|
||||
|
||||
export function SessionMessageItem({
|
||||
export const SessionMessageItem = memo(function SessionMessageItem({
|
||||
message,
|
||||
isActive,
|
||||
searchQuery,
|
||||
setRef,
|
||||
onCopy,
|
||||
}: SessionMessageItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const isLong = message.content.length > COLLAPSE_THRESHOLD;
|
||||
const hasSearchMatch =
|
||||
isLong &&
|
||||
!expanded &&
|
||||
!!searchQuery &&
|
||||
message.content.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const collapsed = isLong && !expanded && !hasSearchMatch;
|
||||
const displayContent = collapsed
|
||||
? message.content.slice(0, COLLAPSED_LENGTH) + "…"
|
||||
: message.content;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setRef}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2.5 relative group transition-all min-w-0",
|
||||
"rounded-lg border px-3 py-2.5 relative group transition-shadow min-w-0",
|
||||
message.role.toLowerCase() === "user"
|
||||
? "bg-primary/5 border-primary/20 ml-8"
|
||||
: message.role.toLowerCase() === "assistant"
|
||||
@@ -76,9 +88,36 @@ export function SessionMessageItem({
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-relaxed min-w-0">
|
||||
{searchQuery
|
||||
? highlightText(message.content, searchQuery)
|
||||
: message.content}
|
||||
? highlightText(displayContent, searchQuery)
|
||||
: displayContent}
|
||||
</div>
|
||||
{isLong && !hasSearchMatch && (
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="flex items-center gap-1 mt-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronUp className="size-3" />
|
||||
{t("sessionManager.collapseContent", {
|
||||
defaultValue: "收起",
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="size-3" />
|
||||
{t("sessionManager.expandContent", {
|
||||
defaultValue: "展开完整内容",
|
||||
})}
|
||||
<span className="text-muted-foreground/60">
|
||||
({Math.round(message.content.length / 1000)}k)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SettingsFormState } from "@/hooks/useSettings";
|
||||
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
|
||||
import { ToggleRow } from "@/components/ui/toggle-row";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { isLinux } from "@/lib/platform";
|
||||
|
||||
interface WindowSettingsProps {
|
||||
settings: SettingsFormState;
|
||||
@@ -75,6 +76,18 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
|
||||
onChange({ minimizeToTrayOnClose: value })
|
||||
}
|
||||
/>
|
||||
|
||||
{isLinux() && (
|
||||
<ToggleRow
|
||||
icon={<AppWindow className="h-4 w-4 text-amber-500" />}
|
||||
title={t("settings.useAppWindowControls")}
|
||||
description={t("settings.useAppWindowControlsDescription")}
|
||||
checked={!!settings.useAppWindowControls}
|
||||
onCheckedChange={(value) =>
|
||||
onChange({ useAppWindowControls: value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Table,
|
||||
@@ -31,24 +31,33 @@ import {
|
||||
interface RequestLogTableProps {
|
||||
appType?: string;
|
||||
refreshIntervalMs: number;
|
||||
timeRange?: "1d" | "7d" | "30d";
|
||||
}
|
||||
|
||||
const ONE_DAY_SECONDS = 24 * 60 * 60;
|
||||
const MAX_FIXED_RANGE_SECONDS = 30 * ONE_DAY_SECONDS;
|
||||
|
||||
const TIME_RANGE_SECONDS: Record<string, number> = {
|
||||
"1d": ONE_DAY_SECONDS,
|
||||
"7d": 7 * ONE_DAY_SECONDS,
|
||||
"30d": 30 * ONE_DAY_SECONDS,
|
||||
};
|
||||
|
||||
type TimeMode = "rolling" | "fixed";
|
||||
|
||||
export function RequestLogTable({
|
||||
appType: dashboardAppType,
|
||||
refreshIntervalMs,
|
||||
timeRange = "1d",
|
||||
}: RequestLogTableProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const rollingWindowSeconds = TIME_RANGE_SECONDS[timeRange] ?? ONE_DAY_SECONDS;
|
||||
|
||||
const getRollingRange = () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const oneDayAgo = now - ONE_DAY_SECONDS;
|
||||
return { startDate: oneDayAgo, endDate: now };
|
||||
return { startDate: now - rollingWindowSeconds, endDate: now };
|
||||
};
|
||||
|
||||
const [appliedTimeMode, setAppliedTimeMode] = useState<TimeMode>("rolling");
|
||||
@@ -57,9 +66,15 @@ export function RequestLogTable({
|
||||
const [appliedFilters, setAppliedFilters] = useState<LogFilters>({});
|
||||
const [draftFilters, setDraftFilters] = useState<LogFilters>({});
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageInput, setPageInput] = useState("");
|
||||
const pageSize = 20;
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
// Reset page when the dashboard time range changes
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [timeRange]);
|
||||
|
||||
// When dashboard-level app filter is active (not "all"), override the local appType filter
|
||||
const dashboardAppTypeActive = dashboardAppType && dashboardAppType !== "all";
|
||||
const effectiveFilters: LogFilters = dashboardAppTypeActive
|
||||
@@ -69,7 +84,7 @@ export function RequestLogTable({
|
||||
const { data: result, isLoading } = useRequestLogs({
|
||||
filters: effectiveFilters,
|
||||
timeMode: appliedTimeMode,
|
||||
rollingWindowSeconds: ONE_DAY_SECONDS,
|
||||
rollingWindowSeconds,
|
||||
page,
|
||||
pageSize,
|
||||
options: {
|
||||
@@ -131,6 +146,15 @@ export function RequestLogTable({
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleGoToPage = () => {
|
||||
const trimmed = pageInput.trim();
|
||||
if (!/^\d+$/.test(trimmed)) return;
|
||||
const parsed = Number(trimmed);
|
||||
if (parsed < 1 || parsed > totalPages) return;
|
||||
setPage(parsed - 1);
|
||||
setPageInput("");
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
const key = {
|
||||
timeMode: appliedTimeMode,
|
||||
@@ -561,30 +585,33 @@ export function RequestLogTable({
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 页码按钮 */}
|
||||
{(() => {
|
||||
const pages: (number | string)[] = [];
|
||||
if (totalPages <= 7) {
|
||||
// 3 head + 3 tail + 3 neighborhood = 9 max distinct pages
|
||||
if (totalPages <= 9) {
|
||||
for (let i = 0; i < totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(0);
|
||||
if (page > 2) pages.push("...");
|
||||
const pageSet = new Set<number>();
|
||||
for (let i = 0; i < 3; i++) pageSet.add(i);
|
||||
for (let i = totalPages - 3; i < totalPages; i++)
|
||||
pageSet.add(i);
|
||||
for (
|
||||
let i = Math.max(1, page - 1);
|
||||
i <= Math.min(totalPages - 2, page + 1);
|
||||
let i = Math.max(0, page - 1);
|
||||
i <= Math.min(totalPages - 1, page + 1);
|
||||
i++
|
||||
) {
|
||||
pages.push(i);
|
||||
)
|
||||
pageSet.add(i);
|
||||
const sorted = Array.from(pageSet).sort((a, b) => a - b);
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
if (i > 0 && sorted[i] - sorted[i - 1] > 1) {
|
||||
pages.push(`ellipsis-${i}`);
|
||||
}
|
||||
pages.push(sorted[i]);
|
||||
}
|
||||
if (page < totalPages - 3) pages.push("...");
|
||||
pages.push(totalPages - 1);
|
||||
}
|
||||
return pages.map((p, idx) =>
|
||||
return pages.map((p) =>
|
||||
typeof p === "string" ? (
|
||||
<span
|
||||
key={`ellipsis-${idx}`}
|
||||
className="px-2 text-muted-foreground"
|
||||
>
|
||||
<span key={p} className="px-2 text-muted-foreground">
|
||||
...
|
||||
</span>
|
||||
) : (
|
||||
@@ -608,6 +635,21 @@ export function RequestLogTable({
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={pageInput}
|
||||
onChange={(e) => setPageInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleGoToPage();
|
||||
}}
|
||||
placeholder={t("usage.pageInputPlaceholder")}
|
||||
className="h-8 w-16 text-center text-xs"
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={handleGoToPage}>
|
||||
{t("usage.goToPage")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { RequestLogTable } from "./RequestLogTable";
|
||||
import { ProviderStatsTable } from "./ProviderStatsTable";
|
||||
import { ModelStatsTable } from "./ModelStatsTable";
|
||||
import type { AppTypeFilter, TimeRange } from "@/types/usage";
|
||||
import { useUsageSummary } from "@/lib/query/usage";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
BarChart3,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
} from "@/components/ui/accordion";
|
||||
import { PricingConfigPanel } from "@/components/usage/PricingConfigPanel";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fmtUsd, parseFiniteNumber } from "./format";
|
||||
|
||||
const APP_FILTER_OPTIONS: AppTypeFilter[] = [
|
||||
"all",
|
||||
@@ -57,11 +55,6 @@ export function UsageDashboard() {
|
||||
|
||||
const days = timeRange === "1d" ? 1 : timeRange === "7d" ? 7 : 30;
|
||||
|
||||
// Summary data for the app filter bar
|
||||
const { data: summaryData } = useUsageSummary(days, appType, {
|
||||
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
@@ -135,17 +128,6 @@ export function UsageDashboard() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>
|
||||
{(summaryData?.totalRequests ?? 0).toLocaleString()}{" "}
|
||||
{t("usage.requestsLabel")}
|
||||
</span>
|
||||
<span className="text-border">|</span>
|
||||
<span>
|
||||
{fmtUsd(parseFiniteNumber(summaryData?.totalCost) ?? 0, 4)}{" "}
|
||||
{t("usage.costLabel")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UsageSummaryCards
|
||||
@@ -188,6 +170,7 @@ export function UsageDashboard() {
|
||||
<RequestLogTable
|
||||
appType={appType}
|
||||
refreshIntervalMs={refreshIntervalMs}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -662,23 +662,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "micu",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "X-Code API",
|
||||
websiteUrl: "https://x-code.cc",
|
||||
apiKeyUrl: "https://x-code.cc",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://x-code.cc",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
},
|
||||
},
|
||||
endpointCandidates: ["https://x-code.cc"],
|
||||
category: "third_party",
|
||||
isPartner: true, // 合作伙伴
|
||||
partnerPromotionKey: "x-code", // 促销信息 i18n key
|
||||
icon: "xcode",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "CTok.ai",
|
||||
websiteUrl: "https://ctok.ai",
|
||||
@@ -863,7 +846,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
},
|
||||
{
|
||||
name: "PIPELLM",
|
||||
websiteUrl: "https://www.pipellm.ai",
|
||||
websiteUrl: "https://code.pipellm.ai",
|
||||
apiKeyUrl: "https://code.pipellm.ai/login?ref=uvw650za",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
|
||||
@@ -333,23 +333,6 @@ requires_openai_auth = true`,
|
||||
icon: "micu",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "X-Code API",
|
||||
websiteUrl: "https://x-code.cc",
|
||||
apiKeyUrl: "https://x-code.cc",
|
||||
auth: generateThirdPartyAuth(""),
|
||||
config: generateThirdPartyConfig(
|
||||
"x-code",
|
||||
"https://x-code.cc/v1",
|
||||
"gpt-5.4",
|
||||
),
|
||||
endpointCandidates: ["https://x-code.cc/v1"],
|
||||
category: "third_party",
|
||||
isPartner: true, // 合作伙伴
|
||||
partnerPromotionKey: "x-code", // 促销信息 i18n key
|
||||
icon: "xcode",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "CTok.ai",
|
||||
websiteUrl: "https://ctok.ai",
|
||||
@@ -408,7 +391,7 @@ model_auto_compact_token_limit = 9000000`,
|
||||
},
|
||||
{
|
||||
name: "PIPELLM",
|
||||
websiteUrl: "https://www.pipellm.ai",
|
||||
websiteUrl: "https://code.pipellm.ai",
|
||||
apiKeyUrl: "https://code.pipellm.ai/login?ref=uvw650za",
|
||||
auth: {
|
||||
OPENAI_API_KEY: "",
|
||||
|
||||
@@ -1009,7 +1009,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
{
|
||||
name: "PIPELLM",
|
||||
websiteUrl: "https://www.pipellm.ai",
|
||||
websiteUrl: "https://code.pipellm.ai",
|
||||
apiKeyUrl: "https://code.pipellm.ai/login?ref=uvw650za",
|
||||
settingsConfig: {
|
||||
baseUrl: "https://cc-api.pipellm.ai",
|
||||
|
||||
@@ -996,7 +996,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
{
|
||||
name: "PIPELLM",
|
||||
websiteUrl: "https://www.pipellm.ai",
|
||||
websiteUrl: "https://code.pipellm.ai",
|
||||
apiKeyUrl: "https://code.pipellm.ai/login?ref=uvw650za",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/anthropic",
|
||||
@@ -1291,36 +1291,6 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "X-Code API",
|
||||
websiteUrl: "https://x-code.cc",
|
||||
apiKeyUrl: "https://x-code.cc",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/anthropic",
|
||||
name: "X-Code API",
|
||||
options: {
|
||||
baseURL: "https://x-code.cc/v1",
|
||||
apiKey: "",
|
||||
setCacheKey: true,
|
||||
},
|
||||
models: {
|
||||
"claude-opus-4-6": { name: "Claude Opus 4.6" },
|
||||
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
|
||||
},
|
||||
},
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "x-code",
|
||||
icon: "xcode",
|
||||
iconColor: "#000000",
|
||||
templateValues: {
|
||||
apiKey: {
|
||||
label: "API Key",
|
||||
placeholder: "",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CTok.ai",
|
||||
websiteUrl: "https://ctok.ai",
|
||||
|
||||
@@ -23,7 +23,11 @@ import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||
* Hook for managing provider actions (add, update, delete, switch)
|
||||
* Extracts business logic from App.tsx
|
||||
*/
|
||||
export function useProviderActions(activeApp: AppId, isProxyRunning?: boolean) {
|
||||
export function useProviderActions(
|
||||
activeApp: AppId,
|
||||
isProxyRunning?: boolean,
|
||||
isProxyTakeover?: boolean,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -185,6 +189,18 @@ export function useProviderActions(activeApp: AppId, isProxyRunning?: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
// Block official providers when proxy takeover is active
|
||||
if (isProxyTakeover && provider.category === "official") {
|
||||
toast.error(
|
||||
t("notifications.officialBlockedByProxy", {
|
||||
defaultValue:
|
||||
"代理接管模式下不能切换到官方供应商,使用代理访问官方 API 可能导致账号被封禁",
|
||||
}),
|
||||
{ duration: 6000 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await switchProviderMutation.mutateAsync(provider.id);
|
||||
await syncClaudePlugin(provider);
|
||||
@@ -220,7 +236,14 @@ export function useProviderActions(activeApp: AppId, isProxyRunning?: boolean) {
|
||||
// 错误提示由 mutation 处理
|
||||
}
|
||||
},
|
||||
[switchProviderMutation, syncClaudePlugin, activeApp, isProxyRunning, t],
|
||||
[
|
||||
switchProviderMutation,
|
||||
syncClaudePlugin,
|
||||
activeApp,
|
||||
isProxyRunning,
|
||||
isProxyTakeover,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
// 删除供应商
|
||||
|
||||
@@ -81,6 +81,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
||||
...data,
|
||||
showInTray: data.showInTray ?? true,
|
||||
minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true,
|
||||
useAppWindowControls: data.useAppWindowControls ?? false,
|
||||
enableClaudePluginIntegration:
|
||||
data.enableClaudePluginIntegration ?? false,
|
||||
silentStartup: data.silentStartup ?? false,
|
||||
@@ -105,6 +106,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
||||
({
|
||||
showInTray: true,
|
||||
minimizeToTrayOnClose: true,
|
||||
useAppWindowControls: false,
|
||||
enableClaudePluginIntegration: false,
|
||||
skipClaudeOnboarding: false,
|
||||
language: readPersistedLanguage(),
|
||||
@@ -139,6 +141,7 @@ export function useSettingsForm(): UseSettingsFormResult {
|
||||
...serverData,
|
||||
showInTray: serverData.showInTray ?? true,
|
||||
minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true,
|
||||
useAppWindowControls: serverData.useAppWindowControls ?? false,
|
||||
enableClaudePluginIntegration:
|
||||
serverData.enableClaudePluginIntegration ?? false,
|
||||
silentStartup: serverData.silentStartup ?? false,
|
||||
|
||||
@@ -47,13 +47,37 @@ export function useStreamCheck(appId: AppId) {
|
||||
// 降级状态也重置熔断器,因为至少能通信
|
||||
resetCircuitBreaker.mutate({ providerId, appType: appId });
|
||||
} else {
|
||||
toast.error(
|
||||
t("streamCheck.failed", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 检查失败: ${result.message}`,
|
||||
}),
|
||||
);
|
||||
const httpStatus = result.httpStatus;
|
||||
const hintKey = httpStatus
|
||||
? `streamCheck.httpHint.${httpStatus >= 500 ? "5xx" : httpStatus}`
|
||||
: null;
|
||||
const description =
|
||||
(hintKey ? t(hintKey, { defaultValue: "" }) : "") || undefined;
|
||||
|
||||
// 401/403/400 = 检查被拒(供应商可能正常);429/5xx = 临时问题
|
||||
const isProbeRejection =
|
||||
httpStatus != null &&
|
||||
([401, 403, 400, 429].includes(httpStatus) || httpStatus >= 500);
|
||||
|
||||
if (isProbeRejection) {
|
||||
toast.warning(
|
||||
t("streamCheck.rejected", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 检查被拒: ${result.message}`,
|
||||
}),
|
||||
{ description, duration: 8000, closeButton: true },
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
t("streamCheck.failed", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 检查失败: ${result.message}`,
|
||||
}),
|
||||
{ description, duration: 8000, closeButton: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
+69
-51
@@ -91,7 +91,11 @@
|
||||
"switchToChinese": "Switch to Chinese",
|
||||
"switchToEnglish": "Switch to English",
|
||||
"enterEditMode": "Enter Edit Mode",
|
||||
"exitEditMode": "Exit Edit Mode"
|
||||
"exitEditMode": "Exit Edit Mode",
|
||||
"windowMinimize": "Minimize window",
|
||||
"windowMaximize": "Maximize window",
|
||||
"windowRestore": "Restore window",
|
||||
"windowClose": "Close window"
|
||||
},
|
||||
"provider": {
|
||||
"tabProvider": "Provider",
|
||||
@@ -104,6 +108,7 @@
|
||||
"currentlyUsing": "Currently Using",
|
||||
"enable": "Enable",
|
||||
"inUse": "In Use",
|
||||
"blockedByProxy": "Blocked",
|
||||
"editProvider": "Edit Provider",
|
||||
"editProviderHint": "Configuration will be applied to the current provider immediately after update.",
|
||||
"deleteProvider": "Delete Provider",
|
||||
@@ -189,19 +194,22 @@
|
||||
"deleteFailed": "Failed to delete provider: {{error}}",
|
||||
"settingsSaved": "Settings saved",
|
||||
"settingsSaveFailed": "Failed to save settings: {{error}}",
|
||||
"proxyRequiredForSwitch": "This provider {{reason}}, requires the proxy service to work properly. Start the proxy first.",
|
||||
"proxyRequiredForSwitch": "This provider {{reason}}, requires the routing service to work properly. Start routing first.",
|
||||
"proxyReasonCopilot": "uses GitHub Copilot as a Claude provider",
|
||||
"proxyReasonOpenAIChat": "uses OpenAI Chat API format",
|
||||
"proxyReasonOpenAIResponses": "uses OpenAI Responses API format",
|
||||
"proxyReasonFullUrl": "has full URL connection mode enabled",
|
||||
"openAIFormatHint": "This provider uses OpenAI-compatible format and requires the proxy service to be enabled",
|
||||
"copilotProxyHint": "GitHub Copilot as a Claude provider always requires the local proxy; the proxy automatically selects Chat Completions or Responses based on the current model.",
|
||||
"openAIFormatHint": "This provider uses OpenAI-compatible format and requires the routing service to be enabled",
|
||||
"copilotProxyHint": "GitHub Copilot as a Claude provider always requires local routing; routing automatically selects Chat Completions or Responses based on the current model.",
|
||||
"openLinkFailed": "Failed to open link",
|
||||
"openclawModelsRegistered": "Models have been registered to /model list",
|
||||
"openclawDefaultModelSet": "Set as default model",
|
||||
"openclawDefaultModelSetFailed": "Failed to set default model",
|
||||
"openclawNoModels": "No models configured",
|
||||
"backfillWarning": "Switched successfully, but failed to save changes back to the previous provider"
|
||||
"backfillWarning": "Switched successfully, but failed to save changes back to the previous provider",
|
||||
"windowControlFailed": "Window control failed: {{error}}",
|
||||
"officialBlockedByProxy": "Cannot switch to official provider while local routing is active. Using routing with official APIs may cause account bans.",
|
||||
"proxyOfficialWarning": "Current provider {{name}} is official. Consider switching to a third-party provider before using local routing."
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "Delete Provider",
|
||||
@@ -209,8 +217,8 @@
|
||||
"removeProvider": "Remove Provider",
|
||||
"removeProviderMessage": "Are you sure you want to remove provider \"{{name}}\" from the configuration?\n\nAfter removal, this provider will no longer be active, but the configuration data will be retained in CC Switch. You can re-add it at any time.",
|
||||
"proxy": {
|
||||
"title": "Enable Local Proxy",
|
||||
"message": "Local proxy is an advanced feature. Please make sure you understand how it works before enabling.\n\nWe recommend consulting the relevant documentation or your provider for proper configuration.",
|
||||
"title": "Enable Local Routing",
|
||||
"message": "Local routing is an advanced feature. Please make sure you understand how it works before enabling.\n\nWe recommend consulting the relevant documentation or your provider for proper configuration.",
|
||||
"confirm": "I understand, enable"
|
||||
},
|
||||
"failover": {
|
||||
@@ -245,7 +253,7 @@
|
||||
"tabGeneral": "General",
|
||||
"tabAuth": "Auth",
|
||||
"tabAdvanced": "Advanced",
|
||||
"tabProxy": "Proxy",
|
||||
"tabProxy": "Routing",
|
||||
"authCenter": {
|
||||
"title": "OAuth Authentication Center",
|
||||
"description": "Use your other subscriptions in Claude Code — please be mindful of compliance risks.",
|
||||
@@ -259,10 +267,10 @@
|
||||
"description": "Manage storage paths for Claude, Codex and Gemini configurations"
|
||||
},
|
||||
"proxy": {
|
||||
"title": "Local Proxy",
|
||||
"description": "Control proxy service toggle, view status and port info",
|
||||
"enableFeature": "Show Proxy Toggle on Main Page",
|
||||
"enableFeatureDescription": "When enabled, the proxy and failover toggles will appear at the top of the main page",
|
||||
"title": "Local Routing",
|
||||
"description": "Control routing service toggle, view status and port info",
|
||||
"enableFeature": "Show Routing Toggle on Main Page",
|
||||
"enableFeatureDescription": "When enabled, the routing and failover toggles will appear at the top of the main page",
|
||||
"enableFailoverToggle": "Show Failover Toggle on Main Page",
|
||||
"enableFailoverToggleDescription": "When enabled, the failover toggle will appear independently at the top of the main page",
|
||||
"running": "Running",
|
||||
@@ -494,6 +502,8 @@
|
||||
"autoLaunchFailed": "Failed to set auto-launch",
|
||||
"minimizeToTray": "Minimize to tray on close",
|
||||
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
||||
"useAppWindowControls": "Enable app-level window controls",
|
||||
"useAppWindowControlsDescription": "Use built-in minimize, maximize/restore, and close buttons in the app header.",
|
||||
"enableClaudePluginIntegration": "Apply to Claude Code extension",
|
||||
"enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app",
|
||||
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
|
||||
@@ -612,7 +622,7 @@
|
||||
"saving": "Saving...",
|
||||
"globalProxy": {
|
||||
"label": "Global Proxy",
|
||||
"hint": "Proxy all requests (API, Skills download, etc.). Leave empty for direct connection.",
|
||||
"hint": "Proxy all requests (API, Skills download, etc.). When local routing is enabled, app traffic is also routed through this proxy. Leave empty for direct connection.",
|
||||
"username": "Username (optional)",
|
||||
"password": "Password (optional)",
|
||||
"test": "Test Connection",
|
||||
@@ -715,7 +725,9 @@
|
||||
"copyCommand": "Copy Command",
|
||||
"copyMessage": "Copy Message",
|
||||
"messageCopied": "Message copied",
|
||||
"conversationHistory": "Conversation History"
|
||||
"conversationHistory": "Conversation History",
|
||||
"expandContent": "Expand full content",
|
||||
"collapseContent": "Collapse"
|
||||
},
|
||||
"console": {
|
||||
"providerSwitchReceived": "Received provider switch event:",
|
||||
@@ -773,7 +785,6 @@
|
||||
"siliconflow": "SiliconFlow is an official partner of CC Switch",
|
||||
"ucloud": "Compshare offers an exclusive bonus for CC Switch users — register via this link to get ¥5 platform trial credit!",
|
||||
"micu": "Micu is an official partner of CC Switch",
|
||||
"x-code": "XCodeAPI offers a special bonus for CC Switch users — register via this link and get 10% extra credit on your first order (contact admin to claim)",
|
||||
"ctok": "Join the CTok community on the official website and subscribe to a plan.",
|
||||
"ddshub": "DDSHub offers a special bonus for CC Switch users — register via this link and get 10% extra credit on your first top-up (contact group admin to claim)!",
|
||||
"lionccapi": "LionCCAPI offers exclusive benefits for CC Switch users. After registration, add WeChat HSQBJ088888888 and mention 'cc-switch' to receive $10 free credits!",
|
||||
@@ -812,12 +823,12 @@
|
||||
"fullUrlLabel": "Full URL",
|
||||
"fullUrlEnabled": "Full URL Mode",
|
||||
"fullUrlDisabled": "Mark as Full URL",
|
||||
"fullUrlHint": "💡 Enter the full request URL. This mode requires the proxy to be enabled, and the proxy will use the URL as-is without appending a path",
|
||||
"fullUrlHint": "💡 Enter the full request URL. This mode requires routing to be enabled, and routing will use the URL as-is without appending a path",
|
||||
"fullUrlHintGeminiNative": "💡 In Gemini Native full URL mode, two inputs are supported: 1. official/structured Gemini URLs, which will still be normalized to the requested model and streaming method; 2. opaque custom relay URLs, which will be used mostly as-is with only query parameters appended",
|
||||
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (Requires proxy)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent (Requires proxy)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires routing)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (Requires routing)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent (Requires routing)",
|
||||
"authField": "Auth Field",
|
||||
"authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN (Default)",
|
||||
"authFieldApiKey": "ANTHROPIC_API_KEY",
|
||||
@@ -937,11 +948,6 @@
|
||||
"testPrompt": "Test Prompt",
|
||||
"degradedThreshold": "Degraded Threshold (ms)",
|
||||
"maxRetries": "Max Retries",
|
||||
"proxyConfig": "Proxy Config",
|
||||
"useCustomProxy": "Use separate proxy",
|
||||
"proxyConfigDesc": "Configure separate network proxy for this provider. Uses system proxy or global settings when disabled.",
|
||||
"proxyUsername": "Username (optional)",
|
||||
"proxyPassword": "Password (optional)",
|
||||
"pricingConfig": "Pricing Config",
|
||||
"useCustomPricing": "Use separate config",
|
||||
"pricingConfigDesc": "Configure separate pricing parameters for this provider. Uses global defaults when disabled.",
|
||||
@@ -1085,7 +1091,7 @@
|
||||
},
|
||||
"dataSources": "Data Sources",
|
||||
"dataSource": {
|
||||
"proxy": "Proxy",
|
||||
"proxy": "Routing",
|
||||
"session_log": "Session Log",
|
||||
"codex_db": "Codex DB",
|
||||
"codex_session": "Codex Session",
|
||||
@@ -1100,6 +1106,8 @@
|
||||
"failed": "Session sync failed"
|
||||
},
|
||||
"totalRecords": "{{total}} records total",
|
||||
"goToPage": "Go",
|
||||
"pageInputPlaceholder": "Page",
|
||||
"modelPricing": "Model Pricing",
|
||||
"loadPricingError": "Failed to load pricing data",
|
||||
"modelPricingDesc": "Configure token costs for each model",
|
||||
@@ -1935,9 +1943,9 @@
|
||||
"addressCopied": "Address copied",
|
||||
"currentProvider": "Current Provider:",
|
||||
"waitingFirstRequest": "Current Provider: Waiting for first request...",
|
||||
"stoppedTitle": "Proxy Service Stopped",
|
||||
"stoppedTitle": "Routing Service Stopped",
|
||||
"stoppedDescription": "Use the toggle above to start the service",
|
||||
"openSettings": "Configure Proxy Service",
|
||||
"openSettings": "Configure Routing Service",
|
||||
"stats": {
|
||||
"activeConnections": "Active Connections",
|
||||
"totalRequests": "Total Requests",
|
||||
@@ -1946,14 +1954,14 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Proxy Service Settings",
|
||||
"description": "Configure local proxy server listening address, port and runtime parameters. Changes take effect immediately after saving.",
|
||||
"title": "Routing Service Settings",
|
||||
"description": "Configure local routing server listening address, port and runtime parameters. Changes take effect immediately after saving.",
|
||||
"alert": {
|
||||
"autoApply": "Changes will be automatically synced to the running proxy service without manual restart."
|
||||
"autoApply": "Changes will be automatically synced to the running routing service without manual restart."
|
||||
},
|
||||
"basic": {
|
||||
"title": "Basic Settings",
|
||||
"description": "Configure proxy service listening address and port."
|
||||
"description": "Configure routing service listening address and port."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced Parameters",
|
||||
@@ -1967,12 +1975,12 @@
|
||||
"listenAddress": {
|
||||
"label": "Listen Address",
|
||||
"placeholder": "127.0.0.1",
|
||||
"description": "IP address the proxy server listens on (recommended: 127.0.0.1)"
|
||||
"description": "IP address the routing server listens on (recommended: 127.0.0.1)"
|
||||
},
|
||||
"listenPort": {
|
||||
"label": "Listen Port",
|
||||
"placeholder": "15721",
|
||||
"description": "Port number the proxy server listens on (1024 ~ 65535)"
|
||||
"description": "Port number the routing server listens on (1024 ~ 65535)"
|
||||
},
|
||||
"maxRetries": {
|
||||
"label": "Max Retries",
|
||||
@@ -1986,7 +1994,7 @@
|
||||
},
|
||||
"enableLogging": {
|
||||
"label": "Enable Logging",
|
||||
"description": "Log all proxy requests for troubleshooting"
|
||||
"description": "Log all routing requests for troubleshooting"
|
||||
},
|
||||
"streamingFirstByteTimeout": {
|
||||
"label": "Streaming First Byte Timeout (sec)",
|
||||
@@ -2017,29 +2025,29 @@
|
||||
"save": "Save Configuration"
|
||||
},
|
||||
"toast": {
|
||||
"saved": "Proxy configuration saved",
|
||||
"saved": "Routing configuration saved",
|
||||
"saveFailed": "Save failed: {{error}}"
|
||||
},
|
||||
"invalidPort": "Invalid port, please enter a number between 1024-65535",
|
||||
"invalidAddress": "Invalid address, please enter a valid IP address (e.g. 127.0.0.1) or localhost",
|
||||
"configSaved": "Proxy configuration saved",
|
||||
"configSaved": "Routing configuration saved",
|
||||
"configSaveFailed": "Failed to save configuration",
|
||||
"restartRequired": "Restart proxy service for address or port changes to take effect"
|
||||
"restartRequired": "Restart routing service for address or port changes to take effect"
|
||||
},
|
||||
"switchFailed": "Switch failed: {{error}}",
|
||||
"takeover": {
|
||||
"hint": "Select apps to take over — once enabled, requests from that app will be routed through the local proxy",
|
||||
"enabled": "{{app}} takeover enabled",
|
||||
"disabled": "{{app}} takeover disabled",
|
||||
"failed": "Failed to toggle takeover",
|
||||
"hint": "Select apps to route — once enabled, requests from that app will go through local routing",
|
||||
"enabled": "{{app}} routing enabled",
|
||||
"disabled": "{{app}} routing disabled",
|
||||
"failed": "Failed to toggle routing",
|
||||
"tooltip": {
|
||||
"active": "{{appLabel}} is intercepting - {{address}}:{{port}}\nSwitch provider for hot switching",
|
||||
"broken": "{{appLabel}} is intercepting, but proxy service is not running",
|
||||
"inactive": "Intercept {{appLabel}}'s live config to route requests through local proxy"
|
||||
"active": "{{appLabel}} is routing - {{address}}:{{port}}\nSwitch provider for hot switching",
|
||||
"broken": "{{appLabel}} is routing, but routing service is not running",
|
||||
"inactive": "Route {{appLabel}}'s requests through local routing"
|
||||
}
|
||||
},
|
||||
"failover": {
|
||||
"proxyRequired": "Proxy service must be started to configure failover",
|
||||
"proxyRequired": "Routing service must be started to configure failover",
|
||||
"autoSwitch": "Auto Failover",
|
||||
"autoSwitchDescription": "When enabled, switches to queue P1 immediately and automatically tries the next provider in the queue on failures"
|
||||
},
|
||||
@@ -2103,10 +2111,10 @@
|
||||
"failed": "Failed to toggle logging"
|
||||
},
|
||||
"server": {
|
||||
"started": "Proxy service started - {{address}}:{{port}}",
|
||||
"startFailed": "Failed to start proxy service: {{detail}}"
|
||||
"started": "Routing service started - {{address}}:{{port}}",
|
||||
"startFailed": "Failed to start routing service: {{detail}}"
|
||||
},
|
||||
"stoppedWithRestore": "Proxy service stopped, all takeover configs restored",
|
||||
"stoppedWithRestore": "Routing service stopped, all routing configs restored",
|
||||
"stopWithRestoreFailed": "Stop failed: {{detail}}"
|
||||
},
|
||||
"streamCheck": {
|
||||
@@ -2124,11 +2132,21 @@
|
||||
"operational": "{{providerName}} is operational ({{responseTimeMs}}ms)",
|
||||
"degraded": "{{providerName}} is slow ({{responseTimeMs}}ms)",
|
||||
"failed": "{{providerName}} check failed: {{message}}",
|
||||
"error": "{{providerName}} check error: {{error}}"
|
||||
"rejected": "{{providerName}} check rejected: {{message}}",
|
||||
"error": "{{providerName}} check error: {{error}}",
|
||||
"httpHint": {
|
||||
"400": "Provider rejected request format. Health check probe may differ from actual usage.",
|
||||
"401": "API key may be invalid, or provider uses OAuth auth. Check failure doesn't mean it's unusable.",
|
||||
"402": "Account quota or billing issue.",
|
||||
"403": "Provider blocked this request. Some verify client identity; may work fine in actual use.",
|
||||
"404": "Endpoint not found. Check base URL and API path.",
|
||||
"429": "Too many requests. Try again later.",
|
||||
"5xx": "Provider internal error. Likely temporary."
|
||||
}
|
||||
},
|
||||
"proxyConfig": {
|
||||
"proxyEnabled": "Proxy Enabled",
|
||||
"appTakeover": "Proxy Enabled",
|
||||
"proxyEnabled": "Routing Master Switch",
|
||||
"appTakeover": "Routing Enabled",
|
||||
"perAppConfig": "Per-App Config",
|
||||
"circuitBreaker": "Circuit Breaker",
|
||||
"circuitBreakerSettings": "Circuit Breaker Settings",
|
||||
|
||||
+70
-52
@@ -91,7 +91,11 @@
|
||||
"switchToChinese": "中国語に切り替え",
|
||||
"switchToEnglish": "英語に切り替え",
|
||||
"enterEditMode": "編集モードに入る",
|
||||
"exitEditMode": "編集モードを終了"
|
||||
"exitEditMode": "編集モードを終了",
|
||||
"windowMinimize": "ウィンドウを最小化",
|
||||
"windowMaximize": "ウィンドウを最大化",
|
||||
"windowRestore": "ウィンドウを元に戻す",
|
||||
"windowClose": "ウィンドウを閉じる"
|
||||
},
|
||||
"provider": {
|
||||
"tabProvider": "プロバイダー",
|
||||
@@ -104,6 +108,7 @@
|
||||
"currentlyUsing": "現在使用中",
|
||||
"enable": "有効化",
|
||||
"inUse": "使用中",
|
||||
"blockedByProxy": "ブロック",
|
||||
"editProvider": "プロバイダーを編集",
|
||||
"editProviderHint": "保存すると現在のプロバイダーにすぐ反映されます。",
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
@@ -189,19 +194,22 @@
|
||||
"deleteFailed": "プロバイダーの削除に失敗しました: {{error}}",
|
||||
"settingsSaved": "設定を保存しました",
|
||||
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
|
||||
"proxyRequiredForSwitch": "このプロバイダーは{{reason}}、プロキシサービスが必要です。先にプロキシを起動してください",
|
||||
"proxyRequiredForSwitch": "このプロバイダーは{{reason}}、ルーティングサービスが必要です。先にルーティングを起動してください",
|
||||
"proxyReasonCopilot": "GitHub Copilot を Claude プロバイダーとして使用しており",
|
||||
"proxyReasonOpenAIChat": "OpenAI Chat API フォーマットを使用しており",
|
||||
"proxyReasonOpenAIResponses": "OpenAI Responses API フォーマットを使用しており",
|
||||
"proxyReasonFullUrl": "完全 URL 接続モードが有効になっており",
|
||||
"openAIFormatHint": "このプロバイダーは OpenAI 互換フォーマットを使用しており、プロキシサービスの有効化が必要です",
|
||||
"copilotProxyHint": "GitHub Copilot を Claude プロバイダーとして使用する場合、ローカルプロキシが常に必要です。プロキシは現在のモデルに応じて Chat Completions または Responses を自動的に選択します。",
|
||||
"openAIFormatHint": "このプロバイダーは OpenAI 互換フォーマットを使用しており、ルーティングサービスの有効化が必要です",
|
||||
"copilotProxyHint": "GitHub Copilot を Claude プロバイダーとして使用する場合、ローカルルーティングが常に必要です。ルーティングは現在のモデルに応じて Chat Completions または Responses を自動的に選択します。",
|
||||
"openLinkFailed": "リンクを開けませんでした",
|
||||
"openclawModelsRegistered": "モデルが /model リストに登録されました",
|
||||
"openclawDefaultModelSet": "デフォルトモデルに設定しました",
|
||||
"openclawDefaultModelSetFailed": "デフォルトモデルの設定に失敗しました",
|
||||
"openclawNoModels": "モデルが設定されていません",
|
||||
"backfillWarning": "切り替え成功しましたが、前のプロバイダーへの設定保存に失敗しました"
|
||||
"backfillWarning": "切り替え成功しましたが、前のプロバイダーへの設定保存に失敗しました",
|
||||
"windowControlFailed": "ウィンドウ操作に失敗しました: {{error}}",
|
||||
"officialBlockedByProxy": "ローカルルーティングモード中は公式プロバイダーに切り替えできません。ルーティング経由で公式 API にアクセスするとアカウントが停止される可能性があります。",
|
||||
"proxyOfficialWarning": "現在のプロバイダー {{name}} は公式です。ローカルルーティングを使用する前にサードパーティプロバイダーに切り替えてください。"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
@@ -209,8 +217,8 @@
|
||||
"removeProvider": "プロバイダーを解除",
|
||||
"removeProviderMessage": "プロバイダー「{{name}}」を設定から解除してもよろしいですか?\n\n解除後、このプロバイダーは無効になりますが、設定データは CC Switch に保持されます。いつでも再追加できます。",
|
||||
"proxy": {
|
||||
"title": "ローカルプロキシの有効化",
|
||||
"message": "ローカルプロキシは上級機能です。有効にする前に、その仕組みを理解していることをご確認ください。\n\n適切な設定方法については、関連ドキュメントまたはプロバイダーにご相談ください。",
|
||||
"title": "ローカルルーティングの有効化",
|
||||
"message": "ローカルルーティングは上級機能です。有効にする前に、その仕組みを理解していることをご確認ください。\n\n適切な設定方法については、関連ドキュメントまたはプロバイダーにご相談ください。",
|
||||
"confirm": "理解しました、有効にする"
|
||||
},
|
||||
"failover": {
|
||||
@@ -245,7 +253,7 @@
|
||||
"tabGeneral": "一般",
|
||||
"tabAuth": "認証",
|
||||
"tabAdvanced": "詳細",
|
||||
"tabProxy": "プロキシ",
|
||||
"tabProxy": "ルーティング",
|
||||
"authCenter": {
|
||||
"title": "OAuth 認証センター",
|
||||
"description": "Claude Code で他のサブスクリプションをご利用いただけます。コンプライアンスリスクにご注意ください。",
|
||||
@@ -259,10 +267,10 @@
|
||||
"description": "Claude、Codex、Gemini の設定保存パスを管理"
|
||||
},
|
||||
"proxy": {
|
||||
"title": "ローカルプロキシ",
|
||||
"description": "プロキシサービスの切り替え、ステータスとポート情報を表示",
|
||||
"enableFeature": "メインページにプロキシ切り替えを表示",
|
||||
"enableFeatureDescription": "有効にすると、メインページ上部にプロキシとフェイルオーバーの切り替えが表示されます",
|
||||
"title": "ローカルルーティング",
|
||||
"description": "ルーティングサービスの切り替え、ステータスとポート情報を表示",
|
||||
"enableFeature": "メインページにルーティング切り替えを表示",
|
||||
"enableFeatureDescription": "有効にすると、メインページ上部にルーティングとフェイルオーバーの切り替えが表示されます",
|
||||
"enableFailoverToggle": "メインページにフェイルオーバー切り替えを表示",
|
||||
"enableFailoverToggleDescription": "有効にすると、メインページ上部にフェイルオーバー切り替えが独立して表示されます",
|
||||
"running": "実行中",
|
||||
@@ -494,6 +502,8 @@
|
||||
"autoLaunchFailed": "自動起動の設定に失敗しました",
|
||||
"minimizeToTray": "閉じるときトレイへ最小化",
|
||||
"minimizeToTrayDescription": "チェックすると閉じるボタンでトレイに隠し、オフならアプリを終了します。",
|
||||
"useAppWindowControls": "アプリ内ウィンドウボタンを有効化",
|
||||
"useAppWindowControlsDescription": "有効にすると、アプリヘッダーに最小化・最大化/復元・閉じるボタンを表示します。",
|
||||
"enableClaudePluginIntegration": "Claude Code 拡張に適用",
|
||||
"enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します",
|
||||
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
|
||||
@@ -612,14 +622,14 @@
|
||||
"saving": "保存中...",
|
||||
"globalProxy": {
|
||||
"label": "グローバルプロキシ",
|
||||
"hint": "すべてのリクエスト(API、Skills ダウンロードなど)をプロキシ経由で送信します。空欄で直接接続。",
|
||||
"hint": "すべてのリクエスト(API、Skills ダウンロードなど)をプロキシ経由で送信します。ローカルルーティング有効時、アプリのリクエストもこのプロキシを経由します。空欄で直接接続。",
|
||||
"username": "ユーザー名(任意)",
|
||||
"password": "パスワード(任意)",
|
||||
"test": "接続テスト",
|
||||
"scan": "ローカルプロキシをスキャン",
|
||||
"clear": "クリア",
|
||||
"scanFailed": "スキャンに失敗しました: {{error}}",
|
||||
"saved": "プロキシ設定を保存しました",
|
||||
"saved": "ルーティング設定を保存しました",
|
||||
"saveFailed": "保存に失敗しました: {{error}}",
|
||||
"testSuccess": "接続成功!遅延 {{latency}}ms",
|
||||
"testFailed": "接続に失敗しました: {{error}}",
|
||||
@@ -715,7 +725,9 @@
|
||||
"copyCommand": "コマンドをコピー",
|
||||
"copyMessage": "メッセージをコピー",
|
||||
"messageCopied": "メッセージがコピーされました",
|
||||
"conversationHistory": "会話履歴"
|
||||
"conversationHistory": "会話履歴",
|
||||
"expandContent": "全文を表示",
|
||||
"collapseContent": "折りたたむ"
|
||||
},
|
||||
"console": {
|
||||
"providerSwitchReceived": "プロバイダー切り替えイベントを受信:",
|
||||
@@ -773,7 +785,6 @@
|
||||
"siliconflow": "SiliconFlow は CC Switch の公式パートナーです",
|
||||
"ucloud": "Compshare は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、5元のプラットフォーム体験クレジットがもらえます!",
|
||||
"micu": "Micu は CC Switch の公式パートナーです",
|
||||
"x-code": "XCodeAPI は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、初回注文で 10% の追加クレジットがもらえます(管理者に連絡して受け取り)",
|
||||
"ctok": "公式サイトで CTok コミュニティに参加し、プランを購読してください。",
|
||||
"ddshub": "DDSHub は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、初回チャージで 10% の追加クレジットがもらえます(グループ管理者に連絡して受け取り)!",
|
||||
"lionccapi": "LionCCAPIはCC Switchユーザーに特別特典を提供しています。登録後、WeChat HSQBJ088888888を追加し、「cc-switch」と伝えると$10分のクレジットがもらえます!",
|
||||
@@ -812,12 +823,12 @@
|
||||
"fullUrlLabel": "フル URL",
|
||||
"fullUrlEnabled": "フル URL モード",
|
||||
"fullUrlDisabled": "フル URL として設定",
|
||||
"fullUrlHint": "💡 完全なリクエスト URL を入力してください。このモードはプロキシを有効にして使用する必要があり、プロキシはこの URL をそのまま使用し、パスを追加しません",
|
||||
"fullUrlHint": "💡 完全なリクエスト URL を入力してください。このモードはルーティングを有効にして使用する必要があり、ルーティングはこの URL をそのまま使用し、パスを追加しません",
|
||||
"fullUrlHintGeminiNative": "💡 Gemini Native のフル URL モードでは 2 種類の入力を扱えます。1. 公式/構造化された Gemini URL は、要求されたモデルやストリーミング方式に合わせて正規化されます。2. カスタム relay の opaque な完全 URL は、主にそのまま使用され、必要なクエリだけ追加されます",
|
||||
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API(プロキシが必要)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent(プロキシが必要)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(ルーティングが必要)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API(ルーティングが必要)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent(ルーティングが必要)",
|
||||
"authField": "認証フィールド",
|
||||
"authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(デフォルト)",
|
||||
"authFieldApiKey": "ANTHROPIC_API_KEY",
|
||||
@@ -937,11 +948,6 @@
|
||||
"testPrompt": "テストプロンプト",
|
||||
"degradedThreshold": "低下閾値(ミリ秒)",
|
||||
"maxRetries": "最大リトライ回数",
|
||||
"proxyConfig": "プロキシ設定",
|
||||
"useCustomProxy": "個別プロキシを使用",
|
||||
"proxyConfigDesc": "このプロバイダーに個別のネットワークプロキシを設定します。無効の場合はシステムプロキシまたはグローバル設定を使用します。",
|
||||
"proxyUsername": "ユーザー名(任意)",
|
||||
"proxyPassword": "パスワード(任意)",
|
||||
"pricingConfig": "課金設定",
|
||||
"useCustomPricing": "個別設定を使用",
|
||||
"pricingConfigDesc": "このプロバイダーに個別の課金パラメータを設定します。無効の場合はグローバル設定を使用します。",
|
||||
@@ -1085,7 +1091,7 @@
|
||||
},
|
||||
"dataSources": "データソース",
|
||||
"dataSource": {
|
||||
"proxy": "プロキシ",
|
||||
"proxy": "ルーティング",
|
||||
"session_log": "セッションログ",
|
||||
"codex_db": "Codex DB",
|
||||
"codex_session": "Codex セッション",
|
||||
@@ -1100,6 +1106,8 @@
|
||||
"failed": "セッション同期に失敗しました"
|
||||
},
|
||||
"totalRecords": "全 {{total}} 件",
|
||||
"goToPage": "移動",
|
||||
"pageInputPlaceholder": "ページ",
|
||||
"modelPricing": "モデル料金",
|
||||
"loadPricingError": "料金データの読み込みに失敗しました",
|
||||
"modelPricingDesc": "各モデルのトークンコストを設定",
|
||||
@@ -1935,9 +1943,9 @@
|
||||
"addressCopied": "アドレスをコピーしました",
|
||||
"currentProvider": "現在のプロバイダー:",
|
||||
"waitingFirstRequest": "現在のプロバイダー: 最初のリクエスト待ち...",
|
||||
"stoppedTitle": "プロキシサービス停止中",
|
||||
"stoppedTitle": "ルーティングサービス停止中",
|
||||
"stoppedDescription": "上のトグルでサービスを開始できます",
|
||||
"openSettings": "プロキシサービスを設定",
|
||||
"openSettings": "ルーティングサービスを設定",
|
||||
"stats": {
|
||||
"activeConnections": "アクティブ接続",
|
||||
"totalRequests": "総リクエスト数",
|
||||
@@ -1946,14 +1954,14 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "プロキシサービス設定",
|
||||
"description": "ローカルプロキシサーバーのリッスンアドレス、ポート、実行パラメータを設定します。保存後すぐに反映されます。",
|
||||
"title": "ルーティングサービス設定",
|
||||
"description": "ローカルルーティングサーバーのリッスンアドレス、ポート、実行パラメータを設定します。保存後すぐに反映されます。",
|
||||
"alert": {
|
||||
"autoApply": "変更は実行中のプロキシサービスに自動的に同期され、手動での再起動は不要です。"
|
||||
"autoApply": "変更は実行中のルーティングサービスに自動的に同期され、手動での再起動は不要です。"
|
||||
},
|
||||
"basic": {
|
||||
"title": "基本設定",
|
||||
"description": "プロキシサービスのリッスンアドレスとポートを設定します。"
|
||||
"description": "ルーティングサービスのリッスンアドレスとポートを設定します。"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "詳細パラメータ",
|
||||
@@ -1967,12 +1975,12 @@
|
||||
"listenAddress": {
|
||||
"label": "リッスンアドレス",
|
||||
"placeholder": "127.0.0.1",
|
||||
"description": "プロキシサーバーがリッスンするIPアドレス(推奨: 127.0.0.1)"
|
||||
"description": "ルーティングサーバーがリッスンするIPアドレス(推奨: 127.0.0.1)"
|
||||
},
|
||||
"listenPort": {
|
||||
"label": "リッスンポート",
|
||||
"placeholder": "15721",
|
||||
"description": "プロキシサーバーがリッスンするポート番号(1024 ~ 65535)"
|
||||
"description": "ルーティングサーバーがリッスンするポート番号(1024 ~ 65535)"
|
||||
},
|
||||
"maxRetries": {
|
||||
"label": "最大リトライ回数",
|
||||
@@ -1986,7 +1994,7 @@
|
||||
},
|
||||
"enableLogging": {
|
||||
"label": "ログ記録を有効化",
|
||||
"description": "トラブルシューティングのためにすべてのプロキシリクエストを記録"
|
||||
"description": "トラブルシューティングのためにすべてのルーティングリクエストを記録"
|
||||
},
|
||||
"streamingFirstByteTimeout": {
|
||||
"label": "ストリーミング初回バイトタイムアウト(秒)",
|
||||
@@ -2017,29 +2025,29 @@
|
||||
"save": "設定を保存"
|
||||
},
|
||||
"toast": {
|
||||
"saved": "プロキシ設定を保存しました",
|
||||
"saved": "ルーティング設定を保存しました",
|
||||
"saveFailed": "保存に失敗しました: {{error}}"
|
||||
},
|
||||
"invalidPort": "無効なポートです。1024〜65535 の数値を入力してください",
|
||||
"invalidAddress": "無効なアドレスです。有効な IP アドレス(例: 127.0.0.1)または localhost を入力してください",
|
||||
"configSaved": "プロキシ設定を保存しました",
|
||||
"configSaved": "ルーティング設定を保存しました",
|
||||
"configSaveFailed": "設定の保存に失敗しました",
|
||||
"restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です"
|
||||
"restartRequired": "アドレスまたはポートの変更を反映するにはルーティングサービスの再起動が必要です"
|
||||
},
|
||||
"switchFailed": "切り替えに失敗しました: {{error}}",
|
||||
"takeover": {
|
||||
"hint": "テイクオーバーするアプリを選択します。有効にすると、そのアプリのリクエストはローカルプロキシ経由で転送されます",
|
||||
"enabled": "{{app}} テイクオーバー有効",
|
||||
"disabled": "{{app}} テイクオーバー無効",
|
||||
"failed": "テイクオーバーの切り替えに失敗しました",
|
||||
"hint": "ルーティングするアプリを選択します。有効にすると、そのアプリのリクエストはローカルルーティング経由で転送されます",
|
||||
"enabled": "{{app}} ルーティング有効",
|
||||
"disabled": "{{app}} ルーティング無効",
|
||||
"failed": "ルーティングの切り替えに失敗しました",
|
||||
"tooltip": {
|
||||
"active": "{{appLabel}} がインターセプト中 - {{address}}:{{port}}\nホットスイッチングのためプロバイダを切り替え",
|
||||
"broken": "{{appLabel}} がインターセプト中ですが、プロキシサービスが実行されていません",
|
||||
"inactive": "{{appLabel}} のライブ設定をインターセプトしてリクエストをローカルプロキシ経由でルーティング"
|
||||
"active": "{{appLabel}} がルーティング中 - {{address}}:{{port}}\nホットスイッチングのためプロバイダを切り替え",
|
||||
"broken": "{{appLabel}} がルーティング中ですが、ルーティングサービスが実行されていません",
|
||||
"inactive": "{{appLabel}} のリクエストをローカルルーティング経由でルーティング"
|
||||
}
|
||||
},
|
||||
"failover": {
|
||||
"proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります",
|
||||
"proxyRequired": "フェイルオーバーを設定するには、ルーティングサービスを先に起動する必要があります",
|
||||
"autoSwitch": "自動フェイルオーバー",
|
||||
"autoSwitchDescription": "有効にするとキューの P1 に即時切り替え、リクエスト失敗時はキュー内の次のプロバイダーを自動で試行します"
|
||||
},
|
||||
@@ -2103,10 +2111,10 @@
|
||||
"failed": "ログ状態の切り替えに失敗しました"
|
||||
},
|
||||
"server": {
|
||||
"started": "プロキシサービスが開始されました - {{address}}:{{port}}",
|
||||
"startFailed": "プロキシサービスの開始に失敗しました: {{detail}}"
|
||||
"started": "ルーティングサービスが開始されました - {{address}}:{{port}}",
|
||||
"startFailed": "ルーティングサービスの開始に失敗しました: {{detail}}"
|
||||
},
|
||||
"stoppedWithRestore": "プロキシサービスが停止し、すべてのテイクオーバー設定が復元されました",
|
||||
"stoppedWithRestore": "ルーティングサービスが停止し、すべてのルーティング設定が復元されました",
|
||||
"stopWithRestoreFailed": "停止に失敗しました: {{detail}}"
|
||||
},
|
||||
"streamCheck": {
|
||||
@@ -2124,11 +2132,21 @@
|
||||
"operational": "{{providerName}} は正常に動作しています ({{responseTimeMs}}ms)",
|
||||
"degraded": "{{providerName}} の応答が遅いです ({{responseTimeMs}}ms)",
|
||||
"failed": "{{providerName}} のチェックに失敗しました: {{message}}",
|
||||
"error": "{{providerName}} のチェックでエラーが発生しました: {{error}}"
|
||||
"rejected": "{{providerName}} のチェックが拒否されました: {{message}}",
|
||||
"error": "{{providerName}} のチェックでエラーが発生しました: {{error}}",
|
||||
"httpHint": {
|
||||
"400": "リクエスト形式が拒否されました。ヘルスチェックの形式は実際の使用と異なる場合があります。",
|
||||
"401": "APIキーが無効か、OAuthなどの認証方式を使用しています。チェック失敗は実際に使えないことを意味しません。",
|
||||
"402": "アカウントの利用枠または請求の問題です。",
|
||||
"403": "プロバイダーがリクエストを拒否しました。実際の使用では正常に動作する場合があります。",
|
||||
"404": "エンドポイントが見つかりません。Base URLとAPIパスを確認してください。",
|
||||
"429": "リクエストが多すぎます。しばらくしてからお試しください。",
|
||||
"5xx": "プロバイダーの内部エラーです。一時的な問題の可能性が高いです。"
|
||||
}
|
||||
},
|
||||
"proxyConfig": {
|
||||
"proxyEnabled": "プロキシ有効",
|
||||
"appTakeover": "プロキシ有効",
|
||||
"proxyEnabled": "ルーティング総スイッチ",
|
||||
"appTakeover": "ルーティング有効",
|
||||
"perAppConfig": "アプリ別設定",
|
||||
"circuitBreaker": "サーキットブレーカー",
|
||||
"circuitBreakerSettings": "サーキットブレーカー設定",
|
||||
|
||||
+69
-51
@@ -91,7 +91,11 @@
|
||||
"switchToChinese": "切换到中文",
|
||||
"switchToEnglish": "切换到英文",
|
||||
"enterEditMode": "进入编辑模式",
|
||||
"exitEditMode": "退出编辑模式"
|
||||
"exitEditMode": "退出编辑模式",
|
||||
"windowMinimize": "最小化窗口",
|
||||
"windowMaximize": "最大化窗口",
|
||||
"windowRestore": "还原窗口",
|
||||
"windowClose": "关闭窗口"
|
||||
},
|
||||
"provider": {
|
||||
"tabProvider": "供应商",
|
||||
@@ -104,6 +108,7 @@
|
||||
"currentlyUsing": "当前使用",
|
||||
"enable": "启用",
|
||||
"inUse": "使用中",
|
||||
"blockedByProxy": "已拦截",
|
||||
"editProvider": "编辑供应商",
|
||||
"editProviderHint": "更新配置后将立即应用到当前供应商。",
|
||||
"deleteProvider": "删除供应商",
|
||||
@@ -189,19 +194,22 @@
|
||||
"deleteFailed": "删除供应商失败:{{error}}",
|
||||
"settingsSaved": "设置已保存",
|
||||
"settingsSaveFailed": "保存设置失败:{{error}}",
|
||||
"proxyRequiredForSwitch": "此供应商{{reason}},需要代理服务才能正常使用,请先启动代理",
|
||||
"proxyRequiredForSwitch": "此供应商{{reason}},需要路由服务才能正常使用,请先启动路由",
|
||||
"proxyReasonCopilot": "使用 GitHub Copilot 作为 Claude 供应商",
|
||||
"proxyReasonOpenAIChat": "使用 OpenAI Chat 接口格式",
|
||||
"proxyReasonOpenAIResponses": "使用 OpenAI Responses 接口格式",
|
||||
"proxyReasonFullUrl": "开启了完整 URL 连接模式",
|
||||
"openAIFormatHint": "此供应商使用 OpenAI 兼容格式,需要开启代理服务才能正常使用",
|
||||
"copilotProxyHint": "GitHub Copilot 作为 Claude 供应商时始终需要本地代理;代理会根据当前模型自动选择 Chat Completions 或 Responses。",
|
||||
"openAIFormatHint": "此供应商使用 OpenAI 兼容格式,需要开启路由服务才能正常使用",
|
||||
"copilotProxyHint": "GitHub Copilot 作为 Claude 供应商时始终需要本地路由;路由会根据当前模型自动选择 Chat Completions 或 Responses。",
|
||||
"openLinkFailed": "链接打开失败",
|
||||
"openclawModelsRegistered": "模型已注册到 /model 列表",
|
||||
"openclawDefaultModelSet": "已设为默认模型",
|
||||
"openclawDefaultModelSetFailed": "设置默认模型失败",
|
||||
"openclawNoModels": "该供应商没有配置模型",
|
||||
"backfillWarning": "切换成功,但旧供应商配置回填失败,您手动修改的配置可能未保存"
|
||||
"backfillWarning": "切换成功,但旧供应商配置回填失败,您手动修改的配置可能未保存",
|
||||
"windowControlFailed": "窗口控制失败:{{error}}",
|
||||
"officialBlockedByProxy": "本地路由模式下不能切换到官方供应商,使用路由访问官方 API 可能导致账号被封禁",
|
||||
"proxyOfficialWarning": "当前供应商 {{name}} 是官方供应商,建议切换到第三方供应商后再使用本地路由"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "删除供应商",
|
||||
@@ -209,8 +217,8 @@
|
||||
"removeProvider": "移除供应商",
|
||||
"removeProviderMessage": "确定要从配置中移除供应商 \"{{name}}\" 吗?\n\n移除后该供应商将不再生效,但配置数据会保留在 CC Switch 中,您可以随时重新添加。",
|
||||
"proxy": {
|
||||
"title": "启用本地代理服务",
|
||||
"message": "本地代理是一项高级功能,启用前请确保您已了解其工作原理。\n\n建议先查阅相关文档或咨询您的供应商,以获取正确的配置方式。",
|
||||
"title": "启用本地路由服务",
|
||||
"message": "本地路由是一项高级功能,启用前请确保您已了解其工作原理。\n\n建议先查阅相关文档或咨询您的供应商,以获取正确的配置方式。",
|
||||
"confirm": "我已了解,继续启用"
|
||||
},
|
||||
"failover": {
|
||||
@@ -245,7 +253,7 @@
|
||||
"tabGeneral": "通用",
|
||||
"tabAuth": "认证",
|
||||
"tabAdvanced": "高级",
|
||||
"tabProxy": "代理",
|
||||
"tabProxy": "路由",
|
||||
"authCenter": {
|
||||
"title": "OAuth 认证中心",
|
||||
"description": "在 Claude Code 中使用您的其他订阅,请注意合规风险。",
|
||||
@@ -259,10 +267,10 @@
|
||||
"description": "管理 Claude、Codex 和 Gemini 的配置存储路径"
|
||||
},
|
||||
"proxy": {
|
||||
"title": "本地代理",
|
||||
"description": "控制代理服务开关、查看状态与端口信息",
|
||||
"enableFeature": "在主页面显示本地代理开关",
|
||||
"enableFeatureDescription": "开启后,主页面顶部将显示代理和故障转移开关",
|
||||
"title": "本地路由",
|
||||
"description": "控制路由服务开关、查看状态与端口信息",
|
||||
"enableFeature": "在主页面显示本地路由开关",
|
||||
"enableFeatureDescription": "开启后,主页面顶部将显示路由和故障转移开关",
|
||||
"enableFailoverToggle": "在主页面显示故障转移开关",
|
||||
"enableFailoverToggleDescription": "开启后,主页面顶部将独立显示故障转移开关",
|
||||
"running": "运行中",
|
||||
@@ -494,6 +502,8 @@
|
||||
"autoLaunchFailed": "设置开机自启失败",
|
||||
"minimizeToTray": "关闭时最小化到托盘",
|
||||
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
||||
"useAppWindowControls": "启用应用级窗口按钮",
|
||||
"useAppWindowControlsDescription": "开启后使用应用自建的最小化、最大化/还原、关闭按钮;关闭后沿用系统窗口模式。",
|
||||
"enableClaudePluginIntegration": "应用到 Claude Code 插件",
|
||||
"enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换",
|
||||
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
|
||||
@@ -612,7 +622,7 @@
|
||||
"saving": "正在保存...",
|
||||
"globalProxy": {
|
||||
"label": "全局代理",
|
||||
"hint": "代理所有请求(API、Skills 下载等)。留空表示直连。",
|
||||
"hint": "代理所有请求(API、Skills 下载等)。开启本地路由时,应用的请求也会经过此代理。留空表示直连。",
|
||||
"username": "用户名(可选)",
|
||||
"password": "密码(可选)",
|
||||
"test": "测试连接",
|
||||
@@ -715,7 +725,9 @@
|
||||
"copyCommand": "复制命令",
|
||||
"copyMessage": "复制消息",
|
||||
"messageCopied": "已复制消息内容",
|
||||
"conversationHistory": "对话记录"
|
||||
"conversationHistory": "对话记录",
|
||||
"expandContent": "展开完整内容",
|
||||
"collapseContent": "收起"
|
||||
},
|
||||
"console": {
|
||||
"providerSwitchReceived": "收到供应商切换事件:",
|
||||
@@ -773,7 +785,6 @@
|
||||
"siliconflow": "硅基流动是 CC Switch 的官方合作伙伴",
|
||||
"ucloud": "优云智算为CC Switch 的用户提供了特殊优惠,通过此链接注册,可以获得五元平台体验金!",
|
||||
"micu": "Micu 是 CC Switch 的官方合作伙伴",
|
||||
"x-code": "XCodeAPI 为CC Switch 的用户提供特别福利,使用此链接注册后首单加赠10%的额度(联系站长领取)",
|
||||
"ctok": "官网加入CTok社群,订阅套餐。",
|
||||
"ddshub": "呆呆兽为CC Switch 的用户提供了特别福利,通过此链接注册后,首单充值可额外赠送 10% 额度(联系群主领取)!",
|
||||
"lionccapi": "LionCCAPI 为 CC Switch 的用户提供了特别福利,注册后添加客服微信HSQBJ088888888,发暗号cc-switch备注即可送10美金额度!",
|
||||
@@ -813,12 +824,12 @@
|
||||
"fullUrlLabel": "完整 URL",
|
||||
"fullUrlEnabled": "完整 URL 模式",
|
||||
"fullUrlDisabled": "标记为完整 URL",
|
||||
"fullUrlHint": "💡 请填写完整请求 URL,并且必须开启代理后使用;代理将直接使用此 URL,不拼接路径",
|
||||
"fullUrlHint": "💡 请填写完整请求 URL,并且必须开启路由后使用;路由将直接使用此 URL,不拼接路径",
|
||||
"fullUrlHintGeminiNative": "💡 Gemini Native 下,完整 URL 模式同时兼容两类地址:1. 官方/标准 Gemini URL,代理会按模型和流式参数自动归一化;2. 自定义 relay 的完整 URL,代理会尽量原样使用,只补查询参数,不再强行追加 models 路径",
|
||||
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (需开启代理)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent (需开启代理)",
|
||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启路由)",
|
||||
"apiFormatOpenAIResponses": "OpenAI Responses API (需开启路由)",
|
||||
"apiFormatGeminiNative": "Gemini Native generateContent (需开启路由)",
|
||||
"authField": "认证字段",
|
||||
"authFieldAuthToken": "ANTHROPIC_AUTH_TOKEN(默认)",
|
||||
"authFieldApiKey": "ANTHROPIC_API_KEY",
|
||||
@@ -938,11 +949,6 @@
|
||||
"testPrompt": "测试提示词",
|
||||
"degradedThreshold": "降级阈值(毫秒)",
|
||||
"maxRetries": "最大重试次数",
|
||||
"proxyConfig": "代理配置",
|
||||
"useCustomProxy": "使用单独代理",
|
||||
"proxyConfigDesc": "为此供应商配置单独的网络代理,不启用时使用系统代理或全局设置。",
|
||||
"proxyUsername": "用户名(可选)",
|
||||
"proxyPassword": "密码(可选)",
|
||||
"pricingConfig": "计费配置",
|
||||
"useCustomPricing": "使用单独配置",
|
||||
"pricingConfigDesc": "为此供应商配置单独的计费参数,不启用时使用全局默认配置。",
|
||||
@@ -1086,7 +1092,7 @@
|
||||
},
|
||||
"dataSources": "数据来源",
|
||||
"dataSource": {
|
||||
"proxy": "代理",
|
||||
"proxy": "路由",
|
||||
"session_log": "会话日志",
|
||||
"codex_db": "Codex 数据库",
|
||||
"codex_session": "Codex 会话日志",
|
||||
@@ -1101,6 +1107,8 @@
|
||||
"failed": "会话同步失败"
|
||||
},
|
||||
"totalRecords": "共 {{total}} 条记录",
|
||||
"goToPage": "跳转",
|
||||
"pageInputPlaceholder": "页码",
|
||||
"modelPricing": "模型定价",
|
||||
"loadPricingError": "加载定价数据失败",
|
||||
"modelPricingDesc": "配置各模型的 Token 成本",
|
||||
@@ -1936,9 +1944,9 @@
|
||||
"addressCopied": "地址已复制",
|
||||
"currentProvider": "当前 Provider:",
|
||||
"waitingFirstRequest": "当前 Provider:等待首次请求…",
|
||||
"stoppedTitle": "代理服务已停止",
|
||||
"stoppedTitle": "路由服务已停止",
|
||||
"stoppedDescription": "使用上方开关即可启动服务",
|
||||
"openSettings": "配置代理服务",
|
||||
"openSettings": "配置路由服务",
|
||||
"stats": {
|
||||
"activeConnections": "活跃连接",
|
||||
"totalRequests": "总请求数",
|
||||
@@ -1947,14 +1955,14 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "代理服务设置",
|
||||
"description": "配置本地代理服务器的监听地址、端口和运行参数,保存后立即生效。",
|
||||
"title": "路由服务设置",
|
||||
"description": "配置本地路由服务器的监听地址、端口和运行参数,保存后立即生效。",
|
||||
"alert": {
|
||||
"autoApply": "保存后将自动同步到正在运行的代理服务,无需手动重启。"
|
||||
"autoApply": "保存后将自动同步到正在运行的路由服务,无需手动重启。"
|
||||
},
|
||||
"basic": {
|
||||
"title": "基础设置",
|
||||
"description": "配置代理服务监听的地址与端口。"
|
||||
"description": "配置路由服务监听的地址与端口。"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级参数",
|
||||
@@ -1968,12 +1976,12 @@
|
||||
"listenAddress": {
|
||||
"label": "监听地址",
|
||||
"placeholder": "127.0.0.1",
|
||||
"description": "代理服务器监听的 IP 地址(推荐 127.0.0.1)"
|
||||
"description": "路由服务器监听的 IP 地址(推荐 127.0.0.1)"
|
||||
},
|
||||
"listenPort": {
|
||||
"label": "监听端口",
|
||||
"placeholder": "15721",
|
||||
"description": "代理服务器监听的端口号(1024 ~ 65535)"
|
||||
"description": "路由服务器监听的端口号(1024 ~ 65535)"
|
||||
},
|
||||
"maxRetries": {
|
||||
"label": "最大重试次数",
|
||||
@@ -1987,7 +1995,7 @@
|
||||
},
|
||||
"enableLogging": {
|
||||
"label": "启用日志记录",
|
||||
"description": "记录所有代理请求,便于排查问题"
|
||||
"description": "记录所有路由请求,便于排查问题"
|
||||
},
|
||||
"streamingFirstByteTimeout": {
|
||||
"label": "流式首字超时(秒)",
|
||||
@@ -2018,29 +2026,29 @@
|
||||
"save": "保存配置"
|
||||
},
|
||||
"toast": {
|
||||
"saved": "代理配置已保存",
|
||||
"saved": "路由配置已保存",
|
||||
"saveFailed": "保存失败: {{error}}"
|
||||
},
|
||||
"invalidPort": "端口无效,请输入 1024-65535 之间的数字",
|
||||
"invalidAddress": "地址无效,请输入有效的 IP 地址(如 127.0.0.1)或 localhost",
|
||||
"configSaved": "代理配置已保存",
|
||||
"configSaved": "路由配置已保存",
|
||||
"configSaveFailed": "保存配置失败",
|
||||
"restartRequired": "修改地址或端口后需要重启代理服务才能生效"
|
||||
"restartRequired": "修改地址或端口后需要重启路由服务才能生效"
|
||||
},
|
||||
"switchFailed": "切换失败: {{error}}",
|
||||
"takeover": {
|
||||
"hint": "选择要接管的应用,启用后该应用的请求将通过本地代理转发",
|
||||
"enabled": "{{app}} 接管已启用",
|
||||
"disabled": "{{app}} 接管已关闭",
|
||||
"failed": "切换接管状态失败",
|
||||
"hint": "选择要路由的应用,启用后该应用的请求将通过本地路由转发",
|
||||
"enabled": "{{app}} 路由已启用",
|
||||
"disabled": "{{app}} 路由已关闭",
|
||||
"failed": "切换路由状态失败",
|
||||
"tooltip": {
|
||||
"active": "{{appLabel}} 已接管 - {{address}}:{{port}}\n切换该应用供应商为热切换",
|
||||
"broken": "{{appLabel}} 已接管,但代理服务未运行",
|
||||
"inactive": "接管 {{appLabel}} 的 Live 配置,让该应用请求走本地代理"
|
||||
"active": "{{appLabel}} 路由中 - {{address}}:{{port}}\n切换该应用供应商为热切换",
|
||||
"broken": "{{appLabel}} 路由中,但路由服务未运行",
|
||||
"inactive": "路由 {{appLabel}} 的请求,让该应用请求走本地路由"
|
||||
}
|
||||
},
|
||||
"failover": {
|
||||
"proxyRequired": "需要先启动代理服务才能配置故障转移",
|
||||
"proxyRequired": "需要先启动路由服务才能配置故障转移",
|
||||
"autoSwitch": "自动故障转移",
|
||||
"autoSwitchDescription": "开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商"
|
||||
},
|
||||
@@ -2104,10 +2112,10 @@
|
||||
"failed": "切换日志状态失败"
|
||||
},
|
||||
"server": {
|
||||
"started": "代理服务已启动 - {{address}}:{{port}}",
|
||||
"startFailed": "启动代理服务失败: {{detail}}"
|
||||
"started": "路由服务已启动 - {{address}}:{{port}}",
|
||||
"startFailed": "启动路由服务失败: {{detail}}"
|
||||
},
|
||||
"stoppedWithRestore": "代理服务已关闭,已恢复所有接管配置",
|
||||
"stoppedWithRestore": "路由服务已关闭,已恢复所有路由配置",
|
||||
"stopWithRestoreFailed": "停止失败: {{detail}}"
|
||||
},
|
||||
"streamCheck": {
|
||||
@@ -2125,11 +2133,21 @@
|
||||
"operational": "{{providerName}} 运行正常 ({{responseTimeMs}}ms)",
|
||||
"degraded": "{{providerName}} 响应较慢 ({{responseTimeMs}}ms)",
|
||||
"failed": "{{providerName}} 检查失败: {{message}}",
|
||||
"error": "{{providerName}} 检查出错: {{error}}"
|
||||
"rejected": "{{providerName}} 检查被拒: {{message}}",
|
||||
"error": "{{providerName}} 检查出错: {{error}}",
|
||||
"httpHint": {
|
||||
"400": "供应商拒绝了请求格式。健康检查的探测格式可能与实际使用不同。",
|
||||
"401": "API Key 可能无效,或供应商使用 OAuth 等认证方式。检查失败不代表实际不可用。",
|
||||
"402": "账户配额或计费问题。",
|
||||
"403": "供应商拒绝了此请求。部分供应商会校验客户端身份,检查失败但实际使用可能正常。",
|
||||
"404": "接口地址不存在,请检查 Base URL 和 API 路径。",
|
||||
"429": "请求频率过高,请稍后重试。",
|
||||
"5xx": "供应商内部错误,通常是临时问题。"
|
||||
}
|
||||
},
|
||||
"proxyConfig": {
|
||||
"proxyEnabled": "代理总开关",
|
||||
"appTakeover": "代理启用",
|
||||
"proxyEnabled": "路由总开关",
|
||||
"appTakeover": "路由启用",
|
||||
"perAppConfig": "应用配置",
|
||||
"circuitBreaker": "熔断器配置",
|
||||
"circuitBreakerSettings": "熔断器设置",
|
||||
|
||||
@@ -5,7 +5,6 @@ import _dds from "./dds.svg?url";
|
||||
import _eflowcode from "./eflowcode.png";
|
||||
import _pipellm from "./pipellm.png";
|
||||
import _shengsuanyun from "./shengsuanyun.svg?url";
|
||||
import _xcode from "./xcode.svg?url";
|
||||
|
||||
export const icons: Record<string, string> = {
|
||||
aicodemirror: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 1017.97 1056.47"><title>AICodeMirror</title><path fill="#E4906E" fill-rule="nonzero" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/></svg>`,
|
||||
@@ -84,7 +83,6 @@ export const iconUrls: Record<string, string> = {
|
||||
eflowcode: _eflowcode,
|
||||
pipellm: _pipellm,
|
||||
shengsuanyun: _shengsuanyun,
|
||||
xcode: _xcode,
|
||||
};
|
||||
|
||||
export const iconList = [
|
||||
|
||||
@@ -345,13 +345,6 @@ export const iconMetadata: Record<string, IconMetadata> = {
|
||||
keywords: [],
|
||||
defaultColor: "currentColor",
|
||||
},
|
||||
xcode: {
|
||||
name: "xcode",
|
||||
displayName: "Xcode",
|
||||
category: "tool",
|
||||
keywords: ["apple", "xcode", "ide"],
|
||||
defaultColor: "currentColor",
|
||||
},
|
||||
zhipu: {
|
||||
name: "zhipu",
|
||||
displayName: "Zhipu AI",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 286 KiB |
+3
-19
@@ -109,22 +109,6 @@ export interface ProviderTestConfig {
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
// 供应商单独的代理配置
|
||||
export interface ProviderProxyConfig {
|
||||
// 是否启用单独配置(false 时使用全局/系统代理)
|
||||
enabled: boolean;
|
||||
// 代理类型:http, https, socks5
|
||||
proxyType?: "http" | "https" | "socks5";
|
||||
// 代理主机
|
||||
proxyHost?: string;
|
||||
// 代理端口
|
||||
proxyPort?: number;
|
||||
// 代理用户名(可选)
|
||||
proxyUsername?: string;
|
||||
// 代理密码(可选)
|
||||
proxyPassword?: string;
|
||||
}
|
||||
|
||||
export type AuthBindingSource = "provider_config" | "managed_account";
|
||||
|
||||
export interface AuthBinding {
|
||||
@@ -149,8 +133,6 @@ export interface ProviderMeta {
|
||||
partnerPromotionKey?: string;
|
||||
// 供应商单独的模型测试配置
|
||||
testConfig?: ProviderTestConfig;
|
||||
// 供应商单独的代理配置
|
||||
proxyConfig?: ProviderProxyConfig;
|
||||
// 供应商成本倍率
|
||||
costMultiplier?: string;
|
||||
// 供应商计费模式来源
|
||||
@@ -171,7 +153,7 @@ export interface ProviderMeta {
|
||||
apiKeyField?: ClaudeApiKeyField;
|
||||
// 是否将 base_url 视为完整 API 端点(代理直接使用此 URL,不拼接路径)
|
||||
isFullUrl?: boolean;
|
||||
// Prompt cache key for OpenAI-compatible endpoints (improves cache hit rate)
|
||||
// Prompt cache key for OpenAI Responses-compatible endpoints (improves cache hit rate)
|
||||
promptCacheKey?: string;
|
||||
// 供应商类型(用于识别 Copilot 等特殊供应商)
|
||||
providerType?: string;
|
||||
@@ -254,6 +236,8 @@ export interface Settings {
|
||||
showInTray: boolean;
|
||||
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
|
||||
minimizeToTrayOnClose: boolean;
|
||||
// 是否启用应用级窗口控制按钮(最小化/最大化/关闭)
|
||||
useAppWindowControls?: boolean;
|
||||
// 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey)
|
||||
enableClaudePluginIntegration?: boolean;
|
||||
// 跳过 Claude Code 初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding)
|
||||
|
||||
Reference in New Issue
Block a user