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:
YoVinchen
2026-04-15 16:58:26 +08:00
83 changed files with 2903 additions and 1910 deletions
+174 -9
View File
@@ -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}
/>
)}
+1 -1
View File
@@ -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
+83 -57
View File
@@ -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
+49 -10
View File
@@ -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>
);
+61 -19
View File
@@ -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>
)}
+1 -18
View File
@@ -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>
+1 -18
View File
@@ -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: {
+1 -18
View File
@@ -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: "",
+1 -1
View File
@@ -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",
+1 -31
View File
@@ -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",
+25 -2
View File
@@ -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,
],
);
// 删除供应商
+3
View File
@@ -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,
+31 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "熔断器设置",
-2
View File
@@ -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 = [
-7
View File
@@ -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
View File
@@ -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